Chapter 3: P2SH Script Engineering#


Pay-to-Script-Hash (P2SH) is where Bitcoin scripting first becomes practical: any script, however complex, can be locked behind a single 20-byte hash and revealed only when the funds are spent. This chapter uses P2SH to build the two patterns that recur through the rest of the book — multisignature and time locks — and traces both through the stack. It sits between the single-signature P2PKH of Chapter 2 and the script trees of Taproot.

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

3.1 P2SH Architecture: Scripts Behind the Hash#

P2SH lets any script be represented by a compact 20-byte hash, moving the script’s complexity out of the UTXO set and deferring it to spending time.

Two-Stage Verification Model#

P2SH operates through two distinct phases:

Stage 1: Hash verification

OP_HASH160 <script_hash> OP_EQUAL

Stage 2: Script execution

<revealed_script> → Execute as Bitcoin Script

P2SH Address Generation Process#

P2SH follows the same Hash160 → Base58Check pattern covered in Chapter 1, but hashes the script instead of a public key:

Script Serialization → hex_encoded_script
Hash160(script)     → 20_bytes_script_hash  
Version + Base58Check → 3...address (mainnet)

All P2SH addresses begin with ‘3’ on mainnet and ‘2’ on testnet, immediately distinguishing them from P2PKH addresses.

ScriptSig Construction Pattern#

The unlocking script (ScriptSig) for P2SH follows a specific pattern:

<script_data> <serialized_redeem_script>

Where <script_data> contains the values needed to satisfy the redeem script’s conditions, and <serialized_redeem_script> is the original script whose hash matches the locking script.

3.2 2-of-3 Multisig#

A multisig output needs more than one key to spend. A 2-of-3 scheme — any two of three keys — is the common shape for shared custody: no single person can move the funds alone, and no single lost key locks them away either.

The Setup: Three Keys#

Three parties each hold a key, and any two of them can authorize a spend:

  • Alice, Bob, and Carol — three keys, two required.

The redeem script encodes that rule with Bitcoin’s OP_CHECKMULTISIG opcode.

# Example 1: Create multisig P2SH
# Reference: code/chapter03/01_create_multisig_p2sh.py

alice_pk = '02898711e6bf63f5cbe1b38c05e89d6c391c59e9f8f695da44bf3d20ca674c8519'
bob_pk = '0284b5951609b76619a1ce7f48977b4312ebe226987166ef044bfb374ceef63af5'
carol_pk = '0317aa89b43f46a0c0cdbd9a302f2508337ba6a06d123854481b52de9c20996011'
redeem_script = Script(['OP_2', alice_pk, bob_pk, carol_pk, 'OP_3', 'OP_CHECKMULTISIG'])
p2sh_addr = P2shAddress.from_script(redeem_script)
print(f"Redeem Script: {redeem_script.to_hex()[:32]}...{redeem_script.to_hex()[-8:]}")
print(f"P2SH Address: {p2sh_addr.to_string()}")

Key Functions#

Script([...]): Creates a Script object from a list of opcodes and data. The library automatically encodes opcodes like 'OP_2' into their byte representations (0x52).

P2shAddress.from_script(script): Generates a P2SH address by serializing the script to bytes, computing Hash160(script), adding the version byte (0x05 for mainnet, 0xc4 for testnet), and applying Base58Check encoding.

Serialization: The redeem script serializes to 522102898711...601153ae52 (OP_2), three 21-prefixed 33-byte public keys, 53 (OP_3), and ae (OP_CHECKMULTISIG).

# Example 2: Spend multisig P2SH
# Reference: code/chapter03/02_spend_multisig_p2sh.py

alice_sk = PrivateKey('cPeon9fBsW2BxwJTALj3hGzh9vm8C52Uqsce7MzXGS1iFJkPF4AT')
bob_sk = PrivateKey('cSNdLFDf3wjx1rswNL2jKykbVkC6o56o5nYZi4FUkWKjFn2Q5DSG')
redeem_script = Script(['OP_2', alice_pk, bob_pk, carol_pk, 'OP_3', 'OP_CHECKMULTISIG'])
txin = TxInput('4b869865bc4a156d7e0ba14590b5c8971e57b8198af64d88872558ca88a8ba5f', 0)
txout = TxOutput(to_satoshis(0.00000888), P2pkhAddress('myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4').to_script_pub_key())
tx = Transaction([txin], [txout])
alice_sig = alice_sk.sign_input(tx, 0, redeem_script)
bob_sig = bob_sk.sign_input(tx, 0, redeem_script)
txin.script_sig = Script(['OP_0', alice_sig, bob_sig, redeem_script.to_hex()])
signed_tx = tx.serialize()
print(f"Transaction size: {tx.get_size()} bytes")

Multisig Stack Execution (Brief)#

TXID: e68bef534c7536300c3ae5ccd0f79e031cab29d262380a37269151e8ba0fd4e0

Phase 1 (hash verification): ScriptSig pushes OP_0 + alice_sig + bob_sig + redeem_script; the locking script runs OP_HASH160, pushes the expected hash, and OP_EQUAL confirms the match. The OP_0 is there to absorb OP_CHECKMULTISIG’s off-by-one bug, which pops one extra item.

P2SH transition: Bitcoin Core recognizes the OP_HASH160 <hash> OP_EQUAL pattern, resets the stack to its post-scriptSig state (discarding the TRUE), and runs the revealed redeem script on a clean stack.

Phase 2 (redeem script): OP_2, the three public keys, OP_3, then OP_CHECKMULTISIG verifies Alice’s and Bob’s signatures against their keys, satisfies the 2-of-3 threshold, and leaves 1 (true).

3.3 Time Locks with CSV#

CheckSequenceVerify (CSV) enforces a relative time lock: spending is delayed by a number of blocks counted from when the UTXO was created. Below is a real testnet implementation.

A 3-Block Time Lock#

Transaction ID: 34f5bf0cf328d77059b5674e71442ded8cdcfc723d0136733e0dbf180861906f

This transaction combines a CSV time lock with a P2PKH signature check in one redeem script — the shape used for inheritance and escrow conditions.

# Example 3: Create CSV timelock script
# Reference: code/chapter03/03_create_csv_script.py

sk_csv = PrivateKey('cRxebG1hY6vVgS9CSLNaEbEJaXkpZvc6nFeqqGT7v6gcW7MbzKNT')
pk_csv = sk_csv.get_public_key()
seq = Sequence(TYPE_RELATIVE_TIMELOCK, 3)
redeem_csv = Script([seq.for_script(), 'OP_CHECKSEQUENCEVERIFY', 'OP_DROP',
    'OP_DUP', 'OP_HASH160', pk_csv.get_address().to_hash160(), 'OP_EQUALVERIFY', 'OP_CHECKSIG'])
p2sh_csv = P2shAddress.from_script(redeem_csv)
print(f"P2SH Address: {p2sh_csv.to_string()}")
print(f"Time Lock: 3 blocks")

Key Functions#

Sequence(TYPE_RELATIVE_TIMELOCK, blocks): Creates a sequence object for relative block-based delays. The sequence value encodes the time constraint that OP_CHECKSEQUENCEVERIFY enforces.

seq.for_script(): Returns the sequence value formatted for use in script opcodes (pushes the delay value onto the stack).

seq.for_input_sequence(): Returns the sequence value for the transaction input’s sequence field, which CSV validates against.

# Example 4: Spend CSV timelock script
# Reference: code/chapter03/04_spend_csv_script.py

txin_csv = TxInput('34f5bf0cf328d77059b5674e71442ded8cdcfc723d0136733e0dbf180861906f', 0, sequence=seq.for_input_sequence())
txout_csv = TxOutput(to_satoshis(0.00001), P2pkhAddress('myYHJtG3cyoRseuTwvViGHgP2efAvZkYa4').to_script_pub_key())
tx_csv = Transaction([txin_csv], [txout_csv])
sig_csv = sk_csv.sign_input(tx_csv, 0, redeem_csv)
txin_csv.script_sig = Script([sig_csv, pk_csv.to_hex(), redeem_csv.to_hex()])
print(f"Transaction size: {tx_csv.get_size()} bytes")

CSV Stack Execution (Brief)#

Redeem script: OP_3 OP_CHECKSEQUENCEVERIFY OP_DROP followed by a standard P2PKH check.

Flow: PUSH 3 → OP_CHECKSEQUENCEVERIFY validates that the input’s sequence number ≥ 3 blocks since UTXO creation → OP_DROP removes the delay value → OP_DUP → OP_HASH160 → OP_EQUALVERIFY confirms the key hash → OP_CHECKSIG verifies the signature, leaving 1 (true).

If you spend before the delay expires, the transaction is rejected with non-BIP68-final, because nSequence < required_delay.

Where CSV is used: inheritance (funds become spendable by an heir after a set period of owner inactivity), escrow (a fallback path opens after a delay), and Lightning payment channels (CSV enforces settlement delays, giving each party a window to dispute an old state).

3.4 P2SH vs P2PKH: What P2SH Adds, and Where It Stops#

P2SH extends Bitcoin Script from single-signature authorization to multi-party and time-based conditions, while keeping the same compact address format. But it has a limit that motivates everything Taproot does next.

When a P2SH output is spent, the entire redeem script is revealed — every branch, whether or not it was taken. There’s no way to expose only the relevant path. So the structure is linear and fully visible: every signature path, time-lock clause, and fallback condition ends up on chain. And because the redeem script rides in the scriptSig, multisig and inheritance setups carry real size overhead, which means higher fees.

Taproot addresses both directly: complex scripts stay hidden until needed, committed into a tree where only the executed path is ever revealed. That is the thread the next chapters pick up.

Chapter Summary#

P2SH locks a script behind its hash and reveals it only at spending time. We built the two patterns that recur for the rest of the book:

  • MultisigOP_CHECKMULTISIG with a 2-of-3 redeem script, plus the OP_0 workaround for its off-by-one bug.

  • Time locksOP_CHECKSEQUENCEVERIFY for a relative delay, combined with a P2PKH check.

We traced both on the stack, including P2SH’s two-phase execution: verify the script’s hash, then reset the stack and run the revealed script.

One limitation matters most for what follows: P2SH reveals the entire redeem script when spent, every branch included. Taproot’s answer to exactly that — revealing only the branch you use — is what the rest of the book builds toward.

Next. Chapter 4 turns to SegWit: moving the witness out of the transaction body, which fixes malleability and sets up Taproot’s witness-based spending paths.