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_TOALTSTACKandOP_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_1throughOP_16: Push the corresponding numberOP_PUSHBYTES_1throughOP_PUSHBYTES_75: Push N bytes of dataOP_PUSHDATA1/2/4: Push data with 1/2/4-byte length prefix
Stack Manipulation
OP_DUP,OP_DROP,OP_SWAP,OP_OVER,OP_ROTOP_IFDUP: Duplicates top element only if it's truthyOP_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 keyOP_CHECKMULTISIG: M-of-N signature verification (with the infamous off-by-one bug)OP_CHECKSIGADD(Tapscript): ReplacesOP_CHECKMULTISIGwith cleaner batch validation
Flow Control
OP_IF/OP_NOTIF/OP_ELSE/OP_ENDIF: Conditional executionOP_VERIFY: Aborts if top stack value is falsyOP_RETURN: Immediately marks transaction as invalid (used for data embedding)
Timelocks
OP_CHECKLOCKTIMEVERIFY(CLTV, BIP 65): Absolute timelockOP_CHECKSEQUENCEVERIFY(CSV, BIP 112): Relative timelock
Standard Transaction Script Templates
P2PKH (Pay-to-Public-Key-Hash)
`
scriptPubKey: OP_DUP OP_HASH160
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
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_NOTIFconsume onlyOP_0orOP_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_SUCCESSopcodes is the pathway for covenants (OP_CTV,OP_CATrevival 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_IFbranching combined withOP_CHECKSIG,OP_HASH160preimage checks, andOP_CSVtimelocks - 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.