1use crate::ArkAddress;
8use crate::UNSPENDABLE_KEY;
9use bitcoin::hashes::ripemd160;
10use bitcoin::hashes::Hash;
11use bitcoin::opcodes::all::*;
12use bitcoin::taproot::TaprootBuilder;
13use bitcoin::taproot::TaprootSpendInfo;
14use bitcoin::Network;
15use bitcoin::PublicKey;
16use bitcoin::ScriptBuf;
17use bitcoin::Sequence;
18use bitcoin::XOnlyPublicKey;
19use serde::Deserialize;
20use serde::Serialize;
21use std::collections::BTreeMap;
22use std::str::FromStr;
23use thiserror::Error;
24
25#[derive(Debug, Error)]
26pub enum VhtlcError {
27 #[error("Invalid preimage hash length: expected 20 bytes, got {0}")]
28 InvalidPreimageHashLength(usize),
29 #[error("Invalid public key length: expected 32 bytes, got {0}")]
30 InvalidPublicKeyLength(usize),
31 #[error("Invalid locktime: {0}")]
32 InvalidLocktime(String),
33 #[error("Invalid delay: {0}")]
34 InvalidDelay(String),
35 #[error("Taproot construction failed: {0}")]
36 TaprootError(String),
37}
38
39#[derive(Debug, Clone)]
41struct TaprootScriptItem {
42 script: ScriptBuf,
43 weight: u32,
44}
45
46#[derive(Debug, Clone)]
48enum TaprootTreeNode {
49 Leaf {
50 script: ScriptBuf,
51 weight: u32,
52 },
53 Branch {
54 left: Box<TaprootTreeNode>,
55 right: Box<TaprootTreeNode>,
56 weight: u32,
57 },
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct VhtlcOptions {
66 pub sender: XOnlyPublicKey,
67 pub receiver: XOnlyPublicKey,
68 pub server: XOnlyPublicKey,
69 pub preimage_hash: ripemd160::Hash,
70 pub refund_locktime: u32,
71 pub unilateral_claim_delay: Sequence,
72 pub unilateral_refund_delay: Sequence,
73 pub unilateral_refund_without_receiver_delay: Sequence,
74}
75
76impl VhtlcOptions {
77 pub fn validate(&self) -> Result<(), VhtlcError> {
78 if self.refund_locktime == 0 {
79 return Err(VhtlcError::InvalidLocktime(
80 "Refund locktime must be greater than 0".to_string(),
81 ));
82 }
83
84 if !self.unilateral_claim_delay.is_relative_lock_time()
85 || self.unilateral_claim_delay.to_consensus_u32() == 0
86 {
87 return Err(VhtlcError::InvalidDelay(
88 "Unilateral claim delay must be a valid non-zero CSV relative lock time"
89 .to_string(),
90 ));
91 }
92
93 if !self.unilateral_refund_delay.is_relative_lock_time()
94 || self.unilateral_refund_delay.to_consensus_u32() == 0
95 {
96 return Err(VhtlcError::InvalidDelay(
97 "Unilateral refund delay must be a valid non-zero CSV relative lock time"
98 .to_string(),
99 ));
100 }
101
102 if !self
103 .unilateral_refund_without_receiver_delay
104 .is_relative_lock_time()
105 || self
106 .unilateral_refund_without_receiver_delay
107 .to_consensus_u32()
108 == 0
109 {
110 return Err(VhtlcError::InvalidDelay(
111 "Unilateral refund without receiver delay must be a valid non-zero CSV relative lock time"
112 .to_string(),
113 ));
114 }
115
116 Ok(())
117 }
118
119 fn build_taproot(&self) -> Result<TaprootSpendInfo, VhtlcError> {
120 let internal_pubkey = PublicKey::from_str(UNSPENDABLE_KEY)
121 .map_err(|e| VhtlcError::TaprootError(format!("Failed to parse internal key: {e}")))?;
122 let internal_key = XOnlyPublicKey::from(internal_pubkey);
123
124 let scripts = vec![
127 TaprootScriptItem {
128 script: self.claim_script(),
129 weight: 1, },
131 TaprootScriptItem {
132 script: self.refund_script(),
133 weight: 1, },
135 TaprootScriptItem {
136 script: self.refund_without_receiver_script(),
137 weight: 1, },
139 TaprootScriptItem {
140 script: self.unilateral_claim_script(),
141 weight: 1, },
143 TaprootScriptItem {
144 script: self.unilateral_refund_script(),
145 weight: 1, },
147 TaprootScriptItem {
148 script: self.unilateral_refund_without_receiver_script(),
149 weight: 1, },
151 ];
152
153 let tree = Self::taproot_list_to_tree(scripts)?;
155
156 let builder = TaprootBuilder::new();
158 let builder = Self::add_tree_to_builder(builder, &tree, 0)?;
159
160 let secp = bitcoin::secp256k1::Secp256k1::new();
161 let taproot_spend_info = builder
162 .finalize(&secp, internal_key)
163 .map_err(|e| VhtlcError::TaprootError(format!("Failed to finalize taproot: {e:?}")))?;
164
165 Ok(taproot_spend_info)
166 }
167
168 pub fn claim_script(&self) -> ScriptBuf {
172 let preimage_hash = self.preimage_hash;
173
174 ScriptBuf::builder()
175 .push_opcode(OP_HASH160)
176 .push_slice(preimage_hash.as_byte_array())
177 .push_opcode(OP_EQUAL)
178 .push_opcode(OP_VERIFY)
179 .push_x_only_key(&self.receiver)
180 .push_opcode(OP_CHECKSIGVERIFY)
181 .push_x_only_key(&self.server)
182 .push_opcode(OP_CHECKSIG)
183 .into_script()
184 }
185
186 pub fn refund_script(&self) -> ScriptBuf {
190 ScriptBuf::builder()
191 .push_x_only_key(&self.sender)
192 .push_opcode(OP_CHECKSIGVERIFY)
193 .push_x_only_key(&self.receiver)
194 .push_opcode(OP_CHECKSIGVERIFY)
195 .push_x_only_key(&self.server)
196 .push_opcode(OP_CHECKSIG)
197 .into_script()
198 }
199
200 pub fn refund_without_receiver_script(&self) -> ScriptBuf {
204 ScriptBuf::builder()
205 .push_int(self.refund_locktime as i64)
206 .push_opcode(OP_CLTV)
207 .push_opcode(OP_DROP)
208 .push_x_only_key(&self.sender)
209 .push_opcode(OP_CHECKSIGVERIFY)
210 .push_x_only_key(&self.server)
211 .push_opcode(OP_CHECKSIG)
212 .into_script()
213 }
214
215 pub fn unilateral_claim_script(&self) -> ScriptBuf {
219 let preimage_hash = self.preimage_hash;
220 let sequence = self.unilateral_claim_delay;
221
222 ScriptBuf::builder()
223 .push_opcode(OP_HASH160)
224 .push_slice(preimage_hash.as_byte_array())
225 .push_opcode(OP_EQUAL)
226 .push_opcode(OP_VERIFY)
227 .push_int(sequence.to_consensus_u32() as i64)
228 .push_opcode(OP_CSV)
229 .push_opcode(OP_DROP)
230 .push_x_only_key(&self.receiver)
231 .push_opcode(OP_CHECKSIG)
232 .into_script()
233 }
234
235 pub fn unilateral_refund_script(&self) -> ScriptBuf {
239 let sequence = self.unilateral_refund_delay;
240 ScriptBuf::builder()
241 .push_int(sequence.to_consensus_u32() as i64)
242 .push_opcode(OP_CSV)
243 .push_opcode(OP_DROP)
244 .push_x_only_key(&self.sender)
245 .push_opcode(OP_CHECKSIGVERIFY)
246 .push_x_only_key(&self.receiver)
247 .push_opcode(OP_CHECKSIG)
248 .into_script()
249 }
250
251 pub fn unilateral_refund_without_receiver_script(&self) -> ScriptBuf {
255 let sequence = self.unilateral_refund_without_receiver_delay;
256 ScriptBuf::builder()
257 .push_int(sequence.to_consensus_u32() as i64)
258 .push_opcode(OP_CSV)
259 .push_opcode(OP_DROP)
260 .push_x_only_key(&self.sender)
261 .push_opcode(OP_CHECKSIG)
262 .into_script()
263 }
264
265 fn taproot_list_to_tree(
268 scripts: Vec<TaprootScriptItem>,
269 ) -> Result<TaprootTreeNode, VhtlcError> {
270 if scripts.is_empty() {
271 return Err(VhtlcError::TaprootError("Empty script list".to_string()));
272 }
273
274 let mut lst: Vec<TaprootTreeNode> = scripts
276 .into_iter()
277 .map(|item| TaprootTreeNode::Leaf {
278 script: item.script,
279 weight: item.weight,
280 })
281 .collect();
282
283 while lst.len() >= 2 {
285 lst.sort_by(|a, b| {
287 let weight_a = match a {
288 TaprootTreeNode::Leaf { weight, .. } => *weight,
289 TaprootTreeNode::Branch { weight, .. } => *weight,
290 };
291 let weight_b = match b {
292 TaprootTreeNode::Leaf { weight, .. } => *weight,
293 TaprootTreeNode::Branch { weight, .. } => *weight,
294 };
295 weight_b.cmp(&weight_a)
297 });
298
299 let b = lst.pop().expect("an element");
301 let a = lst.pop().expect("an element");
302
303 let weight_a = match &a {
305 TaprootTreeNode::Leaf { weight, .. } => *weight,
306 TaprootTreeNode::Branch { weight, .. } => *weight,
307 };
308 let weight_b = match &b {
309 TaprootTreeNode::Leaf { weight, .. } => *weight,
310 TaprootTreeNode::Branch { weight, .. } => *weight,
311 };
312
313 lst.push(TaprootTreeNode::Branch {
315 weight: weight_a + weight_b,
316 left: Box::new(a),
317 right: Box::new(b),
318 });
319 }
320
321 Ok(lst.into_iter().next().expect("root node"))
323 }
324
325 fn add_tree_to_builder(
327 builder: TaprootBuilder,
328 node: &TaprootTreeNode,
329 depth: u8,
330 ) -> Result<TaprootBuilder, VhtlcError> {
331 match node {
332 TaprootTreeNode::Leaf { script, .. } => builder
333 .add_leaf(depth, script.clone())
334 .map_err(|e| VhtlcError::TaprootError(format!("Failed to add leaf: {e}"))),
335 TaprootTreeNode::Branch { left, right, .. } => {
336 let builder = Self::add_tree_to_builder(builder, left, depth + 1)?;
337 Self::add_tree_to_builder(builder, right, depth + 1)
338 }
339 }
340 }
341}
342
343pub struct VhtlcScript {
353 options: VhtlcOptions,
354 taproot_spend_info: TaprootSpendInfo,
355 network: Network,
356}
357
358impl VhtlcScript {
359 pub fn new(options: VhtlcOptions, network: Network) -> Result<Self, VhtlcError> {
364 options.validate()?;
365
366 let taproot_spend_info = options.build_taproot()?;
367
368 Ok(Self {
369 options,
370 taproot_spend_info,
371 network,
372 })
373 }
374
375 pub fn taproot_spend_info(&self) -> &TaprootSpendInfo {
376 &self.taproot_spend_info
377 }
378
379 pub fn script_pubkey(&self) -> ScriptBuf {
380 ScriptBuf::builder()
381 .push_opcode(OP_PUSHNUM_1)
382 .push_slice(self.taproot_spend_info.output_key().serialize())
383 .into_script()
384 }
385
386 pub fn address(&self) -> ArkAddress {
387 ArkAddress::new(
388 self.network,
389 self.options.server,
390 self.taproot_spend_info().output_key(),
391 )
392 }
393
394 pub fn claim_script(&self) -> ScriptBuf {
398 self.options.claim_script()
399 }
400
401 pub fn refund_script(&self) -> ScriptBuf {
405 self.options.refund_script()
406 }
407
408 pub fn refund_without_receiver_script(&self) -> ScriptBuf {
412 self.options.refund_without_receiver_script()
413 }
414
415 pub fn unilateral_claim_script(&self) -> ScriptBuf {
419 self.options.unilateral_claim_script()
420 }
421
422 pub fn unilateral_refund_script(&self) -> ScriptBuf {
426 self.options.unilateral_refund_script()
427 }
428
429 pub fn unilateral_refund_without_receiver_script(&self) -> ScriptBuf {
433 self.options.unilateral_refund_without_receiver_script()
434 }
435
436 pub fn get_script_map(&self) -> BTreeMap<String, ScriptBuf> {
437 let mut map = BTreeMap::new();
438 map.insert("claim".to_string(), self.claim_script());
439 map.insert("refund".to_string(), self.refund_script());
440 map.insert(
441 "refund_without_receiver".to_string(),
442 self.refund_without_receiver_script(),
443 );
444 map.insert(
445 "unilateral_claim".to_string(),
446 self.unilateral_claim_script(),
447 );
448 map.insert(
449 "unilateral_refund".to_string(),
450 self.unilateral_refund_script(),
451 );
452 map.insert(
453 "unilateral_refund_without_receiver".to_string(),
454 self.unilateral_refund_without_receiver_script(),
455 );
456 map
457 }
458
459 pub fn tapscripts(self) -> Vec<ScriptBuf> {
460 vec![
461 self.claim_script(),
462 self.refund_script(),
463 self.refund_without_receiver_script(),
464 self.unilateral_claim_script(),
465 self.unilateral_refund_script(),
466 self.unilateral_refund_without_receiver_script(),
467 ]
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use bitcoin::hex::DisplayHex;
475 use bitcoin::hex::FromHex;
476 use bitcoin::Network;
477 use bitcoin::PublicKey;
478 use bitcoin::Sequence;
479 use bitcoin::XOnlyPublicKey;
480 use serde::Deserialize;
481 use serde::Serialize;
482 use std::collections::HashMap;
483 use std::fs;
484 use std::str::FromStr;
485
486 #[derive(Debug, Deserialize, Serialize)]
487 struct Fixtures {
488 valid: Vec<ValidTestCase>,
489 invalid: Vec<InvalidTestCase>,
490 }
491
492 #[derive(Debug, Deserialize, Serialize)]
493 struct ValidTestCase {
494 description: String,
495 #[serde(rename = "preimageHash")]
496 preimage_hash: String,
497 receiver: String,
498 sender: String,
499 server: String,
500 #[serde(rename = "refundLocktime")]
501 refund_locktime: u32,
502 #[serde(rename = "unilateralClaimDelay")]
503 unilateral_claim_delay: Delay,
504 #[serde(rename = "unilateralRefundDelay")]
505 unilateral_refund_delay: Delay,
506 #[serde(rename = "unilateralRefundWithoutReceiverDelay")]
507 unilateral_refund_without_receiver_delay: Delay,
508 expected: String,
509 scripts: ScriptHexes,
510 taproot: TaprootInfo,
511 #[serde(rename = "decodedScripts")]
512 decoded_scripts: HashMap<String, String>,
513 }
514
515 #[derive(Debug, Deserialize, Serialize)]
516 struct InvalidTestCase {
517 description: String,
518 #[serde(rename = "preimageHash")]
519 preimage_hash: String,
520 receiver: String,
521 sender: String,
522 server: String,
523 #[serde(rename = "refundLocktime")]
524 refund_locktime: u32,
525 #[serde(rename = "unilateralClaimDelay")]
526 unilateral_claim_delay: Delay,
527 #[serde(rename = "unilateralRefundDelay")]
528 unilateral_refund_delay: Delay,
529 #[serde(rename = "unilateralRefundWithoutReceiverDelay")]
530 unilateral_refund_without_receiver_delay: Delay,
531 error: String,
532 }
533
534 #[derive(Debug, Deserialize, Serialize)]
535 struct ScriptHexes {
536 #[serde(rename = "claimScript")]
537 claim_script: String,
538 #[serde(rename = "refundScript")]
539 refund_script: String,
540 #[serde(rename = "refundWithoutReceiverScript")]
541 refund_without_receiver_script: String,
542 #[serde(rename = "unilateralClaimScript")]
543 unilateral_claim_script: String,
544 #[serde(rename = "unilateralRefundScript")]
545 unilateral_refund_script: String,
546 #[serde(rename = "unilateralRefundWithoutReceiverScript")]
547 unilateral_refund_without_receiver_script: String,
548 }
549
550 #[derive(Debug, Deserialize, Serialize)]
551 struct TaprootInfo {
552 #[serde(rename = "tweakedPublicKey")]
553 tweaked_public_key: String,
554 #[serde(rename = "tapTree")]
555 tap_tree: String,
556 #[serde(rename = "internalKey")]
557 internal_key: String,
558 }
559
560 #[derive(Debug, Deserialize, Serialize)]
561 struct Delay {
562 #[serde(rename = "type")]
563 delay_type: String,
564 value: u32,
565 }
566
567 impl Delay {
568 fn to_sequence(&self) -> Result<Sequence, String> {
569 match self.delay_type.as_str() {
570 "blocks" => {
571 if self.value == 0 {
572 return Err("unilateral claim delay must greater than 0".to_string());
573 }
574 Ok(Sequence::from_height(self.value as u16))
575 }
576 "seconds" => {
577 if self.value < 512 {
578 return Err("seconds timelock must be greater or equal to 512".to_string());
579 }
580 if self.value % 512 != 0 {
581 return Err("seconds timelock must be multiple of 512".to_string());
582 }
583 Sequence::from_seconds_ceil(self.value)
584 .map_err(|e| format!("Invalid seconds value: {e}"))
585 }
586 _ => Err(format!("Unknown delay type: {}", self.delay_type)),
587 }
588 }
589 }
590
591 fn hex_to_bytes20(hex: &str) -> Result<[u8; 20], String> {
592 let bytes = Vec::from_hex(hex).map_err(|e| format!("Invalid hex: {e}"))?;
593 if bytes.len() != 20 {
594 return Err("preimage hash must be 20 bytes".to_string());
595 }
596 let mut arr = [0u8; 20];
597 arr.copy_from_slice(&bytes);
598 Ok(arr)
599 }
600
601 fn pubkey_to_xonly(pubkey_hex: &str) -> XOnlyPublicKey {
602 let pubkey = PublicKey::from_str(pubkey_hex).expect("valid public key");
603 XOnlyPublicKey::from(pubkey.inner)
604 }
605
606 #[test]
607 fn test_vhtlc_with_valid_fixtures() {
608 let fixtures_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/vhtlc_fixtures/vhtlc.json");
609 let fixtures_json = fs::read_to_string(fixtures_path).expect("to read fixtures file");
610 let fixtures: Fixtures = serde_json::from_str(&fixtures_json).expect("to parse fixtures");
611
612 for test_case in fixtures.valid {
613 let preimage_hash =
614 ripemd160::Hash::from_str(&test_case.preimage_hash).expect("valid hash");
615
616 let sender = pubkey_to_xonly(&test_case.sender);
617 let receiver = pubkey_to_xonly(&test_case.receiver);
618 let server = pubkey_to_xonly(&test_case.server);
619
620 let options = VhtlcOptions {
621 sender,
622 receiver,
623 server,
624 preimage_hash,
625 refund_locktime: test_case.refund_locktime,
626 unilateral_claim_delay: test_case
627 .unilateral_claim_delay
628 .to_sequence()
629 .expect("valid delay"),
630 unilateral_refund_delay: test_case
631 .unilateral_refund_delay
632 .to_sequence()
633 .expect("valid delay"),
634 unilateral_refund_without_receiver_delay: test_case
635 .unilateral_refund_without_receiver_delay
636 .to_sequence()
637 .expect("valid delay"),
638 };
639
640 let vhtlc = VhtlcScript::new(options, Network::Testnet).expect("to create VHTLC");
641
642 let claim_hex = vhtlc.claim_script().as_bytes().to_lower_hex_string();
644 assert_eq!(
645 claim_hex, test_case.scripts.claim_script,
646 "Claim script hex mismatch for test case: {}",
647 test_case.description
648 );
649
650 let refund_hex = vhtlc.refund_script().as_bytes().to_lower_hex_string();
651 assert_eq!(
652 refund_hex, test_case.scripts.refund_script,
653 "Refund script hex mismatch for test case: {}",
654 test_case.description
655 );
656
657 let refund_without_receiver_hex = vhtlc
658 .refund_without_receiver_script()
659 .as_bytes()
660 .to_lower_hex_string();
661 assert_eq!(
662 refund_without_receiver_hex, test_case.scripts.refund_without_receiver_script,
663 "Refund without receiver script hex mismatch for test case: {}",
664 test_case.description
665 );
666
667 let unilateral_claim_hex = vhtlc
668 .unilateral_claim_script()
669 .as_bytes()
670 .to_lower_hex_string();
671 assert_eq!(
672 unilateral_claim_hex, test_case.scripts.unilateral_claim_script,
673 "Unilateral claim script hex mismatch for test case: {}",
674 test_case.description
675 );
676
677 let unilateral_refund_hex = vhtlc
678 .unilateral_refund_script()
679 .as_bytes()
680 .to_lower_hex_string();
681 assert_eq!(
682 unilateral_refund_hex, test_case.scripts.unilateral_refund_script,
683 "Unilateral refund script hex mismatch for test case: {}",
684 test_case.description
685 );
686
687 let unilateral_refund_without_receiver_hex = vhtlc
688 .unilateral_refund_without_receiver_script()
689 .as_bytes()
690 .to_lower_hex_string();
691
692 assert_eq!(
693 unilateral_refund_without_receiver_hex,
694 test_case.scripts.unilateral_refund_without_receiver_script,
695 "Unilateral refund without receiver script hex mismatch for test case: {}. Our impl includes CLTV locktime, fixture expects only CSV",
696 test_case.description
697 );
698
699 let taproot_info = vhtlc.taproot_spend_info();
701
702 let internal_key = taproot_info.internal_key();
703 let internal_key_hex = internal_key.serialize().to_lower_hex_string();
704
705 let pubkey = PublicKey::from_str(&test_case.taproot.internal_key)
707 .expect("valid internal key in fixture");
708 let expected_internal = XOnlyPublicKey::from(pubkey.inner)
709 .serialize()
710 .to_lower_hex_string();
711
712 assert_eq!(
713 internal_key_hex, expected_internal,
714 "Internal key mismatch for test case: {}",
715 test_case.description
716 );
717
718 let output_key = taproot_info.output_key();
719 let output_key_hex = output_key.serialize().to_lower_hex_string();
720
721 assert_eq!(
722 output_key_hex, test_case.taproot.tweaked_public_key,
723 "Tweaked public key mismatch for test case: {}",
724 test_case.description
725 );
726
727 let addr = vhtlc.address();
729 let address_str = addr.encode();
730
731 assert_eq!(
732 address_str, test_case.expected,
733 "Address mismatch for test case: {}",
734 test_case.description
735 );
736 }
737 }
738
739 #[test]
740 fn test_vhtlc_with_invalid_fixtures() {
741 let fixtures_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/vhtlc_fixtures/vhtlc.json");
742 let fixtures_json = fs::read_to_string(fixtures_path).expect("to read fixtures file");
743 let fixtures: Fixtures = serde_json::from_str(&fixtures_json).expect("to parse fixtures");
744
745 for test_case in fixtures.invalid {
746 let preimage_hash_result = hex_to_bytes20(&test_case.preimage_hash);
748
749 if let Err(e) = preimage_hash_result {
750 assert!(
751 e.contains(&test_case.error),
752 "Expected error containing '{}', got '{}' for test case: {}",
753 test_case.error,
754 e,
755 test_case.description
756 );
757 continue;
758 }
759
760 if test_case.refund_locktime == 0 {
762 assert!(
763 test_case
764 .error
765 .contains("refund locktime must be greater than 0"),
766 "Expected refund locktime error for test case: {}",
767 test_case.description
768 );
769 continue;
770 }
771
772 let claim_delay_result = test_case.unilateral_claim_delay.to_sequence();
774 if let Err(e) = claim_delay_result {
775 assert!(
776 e.contains(&test_case.error),
777 "Expected error containing '{}', got '{}' for claim delay in test case: {}",
778 test_case.error,
779 e,
780 test_case.description
781 );
782 continue;
783 }
784
785 let refund_delay_result = test_case.unilateral_refund_delay.to_sequence();
786 if let Err(e) = refund_delay_result {
787 assert!(
788 e.contains(&test_case.error),
789 "Expected error containing '{}', got '{}' for refund delay in test case: {}",
790 test_case.error,
791 e,
792 test_case.description
793 );
794 continue;
795 }
796
797 let refund_without_receiver_delay_result = test_case
798 .unilateral_refund_without_receiver_delay
799 .to_sequence();
800 if let Err(e) = refund_without_receiver_delay_result {
801 assert!(
802 e.contains(&test_case.error),
803 "Expected error containing '{}', got '{}' for refund without receiver delay in test case: {}",
804 test_case.error,
805 e,
806 test_case.description
807 );
808 continue;
809 }
810
811 panic!(
813 "Invalid test case '{}' didn't fail as expected",
814 test_case.description
815 );
816 }
817 }
818
819 #[test]
820 fn test_specific_script_encodings() {
821 let sender =
823 pubkey_to_xonly("030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4");
824 let receiver =
825 pubkey_to_xonly("021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b");
826 let server =
827 pubkey_to_xonly("03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88");
828 let preimage_hash =
829 ripemd160::Hash::from_str("4d487dd3753a89bc9fe98401d1196523058251fc").unwrap();
830
831 let options = VhtlcOptions {
832 sender,
833 receiver,
834 server,
835 preimage_hash,
836 refund_locktime: 265,
837 unilateral_claim_delay: Sequence::from_height(17),
838 unilateral_refund_delay: Sequence::from_height(144),
839 unilateral_refund_without_receiver_delay: Sequence::from_height(144),
840 };
841
842 let vhtlc = VhtlcScript::new(options, Network::Testnet).expect("to create VHTLC");
843
844 let claim_script = vhtlc.claim_script();
846 let claim_hex = claim_script.as_bytes().to_lower_hex_string();
847 let expected_claim = "a9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac";
848 assert_eq!(
849 claim_hex, expected_claim,
850 "Claim script should match fixture"
851 );
852
853 let unilateral_claim = vhtlc.unilateral_claim_script();
855 let unilateral_claim_hex = unilateral_claim.as_bytes().to_lower_hex_string();
856
857 assert!(
859 unilateral_claim_hex.contains("0111"),
860 "Should contain CSV value 17 as 0x0111"
861 );
862
863 let expected_unilateral_claim = "a9144d487dd3753a89bc9fe98401d1196523058251fc87690111b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac";
864 assert_eq!(
865 unilateral_claim_hex, expected_unilateral_claim,
866 "Unilateral claim script should match fixture"
867 );
868 }
869}