Test
2. How to Decode a scriptPubKey Manually
What Is a scriptPubKey?
The scriptPubKey is the locking script attached to each transaction output (UTXO). It defines the conditions that must be satisfied to spend that output. When a new transaction wants to spend a UTXO, it must provide a scriptSig (and possibly witness data) that, when combined with the scriptPubKey, results in a valid execution.
The scriptPubKey is embedded in the transaction output structure:
[value: 8 bytes little-endian][scriptPubKey length: varint][scriptPubKey bytes]
Common scriptPubKey Patterns
P2PK — Pay to Public Key
<pubkey length> <pubkey> OP_CHECKSIG
Example (uncompressed public key, 65 bytes):
41
04{64-byte-pubkey}
ac
Example (compressed public key, 33 bytes):
21
02{32-byte-pubkey}
ac
Manual decode:
41 → Push next 65 bytes
04... → Uncompressed public key (04 prefix + 32-byte X + 32-byte Y)
ac → OP_CHECKSIG
P2PKH — Pay to Public Key Hash
OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG
Hex template:
76 a9 14 {20 bytes} 88 ac
P2SH — Pay to Script Hash
OP_HASH160 <20-byte-hash> OP_EQUAL
Hex template:
a9 14 {20 bytes} 87
P2SH is recognizable because the script is only 23 bytes long and follows this exact pattern. The hash is of the redeem script that the spender must reveal.
P2WPKH — Pay to Witness Public Key Hash
OP_0 <20-byte-hash>
Hex template:
00 14 {20 bytes}
This is 22 bytes total. OP_0 (version byte 0) followed by a 20-byte push signals native SegWit v0 P2WPKH.
P2WSH — Pay to Witness Script Hash
OP_0 <32-byte-hash>
Hex template:
00 20 {32 bytes}
This is 34 bytes. The 32-byte hash is SHA256 of the witness script.
P2TR — Pay to Taproot (SegWit v1)
OP_1 <32-byte-x-only-pubkey>
Hex template:
51 20 {32 bytes}
0x51 = OP_1 (version byte 1). The 32 bytes are the x-only Taproot output key.
Manual Decode Workflow
def decode_scriptpubkey(hex_script: str) -> dict:
data = bytes.fromhex(hex_script)
length = len(data)
# P2PKH: 76 a9 14 [20 bytes] 88 ac
if (length == 25 and data[0] == 0x76 and data[1] == 0xa9
and data[2] == 0x14 and data[23] == 0x88 and data[24] == 0xac):
return {"type": "P2PKH", "hash": data[3:23].hex()}
# P2SH: a9 14 [20 bytes] 87
if length == 23 and data[0] == 0xa9 and data[1] == 0x14 and data[22] == 0x87:
return {"type": "P2SH", "hash": data[2:22].hex()}
# P2WPKH: 00 14 [20 bytes]
if length == 22 and data[0] == 0x00 and data[1] == 0x14:
return {"type": "P2WPKH", "hash": data[2:22].hex()}
# P2WSH: 00 20 [32 bytes]
if length == 34 and data[0] == 0x00 and data[1] == 0x20:
return {"type": "P2WSH", "hash": data[2:34].hex()}
# P2TR: 51 20 [32 bytes]
if length == 34 and data[0] == 0x51 and data[1] == 0x20:
return {"type": "P2TR", "key": data[2:34].hex()}
# P2PK compressed
if length == 35 and data[0] == 0x21 and data[34] == 0xac:
return {"type": "P2PK", "pubkey": data[1:34].hex()}
return {"type": "UNKNOWN", "raw": hex_script}
# Test
print(decode_scriptpubkey("76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac"))
# Output: {'type': 'P2PKH', 'hash': '89abcdefabbaabbaabbaabbaabbaabbaabbaabba'}
OP_RETURN Outputs
OP_RETURN scripts are used to embed arbitrary data in the blockchain. They are provably unspendable:
6a <varint length> <arbitrary data>
6a → OP_RETURN
0c → Push next 12 bytes
48656c6c6f20576f726c64 → "Hello World" in ASCII
These outputs carry zero value and cannot be spent, but their data is permanently recorded on-chain.
Pro Tip
When debugging scripts, always start with a high-level disassembly before diving into the stack trace. Tools like bitcoin-cli decodescript are your first line of defense in identifying standard script patterns.
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: