Chapter 2: Bitcoin Script Fundamentals#


Bitcoin’s true innovation lies not only in digital signatures or decentralized consensus, but in its programmable money system. Every Bitcoin transaction is essentially a computer program defining spending conditions. This chapter explores the foundational concepts that make Taproot possible: the UTXO model and Bitcoin Script.

# Chapter environment: bitcoinutils (load once, reuse in subsequent code cells)
from bitcoinutils.setup import setup
from bitcoinutils.utils import to_satoshis
from bitcoinutils.transactions import Transaction, TxInput, TxOutput
from bitcoinutils.keys import P2wpkhAddress, P2pkhAddress, PrivateKey
from bitcoinutils.script import Script

2.1 UTXO Model: Digital Cash, Not Digital Banking#

Before diving into scripts, we must understand how Bitcoin represents value. Unlike traditional banking that maintains account balances, Bitcoin uses the Unspent Transaction Output (UTXO) model—a system more like physical cash than digital bank accounts.

Cash vs Banking: Mental Model#

Traditional Banking (account model):

  • Account shows balance: $500

  • Spending $350 simply deducts from balance

  • Result: Balance updated to $150

  • No need to handle ‘change’

Bitcoin UTXO Model (cash model):

  • No ‘$500 balance’

  • Instead: concrete ‘bills’: one $200 and three $100

  • To spend $350, must provide $400 in bills ($200 + $100 + $100)

  • Receive $50 change as new ‘bills’

  • Result: Now have one $100 and one $50

This cash-like behavior is fundamental to Bitcoin’s design and security model.

UTXO Model in Practice#

Let’s trace a simple transaction between Alice and Bob:

Initial state:

  • Alice has a 10 BTC UTXO

  • Bob has no bitcoin

Alice sends 7 BTC to Bob:

  1. Transaction input: Alice’s 10 BTC UTXO (must be fully consumed)

  2. Transaction outputs:

    • 7 BTC to Bob (new UTXO)

    • 3 BTC change to Alice (new UTXO)

  3. Result: Original 10 BTC UTXO destroyed, two new UTXOs created

UTXO identification: Each UTXO is uniquely identified by transaction_id:output_index

  • Bob’s UTXO: TX123:0 (7 BTC)

  • Alice’s change: TX123:1 (3 BTC)

UTXO Key Properties#

Full consumption: UTXOs must be fully spent—no partial spends.

Atomic creation: Transactions either fully succeed (all inputs consumed, all outputs created) or fully fail.

Change handling: Any difference between input and output amounts becomes fees unless explicitly returned as change.

Parallel processing: Since each UTXO can only be spent once, multiple transactions can be verified in parallel without complex state management.

2.2 Bitcoin Script and P2PKH Fundamentals#

Bitcoin Script: Programmable Spending Conditions#

Each UTXO contains not only amount—it also contains a locking script (ScriptPubKey) defining spending conditions. To spend a UTXO, one must provide an unlocking script (ScriptSig) that satisfies these conditions.

Script Architecture#

Unlocking Script (ScriptSig) + Locking Script (ScriptPubKey) → Valid/Invalid

Locking script (ScriptPubKey):

  • Attached to each UTXO output

  • Defines spending conditions

  • Example: “Only someone who can provide a valid signature for public key X may spend”

Unlocking script (ScriptSig):

  • Provided when spending the UTXO

  • Contains data needed to satisfy the locking script

  • Example: “Here is my signature and public key”

Verification process:

  • Combine unlocking and locking scripts

  • Execute as a single program

  • If final result is TRUE, UTXO can be spent

Stack-Based Execution#

Bitcoin Script uses a stack-based execution model, similar to Forth or PostScript. Operations act on a Last-In-First-Out (LIFO) stack:

Initial stack: empty

│ (empty)                               │
└───────────────────────────────────────┘

PUSH 3

│ 3                                     │
└───────────────────────────────────────┘

PUSH 5


│ 5                                     │
│ 3                                     │
└───────────────────────────────────────┘

ADD operation


│ 8                                     │
└───────────────────────────────────────┘

Operation:

Pop two numbers from stack: 5 (top) and 3 Execute addition: 5 + 3 = 8 Push result 8 back onto stack

This simple model supports complex spending conditions while remaining predictable and secure.

P2PKH: The Foundation Script#

Pay-to-Public-Key-Hash (P2PKH) is Bitcoin’s most basic script type and the foundation for understanding more complex scripts like those used in Taproot.

P2PKH locking script

OP_DUP OP_HASH160 <pubkey_hash> OP_EQUALVERIFY OP_CHECKSIG

This script means: “This UTXO can be spent by anyone who provides a public key that hashes to pubkey_hash and a valid signature from the corresponding private key.”

P2PKH unlocking script

<signature> <public_key>

Spender provides:

  • Digital signature proving ownership of private key

  • The public key itself (will be hashed and verified)

Real Example: Satoshi to Hal Finney#

Let’s examine the famous first Bitcoin transaction: Satoshi Nakamoto sending 10 BTC to Hal Finney.

Transaction ID:f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16

Transaction structure:

  • Input: Satoshi’s coinbase UTXO (50 BTC from mining)

  • Outputs:

    • 10 BTC to Hal Finney

    • 40 BTC change to Satoshi

Note: This early transaction used P2PK (Pay-to-Public-Key) not P2PKH, embedding the public key directly in the locking script. Modern Bitcoin uses P2PKH for better security and space efficiency.

P2PKH Execution (Hal Finney Example)#

Locking:OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG
Unlocking:<signature> <public_key>

Flow: sig + pk pushed → OP_DUP → OP_HASH160 → hash match → OP_CHECKSIG → 1 (TRUE)

    
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_DUP: Duplicate top-of-stack (public key):

    
│ 02898711...8519 (public_key)          │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_HASH160: Hash the top-of-stack:

    
│ 340cfcff...7a571 (hash160_result)     │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. Push expected hash: From locking script:

    
│ 340cfcff...7a571 (expected_hash)      │
│ 340cfcff...7a571 (computed_hash)      │
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
  1. OP_EQUALVERIFY: Compare top two stack items, remove both if equal:

    
│ 02898711...8519 (public_key)          │
│ 30440220...914f01 (signature)         │
└───────────────────────────────────────┘
(If hash doesn't match, script fails)
  1. OP_CHECKSIG: Verify public key and transaction signature:


│ 1 (TRUE)                              │
└───────────────────────────────────────┘
  1. Final check: If stack top is non-zero, script succeeds.

P2PKH Security Properties#

Hash preimage resistance: Public key stays hidden until first spend, protecting against potential ECDSA quantum attacks.

Signature verification: Cryptographic proof that spender controls the private key corresponding to the pubkey hash.

Transaction integrity: Signature covers transaction details, preventing modification after signing.

Replay protection: Signature is specific to the transaction, cannot be reused.

2.3 Practical Implementation: Building P2PKH Transactions#

Building a Real Testnet Legacy-to-SegWit Transaction#

Let’s build a complete P2PKH transaction step by step, explain each component, then trace script execution with real data.

# Example 1: Build P2PKH transaction
# Reference: code/chapter02/01_build_p2pkh_transaction.py

setup('testnet')
private_key = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
public_key = private_key.get_public_key()
from_address = P2pkhAddress('myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4')
to_address = P2wpkhAddress('tb1qckeg66a6jx3xjw5mrpmte5ujjv3cjrajtvm9r4')

txin = TxInput('34b90a15d0a9ec9ff3d7bed2536533c73278a9559391cb8c9778b7e7141806f7', 1)
txout = TxOutput(to_satoshis(0.00029400), to_address.to_script_pub_key())
tx = Transaction([txin], [txout])
p2pkh_script = from_address.to_script_pub_key()
signature = private_key.sign_input(tx, 0, p2pkh_script)
txin.script_sig = Script([signature, public_key.to_hex()])
signed_tx = tx.serialize()

print(f"Transaction size: {tx.get_size()} bytes")

Key Functions#

TxInput(txid, vout) | TxOutput(amount, script_pubkey) | Transaction([txin], [txout])
sign_input(tx, idx, script) | Script([sig, pk]) → script_sig

Real Data Analysis and Stack Execution#

TXID: bf41b47481a9d1c99af0b62bb36bc864182312f39a3e1e06c8f6304ba8e58355

ScriptSig:473044...8519 (signature + public key)
ScriptPubKey:76a914c5b28d6b...890fb288ac(OP_DUP OP_HASH160 hash OP_EQUALVERIFY OP_CHECKSIG)

P2PKH Stack Execution (Brief)#

│ sig │ → │ pk, sig │ → OP_DUP → │ pk, pk, sig │ → OP_HASH160 → hash match → OP_CHECKSIG → │ 1 (TRUE) │

From P2PKH to Advanced Scripts#

P2PKH provides the foundation for understanding Bitcoin’s programmable money system, but it’s just the beginning. The same principles—stack-based execution, cryptographic verification, and conditional logic—support more complex scripts we’ll explore in later chapters:

P2SH (Pay-to-Script-Hash):

  • Supports complex spending conditions while keeping addresses short

  • Moves script complexity from blockchain to spender

  • Foundation for wrapping SegWit and multisig schemes

P2WPKH (Pay-to-Witness-Public-Key-Hash):

  • SegWit’s P2PKH equivalent, more efficient

  • Separates signature data from transaction data

  • Reduces transaction malleability and enables Lightning Network

P2TR (Pay-to-Taproot):

  • The pinnacle of Bitcoin script evolution

  • Supports complex smart contracts that look like simple payments

  • Combines Schnorr signatures with Merkle trees for maximum flexibility

Each evolution maintains backward compatibility while adding new features. Understanding P2PKH’s stack execution model is crucial because Taproot uses the same fundamental approach with more complex cryptographic primitives and script structures.

In the next chapter, we’ll dive into these address types, examine their script structures, and understand how each improvement builds on lessons from P2PKH.

Chapter Summary#

This chapter established the foundational concepts that make Taproot possible:

UTXO model: Bitcoin represents value as discrete, spendable outputs rather than account balances. Each UTXO must be fully consumed, creating a cash-like system supporting parallel verification without complex state management.

Script system: Each UTXO contains programmable spending conditions via the locking script (ScriptPubKey). Spending requires an unlocking script (ScriptSig) that satisfies these when executed together.

Stack execution: Bitcoin Script uses a simple stack-based model for conditions, operating on a LIFO stack to verify spending authorization.

P2PKH implementation: The basic script type demonstrates signature and pubkey verification in seven steps: provide signature and pubkey, duplicate, hash, compare, and verify signature.

Practical development: With tools like bitcoinutils, developers can build, sign, and broadcast P2PKH transactions while understanding underlying cryptographic operations and stack execution.

Understanding these concepts is essential because Taproot builds on them, using the same stack-based execution model while introducing new cryptographic primitives and script structures. The journey from simple P2PKH to Taproot’s complex spending conditions illustrates Bitcoin’s evolution from basic digital cash to sophisticated financial application platform—while maintaining the security and simplicity that make Bitcoin unique.