Bitcoin Script: the Stack Language
The locks and keys of the previous page are written in a tiny programming language called Script. It is unlike almost any language you’ve met: it has no variables, no functions, no loops, and it runs on a single stack of bytes. These omissions are not laziness — they are the whole point. This page explains why Bitcoin chose such a constrained language and then walks a real script through the stack, step by step, until it returns true.
A stack machine, not a CPU
Section titled “A stack machine, not a CPU”Script is stack-based. There’s one data structure — a last-in-first-out stack — and the program is a flat list of items processed left to right:
- Data pushes (a signature, a public key, a number) go onto the stack.
- Opcodes (
OP_…) pop some items off, do something, and push the result back.
program: <a> <b> OP_ADD
step stack (top on right) push <a> a push <b> a b OP_ADD (a+b) ← popped a and b, pushed their sumA transaction’s spend is authorized if, after running the unlocking script and then the locking script (see locking & unlocking), the stack’s top value is true (non-zero) and nothing failed along the way.
Deliberately NOT Turing-complete
Section titled “Deliberately NOT Turing-complete”The single most important design decision in Script is what it cannot do: it has no loops and no
unbounded recursion. There is OP_IF/OP_ELSE for branching, but no jump-backwards, no while.
Every script therefore runs in a number of steps bounded by its own length, and always halts.
Why give up that power? Because a decentralized ledger has a brutal constraint that a normal computer doesn’t: every full node on Earth must execute every script, and must agree on the outcome.
| If Script had loops… | Consequence |
|---|---|
| A script could run forever | The classic halting problem — no node could know in advance whether validation would terminate |
| An attacker writes an infinite-loop lock | Every validating node hangs forever trying to verify it — a free, global denial-of-service |
| Run time becomes unpredictable | Nodes couldn’t bound the cost of verifying a block |
By forbidding loops, Bitcoin guarantees that validation cost is predictable and bounded before execution even begins. Ethereum solved the same danger differently — it allows loops but charges “gas” per step and aborts when gas runs out. Bitcoin’s answer is blunter and simpler: don’t allow the dangerous construct in the first place. For a base layer whose only job is to let strangers cheaply agree, “no surprises” beats “more expressive.”
A starter vocabulary of opcodes
Section titled “A starter vocabulary of opcodes”You’ll recognize most real-world locks from just a handful of opcodes:
| Opcode | What it does |
|---|---|
OP_DUP | Duplicate the top stack item |
OP_HASH160 | Pop top item, push RIPEMD160(SHA256(item)) |
OP_EQUAL | Pop two items, push true if they’re equal |
OP_EQUALVERIFY | Same as OP_EQUAL but fail the script if not equal (no value left behind) |
OP_CHECKSIG | Pop a public key and a signature; push true if the signature is valid for this transaction |
OP_CHECKMULTISIG | Verify M-of-N signatures against N public keys |
The …VERIFY suffix is a common pattern: check a condition and abort immediately if it fails, rather
than leaving a true/false on the stack to deal with later.
Stepping through a P2PKH spend
Section titled “Stepping through a P2PKH spend”The most common legacy lock is Pay-to-Public-Key-Hash (P2PKH). The output’s locking script and the input’s unlocking script are:
scriptPubKey (lock): OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG scriptSig (unlock): <signature> <pubKey>We run the unlocking script first, then the locking script against the resulting stack. Watch the stack (top on the right):
action stack ───────────────────────────────────────────────── push <signature> sig push <pubKey> sig pubKey ── now the locking script runs ── OP_DUP sig pubKey pubKey OP_HASH160 sig pubKey hash(pubKey) push <pubKeyHash> sig pubKey hash(pubKey) pubKeyHash OP_EQUALVERIFY sig pubKey ← the two hashes matched; both popped, script continues (would abort if not) OP_CHECKSIG true ← sig is valid for pubKey & this txRead in plain English, the lock says: “Whoever spends me must (1) reveal a public key that hashes to
this committed value, and (2) provide a signature that key produced over this very transaction.”
OP_EQUALVERIFY enforces “right key”; OP_CHECKSIG enforces “right owner of that key.” End with
true on the stack → the spend is authorized.
The thread
Section titled “The thread”How does a stack language help untrusting strangers agree on one ledger? Because it makes “is this spend valid?” a question with one objective, cheaply-computable answer that every node reaches identically. No loops means every script halts; a fixed opcode set means every node computes byte-for-byte the same stack; a final true/false means there’s nothing to interpret or negotiate. Script deliberately gives up power so that verification stays deterministic, bounded, and universal — the bedrock properties strangers need to converge on a single ledger without trusting one another or any referee.
Check your understanding
Section titled “Check your understanding”- Describe how a stack machine executes
<data>items andOP_…opcodes. - Why is it a deliberate safety choice that Script has no loops? What attack does this prevent?
- How does Ethereum address the same danger differently, and why might Bitcoin prefer its blunter approach for a base layer?
- Walk the P2PKH script through the stack and explain what
OP_EQUALVERIFYandOP_CHECKSIGeach guarantee. - What does the
…VERIFYsuffix do, and why is that pattern useful?