You've just finished writing a multisig spending condition. Two-of-three keys, a timelock, a hash preimage check. You tested the happy path. Everything passes. Then someone tries to claim the output with a perfectly valid signature set, in a slightly different order than you expected, and the transaction fails silently at the mempool. Or worse: it succeeds when it shouldn't.
This is the quiet danger inside Bitcoin Script's stack evaluation order. It bites developers more than almost any other category of bug, and it does so without ceremony.
A Stack That Only Reads Forward
Bitcoin Script is not a general programming language. No loops, no named variables, no function calls. It is a stack machine: instructions execute left to right, top to bottom, consuming and producing values on a last-in-first-out stack, where every opcode either pushes data onto that stack or pops data off it and acts on the result.
Position is meaning. The order in which you push data determines which opcode sees which value, and swapping two pushes means swapping the arguments to every operation that follows. In most programming languages, `verify(a, b)` and `verify(b, a)` might be equivalent. In Script, `OP_CHECKSIG` pops the top two stack items as (signature, pubkey), strictly in that order. Push them reversed and you have handed a pubkey to the signature slot and a signature to the pubkey slot. The check fails. No error message. Just a `0` on the stack.
That failure mode is at least benign. The output stays unspendable by the wrong party. The genuinely dangerous cases involve conditional opcodes.
Where `OP_IF` Turns Ordering Into a Security Question
`OP_IF` pops the top stack item and branches on whether it's truthy. Everything that sets up the condition for that branch must therefore be pushed before `OP_IF` is reached. Obvious enough in isolation. It stops being obvious the moment you combine multiple conditions, which is precisely when developers stop checking.
Consider a contract with two spending paths: a 2-of-2 multisig for the cooperative close, and a single-key timelock for the unilateral exit. A common pattern:
``` OP_IF <Alice_pubkey> <Bob_pubkey> OP_2 OP_CHECKMULTISIG OP_ELSE <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <Alice_pubkey> OP_CHECKSIG OP_ENDIF ```
The witness must push a flag, `1` or `0`, to tell the script which branch to take. That flag sits at the top of the stack when `OP_IF` fires. But `OP_CHECKMULTISIG` carries a well-documented off-by-one quirk: it pops one extra item from the stack beyond the signatures and pubkeys it actually needs. That extra pop is supposed to consume a dummy `OP_0` the spender pushes at the bottom of their witness stack.
Forget the dummy element, or place the branch flag in a position where the off-by-one consumes it instead, and the branch condition is gone. The script may execute the wrong branch entirely with a witness that looks valid. In a two-party channel contract, that could mean Alice's unilateral exit key satisfies the cooperative-close branch if the stack happens to be arranged permissively.
This is not hypothetical. The `OP_CHECKMULTISIG` dummy-element issue was significant enough that BIP 147, deployed as part of Segregated Witness, made the dummy element consensus-enforced to be exactly `OP_0`, precisely to foreclose the stack manipulation that the earlier ambiguity permitted. The fix is in the record; the lesson is that the protocol's own deployment history is, in effect, a catalog of ordering hazards.
A Worked Scenario Worth Sitting With
Two developers, call them Priya and Dmitri, build a payment contract for a six-month escrow. Priya writes the locking script. Dmitri writes the wallet code that constructs the spending transaction.
Priya's script requires a hash preimage to prove knowledge of a secret, then a signature from the recipient's key:
``` OP_SHA256 <expected_hash> OP_EQUALVERIFY <Recipient_pubkey> OP_CHECKSIG ```
Dmitri's wallet pushes witness data in this order: `[signature, preimage]`. On the stack, `signature` sits at the bottom and `preimage` sits on top. `OP_SHA256` pops `preimage`, hashes it, pushes the result. `OP_EQUALVERIFY` checks equality. Passes. `OP_CHECKSIG` pops `signature`. Correct. Works.
Three months later, Dmitri updates the wallet to normalize witness data alphabetically for a different contract. Now the order is `[preimage, signature]`. The stack flips. `OP_SHA256` pops `signature` (a 71-byte DER blob), hashes it, gets a completely wrong value. `OP_EQUALVERIFY` fails. The honest recipient cannot claim their funds until someone diagnoses the wallet bug. The 0.5 BTC sits locked for weeks.
No theft. Just a very expensive, very embarrassing ordering mistake. The script did exactly what it was told.
The Miniscript Answer, and Its Limits
Miniscript, a structured language for writing Bitcoin Script that compiles down to valid opcodes, was designed specifically to make evaluation order a solved problem rather than a per-developer hazard. You describe spending conditions in a composable, human-readable policy language; the compiler handles the stack choreography. It also performs static analysis to prove the script satisfies certain properties: no unexpected items left on the stack, no ambiguous satisfaction paths, no malleable witnesses.
For most multisig and timelock combinations, Miniscript is the right answer. Full stop. Developers who insist on reasoning through stack state manually for standard constructions, when a formally verified compiler exists, are not being rigorous; they are being inefficient.
But Miniscript does not cover every opcode. `OP_CAT`, being discussed for potential re-enablement via a soft fork, covenant opcodes proposed in various BIPs, and any script that reaches beyond Miniscript's supported fragment types still require manual stack reasoning. At that frontier, ordering bugs return, harder now because the scripts are more novel and less reviewed by anyone other than their authors.
There is also what Miniscript cannot statically verify: runtime values. If a contract's branch condition depends on data known only at spend time, a well-formed script can still fail if the spender constructs the witness in an unexpected order. The script is valid. The witness is malformed. The node rejects it. The developer, staring at a structurally correct script, is confused.
One further note for anyone writing new contracts: Tapscript, the Script version used in Taproot outputs, replaced `OP_CHECKMULTISIG` with `OP_CHECKSIGADD`, eliminating the dummy-element problem at the design level. Taproot's script path is the cleaner surface, and there is no good reason to default to legacy script for new work.
The Real Discipline Here
Stack-based evaluation order is not a flaw in Bitcoin's design. It is a deliberate constraint that makes Script simple enough to analyze formally, like a legal instrument stripped of ambiguous pronouns, which is exactly what you want in a system that secures value with no recourse on error. The bugs do not come from the model. They come from treating Script as though named variables provide the safety that position alone must provide.
So here is the question worth sitting with before you ship any locking script: when did you last trace every possible witness stack manually, item by item, opcode by opcode, before opening a compiler?
The developers who avoid these bugs share one habit. After writing any locking script, they draw the stack state as a column of boxes and redraw it after each instruction, for every spending path, before touching anything else. It is tedious in the way that reading a contract twice before signing is tedious. It takes twenty minutes for a moderately complex script. It has kept more than a few contracts from a very permanent kind of broken, the kind where the funds are not stolen but simply unreachable, which is its own category of loss.