Introduction

Bitcoin Script is the intentionally constrained, stack-based programming language that governs every Bitcoin transaction. Unlike Ethereum's Turing-complete EVM, Script is deliberately non-Turing-complete—it has no loops, no persistent state, and terminates deterministically. This design philosophy prioritizes security and predictability over expressiveness. Yet within these constraints lies a surprisingly powerful system that underpins multisig wallets, timelocks, HTLCs (Hash Time-Locked Contracts), and the Lightning Network.

This guide assumes you understand UTXO mechanics, transaction structure, and basic cryptographic primitives. We'll go deep into the execution model, opcode categories, real script examples, and the edge cases that have historically caused consensus bugs.

The Execution Model: Dual-Stack Architecture

Bitcoin Script uses two stacks:

  • Main stack: Where all primary operations occur
  • Alt stack: A secondary stack accessible via OP_TOALTSTACK and OP_FROMALTSTACK

Transaction validation works by concatenating (conceptually) the scriptSig (unlocking script) and scriptPubKey (locking script). Post-SegWit, the witness data is separated, but the logical evaluation remains:

1. The scriptSig is executed, pushing data onto the stack

2. The stack state carries over into scriptPubKey execution

3. If the top stack element is truthy (non-zero) and execution didn't abort, the transaction is valid

> Critical note: Since BIP 16 (P2SH), the scripts are no longer literally concatenated. The scriptSig is evaluated first in isolation, then the stack is copied and the scriptPubKey is evaluated. This fixed the OP_RETURN concatenation attack where a malicious scriptSig could manipulate scriptPubKey execution.

Opcode Taxonomy

Bitcoin Script contains ~186 defined opcodes (0x00–0xFF), though many are disabled or reserved. Key categories:

Constants & Push Operations

  • OP_0 (0x00): Pushes empty byte array (falsy)
  • OP_1 through OP_16: Push the corresponding number
  • OP_PUSHBYTES_1 through OP_PUSHBYTES_75: Push N bytes of data
  • OP_PUSHDATA1/2/4: Push data with 1/2/4-byte length prefix

Stack Manipulation

  • OP_DUP, OP_DROP, OP_SWAP, OP_OVER, OP_ROT
  • OP_IFDUP: Duplicates top element only if it's truthy
  • OP_DEPTH: Pushes the current stack size

Cryptographic Operations

  • OP_SHA256, OP_HASH160 (SHA256 + RIPEMD160), OP_HASH256 (double SHA256)
  • OP_CHECKSIG: Verifies an ECDSA/Schnorr signature against a public key
  • OP_CHECKMULTISIG: M-of-N signature verification (with the infamous off-by-one bug)
  • OP_CHECKSIGADD (Tapscript): Replaces OP_CHECKMULTISIG with cleaner batch validation

Flow Control

  • OP_IF / OP_NOTIF / OP_ELSE / OP_ENDIF: Conditional execution
  • OP_VERIFY: Aborts if top stack value is falsy
  • OP_RETURN: Immediately marks transaction as invalid (used for data embedding)

Timelocks

  • OP_CHECKLOCKTIMEVERIFY (CLTV, BIP 65): Absolute timelock
  • OP_CHECKSEQUENCEVERIFY (CSV, BIP 112): Relative timelock

Standard Transaction Script Templates

P2PKH (Pay-to-Public-Key-Hash)

`

scriptPubKey: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG

scriptSig:

`

Execution trace:

1. Push , → stack: [sig, pubKey]

2. OP_DUP[sig, pubKey, pubKey]

3. OP_HASH160[sig, pubKey, hash(pubKey)]

4. Push [sig, pubKey, hash(pubKey), pubKeyHash]

5. OP_EQUALVERIFY → verifies hashes match → [sig, pubKey]

6. OP_CHECKSIG → verifies signature → [true]

P2SH (Pay-to-Script-Hash, BIP 16)

`

scriptPubKey: OP_HASH160 OP_EQUAL

scriptSig: <...inputs...>

`

The redeemScript is deserialized and executed as a second validation pass. This enables complex scripts while keeping the on-chain scriptPubKey compact.

P2WSH (Pay-to-Witness-Script-Hash, BIP 141)

`

scriptPubKey: OP_0 <32-byte-SHA256-of-witnessScript>

witness: <...inputs...>

`

SegWit v0 moves the unlocking data to the witness field, fixing transaction malleability and enabling fee discounts via the weight system (witness bytes count as 0.25 vbytes).

P2TR (Pay-to-Taproot, BIP 341/342)

`

scriptPubKey: OP_1 <32-byte-tweaked-pubkey>

`

Taproot introduces two spending paths:

  • Key path: A single Schnorr signature against the tweaked public key (indistinguishable from a simple payment)
  • Script path: Reveal a Merkle branch to a specific leaf script (executed under Tapscript rules)

Tapscript replaces OP_CHECKMULTISIG with OP_CHECKSIGADD, uses Schnorr signatures exclusively, and introduces the OP_SUCCESS opcode range for future soft-fork upgradability.

Critical Edge Cases & Historical Bugs

The OP_CHECKMULTISIG Off-By-One Bug

OP_CHECKMULTISIG consumes M+N+2 stack elements instead of M+N+1 due to a bug in the original implementation. An extra dummy element (must be OP_0 per BIP 147 NULLDUMMY rule) is required. Tapscript eliminates this entirely.

Script Number Encoding

Script integers use variable-length little-endian sign-magnitude encoding, not two's complement. The empty byte array is zero. 0x80 is negative zero (falsy). Numbers are limited to 4 bytes in consensus (range: -2^31+1 to 2^31-1), though OP_CHECKLOCKTIMEVERIFY interprets 5-byte values.

MINIMALDATA & MINIMALIF

Standardness rules (BIP 62, then policy) require:

  • Data pushes use the smallest possible opcode
  • OP_IF/OP_NOTIF consume only OP_0 or OP_1 (not arbitrary truthy values)

These are consensus rules in SegWit v0+ but only policy rules for legacy scripts.

OP_CODESEPARATOR

Rarely used but still consensus-valid. In legacy scripts, it modifies the subscript used for signature hashing. In Tapscript, it has a well-defined role: it updates the "last executed codeseparator position" included in the signature hash, enabling fine-grained signature delegation.

Resource Limits

  • Script size: 10,000 bytes (legacy/SegWit v0), no limit in Tapscript (bounded by block weight)
  • Stack element size: 520 bytes (legacy/SegWit v0), 520 bytes in Tapscript
  • Opcode count: 201 non-push opcodes per script (legacy/SegWit v0), replaced by sigops budget in Tapscript (50 sigops base + 1 per 50 witness bytes)
  • Stack depth: 1,000 elements

Practical Implications for 2025

  • Tapscript's extensibility via OP_SUCCESS opcodes is the pathway for covenants (OP_CTV, OP_CAT revival discussions, OP_VAULT)
  • BitVM exploits Script's existing opcodes to simulate arbitrary computation through fraud proofs, enabling trust-minimized bridges
  • Lightning Network HTLCs rely on OP_IF branching combined with OP_CHECKSIG, OP_HASH160 preimage checks, and OP_CSV timelocks
  • Understanding Script at this level is essential for auditing Taproot-based protocols, Ark, RGB, and other overlay systems

Conclusion

Bitcoin Script's apparent simplicity conceals a nuanced system with decades of accumulated consensus rules, edge cases, and deliberate constraints. Its non-Turing-complete design is not a limitation but a feature—enabling formal reasoning about transaction validity without the halting problem. As Bitcoin's protocol layer evolves through Tapscript extensions and proposed covenants, deep Script literacy becomes not optional but essential for advanced Bitcoin developers.