Skip to main content

bsv/script/templates/
p2pkh.rs

1//! P2PKH (Pay-to-Public-Key-Hash) script template.
2//!
3//! The most common Bitcoin transaction type. Locks funds to a public key hash
4//! and unlocks with a signature and the corresponding public key.
5//! Translates the TS SDK P2PKH.ts.
6
7use 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/// P2PKH script template for creating standard pay-to-public-key-hash scripts.
21///
22/// Can be configured for locking (with a public key hash) or unlocking
23/// (with a private key). The struct stores both fields; which one is
24/// used depends on whether lock() or sign() is called.
25#[derive(Clone, Debug)]
26pub struct P2PKH {
27    /// The 20-byte public key hash for locking.
28    pub public_key_hash: Option<[u8; 20]>,
29    /// The private key for unlocking (signing).
30    pub private_key: Option<PrivateKey>,
31    /// Sighash scope for signing (default: SIGHASH_ALL | SIGHASH_FORKID).
32    pub sighash_type: u32,
33}
34
35impl P2PKH {
36    /// Create a P2PKH template configured for locking with a known public key hash.
37    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    /// Create a P2PKH template from a Base58Check-encoded Bitcoin address.
46    ///
47    /// Decodes the address, extracts the 20-byte public key hash,
48    /// and configures the template for locking.
49    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    /// Create a P2PKH template configured for unlocking with a private key.
66    ///
67    /// Also derives the public key hash for locking capability.
68    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    /// Create an unlocking script from a sighash preimage.
82    ///
83    /// Signs the SHA-256 hash of the preimage with the private key and
84    /// produces: `<signature_DER + sighash_byte> <compressed_pubkey>`
85    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        // Double-hash the preimage (hash256 = sha256(sha256(x))) to match
92        // what OP_CHECKSIG uses for verification.
93        let msg_hash = hash256(preimage);
94
95        // Sign the 32-byte hash directly
96        let sig = ecdsa_sign(&msg_hash, key.bn(), true)
97            .map_err(|e| ScriptError::InvalidSignature(format!("ECDSA sign failed: {}", e)))?;
98
99        // Build checksig format: DER + sighash byte
100        let mut sig_bytes = sig.to_der();
101        sig_bytes.push(self.sighash_type as u8);
102
103        // Get compressed public key
104        let pubkey = key.to_public_key();
105        let pubkey_bytes = pubkey.to_der();
106
107        // Build unlocking script: <sig> <pubkey>
108        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    /// Estimate the byte length of the unlocking script.
117    ///
118    /// Typical P2PKH unlock is approximately 108 bytes:
119    /// 1 byte push opcode + 73 bytes max DER sig with sighash byte,
120    /// plus 1 byte push opcode + 33 bytes compressed pubkey.
121    pub fn estimate_unlock_length(&self) -> usize {
122        // 1 (push) + 73 (max DER sig + sighash) + 1 (push) + 33 (compressed pubkey)
123        108
124    }
125}
126
127impl ScriptTemplateLock for P2PKH {
128    /// Create a P2PKH locking script.
129    ///
130    /// Produces: OP_DUP OP_HASH160 <20-byte pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG
131    /// Total: 25 bytes when serialized.
132    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    // -----------------------------------------------------------------------
167    // P2PKH lock: produces correct 25-byte script
168    // -----------------------------------------------------------------------
169
170    #[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        // OP_DUP(0x76) OP_HASH160(0xa9) PUSH20(0x14) <20 bytes hash> OP_EQUALVERIFY(0x88) OP_CHECKSIG(0xac)
180        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    // -----------------------------------------------------------------------
189    // P2PKH lock: hex matches known P2PKH locking script
190    // -----------------------------------------------------------------------
191
192    #[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    // -----------------------------------------------------------------------
202    // P2PKH lock: from real private key
203    // -----------------------------------------------------------------------
204
205    #[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        // Verify the hash matches the key's public key hash
215        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    // -----------------------------------------------------------------------
221    // P2PKH unlock: produces 2-chunk script (sig + pubkey)
222    // -----------------------------------------------------------------------
223
224    #[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        // First chunk: signature (DER + sighash byte)
239        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        // DER signature is typically 70-73 bytes + 1 sighash byte
246        assert!(
247            sig_data.len() >= 70 && sig_data.len() <= 74,
248            "signature length should be 70-74, got {}",
249            sig_data.len()
250        );
251        // Last byte should be sighash type
252        assert_eq!(
253            *sig_data.last().unwrap(),
254            (SIGHASH_ALL | SIGHASH_FORKID) as u8,
255            "last byte should be sighash type"
256        );
257
258        // Second chunk: compressed public key (33 bytes)
259        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    // -----------------------------------------------------------------------
274    // P2PKH estimate_unlock_length
275    // -----------------------------------------------------------------------
276
277    #[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        // Should be approximately 107-108
283        assert!(
284            estimate >= 100 && estimate <= 120,
285            "estimate should be ~107-108, got {}",
286            estimate
287        );
288    }
289
290    // -----------------------------------------------------------------------
291    // P2PKH from_address: correctly extracts hash and creates lock
292    // -----------------------------------------------------------------------
293
294    #[test]
295    fn test_p2pkh_from_address() {
296        // Known address for key=1: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
297        let address = "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH";
298        let p2pkh = P2PKH::from_address(address).unwrap();
299        let lock_script = p2pkh.lock().unwrap();
300
301        // The hash should match key=1's pubkey hash
302        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    // -----------------------------------------------------------------------
311    // P2PKH lock error: no hash
312    // -----------------------------------------------------------------------
313
314    #[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    // -----------------------------------------------------------------------
325    // P2PKH unlock error: no key
326    // -----------------------------------------------------------------------
327
328    #[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    // -----------------------------------------------------------------------
336    // P2PKH: ScriptTemplateUnlock trait
337    // -----------------------------------------------------------------------
338
339    #[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        // Use trait method
345        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    // -----------------------------------------------------------------------
358    // P2PKH: ASM format verification
359    // -----------------------------------------------------------------------
360
361    #[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}