TeachMeBitcoin

Flow Control Script Attack Vectors

From TeachMeBitcoin, the free encyclopedia Reading time: 7 min

15. Flow Control Script Attack Vectors

Overview

The flow control opcodes (OP_IF, OP_NOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_TOALTSTACK, OP_FROMALTSTACK) introduce powerful expressiveness into Bitcoin Script, but also open up a set of security vulnerabilities and attack surface areas. Understanding these attack vectors is essential for anyone writing, auditing, or deploying Bitcoin scripts in production.

Attack Vector 1: Malleability via Conditional Branch Selection

Prior to SegWit, the scriptSig (unlocking script) was included in the transaction hash used for transaction IDs. This meant that a third party could potentially modify which branch of an OP_IF was taken by changing the condition value, altering the transaction ID without invalidating it.

; Vulnerable pre-SegWit script:
; scriptPubKey:
OP_IF
  <pubkeyA> OP_CHECKSIG
OP_ELSE
  <pubkeyB> OP_CHECKSIG
OP_ENDIF

; scriptSig option 1 (Alice path):
<sig_A> OP_1

; scriptSig option 2 (Bob path):
<sig_B> OP_0

; Attack: a malicious relay node could, in theory, 
; substitute OP_1 for a non-minimal truthy value (e.g., 0x02)
; changing the txid without invalidating the spend

Mitigation: SegWit moves the witness outside the transaction hash, eliminating scriptSig malleability. Additionally, the minimal push rule in SegWit witness scripts (requiring exactly 0x00 or 0x01 for boolean conditions in OP_IF) closes this vector entirely.

; SegWit fix: non-minimal booleans fail in witness scripts
; 0x02 as OP_IF condition → SCRIPT_ERR_MINIMALIF

Attack Vector 2: MINIMALIF Bypass Attempts

The MINIMALIF rule (enforced in SegWit v0 and Tapscript) requires that the argument to OP_IF/OP_NOTIF be exactly 0x (false) or 0x01 (true). Attackers might attempt to exploit scripts that accept non-minimal booleans in legacy contexts.

; Legacy script (pre-SegWit) — accepts any non-zero as true:
OP_IF
  ...
OP_ENDIF

; An attacker can spend the IF branch with any non-zero value:
0x02   ; truthy, but non-minimal
0xff   ; truthy, but non-minimal
0x0001 ; truthy, but non-minimal (multi-byte)

; In SegWit witness scripts, this causes immediate failure:
; SCRIPT_ERR_MINIMALIF

Impact: In legacy scripts, non-minimal booleans are valid and can be used to obscure which branch was intended, complicating off-chain analysis and potentially enabling certain covert channel attacks.

Attack Vector 3: Alt Stack Imbalance

If OP_TOALTSTACK is called without a corresponding OP_FROMALTSTACK, values remain on the alt stack. While this does not directly cause security failures (leftover alt stack items are simply ignored at script completion in most implementations), it can indicate bugs that might be exploitable in future script versions or unusual execution environments.

; Problematic script: alt stack not cleaned up
<value>
OP_TOALTSTACK
<condition>
OP_IF
  OP_FROMALTSTACK   ; retrieved in the true branch
  OP_CHECKSIG
OP_ELSE
  ; FORGOT OP_FROMALTSTACK in else branch!
  ; Alt stack still has <value>
  OP_DROP
OP_ENDIF

; If false branch is taken: alt stack has orphaned item
; Script may still succeed, but alt stack is non-empty

Mitigation: Rigorous static analysis and script testing across all execution paths. Script authors should use tools that trace both main stack and alt stack through every branch.

Attack Vector 4: Conditional Script Inflation

In some SegWit contexts, the number of opcodes executed is counted and limited. By crafting deeply nested OP_IF structures, an attacker might attempt to create scripts that are expensive to validate even if they fail quickly.

; Deeply nested failing script — expensive to parse:
OP_0
OP_IF
  OP_0
  OP_IF
    OP_0
    OP_IF
      ... (100 levels deep) ...
    OP_ENDIF
  OP_ENDIF
OP_ENDIF

; Pre-BIP 342: such scripts required full parsing
; even though they would always fail immediately

Mitigation: Bitcoin Core enforces a maximum of 201 opcodes per non-SegWit script and has per-input script evaluation cost limits. BIP 342 (Tapscript) introduces per-opcode cost accounting to prevent this.

Attack Vector 5: Unspendable Scripts with Hidden Conditions

An attacker or buggy script author might create a script that appears to have multiple spending paths but is actually always-false due to impossible conditions:

; Script that looks valid but is unspendable:
OP_DUP OP_HASH160 <hash_A> OP_EQUALVERIFY
OP_IF
  ; This branch requires the pubkey to hash to hash_A AND be <pubkey_B>
  ; But hash_A is not HASH160(pubkey_B) — contradiction!
  <pubkey_B> OP_CHECKSIG
OP_ELSE
  ; Else branch also has impossible condition
  <hash_B> OP_EQUAL OP_VERIFY
  <pubkey_C> OP_CHECKSIG
OP_ENDIF
; Funds sent here are permanently locked

Mitigation: Never send funds to a script without first verifying all spending paths work by testing them against a test transaction. Use script analysis tools or formal verification frameworks.

Attack Vector 6: OP_RETURN Data Exfiltration

While OP_RETURN is not directly a flow control opcode, it interacts with script analysis tools. Malicious scripts might use OP_RETURN outputs to exfiltrate data (e.g., partial private key material) encoded in the data field. This is a covert channel attack where the attacker who controls a signing device can leak key material through OP_RETURN data.

; Malicious signing device creates transaction with:
; Output 0: Normal P2PKH payment
; Output 1: OP_RETURN <partial_private_key_bits>

; The OP_RETURN output encodes stolen key material
; that is broadcast to the entire Bitcoin network
; and stored permanently on-chain

OP_RETURN 0x<leaked_key_bytes>

Mitigation: Hardware wallets and signing devices should implement strict policies about what OP_RETURN data is permissible. Some implementations allow users to inspect and approve all OP_RETURN outputs before signing.

Attack Vector 7: Script Path Confusion in Taproot

In Taproot, the key path spend and script path spend are selected by the structure of the witness. A poorly designed wallet might accept a signature for the wrong spending path or confuse which Tapscript leaf is being executed.

; Taproot output with two leaves:
; Leaf A: Alice key + short timelock
; Leaf B: Bob key (with no timelock)

; If a wallet incorrectly validates leaf B's spend
; as if it were leaf A, Bob might spend before the timelock
; the user intended to enforce

; Leaf A script:
<alice_pubkey> OP_CHECKSIGVERIFY
<short_locktime> OP_CSV

; Leaf B script:
<bob_pubkey> OP_CHECKSIG

; The two scripts look similar but have critically different conditions

Mitigation: Wallet implementations must carefully track which Tapscript leaf is being spent, verify the control block's Merkle path, and ensure the correct script is being evaluated before presenting for user approval.

Attack Vector 8: Stack Depth Overflow

Bitcoin's combined main+alt stack is limited to 1000 elements. A crafted script might attempt to push elements onto both stacks to exhaust the limit, causing a script that should be valid to fail:

; Adversarial script bloating the alt stack:
<v1> OP_TOALTSTACK
<v2> OP_TOALTSTACK
... (repeated 998 more times)
<v1000> OP_TOALTSTACK   ; alt stack now has 1000 items
<new_value>             ; any further push → stack overflow

In practice, this requires the attacker to control script execution, so the real risk is in contract designs where the spender can control how many values are pushed before the critical verification steps.

Mitigation: Script authors should compute the maximum possible stack depth across all execution paths and verify it stays well below 1000. Formal script analysis tools can automate this.

Attack Vector 9: Time-of-Check / Time-of-Use in Multi-Branch Scripts

In scripts with multiple spending paths (time-based and condition-based), there is a window between when a condition is evaluated on-chain and when the transaction is confirmed. An attacker might attempt to race between paths.

; Race condition example:
OP_IF
  ; Path A: requires hash preimage (immediate)
  <hash> OP_SHA256 OP_EQUALVERIFY <pubkey> OP_CHECKSIG
OP_ELSE
  ; Path B: requires timelock (delayed)
  <locktime> OP_CLTV OP_DROP <pubkey> OP_CHECKSIG
OP_ENDIF

; If the timelock expires between tx broadcast and confirmation,
; both paths might be technically valid simultaneously
; Miners choose which to include based on fee incentives

Mitigation: Design scripts so that the timeout paths are set conservatively far in the future, giving sufficient confirmation time for the "hot path" to confirm first. Lightning Network protocols use this principle extensively with HTLC expiry windows.

Summary of Mitigations

Attack Type               | Primary Mitigation
--------------------------|--------------------------------
Malleability              | Use SegWit (BIP 141+)
MINIMALIF bypass          | Use SegWit / Tapscript
Alt stack imbalance       | Static analysis, testing
Script inflation DoS      | Opcode limits (BIP 342 cost)
Unspendable scripts       | Pre-deployment testing
OP_RETURN data exfil      | Hardware wallet output policy
Taproot path confusion    | Strict wallet implementation
Stack overflow            | Maximum depth analysis
TOCTOU race               | Conservative timelock values
☕ Help support TeachMeBitcoin

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:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!