PT-2026-41600 · Crates.Io · Zebrad
Published
2026-05-07
·
Updated
2026-05-07
CVSS v4.0
9.2
Critical
| Vector | AV: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:2c63e9aa08cb170b0feb374161bea94720c3e1f5Zebra: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()andSignatureHash():zcash/src/script/interpreter.cpp. - In the ZIP-244 branch,
SignatureHash()explicitly throws whenSIGHASH SINGLEorSIGHASH SINGLE|ANYONECANPAYis used withnIn >= txTo.vout.size():zcash/src/script/interpreter.cpp. CheckSig()catches that exception and returnsfalse, causing the transparent script to fail.
Zebra side:- V5 transparent inputs route into the same FFI-based transparent script verifier used for block validation:
zebra/zebra-consensus/src/transaction.rs. Zebraconverts the decoded hash type and asks its Rust sighash engine for a digest without adding the corresponding-output pre-check thatzcashdenforces first:zebra/zebra-script/src/lib.rs, [zebra/zebra-chain/src/primitives/zcash primitives.rs](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-chain/src/primitives/zcash primitives.rs#L307-L343).Zebraforwards canonicalSIGHASH SINGLEinto the Rust ZIP-244 implementation.- In that implementation, when
input.index() >= bundle.vout.len(), the code usestransparent outputs hash::<TxOut>(&[])instead of erroring: [zcash primitives/src/transaction/sighash v5.rs](https://github.com/zcash/librustzcash/blob/c3425f9c3c7f6deb20720bb78b18f35fbbed8edd/zcash primitives/src/transaction/sighash v5.rs#L101-L107), [zcash primitives/src/transaction/sighash v5.rs](https://github.com/zcash/librustzcash/blob/c3425f9c3c7f6deb20720bb78b18f35fbbed8edd/zcash primitives/src/transaction/sighash v5.rs#L131-L139).
Why this is exploitable:
- the malformed transaction only needs fewer transparent outputs than inputs;
- the attacker signs the digest that
Zebracomputes for the missing-output case; Zebrathen sees a valid transparent signature, whilezcashdnever reaches the same digest because it fails first.
Ordinary path viability:
zcashdordinary mempool admission is not the practical trigger path, because the same ZIP-244SignatureHash()checks fail there first:zcash/src/main.cpp,zcash/src/script/interpreter.cpp.Zebraordinary mempool admission is viable becauseZebrauses the same transparent verifier for mempool and block validation and does not have a separate "one output per input" standardness rule here:zebra/zebra-consensus/src/transaction.rs,zebra/zebrad/src/components/mempool/storage.rs.Zebrais a block-template producer, so the realistic stock path isZebramempool ->Zebragetblocktemplate-> external miner: [zebra/zebra-rpc/src/methods/types/get block template/zip317.rs](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-rpc/src/methods/types/get block template/zip317.rs#L72-L105).
PoC
Validated commits:
zcashd:2c63e9aa08cb170b0feb374161bea94720c3e1f5Zebra:a905fa19e3a91c7b4ead331e2709e6dec5db12cb
Manual reproduction steps:
- Build an otherwise-valid V5 transaction with at least two transparent inputs and only one transparent output.
- Sign input
0normally. - Sign input
1with canonicalSIGHASH SINGLEorSIGHASH SINGLE|ANYONECANPAY. - Use the digest returned by
Zebra's ZIP-244 path, where the missing output contributestransparent outputs hash([]). - Submit the transaction to
Zebraand tozcashd. - Observe:
Zebraaccepts it into the mempool;Zebraselects it intogetblocktemplate;Zebracan mine and accept a block containing it;zcashdrejects 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:
Zebracan accept and template a transaction / block thatzcashdrejects;- this makes the issue both a consensus-divergence problem and a practical
Zebrablock-template safety problem.
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Zebrad