Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
9.3
CVE-2026-52735: Zebra's P2SH Sigop Counter Has Incorrect Block Validation
GHSA-gf9r-m956-97qx
CVE-2026-52735
GHSA-gf9r-m956-97qx
Summary
Zebra's block validation process has a bug that can cause it to accept blocks that other nodes may reject. This can lead to a chain split, where different nodes have different versions of the blockchain. If you run Zebra, you should update to a fixed version to prevent this issue.
What to do
- Update zebra-script to version 7.0.0.
- Update zebrad to version 4.5.0.
- Update conradoplg zebra-script to version 7.0.0.
- Update conradoplg zebrad to version 4.5.0.
Affected software
| Ecosystem | Vendor | Product | Affected versions |
|---|---|---|---|
| rust | – | zebra-script |
<= 6.0.1 Fix: upgrade to 7.0.0
|
| rust | – | zebrad |
<= 4.4.1 Fix: upgrade to 4.5.0
|
| crates.io | conradoplg | zebra-script |
< 7.0.0 Fix: upgrade to 7.0.0
|
| crates.io | conradoplg | zebrad |
< 4.5.0 Fix: upgrade to 4.5.0
|
Original title
zebrad has consensus divergence via P2SH sigop undercount in pure-Rust disabled-opcode parser
Original description
### Am I affected
You are affected if:
1. You run any version of `zebrad` up to and including `v4.4.1`.
2. Your node validates blocks on mainnet, testnet, or any network where both Zebra and zcashd nodes participate.
All default configurations are affected. No feature flags, non-default settings, or special build options are required.
### Summary
Zebra's P2SH sigop counter uses a pure-Rust code path that short-circuits on disabled opcodes (such as `OP_CODESEPARATOR`), returning a partial count of zero for any sigops following the disabled opcode. The reference implementation (zcashd) correctly counts through disabled opcodes in its static sigop analysis. This produces a consensus divergence: Zebra accepts blocks that zcashd rejects when the block-wide `MAX_BLOCK_SIGOPS = 20,000` threshold is crossed on one side but not the other.
An attacker can exploit this without mining capability. Broadcasting transactions that spend P2SH outputs with malicious redeem scripts is sufficient; any Zebra miner who includes those transactions in a block triggers a chain split between Zebra and zcashd validators.
### Details
The P2SH sigop counter at `zebra-script/src/lib.rs:399` calls `script::Code(redeemed_bytes).sig_op_count(true)`, which is a pure-Rust path through `zcash_script-0.4.4`. The legacy (non-P2SH) sigop counter at `lib.rs:282-289` correctly uses the C++ FFI via `interpreter.legacy_sigop_count_script()`. Only the P2SH path bypasses the FFI.
The Rust parser in `zcash_script-0.4.4/src/opcode/mod.rs:1247-1260` treats 16 disabled opcodes (0x7e through 0xab, including `OP_CAT`, `OP_SUBSTR`, `OP_AND`, `OP_OR`, `OP_XOR`, `OP_2MUL`, `OP_2DIV`, `OP_MUL`, `OP_DIV`, `OP_MOD`, `OP_LSHIFT`, `OP_RSHIFT`, and `OP_CODESEPARATOR`) as `Err(Error::Disabled(...))`. The `sig_op_count` function at `iter.rs:104-115` uses `try_fold`, which terminates on the first `Err` and returns the partial sum accumulated so far.
zcashd's `GetOp2` (`script.h:514-562`) returns `true` for all non-push opcodes including the disabled range. Its `GetSigOpCount(true)` (`script.cpp:152-174`) continues counting through disabled opcodes. zcashd rejects disabled opcodes at execution time in the interpreter, not during static sigop analysis.
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.
### Patches
Patched in Zebra 4.4.2. The fix routes the P2SH sigop counter through the same C++ FFI already used by the legacy sigop counter.
### Workarounds
There is no configuration-level workaround. All Zebra nodes validating blocks on a network shared with zcashd are affected. Upgrade as soon as the patched version is available.
### Impact
A chain split between Zebra and zcashd validators. The attacker broadcasts spending transactions referencing P2SH outputs whose redeem scripts contain a disabled opcode followed by `OP_CHECKSIG` or `OP_CHECKMULTISIG` opcodes. When a Zebra miner (estimated ~30% of current network hashrate) includes these transactions in a block, Zebra validators accept the block while zcashd validators reject it with `bad-blk-sigops`. The two halves of the network diverge and every subsequent block extending the Zebra-side tip inherits the divergence.
The attacker does not need mining capability, RPC access, or any special privileges. The cost is the transaction fees for the funding and spending transactions.
### Credit
Reported by `@samsulselfut` via a private GitHub Security Advisory submission.
You are affected if:
1. You run any version of `zebrad` up to and including `v4.4.1`.
2. Your node validates blocks on mainnet, testnet, or any network where both Zebra and zcashd nodes participate.
All default configurations are affected. No feature flags, non-default settings, or special build options are required.
### Summary
Zebra's P2SH sigop counter uses a pure-Rust code path that short-circuits on disabled opcodes (such as `OP_CODESEPARATOR`), returning a partial count of zero for any sigops following the disabled opcode. The reference implementation (zcashd) correctly counts through disabled opcodes in its static sigop analysis. This produces a consensus divergence: Zebra accepts blocks that zcashd rejects when the block-wide `MAX_BLOCK_SIGOPS = 20,000` threshold is crossed on one side but not the other.
An attacker can exploit this without mining capability. Broadcasting transactions that spend P2SH outputs with malicious redeem scripts is sufficient; any Zebra miner who includes those transactions in a block triggers a chain split between Zebra and zcashd validators.
### Details
The P2SH sigop counter at `zebra-script/src/lib.rs:399` calls `script::Code(redeemed_bytes).sig_op_count(true)`, which is a pure-Rust path through `zcash_script-0.4.4`. The legacy (non-P2SH) sigop counter at `lib.rs:282-289` correctly uses the C++ FFI via `interpreter.legacy_sigop_count_script()`. Only the P2SH path bypasses the FFI.
The Rust parser in `zcash_script-0.4.4/src/opcode/mod.rs:1247-1260` treats 16 disabled opcodes (0x7e through 0xab, including `OP_CAT`, `OP_SUBSTR`, `OP_AND`, `OP_OR`, `OP_XOR`, `OP_2MUL`, `OP_2DIV`, `OP_MUL`, `OP_DIV`, `OP_MOD`, `OP_LSHIFT`, `OP_RSHIFT`, and `OP_CODESEPARATOR`) as `Err(Error::Disabled(...))`. The `sig_op_count` function at `iter.rs:104-115` uses `try_fold`, which terminates on the first `Err` and returns the partial sum accumulated so far.
zcashd's `GetOp2` (`script.h:514-562`) returns `true` for all non-push opcodes including the disabled range. Its `GetSigOpCount(true)` (`script.cpp:152-174`) continues counting through disabled opcodes. zcashd rejects disabled opcodes at execution time in the interpreter, not during static sigop analysis.
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.
### Patches
Patched in Zebra 4.4.2. The fix routes the P2SH sigop counter through the same C++ FFI already used by the legacy sigop counter.
### Workarounds
There is no configuration-level workaround. All Zebra nodes validating blocks on a network shared with zcashd are affected. Upgrade as soon as the patched version is available.
### Impact
A chain split between Zebra and zcashd validators. The attacker broadcasts spending transactions referencing P2SH outputs whose redeem scripts contain a disabled opcode followed by `OP_CHECKSIG` or `OP_CHECKMULTISIG` opcodes. When a Zebra miner (estimated ~30% of current network hashrate) includes these transactions in a block, Zebra validators accept the block while zcashd validators reject it with `bad-blk-sigops`. The two halves of the network diverge and every subsequent block extending the Zebra-side tip inherits the divergence.
The attacker does not need mining capability, RPC access, or any special privileges. The cost is the transaction fees for the funding and spending transactions.
### Credit
Reported by `@samsulselfut` via a private GitHub Security Advisory submission.
ghsa CVSS4.0
9.3
Vulnerability type
CWE-684
- https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-gf9r-m956-97qx
- https://github.com/AlfredoG87/zcash_script/blob/v0.4.4/src/script/iter.rs#L104-L...
- https://github.com/ZcashFoundation/zebra/blob/d4cd662c716382f6397d2a730148025a1c...
- https://github.com/ZcashFoundation/zebra/blob/d4cd662c716382f6397d2a730148025a1c...
- https://github.com/advisories/GHSA-gf9r-m956-97qx
- https://github.com/ZcashFoundation/zebra Product
Published: 2 Jul 2026 · Updated: 2 Jul 2026 · First seen: 2 Jul 2026