Skip to main content

bsv_rs/script/templates/
p2pk.rs

1//! P2PK (Pay-to-Public-Key) script template.
2//!
3//! This module provides the [`P2PK`] template for creating and spending
4//! Pay-to-Public-Key outputs. P2PK is simpler than P2PKH — it locks directly
5//! to a public key rather than its hash. The unlock only needs a signature
6//! (the public key is already in the locking script).
7//!
8//! # Locking Script Pattern
9//!
10//! ```text
11//! <pubkey> OP_CHECKSIG
12//! ```
13//!
14//! # Unlocking Script Pattern
15//!
16//! ```text
17//! <signature>
18//! ```
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use bsv_rs::script::templates::P2PK;
24//! use bsv_rs::script::template::{ScriptTemplate, SignOutputs};
25//! use bsv_rs::primitives::ec::PrivateKey;
26//!
27//! let private_key = PrivateKey::random();
28//! let pubkey = private_key.public_key().to_compressed();
29//!
30//! // Create locking script
31//! let template = P2PK::new();
32//! let locking = template.lock(&pubkey)?;
33//!
34//! // Create unlock template
35//! let unlock = P2PK::unlock(&private_key, SignOutputs::All, false);
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::script::op::*;
43use crate::script::template::{
44    compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
45};
46use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
47use crate::Result;
48
49/// P2PK (Pay-to-Public-Key) script template.
50///
51/// Locks funds to a public key directly. Simpler than P2PKH but reveals the
52/// public key in the locking script (before spending).
53#[derive(Debug, Clone, Copy, Default)]
54pub struct P2PK;
55
56impl P2PK {
57    /// Creates a new P2PK template instance.
58    pub fn new() -> Self {
59        Self
60    }
61
62    /// Creates an unlock template for spending a P2PK output.
63    ///
64    /// Unlike P2PKH, only a signature is needed (the public key is already
65    /// in the locking script).
66    pub fn unlock(
67        private_key: &PrivateKey,
68        sign_outputs: SignOutputs,
69        anyone_can_pay: bool,
70    ) -> ScriptTemplateUnlock {
71        let key = private_key.clone();
72        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
73
74        ScriptTemplateUnlock::new(
75            move |context: &SigningContext| {
76                let sighash = context.compute_sighash(scope)?;
77                let signature = key.sign(&sighash)?;
78                let tx_sig = TransactionSignature::new(signature, scope);
79                let sig_bytes = tx_sig.to_checksig_format();
80
81                let mut script = Script::new();
82                script.write_bin(&sig_bytes);
83
84                Ok(UnlockingScript::from_script(script))
85            },
86            || {
87                // Estimate length: signature only (1 push + 72 DER max + 1 sighash byte)
88                74
89            },
90        )
91    }
92
93    /// Signs with a precomputed sighash.
94    pub fn sign_with_sighash(
95        private_key: &PrivateKey,
96        sighash: &[u8; 32],
97        sign_outputs: SignOutputs,
98        anyone_can_pay: bool,
99    ) -> Result<UnlockingScript> {
100        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
101        let signature = private_key.sign(sighash)?;
102        let tx_sig = TransactionSignature::new(signature, scope);
103        let sig_bytes = tx_sig.to_checksig_format();
104
105        let mut script = Script::new();
106        script.write_bin(&sig_bytes);
107
108        Ok(UnlockingScript::from_script(script))
109    }
110}
111
112impl ScriptTemplate for P2PK {
113    /// Creates a P2PK locking script from a compressed (33-byte) or
114    /// uncompressed (65-byte) public key.
115    fn lock(&self, params: &[u8]) -> Result<LockingScript> {
116        if params.len() != 33 && params.len() != 65 {
117            return Err(Error::InvalidDataLength {
118                expected: 33,
119                actual: params.len(),
120            });
121        }
122
123        // Validate key prefix
124        match params[0] {
125            0x02 | 0x03 if params.len() == 33 => {}
126            0x04 | 0x06 | 0x07 if params.len() == 65 => {}
127            _ => {
128                return Err(Error::InvalidPublicKey(
129                    "Invalid public key prefix".to_string(),
130                ));
131            }
132        }
133
134        let chunks = vec![
135            ScriptChunk::new(params.len() as u8, Some(params.to_vec())),
136            ScriptChunk::new_opcode(OP_CHECKSIG),
137        ];
138
139        Ok(LockingScript::from_chunks(chunks))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_p2pk_lock_compressed() {
149        let private_key = PrivateKey::from_hex(
150            "0000000000000000000000000000000000000000000000000000000000000001",
151        )
152        .unwrap();
153        let pubkey = private_key.public_key().to_compressed();
154
155        let template = P2PK::new();
156        let locking = template.lock(&pubkey).unwrap();
157
158        // Should be: <33 bytes> OP_CHECKSIG
159        let chunks = locking.chunks();
160        assert_eq!(chunks.len(), 2);
161        assert_eq!(chunks[0].data.as_ref().unwrap().len(), 33);
162        assert_eq!(chunks[1].op, OP_CHECKSIG);
163
164        // Verify script detection
165        assert!(locking.as_script().is_p2pk());
166    }
167
168    #[test]
169    fn test_p2pk_lock_invalid_length() {
170        let template = P2PK::new();
171
172        assert!(template.lock(&[0x02; 20]).is_err());
173        assert!(template.lock(&[0x02; 32]).is_err());
174        assert!(template.lock(&[0x02; 34]).is_err());
175    }
176
177    #[test]
178    fn test_p2pk_lock_invalid_prefix() {
179        let template = P2PK::new();
180
181        // 33 bytes but wrong prefix
182        let mut bad_key = [0u8; 33];
183        bad_key[0] = 0x05;
184        assert!(template.lock(&bad_key).is_err());
185    }
186
187    #[test]
188    fn test_p2pk_unlock_signature_only() {
189        let private_key = PrivateKey::random();
190        let sighash = [1u8; 32];
191
192        let unlocking =
193            P2PK::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
194
195        // Should have exactly 1 chunk: the signature
196        let chunks = unlocking.chunks();
197        assert_eq!(chunks.len(), 1);
198
199        let sig_data = chunks[0].data.as_ref().unwrap();
200        assert!(sig_data.len() >= 70 && sig_data.len() <= 73);
201        assert_eq!(*sig_data.last().unwrap(), 0x41u8); // SIGHASH_ALL | SIGHASH_FORKID
202    }
203
204    #[test]
205    fn test_p2pk_estimate_length() {
206        let private_key = PrivateKey::random();
207        let unlock = P2PK::unlock(&private_key, SignOutputs::All, false);
208        assert_eq!(unlock.estimate_length(), 74);
209    }
210
211    #[test]
212    fn test_p2pk_asm() {
213        let private_key = PrivateKey::random();
214        let pubkey = private_key.public_key().to_compressed();
215
216        let template = P2PK::new();
217        let locking = template.lock(&pubkey).unwrap();
218
219        let asm = locking.to_asm();
220        assert!(asm.contains("OP_CHECKSIG"));
221        assert!(!asm.contains("OP_DUP")); // No DUP in P2PK (that's P2PKH)
222        assert!(!asm.contains("OP_HASH160")); // No HASH160 in P2PK
223    }
224}