zcash_htlc_builder/
script.rs1use bitcoin::blockdata::opcodes::{self, OP_FALSE, OP_TRUE};
2use bitcoin::blockdata::script::{Builder, Script};
3use bitcoin::hashes::{hash160, Hash};
4use ripemd::Digest;
5use sha2::Sha256;
6
7use crate::{HTLCParams, ZcashNetwork};
8
9#[derive(Clone)]
20pub struct HTLCScriptBuilder {
21 network: ZcashNetwork,
22}
23
24impl HTLCScriptBuilder {
25 pub fn new(network: ZcashNetwork) -> Self {
26 Self { network }
27 }
28
29 pub fn build_htlc_script(&self, params: &HTLCParams) -> Result<Script, HTLCScriptError> {
30 let hash_lock_bytes =
31 hex::decode(¶ms.hash_lock).map_err(|_| HTLCScriptError::InvalidHashLock)?;
32
33 if hash_lock_bytes.len() != 32 {
34 return Err(HTLCScriptError::InvalidHashLockLength);
35 }
36
37 let recipient_pubkey =
38 hex::decode(¶ms.recipient_pubkey).map_err(|_| HTLCScriptError::InvalidPublicKey)?;
39
40 let refund_pubkey =
41 hex::decode(¶ms.refund_pubkey).map_err(|_| HTLCScriptError::InvalidPublicKey)?;
42
43 let script = Builder::new()
44 .push_opcode(opcodes::all::OP_IF)
45 .push_opcode(opcodes::all::OP_SHA256)
46 .push_slice(&hash_lock_bytes)
47 .push_opcode(opcodes::all::OP_EQUALVERIFY)
48 .push_slice(&recipient_pubkey)
49 .push_opcode(opcodes::all::OP_CHECKSIG)
50 .push_opcode(opcodes::all::OP_ELSE)
51 .push_int(params.timelock as i64)
52 .push_opcode(opcodes::all::OP_CLTV)
53 .push_opcode(opcodes::all::OP_DROP)
54 .push_slice(&refund_pubkey)
55 .push_opcode(opcodes::all::OP_CHECKSIG)
56 .push_opcode(opcodes::all::OP_ENDIF)
57 .into_script();
58
59 Ok(script)
60 }
61
62 pub fn script_to_p2sh_address(&self, script: &Script) -> Result<String, HTLCScriptError> {
63 let script_hash = hash160::Hash::hash(script.as_bytes());
64 let prefix = self.network.p2sh_prefix();
65
66 let mut address_bytes = Vec::new();
67 address_bytes.extend_from_slice(&prefix);
68 address_bytes.extend_from_slice(script_hash.as_ref());
69
70 let checksum = self.double_sha256_checksum(&address_bytes);
71 address_bytes.extend_from_slice(&checksum[..4]);
72
73 Ok(bs58::encode(address_bytes).into_string())
74 }
75
76 pub fn build_redeem_input(
77 &self,
78 secret: &str,
79 signature: &[u8],
80 ) -> Result<Script, HTLCScriptError> {
81 let secret_bytes = hex::decode(secret).map_err(|_| HTLCScriptError::InvalidSecret)?;
82
83 let script = Builder::new()
84 .push_slice(signature)
85 .push_slice(&secret_bytes)
86 .push_opcode(OP_TRUE)
87 .into_script();
88
89 Ok(script)
90 }
91
92 pub fn build_refund_input(&self, signature: &[u8]) -> Script {
93 Builder::new()
94 .push_slice(signature)
95 .push_opcode(OP_FALSE)
96 .into_script()
97 }
98
99 pub fn verify_secret(&self, secret: &str, hash_lock: &str) -> bool {
100 let secret_bytes = match hex::decode(secret) {
101 Ok(bytes) => bytes,
102 Err(_) => return false,
103 };
104
105 let mut hasher = Sha256::new();
106 hasher.update(&secret_bytes);
107 let computed_hash = hex::encode(hasher.finalize());
108
109 computed_hash == hash_lock
110 }
111
112 fn double_sha256_checksum(&self, data: &[u8]) -> Vec<u8> {
113 let mut hasher = Sha256::new();
114 hasher.update(data);
115 let first_hash = hasher.finalize();
116
117 let mut hasher = Sha256::new();
118 hasher.update(first_hash);
119 hasher.finalize().to_vec()
120 }
121
122 pub fn p2sh_script_pubkey(&self, script: &Script) -> Script {
123 let script_hash = hash160::Hash::hash(script.as_bytes());
124
125 Builder::new()
126 .push_opcode(opcodes::all::OP_HASH160)
127 .push_slice(script_hash.as_ref())
128 .push_opcode(opcodes::all::OP_EQUAL)
129 .into_script()
130 }
131}
132
133#[derive(Debug, thiserror::Error)]
134pub enum HTLCScriptError {
135 #[error("Invalid hash lock format")]
136 InvalidHashLock,
137
138 #[error("Invalid hash lock length (expected 32 bytes)")]
139 InvalidHashLockLength,
140
141 #[error("Invalid public key format")]
142 InvalidPublicKey,
143
144 #[error("Invalid secret format")]
145 InvalidSecret,
146
147 #[error("Script building failed: {0}")]
148 BuildError(String),
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_build_htlc_script() {
157 let builder = HTLCScriptBuilder::new(ZcashNetwork::Testnet);
158
159 let params = HTLCParams {
160 recipient_pubkey: format!("02{}", "a".repeat(64)),
161 refund_pubkey: format!("03{}", "b".repeat(64)),
162 hash_lock: "a".repeat(64),
163 timelock: 100,
164 amount: "1.0".to_string(),
165 };
166
167 let script = builder.build_htlc_script(¶ms).unwrap();
168 assert!(!script.as_bytes().is_empty());
169 }
170
171 #[test]
172 fn test_verify_secret() {
173 let builder = HTLCScriptBuilder::new(ZcashNetwork::Testnet);
174
175 let secret = "deadbeef";
176 let mut hasher = Sha256::new();
177 hasher.update(hex::decode(secret).unwrap());
178 let hash_lock = hex::encode(hasher.finalize());
179
180 assert!(builder.verify_secret(secret, &hash_lock));
181 assert!(!builder.verify_secret("badbeef", &hash_lock));
182 }
183}