1use std::fmt;
6use std::str::FromStr;
7
8use bitcoin::hashes::sha256::Hash as Sha256Hash;
9use bitcoin::hashes::Hash;
10use bitcoin::secp256k1::schnorr::Signature;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use super::nut00::Witness;
15use super::nut10::Secret;
16use super::nut11::valid_signatures;
17use super::{Conditions, Proof};
18use crate::nut10::get_pubkeys_and_required_sigs;
19use crate::nut11::extract_signatures_from_witness;
20use crate::util::{hex, unix_time};
21use crate::SpendingConditions;
22
23pub mod serde_htlc_witness;
24
25#[derive(Debug, Error)]
27pub enum Error {
28 #[error("Secret is not a HTLC secret")]
30 IncorrectSecretKind,
31 #[error("Locktime in past")]
33 LocktimeInPast,
34 #[error("Invalid signature")]
36 InvalidSignature,
37 #[error("Hash required")]
39 HashRequired,
40 #[error("Hash is not valid")]
42 InvalidHash,
43 #[error("Preimage does not match")]
45 Preimage,
46 #[error("Preimage must be valid hex encoding")]
48 InvalidHexPreimage,
49 #[error("Preimage must be exactly 32 bytes (64 hex characters)")]
51 PreimageInvalidSize,
52 #[error("Witness did not provide signatures")]
54 SignaturesNotProvided,
55 #[error("SIG_ALL proofs must be verified using a different method")]
57 SigAllNotSupportedHere,
58 #[error("HTLC spend conditions are not met")]
60 SpendConditionsNotMet,
61 #[error(transparent)]
63 HexError(#[from] hex::Error),
64 #[error(transparent)]
66 Secp256k1(#[from] bitcoin::secp256k1::Error),
67 #[error(transparent)]
69 NUT11(#[from] super::nut11::Error),
70 #[error(transparent)]
71 Serde(#[from] serde_json::Error),
73}
74
75#[derive(Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct HTLCWitness {
78 pub preimage: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub signatures: Option<Vec<String>>,
83}
84
85impl fmt::Debug for HTLCWitness {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.debug_struct("HTLCWitness")
88 .field("preimage", &"[REDACTED]")
89 .field("signatures", &self.signatures)
90 .finish()
91 }
92}
93
94impl HTLCWitness {
95 pub fn preimage_data(&self) -> Result<[u8; 32], Error> {
101 const REQUIRED_PREIMAGE_BYTES: usize = 32;
102
103 let preimage_bytes = hex::decode(&self.preimage).map_err(|_| Error::InvalidHexPreimage)?;
105
106 if preimage_bytes.len() != REQUIRED_PREIMAGE_BYTES {
108 return Err(Error::PreimageInvalidSize);
109 }
110
111 let mut array = [0u8; 32];
113 array.copy_from_slice(&preimage_bytes);
114 Ok(array)
115 }
116}
117
118impl Proof {
119 pub fn verify_htlc(&self) -> Result<(), Error> {
128 let secret: Secret = self.secret.clone().try_into()?;
129 let spending_conditions: Conditions = secret
130 .secret_data()
131 .tags()
132 .cloned()
133 .unwrap_or_default()
134 .try_into()
135 .map_err(|_| Error::SpendConditionsNotMet)?;
136
137 if spending_conditions.sig_flag == super::SigFlag::SigAll {
138 return Err(Error::SigAllNotSupportedHere);
139 }
140
141 if secret.kind() != super::Kind::HTLC {
142 return Err(Error::IncorrectSecretKind);
143 }
144
145 let now = unix_time();
147 let requirements =
148 super::nut10::get_pubkeys_and_required_sigs(&secret, now).map_err(|err| match err {
149 super::nut10::Error::NUT14(nut14_err) => nut14_err,
150 _ => Error::SpendConditionsNotMet,
151 })?;
152
153 let htlc_witness = match &self.witness {
155 Some(Witness::HTLCWitness(witness)) => witness,
156 _ => {
157 if let Some(refund_path) = &requirements.refund_path {
160 if refund_path.required_sigs == 0 {
161 return Ok(());
162 }
163 }
164 return Err(Error::IncorrectSecretKind);
165 }
166 };
167
168 let preimage_result = verify_htlc_preimage(htlc_witness, &secret);
170
171 if preimage_result.is_ok() {
175 if requirements.required_sigs == 0 {
177 return Ok(());
178 }
179
180 let witness_signatures = htlc_witness
181 .signatures
182 .as_ref()
183 .ok_or(Error::SignaturesNotProvided)?;
184
185 let signatures: Vec<Signature> = witness_signatures
186 .iter()
187 .map(|s| Signature::from_str(s))
188 .collect::<Result<Vec<_>, _>>()?;
189
190 let msg: &[u8] = self.secret.as_bytes();
191 let valid_sig_count = valid_signatures(msg, &requirements.pubkeys, &signatures)?;
192
193 if valid_sig_count >= requirements.required_sigs {
194 Ok(())
195 } else {
196 Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
197 }
198 } else if let Some(refund_path) = &requirements.refund_path {
199 if refund_path.required_sigs == 0 {
202 return Ok(());
204 }
205
206 let witness_signatures = htlc_witness
207 .signatures
208 .as_ref()
209 .ok_or(Error::SignaturesNotProvided)?;
210
211 let signatures: Vec<Signature> = witness_signatures
212 .iter()
213 .map(|s| Signature::from_str(s))
214 .collect::<Result<Vec<_>, _>>()?;
215
216 let msg: &[u8] = self.secret.as_bytes();
217 let valid_sig_count = valid_signatures(msg, &refund_path.pubkeys, &signatures)?;
218
219 if valid_sig_count >= refund_path.required_sigs {
220 Ok(())
221 } else {
222 Err(Error::NUT11(super::nut11::Error::SpendConditionsNotMet))
223 }
224 } else {
225 preimage_result
228 }
229 }
230
231 #[inline]
233 pub fn add_preimage(&mut self, preimage: String) {
234 let signatures = self
235 .witness
236 .as_ref()
237 .map(super::nut00::Witness::signatures)
238 .unwrap_or_default();
239
240 self.witness = Some(Witness::HTLCWitness(HTLCWitness {
241 preimage,
242 signatures,
243 }))
244 }
245}
246
247impl SpendingConditions {
248 pub fn new_htlc(preimage: String, conditions: Option<Conditions>) -> Result<Self, Error> {
250 const MAX_PREIMAGE_BYTES: usize = 32;
251
252 let preimage_bytes = hex::decode(preimage)?;
253
254 if preimage_bytes.len() != MAX_PREIMAGE_BYTES {
255 return Err(Error::PreimageInvalidSize);
256 }
257
258 let htlc = Sha256Hash::hash(&preimage_bytes);
259
260 Ok(Self::HTLCConditions {
261 data: htlc,
262 conditions,
263 })
264 }
265
266 pub fn new_htlc_hash(hash: &str, conditions: Option<Conditions>) -> Result<Self, Error> {
268 let hash = Sha256Hash::from_str(hash).map_err(|_| Error::InvalidHash)?;
269
270 Ok(Self::HTLCConditions {
271 data: hash,
272 conditions,
273 })
274 }
275}
276
277fn verify_htlc_preimage(witness: &HTLCWitness, secret: &Secret) -> Result<(), Error> {
282 use bitcoin::hashes::sha256::Hash as Sha256Hash;
283 use bitcoin::hashes::Hash;
284
285 let hash_lock =
287 Sha256Hash::from_str(secret.secret_data().data()).map_err(|_| Error::InvalidHash)?;
288
289 let preimage_bytes = witness.preimage_data()?;
291
292 let preimage_hash = Sha256Hash::hash(&preimage_bytes);
294
295 if hash_lock.ne(&preimage_hash) {
297 return Err(Error::Preimage);
298 }
299
300 Ok(())
301}
302
303pub(crate) fn verify_sig_all_htlc(first_input: &Proof, msg_to_sign: String) -> Result<(), Error> {
313 let first_secret =
315 Secret::try_from(&first_input.secret).map_err(|_| Error::IncorrectSecretKind)?;
316
317 let current_time = crate::util::unix_time();
319
320 let requirements = get_pubkeys_and_required_sigs(&first_secret, current_time)
322 .map_err(|_| Error::SpendConditionsNotMet)?;
323
324 let htlc_witness = match first_input.witness.as_ref() {
326 Some(super::Witness::HTLCWitness(witness)) => Some(witness),
327 _ => None,
328 };
329
330 let preimage_valid = htlc_witness
332 .map(|w| verify_htlc_preimage(w, &first_secret).is_ok())
333 .unwrap_or(false);
334
335 if !preimage_valid {
338 if let Some(refund_path) = &requirements.refund_path {
339 if refund_path.required_sigs == 0 {
340 return Ok(());
341 }
342 }
343 }
344
345 let first_witness = first_input
347 .witness
348 .as_ref()
349 .ok_or(Error::SignaturesNotProvided)?;
350
351 if preimage_valid {
355 if requirements.required_sigs == 0 {
357 return Ok(());
358 }
359
360 let signatures = extract_signatures_from_witness(first_witness)?;
361 let valid_sig_count = super::nut11::valid_signatures(
362 msg_to_sign.as_bytes(),
363 &requirements.pubkeys,
364 &signatures,
365 )
366 .map_err(|_| Error::InvalidSignature)?;
367
368 if valid_sig_count >= requirements.required_sigs {
369 Ok(())
370 } else {
371 Err(Error::SpendConditionsNotMet)
372 }
373 } else if let Some(refund_path) = &requirements.refund_path {
374 let signatures = extract_signatures_from_witness(first_witness)?;
377 let valid_sig_count = super::nut11::valid_signatures(
378 msg_to_sign.as_bytes(),
379 &refund_path.pubkeys,
380 &signatures,
381 )
382 .map_err(|_| Error::InvalidSignature)?;
383
384 if valid_sig_count >= refund_path.required_sigs {
385 Ok(())
386 } else {
387 Err(Error::SpendConditionsNotMet)
388 }
389 } else {
390 Err(Error::SpendConditionsNotMet)
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use bitcoin::hashes::sha256::Hash as Sha256Hash;
398 use bitcoin::hashes::Hash;
399
400 use super::*;
401 use crate::nuts::nut00::Witness;
402 use crate::nuts::nut10::Kind;
403 use crate::nuts::Nut10Secret;
404 use crate::secret::Secret as SecretString;
405 use crate::{SecretData, SecretKey};
406
407 #[allow(clippy::use_debug)]
408 #[test]
409 fn htlc_witness_debug_redacts_preimage() {
410 let preimage = "known-htlc-preimage";
411 let signature = "public-signature";
412 let witness = HTLCWitness {
413 preimage: preimage.to_string(),
414 signatures: Some(vec![signature.to_string()]),
415 };
416
417 let debug = format!("{witness:?}");
418
419 assert!(!debug.contains(preimage));
420 assert!(debug.contains("preimage: \"[REDACTED]\""));
421 assert!(debug.contains(signature));
422 }
423
424 fn htlc_proof(
425 preimage_bytes: [u8; 32],
426 conditions: Option<Conditions>,
427 witness: Option<Witness>,
428 ) -> Proof {
429 let hash = Sha256Hash::hash(&preimage_bytes);
430 let nut10_secret =
431 Nut10Secret::new(Kind::HTLC, SecretData::new(hash.to_string(), conditions));
432 let secret: SecretString = nut10_secret.try_into().unwrap();
433
434 Proof {
435 amount: crate::Amount::ONE,
436 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
437 secret,
438 c: crate::nuts::nut01::PublicKey::from_hex(
439 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
440 )
441 .unwrap(),
442 witness,
443 dleq: None,
444 p2pk_e: None,
445 }
446 }
447
448 #[test]
456 fn test_verify_htlc_valid() {
457 let preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&preimage_bytes);
460 let hash_str = hash.to_string();
461
462 let nut10_secret = Nut10Secret::new(
463 Kind::HTLC,
464 SecretData::new(hash_str, None::<Vec<Vec<String>>>),
465 );
466 let secret: SecretString = nut10_secret.try_into().unwrap();
467
468 let htlc_witness = HTLCWitness {
469 preimage: hex::encode(preimage_bytes),
470 signatures: None,
471 };
472
473 let proof = Proof {
474 amount: crate::Amount::from(1),
475 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
476 secret,
477 c: crate::nuts::nut01::PublicKey::from_hex(
478 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
479 )
480 .unwrap(),
481 witness: Some(Witness::HTLCWitness(htlc_witness)),
482 dleq: None,
483 p2pk_e: None,
484 };
485
486 assert!(proof.verify_htlc().is_ok());
488 }
489
490 #[test]
491 fn test_htlc_preimage_size_boundaries() {
492 let valid_preimage = hex::encode([42u8; 32]);
493 let short_preimage = hex::encode([42u8; 31]);
494 let long_preimage = hex::encode([42u8; 33]);
495
496 assert!(SpendingConditions::new_htlc(valid_preimage.clone(), None).is_ok());
497 assert!(matches!(
498 SpendingConditions::new_htlc(short_preimage.clone(), None),
499 Err(Error::PreimageInvalidSize)
500 ));
501 assert!(matches!(
502 SpendingConditions::new_htlc(long_preimage.clone(), None),
503 Err(Error::PreimageInvalidSize)
504 ));
505
506 assert!(HTLCWitness {
507 preimage: valid_preimage,
508 signatures: None,
509 }
510 .preimage_data()
511 .is_ok());
512 assert!(matches!(
513 HTLCWitness {
514 preimage: short_preimage,
515 signatures: None,
516 }
517 .preimage_data(),
518 Err(Error::PreimageInvalidSize)
519 ));
520 assert!(matches!(
521 HTLCWitness {
522 preimage: long_preimage,
523 signatures: None,
524 }
525 .preimage_data(),
526 Err(Error::PreimageInvalidSize)
527 ));
528 }
529
530 #[test]
539 fn test_verify_htlc_wrong_preimage() {
540 let correct_preimage_bytes = [42u8; 32];
542 let hash = Sha256Hash::hash(&correct_preimage_bytes);
543 let hash_str = hash.to_string();
544
545 let nut10_secret = Nut10Secret::new(
546 Kind::HTLC,
547 SecretData::new(hash_str, None::<Vec<Vec<String>>>),
548 );
549 let secret: SecretString = nut10_secret.try_into().unwrap();
550
551 let wrong_preimage_bytes = [99u8; 32]; let htlc_witness = HTLCWitness {
554 preimage: hex::encode(wrong_preimage_bytes),
555 signatures: None,
556 };
557
558 let proof = Proof {
559 amount: crate::Amount::from(1),
560 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
561 secret,
562 c: crate::nuts::nut01::PublicKey::from_hex(
563 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
564 )
565 .unwrap(),
566 witness: Some(Witness::HTLCWitness(htlc_witness)),
567 dleq: None,
568 p2pk_e: None,
569 };
570
571 let result = proof.verify_htlc();
573 assert!(result.is_err());
574 assert!(matches!(result.unwrap_err(), Error::Preimage));
575 }
576
577 #[test]
578 fn test_verify_htlc_requires_refund_signature_when_refund_path_is_not_anyone_can_spend() {
579 let refund_key = SecretKey::generate().public_key();
580 let proof = htlc_proof(
581 [42u8; 32],
582 Some(Conditions {
583 locktime: Some(1),
584 refund_keys: Some(vec![refund_key]),
585 num_sigs_refund: Some(1),
586 ..Default::default()
587 }),
588 None,
589 );
590
591 assert!(matches!(
592 proof.verify_htlc(),
593 Err(Error::IncorrectSecretKind)
594 ));
595 }
596
597 #[test]
598 fn test_verify_htlc_rejects_insufficient_receiver_signatures() {
599 let required_key = SecretKey::generate().public_key();
600 let wrong_key = SecretKey::generate();
601 let mut proof = htlc_proof(
602 [42u8; 32],
603 Some(Conditions {
604 pubkeys: Some(vec![required_key]),
605 num_sigs: Some(1),
606 ..Default::default()
607 }),
608 None,
609 );
610 let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
611 proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
612 preimage: hex::encode([42u8; 32]),
613 signatures: Some(vec![signature.to_string()]),
614 }));
615
616 assert!(matches!(
617 proof.verify_htlc(),
618 Err(Error::NUT11(
619 crate::nuts::nut11::Error::SpendConditionsNotMet
620 ))
621 ));
622 }
623
624 #[test]
625 fn test_verify_htlc_rejects_insufficient_refund_signatures() {
626 let refund_key = SecretKey::generate().public_key();
627 let wrong_key = SecretKey::generate();
628 let mut proof = htlc_proof(
629 [42u8; 32],
630 Some(Conditions {
631 locktime: Some(1),
632 refund_keys: Some(vec![refund_key]),
633 num_sigs_refund: Some(1),
634 ..Default::default()
635 }),
636 None,
637 );
638 let signature = wrong_key.sign(proof.secret.as_bytes()).unwrap();
639 proof.witness = Some(Witness::HTLCWitness(HTLCWitness {
640 preimage: hex::encode([99u8; 32]),
641 signatures: Some(vec![signature.to_string()]),
642 }));
643
644 assert!(matches!(
645 proof.verify_htlc(),
646 Err(Error::NUT11(
647 crate::nuts::nut11::Error::SpendConditionsNotMet
648 ))
649 ));
650 }
651
652 #[test]
653 fn test_verify_sig_all_htlc_allows_expired_anyone_can_spend_refund_path() {
654 let proof = htlc_proof(
655 [42u8; 32],
656 Some(Conditions {
657 locktime: Some(1),
658 sig_flag: crate::nuts::SigFlag::SigAll,
659 ..Default::default()
660 }),
661 Some(Witness::HTLCWitness(HTLCWitness {
662 preimage: hex::encode([99u8; 32]),
663 signatures: None,
664 })),
665 );
666
667 assert!(verify_sig_all_htlc(&proof, "sig-all message".to_string()).is_ok());
668 }
669
670 #[test]
678 fn test_verify_htlc_invalid_hash() {
679 let invalid_hash = "not_a_valid_hash";
681
682 let nut10_secret = Nut10Secret::new(
683 Kind::HTLC,
684 SecretData::new(invalid_hash.to_string(), None::<Vec<Vec<String>>>),
685 );
686 let secret: SecretString = nut10_secret.try_into().unwrap();
687
688 let preimage_bytes = [42u8; 32]; let htlc_witness = HTLCWitness {
690 preimage: hex::encode(preimage_bytes),
691 signatures: None,
692 };
693
694 let proof = Proof {
695 amount: crate::Amount::from(1),
696 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
697 secret,
698 c: crate::nuts::nut01::PublicKey::from_hex(
699 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
700 )
701 .unwrap(),
702 witness: Some(Witness::HTLCWitness(htlc_witness)),
703 dleq: None,
704 p2pk_e: None,
705 };
706
707 let result = proof.verify_htlc();
709 assert!(result.is_err());
710 assert!(matches!(result.unwrap_err(), Error::InvalidHash));
711 }
712
713 #[test]
714 fn test_htlc_num_sigs_zero_bypasses_signature_requirement() {
715 let pubkey = crate::nuts::nut01::PublicKey::from_hex(
716 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
717 )
718 .unwrap();
719
720 let preimage_bytes = [42u8; 32];
721 let hash = Sha256Hash::hash(&preimage_bytes);
722 let hash_str = hash.to_string();
723
724 let tags = vec![
725 vec!["pubkeys".to_string(), pubkey.to_string()],
726 vec!["n_sigs".to_string(), "0".to_string()],
727 ];
728
729 let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
730 let conditions_res = crate::nuts::nut10::Conditions::try_from(
733 nut10_secret.secret_data().tags().cloned().unwrap(),
734 );
735 assert!(
736 conditions_res.is_err(),
737 "Conditions should fail to parse due to n_sigs=0"
738 );
739 }
740
741 #[test]
742 fn test_verify_sig_all_htlc_nsigs_zero_bypasses_sig_check() {
743 let preimage_bytes = [42u8; 32];
744 let hash = Sha256Hash::hash(&preimage_bytes);
745 let hash_str = hash.to_string();
746
747 let required_pubkey = crate::nuts::nut01::PublicKey::from_hex(
748 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
749 )
750 .unwrap();
751
752 let tags = vec![
754 vec!["pubkeys".to_string(), required_pubkey.to_string()],
755 vec!["n_sigs".to_string(), "0".to_string()],
756 vec!["sigflag".to_string(), "SIG_ALL".to_string()],
757 ];
758
759 let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
760
761 let conditions_res = crate::nuts::nut10::Conditions::try_from(
762 nut10_secret.secret_data().tags().cloned().unwrap(),
763 );
764 assert!(
765 conditions_res.is_err(),
766 "Conditions should fail to parse due to n_sigs=0"
767 );
768 }
769
770 #[test]
778 fn test_verify_htlc_wrong_witness_type() {
779 let preimage = "test_preimage";
781 let hash = Sha256Hash::hash(preimage.as_bytes());
782 let hash_str = hash.to_string();
783
784 let nut10_secret = Nut10Secret::new(
785 Kind::HTLC,
786 SecretData::new(hash_str, None::<Vec<Vec<String>>>),
787 );
788 let secret: SecretString = nut10_secret.try_into().unwrap();
789
790 let proof = Proof {
792 amount: crate::Amount::from(1),
793 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
794 secret,
795 c: crate::nuts::nut01::PublicKey::from_hex(
796 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
797 )
798 .unwrap(),
799 witness: Some(Witness::P2PKWitness(super::super::nut11::P2PKWitness {
800 signatures: vec![],
801 })),
802 dleq: None,
803 p2pk_e: None,
804 };
805
806 let result = proof.verify_htlc();
808 assert!(result.is_err());
809 assert!(matches!(result.unwrap_err(), Error::IncorrectSecretKind));
810 }
811
812 #[test]
820 fn test_add_preimage() {
821 let preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&preimage_bytes);
823 let hash_str = hash.to_string();
824
825 let nut10_secret = Nut10Secret::new(
826 Kind::HTLC,
827 SecretData::new(hash_str, None::<Vec<Vec<String>>>),
828 );
829 let secret: SecretString = nut10_secret.try_into().unwrap();
830
831 let mut proof = Proof {
832 amount: crate::Amount::from(1),
833 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
834 secret,
835 c: crate::nuts::nut01::PublicKey::from_hex(
836 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
837 )
838 .unwrap(),
839 witness: None,
840 dleq: None,
841 p2pk_e: None,
842 };
843
844 assert!(proof.witness.is_none());
846
847 let preimage_hex = hex::encode(preimage_bytes);
849 proof.add_preimage(preimage_hex.clone());
850
851 assert!(proof.witness.is_some());
853 if let Some(Witness::HTLCWitness(witness)) = &proof.witness {
854 assert_eq!(witness.preimage, preimage_hex);
855 } else {
856 panic!("Expected HTLCWitness");
857 }
858
859 assert!(proof.verify_htlc().is_ok());
861 }
862
863 #[test]
871 fn test_htlc_locktime_and_refund_keys_logic() {
872 use crate::nuts::nut01::PublicKey;
873 use crate::nuts::nut10::Conditions;
874
875 let correct_preimage_bytes = [42u8; 32]; let hash = Sha256Hash::hash(&correct_preimage_bytes);
877 let hash_str = hash.to_string();
878
879 let wrong_preimage_bytes = [99u8; 32];
881
882 let refund_pubkey = PublicKey::from_hex(
886 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
887 )
888 .unwrap();
889
890 let conditions_with_refund = Conditions {
891 locktime: Some(1), pubkeys: None,
893 refund_keys: Some(vec![refund_pubkey]), num_sigs: None,
895 sig_flag: crate::nuts::nut11::SigFlag::default(),
896 num_sigs_refund: None,
897 };
898
899 let nut10_secret = Nut10Secret::new(
900 Kind::HTLC,
901 SecretData::new(hash_str, Some(conditions_with_refund)),
902 );
903 let secret: SecretString = nut10_secret.try_into().unwrap();
904
905 let htlc_witness = HTLCWitness {
906 preimage: hex::encode(wrong_preimage_bytes), signatures: None, };
909
910 let proof = Proof {
911 amount: crate::Amount::from(1),
912 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
913 secret,
914 c: crate::nuts::nut01::PublicKey::from_hex(
915 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
916 )
917 .unwrap(),
918 witness: Some(Witness::HTLCWitness(htlc_witness)),
919 dleq: None,
920 p2pk_e: None,
921 };
922
923 let result = proof.verify_htlc();
929 assert!(
930 result.is_err(),
931 "Should fail when using refund path with refund keys but no signature"
932 );
933 }
934
935 #[test]
936 fn test_htlc_generated_empty_refund_keys_are_omitted() {
937 use crate::nuts::nut10::Conditions;
938
939 let preimage_bytes = [42u8; 32];
940 let hash = Sha256Hash::hash(&preimage_bytes);
941 let hash_str = hash.to_string();
942
943 let conditions = Conditions {
944 locktime: Some(1),
945 pubkeys: None,
946 refund_keys: Some(vec![]),
947 num_sigs: None,
948 sig_flag: crate::nuts::nut11::SigFlag::default(),
949 num_sigs_refund: None,
950 };
951
952 let nut10_secret =
953 Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(conditions)));
954 let secret: SecretString = nut10_secret.try_into().unwrap();
955
956 let htlc_witness = HTLCWitness {
957 preimage: hex::encode([0xffu8; 32]),
958 signatures: None,
959 };
960
961 let proof = Proof {
962 amount: crate::Amount::from(1),
963 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
964 secret,
965 c: crate::nuts::nut01::PublicKey::from_hex(
966 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
967 )
968 .unwrap(),
969 witness: Some(Witness::HTLCWitness(htlc_witness)),
970 dleq: None,
971 p2pk_e: None,
972 };
973
974 assert!(proof.verify_htlc().is_ok());
975 }
976
977 #[test]
978 fn test_htlc_empty_refund_tag_is_rejected() {
979 let preimage_bytes = [42u8; 32];
980 let hash = Sha256Hash::hash(&preimage_bytes);
981 let hash_str = hash.to_string();
982
983 let tags = vec![
984 vec!["locktime".to_string(), "1".to_string()],
985 vec!["refund".to_string()],
986 ];
987
988 let nut10_secret = Nut10Secret::new(Kind::HTLC, SecretData::new(hash_str, Some(tags)));
989 let secret: SecretString = nut10_secret.try_into().unwrap();
990
991 let htlc_witness = HTLCWitness {
992 preimage: hex::encode([0xffu8; 32]),
993 signatures: None,
994 };
995
996 let proof = Proof {
997 amount: crate::Amount::from(1),
998 keyset_id: crate::nuts::nut02::Id::from_str("00deadbeef123456").unwrap(),
999 secret,
1000 c: crate::nuts::nut01::PublicKey::from_hex(
1001 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
1002 )
1003 .unwrap(),
1004 witness: Some(Witness::HTLCWitness(htlc_witness)),
1005 dleq: None,
1006 p2pk_e: None,
1007 };
1008
1009 assert!(matches!(
1010 proof.verify_htlc(),
1011 Err(Error::SpendConditionsNotMet)
1012 ));
1013 }
1014}