bsv/script/templates/
p2pkh.rs1use crate::primitives::ecdsa::ecdsa_sign;
8use crate::primitives::hash::hash256;
9use crate::primitives::private_key::PrivateKey;
10use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
11use crate::primitives::utils::base58_check_decode;
12use crate::script::error::ScriptError;
13use crate::script::locking_script::LockingScript;
14use crate::script::op::Op;
15use crate::script::script::Script;
16use crate::script::script_chunk::ScriptChunk;
17use crate::script::templates::{ScriptTemplateLock, ScriptTemplateUnlock};
18use crate::script::unlocking_script::UnlockingScript;
19
20#[derive(Clone, Debug)]
26pub struct P2PKH {
27 pub public_key_hash: Option<[u8; 20]>,
29 pub private_key: Option<PrivateKey>,
31 pub sighash_type: u32,
33}
34
35impl P2PKH {
36 pub fn from_public_key_hash(hash: [u8; 20]) -> Self {
38 P2PKH {
39 public_key_hash: Some(hash),
40 private_key: None,
41 sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
42 }
43 }
44
45 pub fn from_address(address: &str) -> Result<Self, ScriptError> {
50 let (_prefix, payload) = base58_check_decode(address, 1)
51 .map_err(|e| ScriptError::InvalidAddress(format!("invalid address: {}", e)))?;
52
53 if payload.len() != 20 {
54 return Err(ScriptError::InvalidAddress(format!(
55 "address payload should be 20 bytes, got {}",
56 payload.len()
57 )));
58 }
59
60 let mut hash = [0u8; 20];
61 hash.copy_from_slice(&payload);
62 Ok(Self::from_public_key_hash(hash))
63 }
64
65 pub fn from_private_key(key: PrivateKey) -> Self {
69 let pubkey = key.to_public_key();
70 let hash_vec = pubkey.to_hash();
71 let mut hash = [0u8; 20];
72 hash.copy_from_slice(&hash_vec);
73
74 P2PKH {
75 public_key_hash: Some(hash),
76 private_key: Some(key),
77 sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
78 }
79 }
80
81 pub fn unlock(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
86 let key = self
87 .private_key
88 .as_ref()
89 .ok_or_else(|| ScriptError::InvalidScript("P2PKH: no private key for unlock".into()))?;
90
91 let msg_hash = hash256(preimage);
94
95 let sig = ecdsa_sign(&msg_hash, key.bn(), true)
97 .map_err(|e| ScriptError::InvalidSignature(format!("ECDSA sign failed: {}", e)))?;
98
99 let mut sig_bytes = sig.to_der();
101 sig_bytes.push(self.sighash_type as u8);
102
103 let pubkey = key.to_public_key();
105 let pubkey_bytes = pubkey.to_der();
106
107 let chunks = vec![
109 ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes)),
110 ScriptChunk::new_raw(pubkey_bytes.len() as u8, Some(pubkey_bytes)),
111 ];
112
113 Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
114 }
115
116 pub fn estimate_unlock_length(&self) -> usize {
122 108
124 }
125}
126
127impl ScriptTemplateLock for P2PKH {
128 fn lock(&self) -> Result<LockingScript, ScriptError> {
133 let hash = self.public_key_hash.ok_or_else(|| {
134 ScriptError::InvalidScript("P2PKH: no public key hash for lock".into())
135 })?;
136
137 let chunks = vec![
138 ScriptChunk::new_opcode(Op::OpDup),
139 ScriptChunk::new_opcode(Op::OpHash160),
140 ScriptChunk::new_raw(20, Some(hash.to_vec())),
141 ScriptChunk::new_opcode(Op::OpEqualVerify),
142 ScriptChunk::new_opcode(Op::OpCheckSig),
143 ];
144
145 Ok(LockingScript::from_script(Script::from_chunks(chunks)))
146 }
147}
148
149impl ScriptTemplateUnlock for P2PKH {
150 fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
151 self.unlock(preimage)
152 }
153
154 fn estimate_length(&self) -> Result<usize, ScriptError> {
155 Ok(self.estimate_unlock_length())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 fn bytes_to_hex(bytes: &[u8]) -> String {
163 bytes.iter().map(|b| format!("{:02x}", b)).collect()
164 }
165
166 #[test]
171 fn test_p2pkh_lock_correct_script() {
172 let hash = [0xab; 20];
173 let p2pkh = P2PKH::from_public_key_hash(hash);
174 let lock_script = p2pkh.lock().unwrap();
175
176 let binary = lock_script.to_binary();
177 assert_eq!(binary.len(), 25, "P2PKH locking script should be 25 bytes");
178
179 assert_eq!(binary[0], 0x76, "should start with OP_DUP");
181 assert_eq!(binary[1], 0xa9, "second byte should be OP_HASH160");
182 assert_eq!(binary[2], 0x14, "third byte should be push-20");
183 assert_eq!(&binary[3..23], &hash, "hash should be embedded");
184 assert_eq!(binary[23], 0x88, "should have OP_EQUALVERIFY");
185 assert_eq!(binary[24], 0xac, "should end with OP_CHECKSIG");
186 }
187
188 #[test]
193 fn test_p2pkh_lock_known_hex() {
194 let hash = [0xab; 20];
195 let p2pkh = P2PKH::from_public_key_hash(hash);
196 let lock_script = p2pkh.lock().unwrap();
197 let hex = lock_script.to_hex();
198 assert_eq!(hex, "76a914abababababababababababababababababababab88ac");
199 }
200
201 #[test]
206 fn test_p2pkh_lock_from_private_key() {
207 let key = PrivateKey::from_hex("1").unwrap();
208 let p2pkh = P2PKH::from_private_key(key.clone());
209
210 let lock_script = p2pkh.lock().unwrap();
211 let binary = lock_script.to_binary();
212 assert_eq!(binary.len(), 25);
213
214 let pubkey = key.to_public_key();
216 let expected_hash = pubkey.to_hash();
217 assert_eq!(&binary[3..23], expected_hash.as_slice());
218 }
219
220 #[test]
225 fn test_p2pkh_unlock_produces_two_chunks() {
226 let key = PrivateKey::from_hex("1").unwrap();
227 let p2pkh = P2PKH::from_private_key(key);
228
229 let preimage = b"test sighash preimage";
230 let unlock_script = p2pkh.unlock(preimage).unwrap();
231
232 assert_eq!(
233 unlock_script.chunks().len(),
234 2,
235 "P2PKH unlock should have 2 chunks (sig + pubkey)"
236 );
237
238 let sig_chunk = &unlock_script.chunks()[0];
240 assert!(
241 sig_chunk.data.is_some(),
242 "first chunk should have data (signature)"
243 );
244 let sig_data = sig_chunk.data.as_ref().unwrap();
245 assert!(
247 sig_data.len() >= 70 && sig_data.len() <= 74,
248 "signature length should be 70-74, got {}",
249 sig_data.len()
250 );
251 assert_eq!(
253 *sig_data.last().unwrap(),
254 (SIGHASH_ALL | SIGHASH_FORKID) as u8,
255 "last byte should be sighash type"
256 );
257
258 let pubkey_chunk = &unlock_script.chunks()[1];
260 assert!(pubkey_chunk.data.is_some());
261 let pubkey_data = pubkey_chunk.data.as_ref().unwrap();
262 assert_eq!(
263 pubkey_data.len(),
264 33,
265 "pubkey should be 33 bytes compressed"
266 );
267 assert!(
268 pubkey_data[0] == 0x02 || pubkey_data[0] == 0x03,
269 "pubkey should start with 0x02 or 0x03"
270 );
271 }
272
273 #[test]
278 fn test_p2pkh_estimate_unlock_length() {
279 let hash = [0; 20];
280 let p2pkh = P2PKH::from_public_key_hash(hash);
281 let estimate = p2pkh.estimate_unlock_length();
282 assert!(
284 estimate >= 100 && estimate <= 120,
285 "estimate should be ~107-108, got {}",
286 estimate
287 );
288 }
289
290 #[test]
295 fn test_p2pkh_from_address() {
296 let address = "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH";
298 let p2pkh = P2PKH::from_address(address).unwrap();
299 let lock_script = p2pkh.lock().unwrap();
300
301 let key = PrivateKey::from_hex("1").unwrap();
303 let pubkey = key.to_public_key();
304 let expected_hash = pubkey.to_hash();
305
306 let binary = lock_script.to_binary();
307 assert_eq!(&binary[3..23], expected_hash.as_slice());
308 }
309
310 #[test]
315 fn test_p2pkh_lock_error_no_hash() {
316 let p2pkh = P2PKH {
317 public_key_hash: None,
318 private_key: None,
319 sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
320 };
321 assert!(p2pkh.lock().is_err());
322 }
323
324 #[test]
329 fn test_p2pkh_unlock_error_no_key() {
330 let hash = [0; 20];
331 let p2pkh = P2PKH::from_public_key_hash(hash);
332 assert!(p2pkh.unlock(b"test").is_err());
333 }
334
335 #[test]
340 fn test_p2pkh_trait_sign() {
341 let key = PrivateKey::from_hex("ff").unwrap();
342 let p2pkh = P2PKH::from_private_key(key);
343
344 let unlock_script = p2pkh.sign(b"sighash data").unwrap();
346 assert_eq!(unlock_script.chunks().len(), 2);
347 }
348
349 #[test]
350 fn test_p2pkh_trait_estimate_length() {
351 let key = PrivateKey::from_hex("1").unwrap();
352 let p2pkh = P2PKH::from_private_key(key);
353 let len = p2pkh.estimate_length().unwrap();
354 assert!(len >= 100 && len <= 120);
355 }
356
357 #[test]
362 fn test_p2pkh_lock_asm() {
363 let key = PrivateKey::from_hex("1").unwrap();
364 let pubkey = key.to_public_key();
365 let hash = pubkey.to_hash();
366 let hash_hex = bytes_to_hex(&hash);
367
368 let p2pkh = P2PKH::from_private_key(key);
369 let lock_script = p2pkh.lock().unwrap();
370 let asm = lock_script.to_asm();
371
372 let expected_asm = format!("OP_DUP OP_HASH160 {} OP_EQUALVERIFY OP_CHECKSIG", hash_hex);
373 assert_eq!(asm, expected_asm);
374 }
375}