How Conditionals Work on the Stack
6. How Conditionals Work on the Stack
Overview
Understanding how OP_IF, OP_NOTIF, OP_ELSE, and OP_ENDIF interact with the stack at every step is essential for writing correct Bitcoin scripts. This section provides a deep, step-by-step trace of stack evolution through conditional execution.
The Two Stacks
Bitcoin Script actually operates with two stacks:
-
Main stack — where most operations take place.
-
Alt stack — an auxiliary stack (covered in sections 12–14).
Conditional opcodes only interact with the main stack (to pop the condition value). The execution state is tracked in a separate internal structure (vfExec), not on either stack.
Step-by-Step Stack Trace: OP_IF True Branch
Script: OP_1 OP_IF OP_DUP OP_ENDIF
Initial stack: []
Step 1 — OP_1:
Push 0x01
Stack: [ 0x01 ]
Step 2 — OP_IF:
Pop 0x01, condition = true
Stack: []
vfExec: [ true ]
Step 3 — OP_DUP (inside IF, vfExec all true → execute):
Duplicate top of stack... but stack is empty!
ERROR: OP_DUP on empty stack
; Fix: ensure proper stack setup before entering OP_IF
Corrected version:
Script: OP_2 OP_1 OP_IF OP_DUP OP_ENDIF
Initial stack: []
Step 1 — OP_2: Stack: [ 0x02 ]
Step 2 — OP_1: Stack: [ 0x02, 0x01 ]
Step 3 — OP_IF: pop 0x01, Stack: [ 0x02 ], vfExec: [true]
Step 4 — OP_DUP: duplicate 0x02, Stack: [ 0x02, 0x02 ]
Step 5 — OP_ENDIF: pop vfExec, Stack: [ 0x02, 0x02 ]
Final stack: [ 0x02, 0x02 ]
Step-by-Step Stack Trace: OP_IF False Branch (with OP_ELSE)
Script: <sig> <pubkey> OP_0 OP_IF OP_CHECKSIG OP_ELSE OP_DROP OP_DROP OP_0 OP_ENDIF
Initial witness stack: [ <sig>, <pubkey> ]
Step 1 — OP_0: Stack: [ <sig>, <pubkey>, 0x ]
Step 2 — OP_IF: pop 0x (false), vfExec: [false]
[OP_CHECKSIG skipped]
Step 3 — OP_ELSE: toggle vfExec → [true]
Step 4 — OP_DROP: pop <pubkey>, Stack: [ <sig> ]
Step 5 — OP_DROP: pop <sig>, Stack: []
Step 6 — OP_0: Stack: [ 0x ]
Step 7 — OP_ENDIF: pop vfExec
Final stack: [ 0x ] (script fails — top is false)
Condition Values: What Counts as True?
Bitcoin Script uses the following rules for boolean evaluation:
False (0x): empty byte array
False (0x00): single zero byte
False (0x8000): negative zero (sign bit set, all others zero)
True: ANY other byte sequence
In code terms:
def cast_to_bool(vch):
for i in range(len(vch)):
if vch[i] != 0:
# Non-zero found before last byte
if i == len(vch) - 1 and vch[i] == 0x80:
return False # negative zero
return True
return False # all zeros = false
Ensuring Stack Balance Across Branches
A critical design rule: both branches of an OP_IF/OP_ELSE block should leave the same net number of items on the stack. Violating this causes subtle bugs where the script passes or fails depending on which branch was taken, in unintended ways.
; BAD: branches leave different stack depths
OP_IF
OP_DUP OP_HASH160 ; net +1 item (hash of top)
OP_ELSE
OP_SHA256 ; net 0 items (replace top)
OP_ENDIF
; Stack depth now depends on which path was taken — unpredictable!
; GOOD: both branches leave same depth
OP_IF
OP_HASH160 ; replace top with 20-byte hash
OP_ELSE
OP_SHA256 ; replace top with 32-byte hash
OP_ENDIF
OP_EQUAL ; compare either result to expected value
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: