use crate::error::Error;
use crate::primitives::bsv::TransactionSignature;
use crate::primitives::ec::PrivateKey;
use crate::primitives::encoding::from_base58_check;
use crate::script::op::*;
use crate::script::template::{
compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
};
use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
use crate::Result;
#[derive(Debug, Clone, Copy, Default)]
pub struct P2PKH;
impl P2PKH {
pub fn new() -> Self {
Self
}
pub fn lock_from_address(address: &str) -> Result<LockingScript> {
let (version, payload) = from_base58_check(address)?;
if version.len() != 1 || (version[0] != 0x00 && version[0] != 0x6f) {
return Err(Error::CryptoError(format!(
"Invalid P2PKH address version: expected 0x00 or 0x6f, got 0x{:02x}",
version.first().unwrap_or(&0)
)));
}
if payload.len() != 20 {
return Err(Error::InvalidDataLength {
expected: 20,
actual: payload.len(),
});
}
P2PKH::new().lock(&payload)
}
pub fn unlock(
private_key: &PrivateKey,
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> ScriptTemplateUnlock {
let key = private_key.clone();
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
ScriptTemplateUnlock::new(
move |context: &SigningContext| {
let sighash = context.compute_sighash(scope)?;
let signature = key.sign(&sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
let sig_bytes = tx_sig.to_checksig_format();
let pubkey_bytes = key.public_key().to_compressed();
let mut script = Script::new();
script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
Ok(UnlockingScript::from_script(script))
},
|| {
108
},
)
}
pub fn sign_with_sighash(
private_key: &PrivateKey,
sighash: &[u8; 32],
sign_outputs: SignOutputs,
anyone_can_pay: bool,
) -> Result<UnlockingScript> {
let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
let signature = private_key.sign(sighash)?;
let tx_sig = TransactionSignature::new(signature, scope);
let sig_bytes = tx_sig.to_checksig_format();
let pubkey_bytes = private_key.public_key().to_compressed();
let mut script = Script::new();
script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
Ok(UnlockingScript::from_script(script))
}
}
impl ScriptTemplate for P2PKH {
fn lock(&self, params: &[u8]) -> Result<LockingScript> {
if params.len() != 20 {
return Err(Error::InvalidDataLength {
expected: 20,
actual: params.len(),
});
}
let chunks = vec![
ScriptChunk::new_opcode(OP_DUP),
ScriptChunk::new_opcode(OP_HASH160),
ScriptChunk::new(params.len() as u8, Some(params.to_vec())),
ScriptChunk::new_opcode(OP_EQUALVERIFY),
ScriptChunk::new_opcode(OP_CHECKSIG),
];
Ok(LockingScript::from_chunks(chunks))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_p2pkh_lock_from_pubkey_hash() {
let pubkey_hash = [0u8; 20];
let template = P2PKH::new();
let locking = template.lock(&pubkey_hash).unwrap();
let expected_hex = "76a914000000000000000000000000000000000000000088ac";
assert_eq!(locking.to_hex(), expected_hex);
}
#[test]
fn test_p2pkh_lock_invalid_length() {
let template = P2PKH::new();
let result = template.lock(&[0u8; 19]);
assert!(result.is_err());
let result = template.lock(&[0u8; 21]);
assert!(result.is_err());
}
#[test]
fn test_p2pkh_lock_from_address() {
let private_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let pubkey_hash = private_key.public_key().hash160();
let address = private_key.public_key().to_address();
let locking = P2PKH::lock_from_address(&address).unwrap();
let expected = P2PKH::new().lock(&pubkey_hash).unwrap();
assert_eq!(locking.to_hex(), expected.to_hex());
}
#[test]
fn test_p2pkh_lock_from_address_invalid() {
let result = P2PKH::lock_from_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN3");
assert!(result.is_err());
}
#[test]
fn test_p2pkh_estimate_length() {
let private_key = PrivateKey::random();
let unlock = P2PKH::unlock(&private_key, SignOutputs::All, false);
assert_eq!(unlock.estimate_length(), 108);
}
#[test]
fn test_p2pkh_unlock_creates_valid_script() {
let private_key = PrivateKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
let sighash = [1u8; 32];
let unlocking =
P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
let chunks = unlocking.chunks();
assert_eq!(chunks.len(), 2);
assert!(chunks[0].data.is_some());
let sig_data = chunks[0].data.as_ref().unwrap();
assert!(sig_data.len() >= 70 && sig_data.len() <= 73);
assert_eq!(
sig_data.last().unwrap(),
&0x41_u8 );
assert!(chunks[1].data.is_some());
let pubkey_data = chunks[1].data.as_ref().unwrap();
assert_eq!(pubkey_data.len(), 33);
}
#[test]
fn test_p2pkh_sign_outputs_variants() {
let private_key = PrivateKey::random();
let sighash = [1u8; 32];
let unlocking =
P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x41u8);
let unlocking =
P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::None, false).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x42u8);
let unlocking =
P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::Single, false).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0x43u8);
let unlocking =
P2PKH::sign_with_sighash(&private_key, &sighash, SignOutputs::All, true).unwrap();
let chunks = unlocking.chunks();
let sig_data = chunks[0].data.as_ref().unwrap();
assert_eq!(sig_data.last().unwrap(), &0xC1u8); }
#[test]
fn test_p2pkh_locking_script_to_asm() {
let pubkey_hash = hex::decode("0000000000000000000000000000000000000000").unwrap();
let template = P2PKH::new();
let locking = template.lock(&pubkey_hash).unwrap();
let asm = locking.to_asm();
assert!(asm.contains("OP_DUP"));
assert!(asm.contains("OP_HASH160"));
assert!(asm.contains("OP_EQUALVERIFY"));
assert!(asm.contains("OP_CHECKSIG"));
}
}