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::secp256k1::schnorr::Signature;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use super::nut00::Witness;
12use super::nut10::Secret;
13use super::nut11::valid_signatures;
14use super::{Conditions, Proof};
15use crate::util::{hex, unix_time};
16
17pub mod serde_htlc_witness;
18
19/// NUT14 Errors
20#[derive(Debug, Error)]
21pub enum Error {
22    /// Incorrect secret kind
23    #[error("Secret is not a HTLC secret")]
24    IncorrectSecretKind,
25    /// HTLC locktime has already passed
26    #[error("Locktime in past")]
27    LocktimeInPast,
28    /// Hash Required
29    #[error("Hash required")]
30    HashRequired,
31    /// Hash is not valid
32    #[error("Hash is not valid")]
33    InvalidHash,
34    /// Preimage does not match
35    #[error("Preimage does not match")]
36    Preimage,
37    /// HTLC preimage must be valid hex encoding
38    #[error("Preimage must be valid hex encoding")]
39    InvalidHexPreimage,
40    /// HTLC preimage must be exactly 32 bytes
41    #[error("Preimage must be exactly 32 bytes (64 hex characters)")]
42    PreimageInvalidSize,
43    /// Witness Signatures not provided
44    #[error("Witness did not provide signatures")]
45    SignaturesNotProvided,
46    /// SIG_ALL not supported in this context
47    #[error("SIG_ALL proofs must be verified using a different method")]
48    SigAllNotSupportedHere,
49    /// Secp256k1 error
50    #[error(transparent)]
51    Secp256k1(#[from] bitcoin::secp256k1::Error),
52    /// NUT11 Error
53    #[error(transparent)]
54    NUT11(#[from] super::nut11::Error),
55    #[error(transparent)]
56    /// Serde Error
57    Serde(#[from] serde_json::Error),
58}
59
60/// HTLC Witness
61#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
63pub struct HTLCWitness {
64    /// Preimage
65    pub preimage: String,
66    /// Signatures
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub signatures: Option<Vec<String>>,
69}
70
71impl HTLCWitness {
72    /// Decode the preimage from hex and verify it's exactly 32 bytes
73    ///
74    /// Returns the 32-byte preimage data if valid, or an error if:
75    /// - The hex decoding fails
76    /// - The decoded data is not exactly 32 bytes
77    pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
78        const REQUIRED_PREIMAGE_BYTES: usize = 32;
79
80        // Decode the 64-character hex string to bytes
81        let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
82
83        // Verify the preimage is exactly 32 bytes
84        if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
85            return Err(Error::PreimageInvalidSize);
86        }
87
88        // Convert to fixed-size array
89        let mut array = [0u8; 32];
90        array.copy_from_slice(&preimage_bytes);
91        Ok(array)
92    }
93}
94
95impl Proof {
96    /// Verify HTLC
97    ///
98    /// Per NUT-14, there are two spending pathways:
99    /// 1. Receiver path (preimage + pubkeys): ALWAYS available
100    /// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
101    ///
102    /// The verification tries to determine which path is being used based on
103    /// the witness provided, then validates accordingly.
104    pub fn verify_htlc(&self) -> Result<(), Error> {
105        let secret: Secret = self.secret.clone().try_into()?;
106        let spending_conditions: Conditions = secret
107            .secret_data()
108            .tags()
109            .cloned()
110            .unwrap_or_default()
111            .try_into()?;
112
113        if spending_conditions.sig_flag == super::SigFlag::SigAll {
114            return Err(Error::SigAllNotSupportedHere);
115        }
116
117        if secret.kind() != super::Kind::HTLC {
118            return Err(Error::IncorrectSecretKind);
119        }
120
121        // Get the spending requirements (includes both receiver and refund paths)
122        let now = unix_time();
123        let requirements =
124            super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(Error::NUT11)?;
125
126        // Try to extract HTLC witness - must be correct type
127        let htlc_witness = match &self.witness {
128            Some(Witness::HTLCWitness(witness)) => witness,
129            _ => {
130                // Wrong witness type or no witness
131                // If refund path is available with 0 required sigs, anyone can spend
132                if let Some(refund_path) = &requirements.refund_path {
133                    if refund_path.required_sigs == 0 {
134                        return Ok(());
135                    }
136                }
137                return Err(Error::IncorrectSecretKind);
138            }
139        };
140
141        // Try to verify the preimage and capture the specific error if it fails
142        let preimage_result = super::nut10::verify_htlc_preimage(htlc_witness, &secret);
143
144        // Determine which path to use:
145        // - If preimage is valid → use receiver path (always available)
146        // - If preimage is invalid/missing → try refund path (if available)
147        if preimage_result.is_ok() {
148            // Receiver path: preimage valid, now check signatures against pubkeys
149            if requirements.required_sigs == 0 {
150                return Ok(());
151            }
152
153            let witness_signatures = htlc_witness
154                .signatures
155                .as_ref()
156                .ok_or(Error::SignaturesNotProvided)?;
157
158            let signatures: Vec<Signature> = witness_signatures
159                .iter()
160                .map(|s| Signature::from_str(s))
161                .collect::<Result<Vec<_>, _>>()?;
162
163            let msg: &[u8] = self.secret.as_bytes();
164            let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
165
166            if valid_sig_count >= requirements.required_sigs {
167                Ok(())
168            } else {
169                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
170            }
171        } else if let Some(refund_path) = &requirements.refund_path {
172            // Refund path: preimage not valid/provided, but locktime has passed
173            // Check signatures against refund keys
174            if refund_path.required_sigs == 0 {
175                // Anyone can spend (locktime passed, no refund keys)
176                return Ok(());
177            }
178
179            let witness_signatures = htlc_witness
180                .signatures
181                .as_ref()
182                .ok_or(Error::SignaturesNotProvided)?;
183
184            let signatures: Vec<Signature> = witness_signatures
185                .iter()
186                .map(|s| Signature::from_str(s))
187                .collect::<Result<Vec<_>, _>>()?;
188
189            let msg: &[u8] = self.secret.as_bytes();
190            let valid_sig_count = valid_signatures(msg, &refund_path.pubkeys, &signatures)?;
191
192            if valid_sig_count >= refund_path.required_sigs {
193                Ok(())
194            } else {
195                Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
196            }
197        } else {
198            // No valid preimage and refund path not available (locktime not passed)
199            // Return the specific error from preimage verification
200            preimage_result
201        }
202    }
203
204    /// Add Preimage
205    #[inline]
206    pub fn add_preimage(&mut self, preimage: String) {
207        let signatures = self
208            .witness
209            .as_ref()
210            .map(super::nut00::Witness::signatures)
211            .unwrap_or_default();
212
213        self.witness = Some(Witness::HTLCWitness(HTLCWitness {
214            preimage,
215            signatures,
216        }))
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use bitcoin::hashes::sha256::Hash as Sha256Hash;
223    use bitcoin::hashes::Hash;
224
225    use super::*;
226    use crate::nuts::nut00::Witness;
227    use crate::nuts::nut10::Kind;
228    use crate::nuts::Nut10Secret;
229    use crate::secret::Secret as SecretString;
230
231    /// Tests that verify_htlc correctly accepts a valid HTLC with the correct preimage.
232    ///
233    /// This test ensures that a properly formed HTLC proof with the correct preimage
234    /// passes verification.
235    ///
236    /// Mutant testing: Combined with negative tests, this catches mutations that
237    /// replace verify_htlc with Ok(()) since the negative tests will fail.
238    #[test]
239    fn test_verify_htlc_valid() {
240        // Create a valid HTLC secret with a known preimage (32 bytes)
241        let preimage_bytes = [42u8; 32]; // 32-byte preimage
242        let hash = Sha256Hash::hash(&preimage_bytes);
243        let hash_str = hash.to_string();
244
245        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
246        let secret: SecretString = nut10_secret.try_into().unwrap();
247
248        let htlc_witness = HTLCWitness {
249            preimage: hex::encode(&preimage_bytes),
250            signatures: None,
251        };
252
253        let proof = Proof {
254            amount: crate::Amount::from(1),
255            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
256            secret,
257            c: crate::nuts::nut01::PublicKey::from_hex(
258                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
259            )
260            .unwrap(),
261            witness: Some(Witness::HTLCWitness(htlc_witness)),
262            dleq: None,
263        };
264
265        // Valid HTLC should verify successfully
266        assert!(proof.verify_htlc().is_ok());
267    }
268
269    /// Tests that verify_htlc correctly rejects an HTLC with a wrong preimage.
270    ///
271    /// This test is critical for security - if the verification function doesn't properly
272    /// check the preimage against the hash, an attacker could spend HTLC-locked funds
273    /// without knowing the correct preimage.
274    ///
275    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or remove
276    /// the preimage verification logic.
277    #[test]
278    fn test_verify_htlc_wrong_preimage() {
279        // Create an HTLC secret with a specific hash (32 bytes)
280        let correct_preimage_bytes = [42u8; 32];
281        let hash = Sha256Hash::hash(&correct_preimage_bytes);
282        let hash_str = hash.to_string();
283
284        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
285        let secret: SecretString = nut10_secret.try_into().unwrap();
286
287        // Use a different preimage in the witness
288        let wrong_preimage_bytes = [99u8; 32]; // Different from correct preimage
289        let htlc_witness = HTLCWitness {
290            preimage: hex::encode(&wrong_preimage_bytes),
291            signatures: None,
292        };
293
294        let proof = Proof {
295            amount: crate::Amount::from(1),
296            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
297            secret,
298            c: crate::nuts::nut01::PublicKey::from_hex(
299                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
300            )
301            .unwrap(),
302            witness: Some(Witness::HTLCWitness(htlc_witness)),
303            dleq: None,
304        };
305
306        // Verification should fail with wrong preimage
307        let result = proof.verify_htlc();
308        assert!(result.is_err());
309        assert!(matches!(result.unwrap_err(), Error::Preimage));
310    }
311
312    /// Tests that verify_htlc correctly rejects an HTLC with an invalid hash format.
313    ///
314    /// This test ensures that the verification function properly validates that the
315    /// hash in the secret data is a valid SHA256 hash.
316    ///
317    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
318    /// remove the hash validation logic.
319    #[test]
320    fn test_verify_htlc_invalid_hash() {
321        // Create an HTLC secret with an invalid hash (not a valid hex string)
322        let invalid_hash = "not_a_valid_hash";
323
324        let nut10_secret = Nut10Secret::new(
325            Kind::HTLC,
326            invalid_hash.to_string(),
327            None::<Vec<Vec<String>>>,
328        );
329        let secret: SecretString = nut10_secret.try_into().unwrap();
330
331        let preimage_bytes = [42u8; 32]; // Valid 32-byte preimage
332        let htlc_witness = HTLCWitness {
333            preimage: hex::encode(&preimage_bytes),
334            signatures: None,
335        };
336
337        let proof = Proof {
338            amount: crate::Amount::from(1),
339            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
340            secret,
341            c: crate::nuts::nut01::PublicKey::from_hex(
342                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
343            )
344            .unwrap(),
345            witness: Some(Witness::HTLCWitness(htlc_witness)),
346            dleq: None,
347        };
348
349        // Verification should fail with invalid hash
350        let result = proof.verify_htlc();
351        assert!(result.is_err());
352        assert!(matches!(result.unwrap_err(), Error::InvalidHash));
353    }
354
355    /// Tests that verify_htlc correctly rejects an HTLC with the wrong witness type.
356    ///
357    /// This test ensures that the verification function checks that the witness is
358    /// of the correct type (HTLCWitness) and not some other witness type.
359    ///
360    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
361    /// remove the witness type check.
362    #[test]
363    fn test_verify_htlc_wrong_witness_type() {
364        // Create an HTLC secret
365        let preimage = "test_preimage";
366        let hash = Sha256Hash::hash(preimage.as_bytes());
367        let hash_str = hash.to_string();
368
369        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
370        let secret: SecretString = nut10_secret.try_into().unwrap();
371
372        // Create proof with wrong witness type (P2PKWitness instead of HTLCWitness)
373        let proof = Proof {
374            amount: crate::Amount::from(1),
375            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
376            secret,
377            c: crate::nuts::nut01::PublicKey::from_hex(
378                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
379            )
380            .unwrap(),
381            witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
382                signatures: vec![],
383            })),
384            dleq: None,
385        };
386
387        // Verification should fail with wrong witness type
388        let result = proof.verify_htlc();
389        assert!(result.is_err());
390        assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
391    }
392
393    /// Tests that add_preimage correctly adds a preimage to the proof.
394    ///
395    /// This test ensures that add_preimage actually modifies the witness and doesn't
396    /// just return without doing anything.
397    ///
398    /// Mutant testing: Catches mutations that replace add_preimage with () without
399    /// actually adding the preimage.
400    #[test]
401    fn test_add_preimage() {
402        let preimage_bytes = [42u8; 32]; // 32-byte preimage
403        let hash = Sha256Hash::hash(&preimage_bytes);
404        let hash_str = hash.to_string();
405
406        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
407        let secret: SecretString = nut10_secret.try_into().unwrap();
408
409        let mut proof = Proof {
410            amount: crate::Amount::from(1),
411            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
412            secret,
413            c: crate::nuts::nut01::PublicKey::from_hex(
414                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
415            )
416            .unwrap(),
417            witness: None,
418            dleq: None,
419        };
420
421        // Initially, witness should be None
422        assert!(proof.witness.is_none());
423
424        // Add preimage (hex-encoded)
425        let preimage_hex = hex::encode(&preimage_bytes);
426        proof.add_preimage(preimage_hex.clone());
427
428        // After adding, witness should be Some with HTLCWitness
429        assert!(proof.witness.is_some());
430        if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
431            assert_eq!(witness.preimage, preimage_hex);
432        } else {
433            panic!("Expected HTLCWitness");
434        }
435
436        // The proof with added preimage should verify successfully
437        assert!(proof.verify_htlc().is_ok());
438    }
439
440    /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
441    ///
442    /// This test verifies that when locktime has passed and refund keys are present,
443    /// a signature from the refund keys is required (not anyone-can-spend).
444    ///
445    /// Per NUT-14: After locktime, the refund path requires signatures from refund keys.
446    /// The "anyone can spend" case only applies when locktime passed AND no refund keys.
447    #[test]
448    fn test_htlc_locktime_and_refund_keys_logic() {
449        use crate::nuts::nut01::PublicKey;
450        use crate::nuts::nut11::Conditions;
451
452        let correct_preimage_bytes = [42u8; 32]; // 32-byte preimage
453        let hash = Sha256Hash::hash(&correct_preimage_bytes);
454        let hash_str = hash.to_string();
455
456        // Use WRONG preimage to force using refund path (not receiver path)
457        let wrong_preimage_bytes = [99u8; 32];
458
459        // Test: Locktime has passed (locktime=1) but refund keys ARE present
460        // Since we provide wrong preimage, receiver path fails, so we try refund path.
461        // Refund path with refund keys present should require a signature.
462        let refund_pubkey = PublicKey::from_hex(
463            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
464        )
465        .unwrap();
466
467        let conditions_with_refund = Conditions {
468            locktime: Some(1), // Locktime in past (current time is much larger)
469            pubkeys: None,
470            refund_keys: Some(vec![refund_pubkey]), // Refund key present
471            num_sigs: None,
472            sig_flag: crate::nuts::nut11::SigFlag::default(),
473            num_sigs_refund: None,
474        };
475
476        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, Some(conditions_with_refund));
477        let secret: SecretString = nut10_secret.try_into().unwrap();
478
479        let htlc_witness = HTLCWitness {
480            preimage: hex::encode(&wrong_preimage_bytes), // Wrong preimage!
481            signatures: None,                             // No signature provided
482        };
483
484        let proof = Proof {
485            amount: crate::Amount::from(1),
486            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
487            secret,
488            c: crate::nuts::nut01::PublicKey::from_hex(
489                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
490            )
491            .unwrap(),
492            witness: Some(Witness::HTLCWitness(htlc_witness)),
493            dleq: None,
494        };
495
496        // Should FAIL because:
497        // 1. Wrong preimage means receiver path fails
498        // 2. Falls back to refund path (locktime passed)
499        // 3. Refund keys are present, so signature is required
500        // 4. No signature provided
501        let result = proof.verify_htlc();
502        assert!(
503            result.is_err(),
504            "Should fail when using refund path with refund keys but no signature"
505        );
506    }
507}