P2PKH Script - Full Anatomy and Execution
2. P2PKH Script — Full Anatomy and Execution
Overview
Pay-to-Public-Key-Hash (P2PKH) became the dominant script type through most of Bitcoin's history and remains widely used. Instead of embedding the raw public key in the locking script, P2PKH embeds a hash of the public key. This adds a layer of indirection that improves privacy (the public key is only revealed at spend time) and shortens the locking script.
A P2PKH address is a Base58Check-encoded version of the public key hash, which is what most people think of as a "Bitcoin address" starting with 1.
Script Structure
Locking script (scriptPubKey):
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Unlocking script (scriptSig):
<signature> <pubkey>
Byte-Level Anatomy
Locking script (25 bytes):
76 <- OP_DUP
A9 <- OP_HASH160
14 <- push 20 bytes
<20-byte pubKeyHash>
88 <- OP_EQUALVERIFY
AC <- OP_CHECKSIG
Unlocking script (~106–107 bytes):
47 or 48 <- push 71–72 byte signature
<DER signature + SIGHASH byte>
21 or 41 <- push 33 or 65 byte pubkey
<compressed or uncompressed pubkey>
The public key hash is computed as:
pubKeyHash = RIPEMD160(SHA256(pubkey))
This double-hash is often written as HASH160(pubkey) in shorthand.
Execution Walkthrough
Combined script for evaluation:
<sig> <pubkey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Step 1: Push signature
Stack: [
Step 2: Push public key
Stack: [
Step 3: OP_DUP — duplicates the top stack element
Stack: [
Step 4: OP_HASH160 — hashes top element with SHA256 then RIPEMD160
Stack: [
Step 5: Push expected pubKeyHash from scriptPubKey
Stack: [
Step 6: OP_EQUALVERIFY — pops two top elements, checks equality; fails immediately if unequal
If HASH160(<pubkey>) == <pubKeyHash>:
Stack: [
If not equal:
Script fails immediately ← spender cannot use a wrong public key
Step 7: OP_CHECKSIG — verifies signature against pubkey over serialized tx hash
Stack: [1] (on success)
Address Derivation
import hashlib
import base58
def pubkey_to_p2pkh_address(pubkey_hex: str, mainnet=True) -> str:
pubkey_bytes = bytes.fromhex(pubkey_hex)
# Step 1: SHA256
sha256_hash = hashlib.sha256(pubkey_bytes).digest()
# Step 2: RIPEMD160
rmd160 = hashlib.new('ripemd160')
rmd160.update(sha256_hash)
pubkey_hash = rmd160.digest()
# Step 3: Add version byte
version = b'\x00' if mainnet else b'\x6f'
payload = version + pubkey_hash
# Step 4: Checksum (first 4 bytes of double SHA256)
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
# Step 5: Base58Check encode
return base58.b58encode(payload + checksum).decode()
Why P2PKH Dominates Legacy Addresses
P2PKH solves three key problems with P2PK:
-
Smaller addresses: 20-byte hash fits in a compact, human-readable Base58Check string
-
Pre-image security: Public key not exposed until spend time, giving a smaller quantum attack window
-
Error detection: Base58Check encoding includes a 4-byte checksum
The scriptPubKey for P2PKH is 25 bytes — fixed and predictable — making UTXO management efficient for nodes.
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: