PT-2026-41600 · Crates.Io · Zebrad

Published

2026-05-07

·

Updated

2026-05-07

CVSS v4.0

9.2

Critical

VectorAV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N

Zebra Transparent SIGHASH SINGLE Corresponding-Output Handling Diverges From zcashd

Summary

For V5+ transparent spends, Zebra and zcashd disagree on the same consensus rule: SIGHASH SINGLE must fail when the input index has no corresponding output. zcashd treats this as consensus-invalid under ZIP-244, while Zebra's transparent verification path computes a digest for the missing-output case instead of failing.
The result is a direct block-validity split. A malformed V5 transparent transaction can be accepted by Zebra, retained in Zebra's mempool, selected into Zebra getblocktemplate, mined into a block, and then rejected by zcashd.

Details

Validated code revisions used during analysis:
  • zcashd: 2c63e9aa08cb170b0feb374161bea94720c3e1f5
  • Zebra: a905fa19e3a91c7b4ead331e2709e6dec5db12cb
Scope note:
  • earlier triage material grouped pre-V5 and V5 behavior together;
  • re-execution on the pinned revisions did not reproduce the claimed pre-V5 / V4 reject-side behavior;
  • this advisory therefore covers the V5+ / ZIP-244 variant only.
zcashd side:
  • Transparent scripts in blocks are checked through TransactionSignatureChecker::CheckSig() and SignatureHash(): zcash/src/script/interpreter.cpp.
  • In the ZIP-244 branch, SignatureHash() explicitly throws when SIGHASH SINGLE or SIGHASH SINGLE|ANYONECANPAY is used with nIn >= txTo.vout.size(): zcash/src/script/interpreter.cpp.
  • CheckSig() catches that exception and returns false, causing the transparent script to fail.
Zebra side:
Why this is exploitable:
  • the malformed transaction only needs fewer transparent outputs than inputs;
  • the attacker signs the digest that Zebra computes for the missing-output case;
  • Zebra then sees a valid transparent signature, while zcashd never reaches the same digest because it fails first.
Ordinary path viability:

PoC

Validated commits:
  • zcashd: 2c63e9aa08cb170b0feb374161bea94720c3e1f5
  • Zebra: a905fa19e3a91c7b4ead331e2709e6dec5db12cb
Manual reproduction steps:
  1. Build an otherwise-valid V5 transaction with at least two transparent inputs and only one transparent output.
  2. Sign input 0 normally.
  3. Sign input 1 with canonical SIGHASH SINGLE or SIGHASH SINGLE|ANYONECANPAY.
  4. Use the digest returned by Zebra's ZIP-244 path, where the missing output contributes transparent outputs hash([]).
  5. Submit the transaction to Zebra and to zcashd.
  6. Observe:
  • Zebra accepts it into the mempool;
  • Zebra selects it into getblocktemplate;
  • Zebra can mine and accept a block containing it;
  • zcashd rejects it in the ordinary mempool path.

Impact

This is a direct V5+ transparent consensus split.
Who can trigger it:
  • an ordinary transaction author can craft the malformed V5 transparent transaction;
  • the accept-side stock path is Zebra's mempool and block-template path;
  • an external miner still has to include the transaction in a block for the split to materialize.
Who is impacted:
  • Zebra can accept and template a transaction / block that zcashd rejects;
  • this makes the issue both a consensus-divergence problem and a practical Zebra block-template safety problem.

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-CWFQ-RFCR-8HMP

Affected Products

Zebrad