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.
Under the hood — the hard limits that guarantee a script halts
Section titled “Under the hood — the hard limits that guarantee a script halts”“No loops” bounds iteration, but Bitcoin also caps the raw dimensions of a script so that even a straight-line program can’t blow up a validator. These consensus limits are fixed numbers every node enforces identically (the figures below apply to pre-Taproot script; Taproot’s tapscript replaces the opcode and script-size caps with a size-based budget):
max size of one stack element : 520 bytesmax non-push opcodes / script : 201max items on the stack : 1,000max script length : 10,000 bytesTogether with the absence of loops, these caps mean the work to verify any script is bounded before you run it — there is no input that makes validation arbitrarily expensive. It’s the same instinct as forbidding loops, applied to size instead of time: a base layer that everyone must re-verify cannot afford a single script that costs too much to check.
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.
The architect’s lens
Section titled “The architect’s lens”Step back from the stack and answer the five questions that turn an implementer into an architect:
- Why does it exist? To express a coin’s spending condition as a tiny, deterministic program that every full node can run and agree on — making “is this spend valid?” a question with one objective, cheaply-computable answer.
- What problem does it solve? Programmable ownership without the danger of unbounded computation: no loops and no recursion mean every script provably halts in steps bounded by its own length, so validation cost is known before execution even begins.
- What are the trade-offs? Deliberately not Turing-complete — you give up loops, variables, and general expressiveness, and hard caps (520-byte stack elements, 201 non-push opcodes, 1,000 stack items, 10,000-byte scripts) bound size too. Rich stateful logic is simply out of reach.
- When is it the wrong design? When the application genuinely needs unbounded or stateful computation — Ethereum chose the opposite, allowing loops but metering “gas” per step and aborting when it runs out. For a base layer whose only job is to let strangers cheaply agree, “no surprises” beats “more expressive.”
- What breaks if I remove it? Allow loops and an attacker writes an infinite-loop lock that hangs
every validating node forever — a free, global denial-of-service — and the halting problem means no
node could even predict it in advance. The 2010 batch-disable of
OP_CAT,OP_MUL,OP_LSHIFTand others is this same instinct applied in real time: remove the attack surface rather than trust it.
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?
Show answers
- The program is a flat list processed left to right against a single last-in-first-out stack. Data pushes (a signature, key, number) go onto the stack; opcodes (
OP_…) pop some items, do something, and push the result back. The spend is authorized if, after the unlocking then locking script run, the top value is true (non-zero) and nothing failed. - Because every full node on Earth must execute every script and agree on the outcome, loops would be catastrophic: an infinite-loop lock would hang every validating node forever — a free, global denial-of-service — and run time would be unpredictable. Forbidding loops guarantees every script halts in a number of steps bounded by its own length, so validation cost is predictable and bounded before execution begins.
- Ethereum allows loops but charges “gas” per step and aborts when gas runs out. Bitcoin’s answer is blunter — don’t allow the dangerous construct at all — which for a base layer whose only job is to let strangers cheaply agree means “no surprises” beats “more expressive”: universal, cheap, guaranteed-to-finish verification.
- Run
<sig> <pubKey>then the lock:OP_DUPduplicates the key,OP_HASH160hashes it, the committed<pubKeyHash>is pushed, andOP_EQUALVERIFYchecks the two hashes match (aborting if not) — guaranteeing the right key.OP_CHECKSIGthen verifies the signature is valid for that key and this transaction — guaranteeing the right owner of that key. Ending withtrueauthorizes the spend. - The
…VERIFYsuffix checks a condition and aborts the whole script immediately if it fails, rather than leaving a true/false on the stack to handle later. It’s useful because it cleanly enforces a required condition mid-script without polluting the stack with a result to interpret afterward.