What the interpreter actually checks when Tapscript finishes
Your spending condition is written. The witness satisfies it. The script runs clean, no opcode failure, nothing blows up. You lean back. Then the interpreter turns around and audits what you left on the stack.
That post-execution check is the clean stack rule, and it changed in a subtle but load-bearing way when Taproot activated. Legacy Script enforces a strict version: exactly one item remaining, and it must be truthy. Tapscript enforces something slightly looser in wording but tighter in practice, and the more important shift is structural. The interpreter's job, in that final moment, is simply to count items and read the top one. Everything else is already decided.
Why legacy Script's rule was stricter than you think
In legacy Pay-to-Script-Hash and bare Script, the clean stack rule operated on two levels. First, inside the script itself: exactly one stack item could remain after execution. Second, a policy layer (BIP62, never fully deployed) wanted to ban malleable witness elements. Those two goals got tangled together in ways that gave protocol designers genuine headaches.
Here's a scenario that makes the problem concrete. Alice and Bob both deploy two-of-three multisig wallets using P2SH. Alice's wallet software appends a trailing OP_DROP after OP_CHECKMULTISIG to tidy up the dummy element that multisig historically leaves on the stack. Bob's does not. Both wallets spend successfully. Bob's transactions, though, carry a well-known malleability exposure: a third party can flip that dummy element without invalidating a single signature. The clean stack rule, as deployed in legacy Script, didn't prevent this. It only confirmed that something truthy sat on top.
That dummy element problem is precisely why OP_CHECKMULTISIG was replaced in Tapscript by OP_CHECKSIGADD. No dummy element. No residue. The clean stack rule got simpler because the stack itself got cleaner, and I think that substitution is one of the most underappreciated improvements in the whole Taproot package.
The Tapscript execution path, step by step
When a transaction input spends a Taproot output via a script path (not the key path), the interpreter walks a specific sequence before it touches your script:
- Verify the control block and compute the tagged hash of the script, checking it against the tweaked internal key.
- Identify the leaf version. Version `0xc0` is Tapscript. Unknown versions succeed immediately, which is how future soft forks stay non-breaking.
- Strip the annex from the witness stack if present (the annex is the final witness element starting with `0x50`).
- The remaining witness elements become the initial stack. The script itself is the last item in the control block witness field.
- Execute the script opcode by opcode.
After step 5, the clean stack check runs. Empty stack: fail. More than one item: fail. Exactly one item that is an empty byte vector: fail. Exactly one item that is non-empty: succeed.
Pause on that last condition. An empty byte vector is Bitcoin Script's canonical false. A non-empty byte vector is true, even `0x00 0x00`, a two-byte vector of zeros, because length is what counts. The clean stack check is really just asking whether your script left a single true value behind. That's it.
To make this mechanical: suppose you're writing a Tapscript leaf that implements a hash preimage lock, where the spender must provide a value whose SHA256 equals a committed digest. Your script is `OP_SHA256 <digest> OP_EQUAL`. The witness stack starts with `[preimage]`. After OP_SHA256, the stack holds `[hash_of_preimage]`. After pushing `<digest>`, it holds `[hash_of_preimage, digest]`. After OP_EQUAL, the stack is either `[0x01]` on a match or `[0x]` (empty byte vector) on a miss. One item either way. The clean stack check then passes or fails on exactly that single result. Tidy, and impossible to game with extra witness data.
The soft-fork escape hatch and its implications
Tapscript introduced something legacy Script never had: unknown opcode success. If the interpreter hits an opcode outside the defined Tapscript set, it does not fail. It succeeds immediately and halts. The stack at that point can hold anything.
Does the clean stack rule apply? No. Execution that halts at an unknown opcode is treated as unconditional success, and the post-execution stack check is skipped entirely. This is the mechanism by which future opcodes, proposed constructs like OP_VAULT or OP_CHECKTEMPLATEVERIFY variants, can be introduced as soft forks. Old nodes see an unknown opcode, pass the script, and upgraded nodes enforce the new semantics.
For script authors, this has a practical edge case worth knowing. If you accidentally include a byte sequence mapping to a currently-undefined opcode, your script may pass on nodes that don't understand the new semantics, in ways you never intended. On a live network where most nodes have upgraded, fine. In a test environment running older software, you may see surprising successes that vanish once you upgrade. The interpreter is, in this narrow sense, more permissive than legacy Script ever was. That permissiveness is structural, not a bug, but it rewards careful version tracking.
The thing most analyses skip over
Most commentary on the clean stack rule frames it as preventing junk witness data from bloating the stack. Accurate, but it misses the point.
The deeper purpose is non-malleability. Allow two items to remain after execution and a third party can potentially add or remove witness elements that don't affect the top item's truthiness but do change the transaction's weight and therefore its txid. Segwit moved witness data outside the txid calculation, which already eliminated the worst of it, but the clean stack rule closes a remaining gap. Think of it as a one-in, one-out turnstile at the exit: you cannot satisfy a script with a different-length witness stack than the one the author intended, because any extra items trigger an immediate failure.
If you're building covenant protocols, payment channels, or multi-party signing schemes on Tapscript, this is the guarantee you are quietly relying on every time you commit to a script leaf. The stack ends with one item. That item tells you everything. The story doesn't have a second draft.