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    pub fn verify_htlc(&self) -> Result<(), Error> {
98        let secret: Secret = self.secret.clone().try_into()?;
99        let spending_conditions: Conditions = secret
100            .secret_data()
101            .tags()
102            .cloned()
103            .unwrap_or_default()
104            .try_into()?;
105
106        if spending_conditions.sig_flag == super::SigFlag::SigAll {
107            return Err(Error::SigAllNotSupportedHere);
108        }
109
110        if secret.kind() != super::Kind::HTLC {
111            return Err(Error::IncorrectSecretKind);
112        }
113
114        // Get the appropriate spending conditions based on locktime
115        let now = unix_time();
116        let requirements =
117            super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(Error::NUT11)?;
118
119        // While a Witness is usually needed in a P2PK or HTLC proof, it's not
120        // always needed. If we are past the locktime, and there are no refund
121        // keys, then the proofs are anyone-can-spend:
122        //     NUT-11: "If the tag locktime is the unix time and the mint's local
123        //              clock is greater than locktime, the Proof becomes spendable
124        //              by anyone, except if [there are no refund keys]"
125        // Therefore, this function should not extract any Witness unless it
126        // is needed to get a preimage or signatures.
127
128        // If preimage is needed (before locktime), verify it
129        if requirements.preimage_needed {
130            // Extract HTLC witness
131            let htlc_witness = match &self.witness {
132                Some(Witness::HTLCWitness(witness)) => witness,
133                _ => return Err(Error::IncorrectSecretKind),
134            };
135
136            // Verify preimage using shared function
137            super::nut10::verify_htlc_preimage(htlc_witness, &secret)?;
138        }
139
140        if requirements.required_sigs == 0 {
141            return Ok(());
142        }
143
144        // if we get here, the preimage check (if it was needed) has been done
145        // and we know that at least one signature is required. So, we extract
146        // the witness.signatures and count them:
147
148        // Extract witness signatures
149        let htlc_witness = match &self.witness {
150            Some(Witness::HTLCWitness(witness)) => witness,
151            _ => return Err(Error::IncorrectSecretKind),
152        };
153        let witness_signatures = htlc_witness
154            .signatures
155            .as_ref()
156            .ok_or(Error::SignaturesNotProvided)?;
157
158        // Convert signatures from strings
159        let signatures: Vec<Signature> = witness_signatures
160            .iter()
161            .map(|s| Signature::from_str(s))
162            .collect::<Result<Vec<_>, _>>()?;
163
164        // Count valid signatures using relevant_pubkeys
165        let msg: &[u8] = self.secret.as_bytes();
166        let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
167
168        // Check if we have enough valid signatures
169        if valid_sig_count >= requirements.required_sigs {
170            Ok(())
171        } else {
172            Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
173        }
174    }
175
176    /// Add Preimage
177    #[inline]
178    pub fn add_preimage(&mut self, preimage: String) {
179        let signatures = self
180            .witness
181            .as_ref()
182            .map(|w| w.signatures())
183            .unwrap_or_default();
184
185        self.witness = Some(Witness::HTLCWitness(HTLCWitness {
186            preimage,
187            signatures,
188        }))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use bitcoin::hashes::sha256::Hash as Sha256Hash;
195    use bitcoin::hashes::Hash;
196
197    use super::*;
198    use crate::nuts::nut00::Witness;
199    use crate::nuts::nut10::Kind;
200    use crate::nuts::Nut10Secret;
201    use crate::secret::Secret as SecretString;
202
203    /// Tests that verify_htlc correctly accepts a valid HTLC with the correct preimage.
204    ///
205    /// This test ensures that a properly formed HTLC proof with the correct preimage
206    /// passes verification.
207    ///
208    /// Mutant testing: Combined with negative tests, this catches mutations that
209    /// replace verify_htlc with Ok(()) since the negative tests will fail.
210    #[test]
211    fn test_verify_htlc_valid() {
212        // Create a valid HTLC secret with a known preimage (32 bytes)
213        let preimage_bytes = [42u8; 32]; // 32-byte preimage
214        let hash = Sha256Hash::hash(&preimage_bytes);
215        let hash_str = hash.to_string();
216
217        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
218        let secret: SecretString = nut10_secret.try_into().unwrap();
219
220        let htlc_witness = HTLCWitness {
221            preimage: hex::encode(&preimage_bytes),
222            signatures: None,
223        };
224
225        let proof = Proof {
226            amount: crate::Amount::from(1),
227            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
228            secret,
229            c: crate::nuts::nut01::PublicKey::from_hex(
230                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
231            )
232            .unwrap(),
233            witness: Some(Witness::HTLCWitness(htlc_witness)),
234            dleq: None,
235        };
236
237        // Valid HTLC should verify successfully
238        assert!(proof.verify_htlc().is_ok());
239    }
240
241    /// Tests that verify_htlc correctly rejects an HTLC with a wrong preimage.
242    ///
243    /// This test is critical for security - if the verification function doesn't properly
244    /// check the preimage against the hash, an attacker could spend HTLC-locked funds
245    /// without knowing the correct preimage.
246    ///
247    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or remove
248    /// the preimage verification logic.
249    #[test]
250    fn test_verify_htlc_wrong_preimage() {
251        // Create an HTLC secret with a specific hash (32 bytes)
252        let correct_preimage_bytes = [42u8; 32];
253        let hash = Sha256Hash::hash(&correct_preimage_bytes);
254        let hash_str = hash.to_string();
255
256        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
257        let secret: SecretString = nut10_secret.try_into().unwrap();
258
259        // Use a different preimage in the witness
260        let wrong_preimage_bytes = [99u8; 32]; // Different from correct preimage
261        let htlc_witness = HTLCWitness {
262            preimage: hex::encode(&wrong_preimage_bytes),
263            signatures: None,
264        };
265
266        let proof = Proof {
267            amount: crate::Amount::from(1),
268            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
269            secret,
270            c: crate::nuts::nut01::PublicKey::from_hex(
271                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
272            )
273            .unwrap(),
274            witness: Some(Witness::HTLCWitness(htlc_witness)),
275            dleq: None,
276        };
277
278        // Verification should fail with wrong preimage
279        let result = proof.verify_htlc();
280        assert!(result.is_err());
281        assert!(matches!(result.unwrap_err(), Error::Preimage));
282    }
283
284    /// Tests that verify_htlc correctly rejects an HTLC with an invalid hash format.
285    ///
286    /// This test ensures that the verification function properly validates that the
287    /// hash in the secret data is a valid SHA256 hash.
288    ///
289    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
290    /// remove the hash validation logic.
291    #[test]
292    fn test_verify_htlc_invalid_hash() {
293        // Create an HTLC secret with an invalid hash (not a valid hex string)
294        let invalid_hash = "not_a_valid_hash";
295
296        let nut10_secret = Nut10Secret::new(
297            Kind::HTLC,
298            invalid_hash.to_string(),
299            None::<Vec<Vec<String>>>,
300        );
301        let secret: SecretString = nut10_secret.try_into().unwrap();
302
303        let preimage_bytes = [42u8; 32]; // Valid 32-byte preimage
304        let htlc_witness = HTLCWitness {
305            preimage: hex::encode(&preimage_bytes),
306            signatures: None,
307        };
308
309        let proof = Proof {
310            amount: crate::Amount::from(1),
311            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
312            secret,
313            c: crate::nuts::nut01::PublicKey::from_hex(
314                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
315            )
316            .unwrap(),
317            witness: Some(Witness::HTLCWitness(htlc_witness)),
318            dleq: None,
319        };
320
321        // Verification should fail with invalid hash
322        let result = proof.verify_htlc();
323        assert!(result.is_err());
324        assert!(matches!(result.unwrap_err(), Error::InvalidHash));
325    }
326
327    /// Tests that verify_htlc correctly rejects an HTLC with the wrong witness type.
328    ///
329    /// This test ensures that the verification function checks that the witness is
330    /// of the correct type (HTLCWitness) and not some other witness type.
331    ///
332    /// Mutant testing: Catches mutations that replace verify_htlc with Ok(()) or
333    /// remove the witness type check.
334    #[test]
335    fn test_verify_htlc_wrong_witness_type() {
336        // Create an HTLC secret
337        let preimage = "test_preimage";
338        let hash = Sha256Hash::hash(preimage.as_bytes());
339        let hash_str = hash.to_string();
340
341        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
342        let secret: SecretString = nut10_secret.try_into().unwrap();
343
344        // Create proof with wrong witness type (P2PKWitness instead of HTLCWitness)
345        let proof = Proof {
346            amount: crate::Amount::from(1),
347            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
348            secret,
349            c: crate::nuts::nut01::PublicKey::from_hex(
350                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
351            )
352            .unwrap(),
353            witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
354                signatures: vec![],
355            })),
356            dleq: None,
357        };
358
359        // Verification should fail with wrong witness type
360        let result = proof.verify_htlc();
361        assert!(result.is_err());
362        assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
363    }
364
365    /// Tests that add_preimage correctly adds a preimage to the proof.
366    ///
367    /// This test ensures that add_preimage actually modifies the witness and doesn't
368    /// just return without doing anything.
369    ///
370    /// Mutant testing: Catches mutations that replace add_preimage with () without
371    /// actually adding the preimage.
372    #[test]
373    fn test_add_preimage() {
374        let preimage_bytes = [42u8; 32]; // 32-byte preimage
375        let hash = Sha256Hash::hash(&preimage_bytes);
376        let hash_str = hash.to_string();
377
378        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, None::<Vec<Vec<String>>>);
379        let secret: SecretString = nut10_secret.try_into().unwrap();
380
381        let mut proof = Proof {
382            amount: crate::Amount::from(1),
383            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
384            secret,
385            c: crate::nuts::nut01::PublicKey::from_hex(
386                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
387            )
388            .unwrap(),
389            witness: None,
390            dleq: None,
391        };
392
393        // Initially, witness should be None
394        assert!(proof.witness.is_none());
395
396        // Add preimage (hex-encoded)
397        let preimage_hex = hex::encode(&preimage_bytes);
398        proof.add_preimage(preimage_hex.clone());
399
400        // After adding, witness should be Some with HTLCWitness
401        assert!(proof.witness.is_some());
402        if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
403            assert_eq!(witness.preimage, preimage_hex);
404        } else {
405            panic!("Expected HTLCWitness");
406        }
407
408        // The proof with added preimage should verify successfully
409        assert!(proof.verify_htlc().is_ok());
410    }
411
412    /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
413    ///
414    /// This test catches the mutation that replaces `&&` with `||` at line 83.
415    /// The logic should be: (locktime expired AND no refund keys) → anyone can spend.
416    /// If mutated to OR, it would allow spending when locktime passed even if refund keys exist.
417    ///
418    /// Mutant testing: Catches mutations that replace `&&` with `||` in the locktime check.
419    #[test]
420    fn test_htlc_locktime_and_refund_keys_logic() {
421        use crate::nuts::nut01::PublicKey;
422        use crate::nuts::nut11::Conditions;
423
424        let preimage_bytes = [42u8; 32]; // 32-byte preimage
425        let hash = Sha256Hash::hash(&preimage_bytes);
426        let hash_str = hash.to_string();
427
428        // Test: Locktime has passed (locktime=1) but refund keys ARE present
429        // With correct logic (&&): Since refund_keys.is_none() is false, the "anyone can spend"
430        //                          path is NOT taken, so signature is required
431        // With mutation (||): Since locktime.lt(&unix_time()) is true, it WOULD take the
432        //                     "anyone can spend" path immediately - WRONG!
433        let refund_pubkey = PublicKey::from_hex(
434            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
435        )
436        .unwrap();
437
438        let conditions_with_refund = Conditions {
439            locktime: Some(1), // Locktime in past (current time is much larger)
440            pubkeys: None,
441            refund_keys: Some(vec![refund_pubkey]), // Refund key present
442            num_sigs: None,
443            sig_flag: crate::nuts::nut11::SigFlag::default(),
444            num_sigs_refund: None,
445        };
446
447        let nut10_secret = Nut10Secret::new(Kind::HTLC, hash_str, Some(conditions_with_refund));
448        let secret: SecretString = nut10_secret.try_into().unwrap();
449
450        let htlc_witness = HTLCWitness {
451            preimage: hex::encode(&preimage_bytes),
452            signatures: None, // No signature provided
453        };
454
455        let proof = Proof {
456            amount: crate::Amount::from(1),
457            keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
458            secret,
459            c: crate::nuts::nut01::PublicKey::from_hex(
460                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
461            )
462            .unwrap(),
463            witness: Some(Witness::HTLCWitness(htlc_witness)),
464            dleq: None,
465        };
466
467        // Should FAIL because even though locktime passed, refund keys are present
468        // so the "anyone can spend" shortcut shouldn't apply. A signature is required.
469        // With && this correctly fails. With || it would incorrectly pass.
470        let result = proof.verify_htlc();
471        assert!(
472            result.is_err(),
473            "Should fail when locktime passed but refund keys present without signature"
474        );
475    }
476}