Skip to main content

bsv_rs/script/templates/
p2pkh.rs

1//! P2PKH (Pay-to-Public-Key-Hash) script template.
2//!
3//! This module provides the [`P2PKH`] template for creating and spending
4//! Pay-to-Public-Key-Hash outputs, the most common Bitcoin transaction type.
5//!
6//! # Locking Script Pattern
7//!
8//! ```text
9//! OP_DUP OP_HASH160 <20-byte pubkeyhash> OP_EQUALVERIFY OP_CHECKSIG
10//! ```
11//!
12//! # Unlocking Script Pattern
13//!
14//! ```text
15//! <signature> <publicKey>
16//! ```
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use bsv_rs::script::templates::P2PKH;
22//! use bsv_rs::script::template::{ScriptTemplate, SignOutputs, SigningContext};
23//! use bsv_rs::primitives::ec::PrivateKey;
24//!
25//! let private_key = PrivateKey::random();
26//! let pubkey_hash = private_key.public_key().hash160();
27//!
28//! // Create locking script
29//! let template = P2PKH::new();
30//! let locking = template.lock(&pubkey_hash)?;
31//!
32//! // Create unlock template
33//! let unlock = P2PKH::unlock(&private_key, SignOutputs::All, false);
34//!
35//! // Sign a transaction
36//! let unlocking = unlock.sign(&signing_context)?;
37//! ```
38
39use crate::error::Error;
40use crate::primitives::bsv::TransactionSignature;
41use crate::primitives::ec::PrivateKey;
42use crate::primitives::encoding::from_base58_check;
43use crate::script::op::*;
44use crate::script::template::{
45    compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
46};
47use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
48use crate::Result;
49
50/// P2PKH (Pay-to-Public-Key-Hash) script template.
51///
52/// This is the most common Bitcoin transaction type. The locking script requires
53/// a signature that matches the public key whose hash is embedded in the script.
54///
55/// # Example
56///
57/// ```rust,ignore
58/// use bsv_rs::script::templates::P2PKH;
59/// use bsv_rs::script::template::ScriptTemplate;
60///
61/// let template = P2PKH::new();
62///
63/// // Lock to a pubkey hash
64/// let locking = template.lock(&pubkey_hash)?;
65///
66/// // Or lock to an address string
67/// let locking = P2PKH::lock_from_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2")?;
68/// ```
69#[derive(Debug, Clone, Copy, Default)]
70pub struct P2PKH;
71
72impl P2PKH {
73    /// Creates a new P2PKH template instance.
74    pub fn new() -> Self {
75        Self
76    }
77
78    /// Creates a P2PKH locking script from an address string.
79    ///
80    /// Supports both mainnet (prefix 0x00) and testnet (prefix 0x6f) addresses.
81    ///
82    /// # Arguments
83    ///
84    /// * `address` - A P2PKH address string
85    ///
86    /// # Returns
87    ///
88    /// The locking script, or an error if the address is invalid.
89    ///
90    /// # Example
91    ///
92    /// ```rust,ignore
93    /// use bsv_rs::script::templates::P2PKH;
94    ///
95    /// let locking = P2PKH::lock_from_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2")?;
96    /// ```
97    pub fn lock_from_address(address: &str) -> Result<LockingScript> {
98        let (version, payload) = from_base58_check(address)?;
99
100        // Check version byte: 0x00 (mainnet) or 0x6f (testnet)
101        if version.len() != 1 || (version[0] != 0x00 && version[0] != 0x6f) {
102            return Err(Error::CryptoError(format!(
103                "Invalid P2PKH address version: expected 0x00 or 0x6f, got 0x{:02x}",
104                version.first().unwrap_or(&0)
105            )));
106        }
107
108        if payload.len() != 20 {
109            return Err(Error::InvalidDataLength {
110                expected: 20,
111                actual: payload.len(),
112            });
113        }
114
115        P2PKH::new().lock(&payload)
116    }
117
118    /// Creates an unlock template for spending a P2PKH output.
119    ///
120    /// # Arguments
121    ///
122    /// * `private_key` - The private key for signing
123    /// * `sign_outputs` - Which outputs to sign
124    /// * `anyone_can_pay` - Whether to allow other inputs to be added
125    ///
126    /// # Returns
127    ///
128    /// A [`ScriptTemplateUnlock`] that can sign transaction inputs.
129    ///
130    /// # Example
131    ///
132    /// ```rust,ignore
133    /// use bsv_rs::script::templates::P2PKH;
134    /// use bsv_rs::script::template::{SignOutputs, SigningContext};
135    /// use bsv_rs::primitives::ec::PrivateKey;
136    ///
137    /// let private_key = PrivateKey::random();
138    /// let unlock = P2PKH::unlock(&private_key, SignOutputs::All, false);
139    ///
140    /// // Create signing context
141    /// let context = SigningContext::new(
142    ///     &raw_tx,
143    ///     0,  // input index
144    ///     100_000,  // satoshis
145    ///     &locking_script,
146    /// );
147    ///
148    /// // Sign
149    /// let unlocking_script = unlock.sign(&context)?;
150    /// ```
151    pub fn unlock(
152        private_key: &PrivateKey,
153        sign_outputs: SignOutputs,
154        anyone_can_pay: bool,
155    ) -> ScriptTemplateUnlock {
156        let key = private_key.clone();
157        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
158
159        ScriptTemplateUnlock::new(
160            move |context: &SigningContext| {
161                // Compute the sighash
162                let sighash = context.compute_sighash(scope)?;
163
164                // Sign the sighash
165                let signature = key.sign(&sighash)?;
166                let tx_sig = TransactionSignature::new(signature, scope);
167
168                // Build the unlocking script
169                let sig_bytes = tx_sig.to_checksig_format();
170                let pubkey_bytes = key.public_key().to_compressed();
171
172                let mut script = Script::new();
173                script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
174
175                Ok(UnlockingScript::from_script(script))
176            },
177            || {
178                // Estimate length: signature (1 push + 73 bytes max) + pubkey (1 push + 33 bytes)
179                // = 1 + 73 + 1 + 33 = 108 bytes
180                108
181            },
182        )
183    }
184
185    /// Creates an unlock template that signs with a precomputed sighash.
186    ///
187    /// This is useful when you already have the sighash computed and don't
188    /// need to parse the transaction.
189    ///
190    /// # Arguments
191    ///
192    /// * `private_key` - The private key for signing
193    /// * `sighash` - The precomputed sighash to sign
194    /// * `sign_outputs` - Which outputs to sign (for the scope byte)
195    /// * `anyone_can_pay` - Whether to allow other inputs to be added
196    ///
197    /// # Returns
198    ///
199    /// The unlocking script.
200    pub fn sign_with_sighash(
201        private_key: &PrivateKey,
202        sighash: &[u8; 32],
203        sign_outputs: SignOutputs,
204        anyone_can_pay: bool,
205    ) -> Result<UnlockingScript> {
206        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
207
208        // Sign the sighash
209        let signature = private_key.sign(sighash)?;
210        let tx_sig = TransactionSignature::new(signature, scope);
211
212        // Build the unlocking script
213        let sig_bytes = tx_sig.to_checksig_format();
214        let pubkey_bytes = private_key.public_key().to_compressed();
215
216        let mut script = Script::new();
217        script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
218
219        Ok(UnlockingScript::from_script(script))
220    }
221}
222
223impl ScriptTemplate for P2PKH {
224    /// Creates a P2PKH locking script from a 20-byte public key hash.
225    ///
226    /// # Arguments
227    ///
228    /// * `params` - The 20-byte public key hash
229    ///
230    /// # Returns
231    ///
232    /// The locking script, or an error if the hash is not 20 bytes.
233    ///
234    /// # Example
235    ///
236    /// ```rust,ignore
237    /// use bsv_rs::script::templates::P2PKH;
238    /// use bsv_rs::script::template::ScriptTemplate;
239    ///
240    /// let template = P2PKH::new();
241    /// let locking = template.lock(&pubkey_hash)?;
242    /// ```
243    fn lock(&self, params: &[u8]) -> Result<LockingScript> {
244        if params.len() != 20 {
245            return Err(Error::InvalidDataLength {
246                expected: 20,
247                actual: params.len(),
248            });
249        }
250
251        // Build the P2PKH locking script:
252        // OP_DUP OP_HASH160 <20-byte pubkeyhash> OP_EQUALVERIFY OP_CHECKSIG
253        let chunks = vec![
254            ScriptChunk::new_opcode(OP_DUP),
255            ScriptChunk::new_opcode(OP_HASH160),
256            ScriptChunk::new(params.len() as u8, Some(params.to_vec())),
257            ScriptChunk::new_opcode(OP_EQUALVERIFY),
258            ScriptChunk::new_opcode(OP_CHECKSIG),
259        ];
260
261        Ok(LockingScript::from_chunks(chunks))
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_p2pkh_lock_from_pubkey_hash() {
271        let pubkey_hash = [0u8; 20];
272        let template = P2PKH::new();
273        let locking = template.lock(&pubkey_hash).unwrap();
274
275        // Should produce: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
276        // Hex: 76 a9 14 <20 zeros> 88 ac
277        let expected_hex = "76a914000000000000000000000000000000000000000088ac";
278        assert_eq!(locking.to_hex(), expected_hex);
279    }
280
281    #[test]
282    fn test_p2pkh_lock_invalid_length() {
283        let template = P2PKH::new();
284
285        // Too short
286        let result = template.lock(&[0u8; 19]);
287        assert!(result.is_err());
288
289        // Too long
290        let result = template.lock(&[0u8; 21]);
291        assert!(result.is_err());
292    }
293
294    #[test]
295    fn test_p2pkh_lock_from_address() {
296        // This is a valid mainnet address for pubkey hash of all zeros
297        // Address: 1111111111111111111114oLvT2
298        // Actually let's test with a known address
299        // The address for pubkey hash of 20 zeros is: 1111111111111111111114oLvT2
300
301        // For testing, let's create a simple test
302        let private_key = PrivateKey::from_hex(
303            "0000000000000000000000000000000000000000000000000000000000000001",
304        )
305        .unwrap();
306        let pubkey_hash = private_key.public_key().hash160();
307        let address = private_key.public_key().to_address();
308
309        let locking = P2PKH::lock_from_address(&address).unwrap();
310        let expected = P2PKH::new().lock(&pubkey_hash).unwrap();
311
312        assert_eq!(locking.to_hex(), expected.to_hex());
313    }
314
315    #[test]
316    fn test_p2pkh_lock_from_address_invalid() {
317        // Invalid address (bad checksum)
318        let result = P2PKH::lock_from_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3");
319        assert!(result.is_err());
320    }
321
322    #[test]
323    fn test_p2pkh_estimate_length() {
324        let private_key = PrivateKey::random();
325        let unlock = P2PKH::unlock(&private_key, SignOutputs::All, false);
326
327        // Should estimate 108 bytes
328        assert_eq!(unlock.estimate_length(), 108);
329    }
330
331    #[test]
332    fn test_p2pkh_unlock_creates_valid_script() {
333        let private_key = PrivateKey::from_hex(
334            "0000000000000000000000000000000000000000000000000000000000000001",
335        )
336        .unwrap();
337
338        // Create a simple test case with a mock sighash
339        let sighash = [1u8; 32];
340        let unlocking =
341            P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
342
343        // The unlocking script should have 2 chunks: signature and pubkey
344        let chunks = unlocking.chunks();
345        assert_eq!(chunks.len(), 2);
346
347        // First chunk should be the signature (push data)
348        assert!(chunks[0].data.is_some());
349        let sig_data = chunks[0].data.as_ref().unwrap();
350        // DER signature + 1 byte sighash
351        assert!(sig_data.len() >= 70 && sig_data.len() <= 73);
352
353        // Last byte should be the sighash type
354        assert_eq!(
355            sig_data.last().unwrap(),
356            &0x41_u8 // SIGHASH_ALL | SIGHASH_FORKID
357        );
358
359        // Second chunk should be the compressed public key (33 bytes)
360        assert!(chunks[1].data.is_some());
361        let pubkey_data = chunks[1].data.as_ref().unwrap();
362        assert_eq!(pubkey_data.len(), 33);
363    }
364
365    #[test]
366    fn test_p2pkh_sign_outputs_variants() {
367        let private_key = PrivateKey::random();
368        let sighash = [1u8; 32];
369
370        // Test ALL
371        let unlocking =
372            P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
373        let chunks = unlocking.chunks();
374        let sig_data = chunks[0].data.as_ref().unwrap();
375        assert_eq!(sig_data.last().unwrap(), &0x41u8); // ALL | FORKID
376
377        // Test NONE
378        let unlocking =
379            P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::None, false).unwrap();
380        let chunks = unlocking.chunks();
381        let sig_data = chunks[0].data.as_ref().unwrap();
382        assert_eq!(sig_data.last().unwrap(), &0x42u8); // NONE | FORKID
383
384        // Test SINGLE
385        let unlocking =
386            P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::Single, false).unwrap();
387        let chunks = unlocking.chunks();
388        let sig_data = chunks[0].data.as_ref().unwrap();
389        assert_eq!(sig_data.last().unwrap(), &0x43u8); // SINGLE | FORKID
390
391        // Test ALL | ANYONECANPAY
392        let unlocking =
393            P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, true).unwrap();
394        let chunks = unlocking.chunks();
395        let sig_data = chunks[0].data.as_ref().unwrap();
396        assert_eq!(sig_data.last().unwrap(), &0xC1u8); // ALL | FORKID | ANYONECANPAY
397    }
398
399    #[test]
400    fn test_p2pkh_locking_script_to_asm() {
401        let pubkey_hash = hex::decode("0000000000000000000000000000000000000000").unwrap();
402        let template = P2PKH::new();
403        let locking = template.lock(&pubkey_hash).unwrap();
404
405        let asm = locking.to_asm();
406        assert!(asm.contains("OP_DUP"));
407        assert!(asm.contains("OP_HASH160"));
408        assert!(asm.contains("OP_EQUALVERIFY"));
409        assert!(asm.contains("OP_CHECKSIG"));
410    }
411}