# zebrad has consensus divergence via P2SH sigop undercount in pure-Rust disabled-opcode parser

Zebra's pure-Rust P2SH sigop counter stops at disabled opcodes, undercounting sigops so Zebra accepts blocks zcashd rejects, enabling a chain split.

- Payout: Research
- Severity: critical
- Program: Zebra
- Found by: samsulselfut
- Disclosed: 2026-07-02
- CVE: CVE-2026-52735
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-gf9r-m956-97qx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gf9r-m956-97qx
- Tags: area:web3 domain:web3 impact:dos impact:integrity kind:research platform:ghsa sev:critical tech:rust technique:parser-differential technique:source-review vuln:business-logic vuln:dos

**Target:** zebrad consensus block validation / zebra-script P2SH sigop counter
**Root cause:** P2SH sigop counting uses a pure-Rust parser whose try_fold aborts on disabled opcodes (Err(Disabled)), returning a partial count, while zcashd's static GetSigOpCount counts through disabled opcodes.
**Steps:**
1. Craft a P2SH redeem script `[0xab, OP_CHECKMULTISIG x 50]` (disabled opcode followed by sigop-bearing opcodes).
2. Broadcast transactions spending P2SH outputs with such redeem scripts; Zebra counts 0 sigops, zcashd counts 1,000 per input.
3. Across 21 inputs in one block, Zebra computes 0 while zcashd computes 21,000, crossing MAX_BLOCK_SIGOPS = 20,000.
4. A Zebra miner includes the transactions; Zebra validators accept the block, zcashd validators reject with `bad-blk-sigops`, splitting the chain.
**How found:** Code comparison: noticed the legacy sigop counter at lib.rs:282-289 uses C++ FFI while the P2SH counter at lib.rs:399 uses pure-Rust sig_op_count; traced zcash_script's try_fold early-termination on Err(Disabled) versus zcashd's GetOp2/GetSigOpCount which count through disabled opcodes.
**Exploitation:** Attacker needs no mining capability, only transaction fees. Broadcasting P2SH-spending transactions with disabled-opcode redeem scripts causes any Zebra miner (~30% hashrate) to build a block that Zebra accepts but zcashd rejects, diverging the network; every subsequent block on the Zebra tip inherits the split.
**Escalation:** Single block divergence propagates to all descendant blocks extending the Zebra-side tip, permanently forking the network.
**Insight:** When a reimplementation mixes FFI and pure-Rust paths for the same consensus rule, diff the two paths' error handling: early-return on parse errors silently truncates counts, and any consensus-critical counter must match the reference's treatment of invalid/disabled opcodes.

## Proof of concept (copied exactly from the original report)

**Redeem script with disabled opcode then 50 CHECKMULTISIG**

```
A redeem script of `[0xab, OP_CHECKMULTISIG x 50]` produces: Zebra = 0 sigops, zcashd = 1,000 sigops. Across 21 inputs in a block, Zebra computes 0 while zcashd computes 21,000, crossing the `MAX_BLOCK_SIGOPS = 20,000` threshold on one side only.
```

