From 48c921d346bc95927e85f3ab18665efb28610409 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 28 Aug 2025 18:13:56 +0200 Subject: [PATCH] lightning: implement the htlc script Signed-off-by: Vincenzo Palazzo --- Cargo.toml | 1 + ark-lightning/Cargo.toml | 19 + ark-lightning/examples/vhtlc_example.rs | 56 +++ ark-lightning/scripts/generate_ts_vectors.js | 58 +++ ark-lightning/src/lib.rs | 5 + ark-lightning/src/vhtlc.rs | 407 +++++++++++++++++++ ark-lightning/tests/fixtures/vhtlc.json | 258 ++++++++++++ ark-lightning/tests/vhtlc_fixtures.rs | 387 ++++++++++++++++++ 8 files changed, 1191 insertions(+) create mode 100644 ark-lightning/Cargo.toml create mode 100644 ark-lightning/examples/vhtlc_example.rs create mode 100644 ark-lightning/scripts/generate_ts_vectors.js create mode 100644 ark-lightning/src/lib.rs create mode 100644 ark-lightning/src/vhtlc.rs create mode 100644 ark-lightning/tests/fixtures/vhtlc.json create mode 100644 ark-lightning/tests/vhtlc_fixtures.rs diff --git a/Cargo.toml b/Cargo.toml index fd3794eb..5126715d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "ark-rust-secp256k1", "ark-dlc-sample", "ark-rs", + "ark-lightning", ] resolver = "2" diff --git a/ark-lightning/Cargo.toml b/ark-lightning/Cargo.toml new file mode 100644 index 00000000..1ae4f179 --- /dev/null +++ b/ark-lightning/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ark-lightning" +version = "0.1.0" +edition = "2024" + +[dependencies] +ark-core = { path = "../ark-core" } +bitcoin = { version = "0.32.4", features = ["base64", "rand", "serde"] } +musig = { package = "ark-secp256k1", path = "../ark-rust-secp256k1", features = ["serde", "rand"] } +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" + +[dev-dependencies] +hex = "0.4" +serde_json = "1.0" + +[[example]] +name = "vhtlc_example" +path = "examples/vhtlc_example.rs" diff --git a/ark-lightning/examples/vhtlc_example.rs b/ark-lightning/examples/vhtlc_example.rs new file mode 100644 index 00000000..6ca5f7aa --- /dev/null +++ b/ark-lightning/examples/vhtlc_example.rs @@ -0,0 +1,56 @@ +use ark_lightning::vhtlc::VhtlcOptions; +use ark_lightning::vhtlc::VhtlcScript; +use bitcoin::Sequence; +use bitcoin::XOnlyPublicKey; +use std::str::FromStr; + +fn main() { + // Create test keys for sender, receiver, and server + let sender = XOnlyPublicKey::from_str( + "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + ) + .unwrap(); + + let receiver = XOnlyPublicKey::from_str( + "28845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + ) + .unwrap(); + + let server = XOnlyPublicKey::from_str( + "38845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", + ) + .unwrap(); + + // Create a preimage hash (in a real scenario, this would be the hash of a secret) + let preimage_hash = [42u8; 20]; + + // Configure the VHTLC options + let options = VhtlcOptions { + sender, + receiver, + server, + preimage_hash, + refund_locktime: 100000, // Block height for CLTV + unilateral_claim_delay: Sequence::from_seconds_ceil(3600).unwrap(), // 1 hour + unilateral_refund_delay: Sequence::from_seconds_ceil(7200).unwrap(), // 2 hours + unilateral_refund_without_receiver_delay: Sequence::from_seconds_ceil(10800).unwrap(), /* 3 hours */ + }; + + // Create the VHTLC script + let vhtlc = VhtlcScript::new(options).expect("Failed to create VHTLC"); + + // Get the taproot output key and script pubkey + if let Some(taproot_info) = vhtlc.taproot_info() { + println!("Taproot output key: {}", taproot_info.output_key()); + + if let Some(script_pubkey) = vhtlc.script_pubkey() { + println!("Script pubkey: {}", script_pubkey); + } + } + + // Display all available spending paths + println!("\nAvailable spending paths:"); + for (name, script) in vhtlc.get_script_map() { + println!(" {} - {} bytes", name, script.len()); + } +} diff --git a/ark-lightning/scripts/generate_ts_vectors.js b/ark-lightning/scripts/generate_ts_vectors.js new file mode 100644 index 00000000..74c53055 --- /dev/null +++ b/ark-lightning/scripts/generate_ts_vectors.js @@ -0,0 +1,58 @@ +// Script to generate test vectors from TypeScript SDK for comparison +// Run this in the arkade-os/ts-sdk repository to get the expected hex values + +const { VhtlcScript } = require('../path/to/ts-sdk'); // Adjust path as needed + +// Test data from fixtures (same as used in Rust tests) +const testData = { + preimageHash: Buffer.from('4d487dd3753a89bc9fe98401d1196523058251fc', 'hex'), + receiver: Buffer.from('021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b', 'hex'), + sender: Buffer.from('030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4', 'hex'), + server: Buffer.from('03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88', 'hex'), + refundLocktime: 265, + unilateralClaimDelay: { type: 'blocks', value: 17 }, + unilateralRefundDelay: { type: 'blocks', value: 144 }, + unilateralRefundWithoutReceiverDelay: { type: 'blocks', value: 144 } +}; + +try { + // Create VHTLC instance + const vhtlc = new VhtlcScript(testData); + + console.log('=== TypeScript SDK VHTLC Script Vectors ===\n'); + + // Export all script hex values + const vectors = { + claim: vhtlc.claim().script.toString('hex'), + refund: vhtlc.refund().script.toString('hex'), + refundWithoutReceiver: vhtlc.refundWithoutReceiver().script.toString('hex'), + unilateralClaim: vhtlc.unilateralClaim().script.toString('hex'), + unilateralRefund: vhtlc.unilateralRefund().script.toString('hex'), + unilateralRefundWithoutReceiver: vhtlc.unilateralRefundWithoutReceiver().script.toString('hex') + }; + + // Output for comparison + console.log('TypeScript SDK Script Vectors:'); + console.log('1. Claim Script: ', vectors.claim); + console.log('2. Refund Script: ', vectors.refund); + console.log('3. Refund Without Receiver Script: ', vectors.refundWithoutReceiver); + console.log('4. Unilateral Claim Script: ', vectors.unilateralClaim); + console.log('5. Unilateral Refund Script: ', vectors.unilateralRefund); + console.log('6. Unilateral Refund Without Receiver: ', vectors.unilateralRefundWithoutReceiver); + + // Also output as JSON for easy parsing + console.log('\n=== JSON Format ==='); + console.log(JSON.stringify(vectors, null, 2)); + + // Export taproot address for comparison + const address = vhtlc.address('testnet'); // or 'mainnet' + console.log('\n=== Address ==='); + console.log('TypeScript SDK Address:', address); + +} catch (error) { + console.error('Error generating TypeScript vectors:', error); + console.log('\nPlease ensure:'); + console.log('1. You are running this in the arkade-os/ts-sdk directory'); + console.log('2. The path to VhtlcScript is correct'); + console.log('3. All dependencies are installed (npm install)'); +} \ No newline at end of file diff --git a/ark-lightning/src/lib.rs b/ark-lightning/src/lib.rs new file mode 100644 index 00000000..126bf651 --- /dev/null +++ b/ark-lightning/src/lib.rs @@ -0,0 +1,5 @@ +//! Lightning Network Module for the Ark Lightning Swap +//! +//! Vincenzo Palazzo + +pub mod vhtlc; diff --git a/ark-lightning/src/vhtlc.rs b/ark-lightning/src/vhtlc.rs new file mode 100644 index 00000000..632c2f06 --- /dev/null +++ b/ark-lightning/src/vhtlc.rs @@ -0,0 +1,407 @@ +//! Virtual Hash Time Lock Contract (VHTLC) implementation for Ark Lightning Swaps +//! +//! This module implements VHTLC scripts that enable atomic swaps and conditional +//! payments in the Ark protocol. The VHTLC provides multiple spending paths with +//! different conditions and participants. + +use ark_core::ArkAddress; +use ark_core::UNSPENDABLE_KEY; +use bitcoin::opcodes::all::*; +use bitcoin::taproot::TaprootBuilder; +use bitcoin::taproot::TaprootSpendInfo; +use bitcoin::Network; +use bitcoin::PublicKey; +use bitcoin::ScriptBuf; +use bitcoin::Sequence; +use bitcoin::XOnlyPublicKey; +use serde::Deserialize; +use serde::Serialize; +use std::collections::BTreeMap; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum VhtlcError { + #[error("Invalid preimage hash length: expected 20 bytes, got {0}")] + InvalidPreimageHashLength(usize), + #[error("Invalid public key length: expected 32 bytes, got {0}")] + InvalidPublicKeyLength(usize), + #[error("Invalid locktime: {0}")] + InvalidLocktime(String), + #[error("Invalid delay: {0}")] + InvalidDelay(String), + #[error("Taproot construction failed: {0}")] + TaprootError(String), +} + +/// Represents a script with its weight for taproot tree construction +#[derive(Debug, Clone)] +struct TaprootScriptItem { + script: ScriptBuf, + weight: u32, +} + +/// Internal tree node for building the taproot tree structure +#[derive(Debug, Clone)] +enum TaprootTreeNode { + Leaf { + script: ScriptBuf, + weight: u32, + }, + Branch { + left: Box, + right: Box, + weight: u32, + }, +} + +/// Options for creating a VHTLC (Virtual Hash Time Lock Contract) +/// +/// This structure contains all the necessary parameters to construct a VHTLC, +/// including the public keys of participants and various timeout values. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VhtlcOptions { + pub sender: XOnlyPublicKey, + pub receiver: XOnlyPublicKey, + pub server: XOnlyPublicKey, + pub preimage_hash: [u8; 20], + pub refund_locktime: u32, + pub unilateral_claim_delay: Sequence, + pub unilateral_refund_delay: Sequence, + pub unilateral_refund_without_receiver_delay: Sequence, +} + +impl VhtlcOptions { + pub fn validate(&self) -> Result<(), VhtlcError> { + if self.refund_locktime == 0 { + return Err(VhtlcError::InvalidLocktime( + "Refund locktime must be greater than 0".to_string(), + )); + } + + if !self.unilateral_claim_delay.is_relative_lock_time() + || self.unilateral_claim_delay.to_consensus_u32() == 0 + { + return Err(VhtlcError::InvalidDelay( + "Unilateral claim delay must be a valid non-zero CSV relative lock time" + .to_string(), + )); + } + + if !self.unilateral_refund_delay.is_relative_lock_time() + || self.unilateral_refund_delay.to_consensus_u32() == 0 + { + return Err(VhtlcError::InvalidDelay( + "Unilateral refund delay must be a valid non-zero CSV relative lock time" + .to_string(), + )); + } + + if !self + .unilateral_refund_without_receiver_delay + .is_relative_lock_time() + || self + .unilateral_refund_without_receiver_delay + .to_consensus_u32() + == 0 + { + return Err(VhtlcError::InvalidDelay( + "Unilateral refund without receiver delay must be a valid non-zero CSV relative lock time" + .to_string(), + )); + } + + Ok(()) + } +} + +/// VHTLC Script builder and manager +/// +/// This struct creates and manages VHTLC scripts with six different spending paths: +/// 1. **Claim**: Receiver reveals preimage (collaborative with server) +/// 2. **Refund**: Collaborative refund (all three parties) +/// 3. **Refund without Receiver**: Sender refunds after locktime (with server) +/// 4. **Unilateral Claim**: Receiver claims after delay (no server needed) +/// 5. **Unilateral Refund**: Collaborative unilateral refund after delay +/// 6. **Unilateral Refund without Receiver**: Sender unilateral refund after both timeouts +pub struct VhtlcScript { + options: VhtlcOptions, + taproot_info: Option, +} + +impl VhtlcScript { + /// Creates a new VHTLC script with the given options + /// + /// This will validate the options and build the complete taproot tree + /// with all spending paths. + pub fn new(options: VhtlcOptions) -> Result { + options.validate()?; + let mut script = Self { + options, + taproot_info: None, + }; + script.build_taproot()?; + Ok(script) + } + + /// Creates the claim script where receiver reveals the preimage + /// + /// Requires: preimage hash verification + receiver signature + server signature + pub fn claim_script(&self) -> ScriptBuf { + ScriptBuf::builder() + .push_opcode(OP_HASH160) + .push_slice(&self.options.preimage_hash) + .push_opcode(OP_EQUAL) + .push_opcode(OP_VERIFY) + .push_x_only_key(&self.options.receiver) + .push_opcode(OP_CHECKSIGVERIFY) + .push_x_only_key(&self.options.server) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Creates the collaborative refund script + /// + /// Requires: sender + receiver + server signatures + pub fn refund_script(&self) -> ScriptBuf { + ScriptBuf::builder() + .push_x_only_key(&self.options.sender) + .push_opcode(OP_CHECKSIGVERIFY) + .push_x_only_key(&self.options.receiver) + .push_opcode(OP_CHECKSIGVERIFY) + .push_x_only_key(&self.options.server) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Creates the refund script when receiver is unavailable + /// + /// Requires: CLTV timeout + sender + server signatures + pub fn refund_without_receiver_script(&self) -> ScriptBuf { + ScriptBuf::builder() + .push_int(self.options.refund_locktime as i64) + .push_opcode(OP_CLTV) + .push_opcode(OP_DROP) + .push_x_only_key(&self.options.sender) + .push_opcode(OP_CHECKSIGVERIFY) + .push_x_only_key(&self.options.server) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Creates the unilateral claim script (no server cooperation needed) + /// + /// Requires: preimage hash verification + CSV delay + receiver signature + pub fn unilateral_claim_script(&self) -> ScriptBuf { + let sequence = self.options.unilateral_claim_delay; + ScriptBuf::builder() + .push_opcode(OP_HASH160) + .push_slice(&self.options.preimage_hash) + .push_opcode(OP_EQUAL) + .push_opcode(OP_VERIFY) + .push_int(sequence.to_consensus_u32() as i64) + .push_opcode(OP_CSV) + .push_opcode(OP_DROP) + .push_x_only_key(&self.options.receiver) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Creates the unilateral refund script + /// + /// Requires: CSV delay + sender + receiver signatures + pub fn unilateral_refund_script(&self) -> ScriptBuf { + let sequence = self.options.unilateral_refund_delay; + ScriptBuf::builder() + .push_int(sequence.to_consensus_u32() as i64) + .push_opcode(OP_CSV) + .push_opcode(OP_DROP) + .push_x_only_key(&self.options.sender) + .push_opcode(OP_CHECKSIGVERIFY) + .push_x_only_key(&self.options.receiver) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Creates the unilateral refund script when receiver is unavailable + /// + /// Requires: CSV delay + sender signature + pub fn unilateral_refund_without_receiver_script(&self) -> ScriptBuf { + let sequence = self.options.unilateral_refund_without_receiver_delay; + ScriptBuf::builder() + .push_int(sequence.to_consensus_u32() as i64) + .push_opcode(OP_CSV) + .push_opcode(OP_DROP) + .push_x_only_key(&self.options.sender) + .push_opcode(OP_CHECKSIG) + .into_script() + } + + /// Build a balanced taproot tree from a list of scripts with weights + /// Following the TypeScript algorithm from scure-btc-signer + fn taproot_list_to_tree( + scripts: Vec, + ) -> Result { + if scripts.is_empty() { + return Err(VhtlcError::TaprootError("Empty script list".to_string())); + } + + // Clone input and convert to nodes + let mut lst: Vec = scripts + .into_iter() + .map(|item| TaprootTreeNode::Leaf { + script: item.script, + weight: item.weight, + }) + .collect(); + + // Build tree by combining nodes with smallest weights + while lst.len() >= 2 { + // Sort: elements with smallest weight are at the end of queue + lst.sort_by(|a, b| { + let weight_a = match a { + TaprootTreeNode::Leaf { weight, .. } => *weight, + TaprootTreeNode::Branch { weight, .. } => *weight, + }; + let weight_b = match b { + TaprootTreeNode::Leaf { weight, .. } => *weight, + TaprootTreeNode::Branch { weight, .. } => *weight, + }; + // Reverse comparison to put smallest at end + weight_b.cmp(&weight_a) + }); + + // Pop the two smallest weight nodes + let b = lst.pop().unwrap(); + let a = lst.pop().unwrap(); + + // Calculate combined weight + let weight_a = match &a { + TaprootTreeNode::Leaf { weight, .. } => *weight, + TaprootTreeNode::Branch { weight, .. } => *weight, + }; + let weight_b = match &b { + TaprootTreeNode::Leaf { weight, .. } => *weight, + TaprootTreeNode::Branch { weight, .. } => *weight, + }; + + // Create branch with combined weight + lst.push(TaprootTreeNode::Branch { + weight: weight_a + weight_b, + left: Box::new(a), + right: Box::new(b), + }); + } + + // Return the root node + Ok(lst.into_iter().next().unwrap()) + } + + /// Recursively add tree nodes to TaprootBuilder + fn add_tree_to_builder( + builder: TaprootBuilder, + node: &TaprootTreeNode, + depth: u8, + ) -> Result { + match node { + TaprootTreeNode::Leaf { script, .. } => builder + .add_leaf(depth, script.clone()) + .map_err(|e| VhtlcError::TaprootError(format!("Failed to add leaf: {}", e))), + TaprootTreeNode::Branch { left, right, .. } => { + let builder = Self::add_tree_to_builder(builder, left, depth + 1)?; + Self::add_tree_to_builder(builder, right, depth + 1) + } + } + } + + fn build_taproot(&mut self) -> Result<(), VhtlcError> { + let internal_pubkey = PublicKey::from_str(UNSPENDABLE_KEY).map_err(|e| { + VhtlcError::TaprootError(format!("Failed to parse internal key: {}", e)) + })?; + let internal_key = XOnlyPublicKey::from(internal_pubkey); + + // Create script list with weights + // Lower weight = more likely to be used = shallower in tree + let scripts = vec![ + TaprootScriptItem { + script: self.claim_script(), + weight: 1, // Most likely - collaborative claim + }, + TaprootScriptItem { + script: self.refund_script(), + weight: 1, // Most likely - collaborative refund + }, + TaprootScriptItem { + script: self.refund_without_receiver_script(), + weight: 1, // Less common + }, + TaprootScriptItem { + script: self.unilateral_claim_script(), + weight: 1, // Less common + }, + TaprootScriptItem { + script: self.unilateral_refund_script(), + weight: 1, // Least common + }, + TaprootScriptItem { + script: self.unilateral_refund_without_receiver_script(), + weight: 1, // Least common + }, + ]; + + // Build the tree using the weight-based algorithm + let tree = Self::taproot_list_to_tree(scripts)?; + + // Create TaprootBuilder and add the tree + let builder = TaprootBuilder::new(); + let builder = Self::add_tree_to_builder(builder, &tree, 0)?; + + let secp = bitcoin::secp256k1::Secp256k1::new(); + let taproot_info = builder.finalize(&secp, internal_key).map_err(|e| { + VhtlcError::TaprootError(format!("Failed to finalize taproot: {:?}", e)) + })?; + + self.taproot_info = Some(taproot_info); + Ok(()) + } + + pub fn taproot_info(&self) -> Option<&TaprootSpendInfo> { + self.taproot_info.as_ref() + } + + pub fn script_pubkey(&self) -> Option { + self.taproot_info.as_ref().map(|info| { + ScriptBuf::builder() + .push_opcode(OP_PUSHNUM_1) + .push_slice(info.output_key().serialize()) + .into_script() + }) + } + + pub fn address(&self, network: Network, server: XOnlyPublicKey) -> Option { + ArkAddress::new(network, server, self.taproot_info()?.output_key()).into() + } + + pub fn get_script_map(&self) -> BTreeMap { + let mut map = BTreeMap::new(); + map.insert("claim".to_string(), self.claim_script()); + map.insert("refund".to_string(), self.refund_script()); + map.insert( + "refund_without_receiver".to_string(), + self.refund_without_receiver_script(), + ); + map.insert( + "unilateral_claim".to_string(), + self.unilateral_claim_script(), + ); + map.insert( + "unilateral_refund".to_string(), + self.unilateral_refund_script(), + ); + map.insert( + "unilateral_refund_without_receiver".to_string(), + self.unilateral_refund_without_receiver_script(), + ); + map + } +} diff --git a/ark-lightning/tests/fixtures/vhtlc.json b/ark-lightning/tests/fixtures/vhtlc.json new file mode 100644 index 00000000..fbe4aa02 --- /dev/null +++ b/ark-lightning/tests/fixtures/vhtlc.json @@ -0,0 +1,258 @@ +{ + "valid": [ + { + "description": "CSV locktime > 16", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "blocks", + "value": 17 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "expected": "tark1qz4d2t2czchfaml2l3ad3gwde2qxpd0srhc7wkpnvtg99cnxyz8c3pnvvhnhumhwhqthmlxmdryakwx99s6508y8dunj9sty2p5mr7unh5re63", + "scripts": { + "claimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundScript": "200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundWithoutReceiverScript": "020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "unilateralClaimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc87690111b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundScript": "029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundWithoutReceiverScript": "029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac" + }, + "taproot": { + "tweakedPublicKey": "866c65e77e6eeeb8177dfcdb68c9db38c52c35479c876f2722c1645069b1fb93", + "tapTree": "0601c05ca9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c066200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c049020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c03ea9144d487dd3753a89bc9fe98401d1196523058251fc87690111b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c049029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c027029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac", + "internalKey": "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + "decodedScripts": { + "claimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundScript": "0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundWithoutReceiverScript": "0x0901 CHECKLOCKTIMEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "unilateralClaimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 0x11 CHECKSEQUENCEVERIFY DROP 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundScript": "0x9000 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundWithoutReceiverScript": "0x9000 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIG" + } + }, + { + "description": "CSV locktime <= 16", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "blocks", + "value": 16 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "expected": "tark1qz4d2t2czchfaml2l3ad3gwde2qxpd0srhc7wkpnvtg99cnxyz8c3vyn9exe9gjwcjp5ez0wfhhawvvg0xfenzztjmgp3ddrvkwhw04eztqjn6", + "scripts": { + "claimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundScript": "200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundWithoutReceiverScript": "020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "unilateralClaimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc876960b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundScript": "029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundWithoutReceiverScript": "029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac" + }, + "taproot": { + "tweakedPublicKey": "b0932e4d92a24ec4834c89ee4defd73188799399884b96d018b5a3659d773eb9", + "tapTree": "0601c05ca9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c066200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c049020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c03da9144d487dd3753a89bc9fe98401d1196523058251fc876960b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c049029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c027029000b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac", + "internalKey": "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + "decodedScripts": { + "claimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundScript": "0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundWithoutReceiverScript": "0x0901 CHECKLOCKTIMEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "unilateralClaimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 16 CHECKSEQUENCEVERIFY DROP 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundScript": "0x9000 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundWithoutReceiverScript": "0x9000 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIG" + } + }, + { + "description": "with seconds CSV timelock", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "seconds", + "value": 512 + }, + "unilateralRefundDelay": { + "type": "seconds", + "value": 1024 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "seconds", + "value": 1536 + }, + "expected": "tark1qz4d2t2czchfaml2l3ad3gwde2qxpd0srhc7wkpnvtg99cnxyz8c3f354ncawvx3enha2ydyrmactc6fyuvqppsqpl5k63hzupmrl7ndmz8pnu", + "scripts": { + "claimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundScript": "200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "refundWithoutReceiverScript": "020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac", + "unilateralClaimScript": "a9144d487dd3753a89bc9fe98401d1196523058251fc876903010040b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundScript": "03020040b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac", + "unilateralRefundWithoutReceiverScript": "03030040b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac" + }, + "taproot": { + "tweakedPublicKey": "a634acf1d730d1ccefd511a41efb85e34927180086000fe96d46e2e0763ffa6d", + "tapTree": "0601c05ca9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c066200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c049020901b175200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac01c040a9144d487dd3753a89bc9fe98401d1196523058251fc876903010040b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c04a03020040b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ad201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac01c02803030040b275200192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4ac", + "internalKey": "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + "decodedScripts": { + "claimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundScript": "0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "refundWithoutReceiverScript": "0x0901 CHECKLOCKTIMEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0xaad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88 CHECKSIG", + "unilateralClaimScript": "HASH160 0x4d487dd3753a89bc9fe98401d1196523058251fc EQUAL VERIFY 0x010040 CHECKSEQUENCEVERIFY DROP 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundScript": "0x020040 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIGVERIFY 0x1e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b CHECKSIG", + "unilateralRefundWithoutReceiverScript": "0x030040 CHECKSEQUENCEVERIFY DROP 0x0192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4 CHECKSIG" + } + } + ], + "invalid": [ + { + "description": "Invalid preimageHash length (too short)", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "blocks", + "value": 17 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "error": "preimage hash must be 20 bytes" + }, + { + "description": "Invalid preimageHash length (too long)", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc1234567890abcdef", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "blocks", + "value": 17 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "error": "preimage hash must be 20 bytes" + }, + { + "description": "Zero timelock value", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "blocks", + "value": 0 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "error": "unilateral claim delay must greater than 0" + }, + { + "description": "Invalid refund locktime (zero)", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 0, + "unilateralClaimDelay": { + "type": "blocks", + "value": 17 + }, + "unilateralRefundDelay": { + "type": "blocks", + "value": 144 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "blocks", + "value": 144 + }, + "error": "refund locktime must be greater than 0" + }, + { + "description": "Invalid seconds timelock (not multiple of 512)", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "seconds", + "value": 1000 + }, + "unilateralRefundDelay": { + "type": "seconds", + "value": 1024 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "seconds", + "value": 1536 + }, + "error": "seconds timelock must be multiple of 512" + }, + { + "description": "Invalid seconds timelock (less than 512)", + "preimageHash": "4d487dd3753a89bc9fe98401d1196523058251fc", + "receiver": "021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b", + "sender": "030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4", + "server": "03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88", + "refundLocktime": 265, + "unilateralClaimDelay": { + "type": "seconds", + "value": 512 + }, + "unilateralRefundDelay": { + "type": "seconds", + "value": 511 + }, + "unilateralRefundWithoutReceiverDelay": { + "type": "seconds", + "value": 1536 + }, + "error": "seconds timelock must be greater or equal to 512" + } + ] +} diff --git a/ark-lightning/tests/vhtlc_fixtures.rs b/ark-lightning/tests/vhtlc_fixtures.rs new file mode 100644 index 00000000..831a8224 --- /dev/null +++ b/ark-lightning/tests/vhtlc_fixtures.rs @@ -0,0 +1,387 @@ +use ark_lightning::vhtlc::VhtlcOptions; +use ark_lightning::vhtlc::VhtlcScript; +use bitcoin::Network; +use bitcoin::PublicKey; +use bitcoin::Sequence; +use bitcoin::XOnlyPublicKey; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use std::fs; +use std::str::FromStr; + +#[derive(Debug, Deserialize, Serialize)] +struct Fixtures { + valid: Vec, + invalid: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ValidTestCase { + description: String, + #[serde(rename = "preimageHash")] + preimage_hash: String, + receiver: String, + sender: String, + server: String, + #[serde(rename = "refundLocktime")] + refund_locktime: u32, + #[serde(rename = "unilateralClaimDelay")] + unilateral_claim_delay: Delay, + #[serde(rename = "unilateralRefundDelay")] + unilateral_refund_delay: Delay, + #[serde(rename = "unilateralRefundWithoutReceiverDelay")] + unilateral_refund_without_receiver_delay: Delay, + expected: String, + scripts: ScriptHexes, + taproot: TaprootInfo, + #[serde(rename = "decodedScripts")] + decoded_scripts: HashMap, +} + +#[derive(Debug, Deserialize, Serialize)] +struct InvalidTestCase { + description: String, + #[serde(rename = "preimageHash")] + preimage_hash: String, + receiver: String, + sender: String, + server: String, + #[serde(rename = "refundLocktime")] + refund_locktime: u32, + #[serde(rename = "unilateralClaimDelay")] + unilateral_claim_delay: Delay, + #[serde(rename = "unilateralRefundDelay")] + unilateral_refund_delay: Delay, + #[serde(rename = "unilateralRefundWithoutReceiverDelay")] + unilateral_refund_without_receiver_delay: Delay, + error: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ScriptHexes { + #[serde(rename = "claimScript")] + claim_script: String, + #[serde(rename = "refundScript")] + refund_script: String, + #[serde(rename = "refundWithoutReceiverScript")] + refund_without_receiver_script: String, + #[serde(rename = "unilateralClaimScript")] + unilateral_claim_script: String, + #[serde(rename = "unilateralRefundScript")] + unilateral_refund_script: String, + #[serde(rename = "unilateralRefundWithoutReceiverScript")] + unilateral_refund_without_receiver_script: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct TaprootInfo { + #[serde(rename = "tweakedPublicKey")] + tweaked_public_key: String, + #[serde(rename = "tapTree")] + tap_tree: String, + #[serde(rename = "internalKey")] + internal_key: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Delay { + #[serde(rename = "type")] + delay_type: String, + value: u32, +} + +impl Delay { + fn to_sequence(&self) -> Result { + match self.delay_type.as_str() { + "blocks" => { + if self.value == 0 { + return Err("unilateral claim delay must greater than 0".to_string()); + } + Ok(Sequence::from_height(self.value as u16)) + } + "seconds" => { + if self.value < 512 { + return Err("seconds timelock must be greater or equal to 512".to_string()); + } + if self.value % 512 != 0 { + return Err("seconds timelock must be multiple of 512".to_string()); + } + Sequence::from_seconds_ceil(self.value) + .map_err(|e| format!("Invalid seconds value: {}", e)) + } + _ => Err(format!("Unknown delay type: {}", self.delay_type)), + } + } +} + +fn hex_to_bytes20(hex: &str) -> Result<[u8; 20], String> { + let bytes = hex::decode(hex).map_err(|e| format!("Invalid hex: {}", e))?; + if bytes.len() != 20 { + return Err(format!("preimage hash must be 20 bytes")); + } + let mut arr = [0u8; 20]; + arr.copy_from_slice(&bytes); + Ok(arr) +} + +fn pubkey_to_xonly(pubkey_hex: &str) -> XOnlyPublicKey { + let pubkey = PublicKey::from_str(pubkey_hex).expect("Invalid public key"); + XOnlyPublicKey::from(pubkey.inner) +} + +#[test] +fn test_vhtlc_with_valid_fixtures() { + let fixtures_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/vhtlc.json"); + let fixtures_json = fs::read_to_string(fixtures_path).expect("Failed to read fixtures file"); + let fixtures: Fixtures = + serde_json::from_str(&fixtures_json).expect("Failed to parse fixtures"); + + for test_case in fixtures.valid { + let preimage_hash = hex_to_bytes20(&test_case.preimage_hash) + .expect("Valid fixtures should have valid preimage hash"); + + let sender = pubkey_to_xonly(&test_case.sender); + let receiver = pubkey_to_xonly(&test_case.receiver); + let server = pubkey_to_xonly(&test_case.server); + + let options = VhtlcOptions { + sender, + receiver, + server, + preimage_hash, + refund_locktime: test_case.refund_locktime, + unilateral_claim_delay: test_case + .unilateral_claim_delay + .to_sequence() + .expect("Valid delay"), + unilateral_refund_delay: test_case + .unilateral_refund_delay + .to_sequence() + .expect("Valid delay"), + unilateral_refund_without_receiver_delay: test_case + .unilateral_refund_without_receiver_delay + .to_sequence() + .expect("Valid delay"), + }; + + let vhtlc = VhtlcScript::new(options).expect("Failed to create VHTLC"); + + // Test 1: Verify all script hex encodings + let claim_hex = hex::encode(vhtlc.claim_script().as_bytes()); + assert_eq!( + claim_hex, test_case.scripts.claim_script, + "Claim script hex mismatch for test case: {}", + test_case.description + ); + + let refund_hex = hex::encode(vhtlc.refund_script().as_bytes()); + assert_eq!( + refund_hex, test_case.scripts.refund_script, + "Refund script hex mismatch for test case: {}", + test_case.description + ); + + let refund_without_receiver_hex = + hex::encode(vhtlc.refund_without_receiver_script().as_bytes()); + assert_eq!( + refund_without_receiver_hex, test_case.scripts.refund_without_receiver_script, + "Refund without receiver script hex mismatch for test case: {}", + test_case.description + ); + + let unilateral_claim_hex = hex::encode(vhtlc.unilateral_claim_script().as_bytes()); + assert_eq!( + unilateral_claim_hex, test_case.scripts.unilateral_claim_script, + "Unilateral claim script hex mismatch for test case: {}", + test_case.description + ); + + let unilateral_refund_hex = hex::encode(vhtlc.unilateral_refund_script().as_bytes()); + assert_eq!( + unilateral_refund_hex, test_case.scripts.unilateral_refund_script, + "Unilateral refund script hex mismatch for test case: {}", + test_case.description + ); + + let unilateral_refund_without_receiver_hex = + hex::encode(vhtlc.unilateral_refund_without_receiver_script().as_bytes()); + + assert_eq!( + unilateral_refund_without_receiver_hex, test_case.scripts.unilateral_refund_without_receiver_script, + "Unilateral refund without receiver script hex mismatch for test case: {}. Our impl includes CLTV locktime, fixture expects only CSV", + test_case.description + ); + + // Test 2: Verify taproot information + let taproot_info = vhtlc.taproot_info().expect(&format!( + "Taproot info should be available for test case: {}", + test_case.description + )); + + let internal_key = taproot_info.internal_key(); + let internal_key_hex = hex::encode(internal_key.serialize()); + + // The internal key in fixtures is prefixed with version byte + let pubkey = PublicKey::from_str(&test_case.taproot.internal_key) + .expect("Invalid internal key in fixture"); + let expected_internal = hex::encode(XOnlyPublicKey::from(pubkey.inner).serialize()); + + assert_eq!( + internal_key_hex, expected_internal, + "Internal key mismatch for test case: {}", + test_case.description + ); + + let output_key = taproot_info.output_key(); + let output_key_hex = hex::encode(output_key.serialize()); + + assert_eq!( + output_key_hex, test_case.taproot.tweaked_public_key, + "Tweaked public key mismatch for test case: {}", + test_case.description + ); + + // Test 3: Verify address generation + let addr = vhtlc.address(Network::Testnet, server).expect(&format!( + "Failed to generate address for test case: {}", + test_case.description + )); + let address_str = addr.encode(); + + assert_eq!( + address_str, test_case.expected, + "Address mismatch for test case: {}", + test_case.description + ); + } +} + +#[test] +fn test_vhtlc_with_invalid_fixtures() { + let fixtures_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/vhtlc.json"); + let fixtures_json = fs::read_to_string(fixtures_path).expect("Failed to read fixtures file"); + let fixtures: Fixtures = + serde_json::from_str(&fixtures_json).expect("Failed to parse fixtures"); + + for test_case in fixtures.invalid { + // Try to parse preimage hash + let preimage_hash_result = hex_to_bytes20(&test_case.preimage_hash); + + if let Err(e) = preimage_hash_result { + assert!( + e.contains(&test_case.error), + "Expected error containing '{}', got '{}' for test case: {}", + test_case.error, + e, + test_case.description + ); + continue; + } + + // Check refund locktime + if test_case.refund_locktime == 0 { + assert!( + test_case + .error + .contains("refund locktime must be greater than 0"), + "Expected refund locktime error for test case: {}", + test_case.description + ); + continue; + } + + // Try to convert delays + let claim_delay_result = test_case.unilateral_claim_delay.to_sequence(); + if let Err(e) = claim_delay_result { + assert!( + e.contains(&test_case.error), + "Expected error containing '{}', got '{}' for claim delay in test case: {}", + test_case.error, + e, + test_case.description + ); + continue; + } + + let refund_delay_result = test_case.unilateral_refund_delay.to_sequence(); + if let Err(e) = refund_delay_result { + assert!( + e.contains(&test_case.error), + "Expected error containing '{}', got '{}' for refund delay in test case: {}", + test_case.error, + e, + test_case.description + ); + continue; + } + + let refund_without_receiver_delay_result = test_case + .unilateral_refund_without_receiver_delay + .to_sequence(); + if let Err(e) = refund_without_receiver_delay_result { + assert!( + e.contains(&test_case.error), + "Expected error containing '{}', got '{}' for refund without receiver delay in test case: {}", + test_case.error, e, test_case.description + ); + continue; + } + + // If we got here, all validations passed but they shouldn't have + panic!( + "Invalid test case '{}' didn't fail as expected", + test_case.description + ); + } +} + +#[test] +fn test_specific_script_encodings() { + // Test specific script encoding for the first valid case + let sender = + pubkey_to_xonly("030192e796452d6df9697c280542e1560557bcf79a347d925895043136225c7cb4"); + let receiver = + pubkey_to_xonly("021e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53b"); + let server = + pubkey_to_xonly("03aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88"); + let preimage_hash = hex_to_bytes20("4d487dd3753a89bc9fe98401d1196523058251fc").unwrap(); + + let options = VhtlcOptions { + sender, + receiver, + server, + preimage_hash, + refund_locktime: 265, + unilateral_claim_delay: Sequence::from_height(17), + unilateral_refund_delay: Sequence::from_height(144), + unilateral_refund_without_receiver_delay: Sequence::from_height(144), + }; + + let vhtlc = VhtlcScript::new(options).expect("Failed to create VHTLC"); + + // Verify claim script + let claim_script = vhtlc.claim_script(); + let claim_hex = hex::encode(claim_script.as_bytes()); + let expected_claim = "a9144d487dd3753a89bc9fe98401d1196523058251fc8769201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bad20aad52d58162e9eefeafc7ad8a1cdca8060b5f01df1e7583362d052e266208f88ac"; + assert_eq!( + claim_hex, expected_claim, + "Claim script should match fixture" + ); + + // Verify unilateral claim script (with CSV=17) + let unilateral_claim = vhtlc.unilateral_claim_script(); + let unilateral_claim_hex = hex::encode(unilateral_claim.as_bytes()); + + // Check the CSV encoding for value 17 + assert!( + unilateral_claim_hex.contains("0111"), + "Should contain CSV value 17 as 0x0111" + ); + + let expected_unilateral_claim = "a9144d487dd3753a89bc9fe98401d1196523058251fc87690111b275201e1bb85455fe3f5aed60d101aa4dbdb9e7714f6226769a97a17a5331dadcd53bac"; + assert_eq!( + unilateral_claim_hex, expected_unilateral_claim, + "Unilateral claim script should match fixture" + ); +}