The Anatomy of Bitcoin Scripts: From P2PKH to Taproot

Understanding these primitives is the foundation for everything that follows β€” Lightning, covenant proposals, and beyond.

Published: Nov 23, 2025

Read on Medium β†—

Companion notes to my book Mastering Taproot.


HODLing is the beginning.
But Bitcoin was meant to be programmed.

"Not Just HODLing: Real Bitcoin Script Engineering" starts here.

The Universal Truth

Every Bitcoin addressβ€Šβ€”β€Šfrom the genesis block to the latest Taproot outputβ€Šβ€”β€Šfollows one fundamental pattern:

Lock:   Commit to a condition
Unlock: Reveal proof that you satisfy it

This isn’t a feature of SegWit or Taproot. It’s the DNA of Bitcoin itself.


Evolution of Commit-Reveal

The abstraction deepens. The privacy improves. The core logic remains unchanged.


Locking ScriptΒ Formats

P2PKH

OP_DUP OP_HASH160 <20-byte pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG

P2SH

OP_HASH160 <20-byte script_hash> OP_EQUAL

P2WPKH

OP_0 <20-byte pubkey_hash>

P2WSH

OP_0 <32-byte script_hash>

P2TR

OP_1 <32-byte output_pubkey>

How nodes differentiate SegWit types:

  • OP_0 + 20 bytes β†’ P2WPKH

  • OP_0 + 32 bytes β†’ P2WSH

  • OP_1 + 32 bytes β†’ P2TR


The Verification Flow

Universal Pattern

Step 1: Verify Commit β€” Does the revealed content match what was promised?
Step 2: Execute Script β€” Do the conditions evaluate to true?

P2SH Verification

HASH160(redeem_script) == hash in locking script?
Match β†’ Execute redeem_script

P2WSH Verification

SHA256(witness_script) == hash in locking script?
Match β†’ Execute witness_script

P2TR Script Path Verification

1. Extract internal_pubkey from control block
2. Compute tapleaf_hash from script
3. Calculate merkle_root using Merkle path
4. tweak = TaggedHash("TapTweak", internal_pubkey || merkle_root)
5. computed_pubkey = internal_pubkey + tweak Γ— G
6. computed_pubkey == output_pubkey in locking script?
7. Match β†’ Execute tapscript

Stack Execution Visualized

Example 1:Β P2PKH

Locking Script: OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG

Unlock Data: <signature> <pubkey>

Initial Stack      OP_DUP         OP_HASH160      PUSH hash
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚       β”‚ pubkey  β”‚    β”‚ pk_hash β”‚     β”‚ pk_hash β”‚ ← expected
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig     β”‚       β”‚ pubkey  β”‚    β”‚ pubkey  β”‚     β”‚ pk_hash β”‚ ← computed
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚ sig     β”‚    β”‚ sig     β”‚     β”‚ pubkey  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                                 β”‚ sig     β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

OP_EQUALVERIFY     OP_CHECKSIG      Final
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚        β”‚ 1(true) β”‚      β”‚ 1(true) β”‚ βœ“
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ sig     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example 2: P2SH withΒ Timelock

Redeem Script: OP_3 OP_CSV OP_DROP OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG

Unlock Data: <signature> <pubkey>

Initial Stack      PUSH 3         OP_CSV          OP_DROP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚       β”‚ 3       β”‚    β”‚ 3       β”‚     β”‚ pubkey  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig     β”‚       β”‚ pubkey  β”‚    β”‚ pubkey  β”‚     β”‚ sig     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ sig     β”‚    β”‚ sig     β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 Check nSeq β‰₯ 3

Continues with standard P2PKH logic...

Example 3:Β P2WPKH

Locking Script: OP_0 <20-byte pubkey_hash>

Witness: <signature> <pubkey>

Node sees OP_0 + 20 bytes, implicitly executes P2PKH logic:

Verification: HASH160(pubkey) == hash in locking script? Match β†’ Continue


Execution (implicit P2PKH logic):

Initial Stack      OP_DUP         OP_HASH160      PUSH hash
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚       β”‚ pubkey  β”‚    β”‚ pk_hash β”‚     β”‚ pk_hash β”‚ ← expected
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig     β”‚       β”‚ pubkey  β”‚    β”‚ pubkey  β”‚     β”‚ pk_hash β”‚ ← computed
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚ sig     β”‚    β”‚ sig     β”‚     β”‚ pubkey  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                                 β”‚ sig     β”‚
                                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
OP_EQUALVERIFY     OP_CHECKSIG      Final
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚        β”‚ 1(true) β”‚      β”‚ 1(true) β”‚ βœ“
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ sig     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Difference from P2PKH: Identical logic, unlock data moved from scriptSig to witness.


Example 4: P2WSH (2-of-3 Multisig)

Locking Script: OP_0 <32-byte script_hash>

Witness Script: OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

Witness: OP_0 <sig1> <sig2> <witness_script>

Verification: SHA256(witness_script) == hash? Match β†’ Execute witness_script


Execution:
Initial Stack (witness data excluding witness_script)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sig2    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig1    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0       β”‚ ← CHECKMULTISIG bug placeholder
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
PUSH 2         PUSH pk1       PUSH pk2       PUSH pk3
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2       β”‚    β”‚ pk1     β”‚    β”‚ pk2     β”‚    β”‚ pk3     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig2    β”‚    β”‚ 2       β”‚    β”‚ pk1     β”‚    β”‚ pk2     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig1    β”‚    β”‚ sig2    β”‚    β”‚ 2       β”‚    β”‚ pk1     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0       β”‚    β”‚ sig1    β”‚    β”‚ sig2    β”‚    β”‚ 2       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
               β”‚ 0       β”‚    β”‚ sig1    β”‚    β”‚ sig2    β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                              β”‚ 0       β”‚    β”‚ sig1    β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                             β”‚ 0       β”‚
                                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜



PUSH 3                    OP_CHECKMULTISIG           Final
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3       β”‚               β”‚ 1(true) β”‚                β”‚ 1(true) β”‚ βœ“
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ pk3     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€               Verify: at least 2 of 3
β”‚ pk2     β”‚               signatures are valid
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ pk1     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 2       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig2    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sig1    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0       β”‚ ← consumed (legacy bug)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The CHECKMULTISIG bug: Due to an off-by-one error in the original implementation, an extra stack element is consumed. Hence the dummy OP_0 at the start of witness.


Example 5: Taproot ScriptΒ Path

Tapscript: OP_2 OP_CSV OP_DROP <pubkey> OP_CHECKSIG

Witness: <signature> <script> <control_block>

Verification: control_block + script β†’ reconstruct output_pubkey β†’ match locking script

Execution:
Initial Stack      PUSH 2         OP_CSV          OP_DROP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sig     β”‚       β”‚ 2       β”‚    β”‚ 2       β”‚     β”‚ sig     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ sig     β”‚    β”‚ sig     β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 Check nSeq β‰₯ 2



PUSH pubkey        OP_CHECKSIG        Final
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pubkey  β”‚        β”‚ 1(true) β”‚        β”‚ 1(true) β”‚ βœ“
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ sig     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What SegWit ActuallyΒ Changed

SegWit wasn’t a revolution. It was a relocation:


What Taproot ActuallyΒ Changed

Taproot optimized the commit-reveal paradigm:

Control Block Structure:

[1 byte: leaf_version + parity] [32 bytes: internal_pubkey] [32 bytes Γ— N: Merkle path]

Verification Formula:

output_pubkey = internal_pubkey + TaggedHash("TapTweak", internal_pubkey || merkle_root) Γ— G

Key Takeaways

  1. Every address type is commit-revealβ€Šβ€”β€ŠLock with a hash/pubkey commitment, unlock by revealing the preimage

  2. Locking scripts are routing labelsβ€Šβ€”β€ŠNodes use the format to determine validation rules; they don’t execute on stack

  3. Verification precedes executionβ€Šβ€”β€ŠFirst prove β€œyou have the right to run this script”, then verify β€œdoes the script pass”

  4. Stack execution is universalβ€Šβ€”β€ŠRegardless of address type, it’s all push, pop, verify signatures, check timelocks

  5. The trajectory is clear:

  • Commitments grow more abstract (pubkey β†’ hash β†’ tweak)

  • Privacy improves (expose everything β†’ expose only what’s used)

  • Efficiency increases (fee optimization, verification optimization)


Understanding these primitives is the foundation for everything that followsβ€Šβ€”β€ŠLightning channels, covenant proposals, and whatever comes next.

By Aaron Recompile on November 28, 2025.

Canonical link

Exported from Medium on July 3, 2026.

Originally published at medium.com.