zcash_htlc_builder/
script.rs

1use 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/// Build P2SH HTLC script according to ZIP-300
10///
11/// Script format:
12/// OP_IF
13///     OP_SHA256 <hash_lock> OP_EQUALVERIFY
14///     <recipient_pubkey> OP_CHECKSIG
15/// OP_ELSE
16///     <timelock> OP_CHECKLOCKTIMEVERIFY OP_DROP
17///     <refund_pubkey> OP_CHECKSIG
18/// OP_ENDIF
19#[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(&params.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(&params.recipient_pubkey).map_err(|_| HTLCScriptError::InvalidPublicKey)?;
39
40        let refund_pubkey =
41            hex::decode(&params.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(&params).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}