You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every shielded proof and signature is verified twice: once when a transaction arrives over mempool gossip, and again when it arrives inside a block. Zebra used to avoid the second pass by skipping whole-transaction verification for block transactions already accepted into the mempool, but that bypass was removed in #10494 as a security fix without a replacement, so near-tip block validation now re-runs its most expensive work — Groth16/Halo2 proof and RedJubjub/RedPallas signature verification — for transactions the node has already verified.
The removed bypass kept breaking for a structural reason: it cached a height-dependent proposition — valid(tx, height, block time, spent outputs) — under a key that did not determine it. Expiry, lock time, the consensus branch id, the Orchard soft-fork gates and the proof-size rule all move with height, and the network upgrade selects which Orchard circuit verifying key applies. Any replacement must reuse only work whose result is fully determined by its cache key.
User Story
As a node operator, I want blocks whose transactions my node already verified in the mempool to validate without re-running shielded proof and signature verification, so that block validation latency near the tip — and with it propagation delay and orphan risk — drops.
As a Zebra developer, I want any verification reuse to be keyed by data that fully determines the verified statement, so that the reuse cannot repeat the height-dependent staleness bugs of the removed whole-transaction bypass.
As a mining pool operator, I want submitblock latency reduced without an opt-out flag that skips consensus checks, so that block submission stays fast while every check still runs.
Acceptance Criteria
Shielded bundle verification results are cached at the bundle-verification boundary (verify_sapling_bundle in zebra-consensus/src/primitives/sapling.rs and queue_orchard_bundle in zebra-consensus/src/primitives/halo2.rs), not at the whole-transaction level; on a cache hit the transaction verifier in zebra-consensus/src/transaction.rs still runs end to end at the block's height, time, and spent outputs.
The cache key fully determines the verified statement: the transaction's unmined ID (witnessed ZIP 244 authorizing-data digest for v5/v6; legacy full-serialization hash for v4), the sighash the bundle was verified against, and the shielded pool of the bundle. No height, block time, or other chain-context data may influence a cached verdict.
Each Orchard circuit version has its own cache alongside its own verifying key, so an entry can only be read back under the verifying key it was written against; transactions without a witnessed ID are verified every time.
Only Ok results are cached: batch errors are never recorded as per-item verdicts, and the caching service's poll_ready does not surface inner-service (batch worker) errors for items whose result the cache already holds.
Cache capacity is bounded, and hits, misses, inserts, evictions and size are reported under zebra.consensus.cache.* with verifier labels consistent with zebra.consensus.batch.duration_seconds.
Unit/property tests in src/**/tests/ cover: cache-key completeness for both verifiers over real mainnet bundles; hit, miss, eviction, no reuse across items, no memory of failures, cancellation, and readiness against a stub verifier; and rejection of a real Sapling bundle replayed under a different consensus branch id.
End-to-end tests through the transaction verifiers show: a mempool verification is reused by the block that mines the transaction; a block one height past the transaction's expiry is still rejected; and an authorizing-data twin with an identical txid is fully re-verified and rejected (regression coverage for the CVE-2026-34377 vulnerability class).
A changelog fragment is added under .changes/ for the user-visible performance change.
The change is reviewed by two reviewers with consensus expertise (see Expertise Required).
Implementation Details
The reuse is implemented as a generic caching tower service wrapper (new module zebra-consensus/src/primitives/cache.rs, registered in zebra-consensus/src/primitives.rs) layered over the existing Sapling and Orchard batch verifiers. verify(bundle, sighash, vk) is a pure function, so a cache hit is bit-identical to the computation it replaces; only the oneshot inside verify_sapling_bundle / queue_orchard_bundle is short-circuited, and all height-dependent checks in zebra-consensus/src/transaction.rs run unconditionally.
Key design points: the witnessed ID's ZIP 244 authorizing-data digest commits to the proofs and signatures (the txid alone does not — that gap was CVE-2026-34377); the sighash is a separate key component because a v5/v6 sighash also commits to spent transparent output amounts and scripts, and a v4 shielded sighash commits to the block's consensus branch id, which the v4 ID does not; the pool component separates the Orchard and Ironwood bundles of a single v6 transaction, which share an ID and sighash. Failure results are not cached because a batch error is not per-item evidence (Fallback re-verifies items singly, and a service error can mean the batch worker shut down rather than an invalid item); caching it would make the node reject a valid block. The miss path acquires inner readiness inside call so hits do not hold Batch semaphore permits. Each cache holds 20,000 keys — several blocks of history plus a full mempool.
Alternatives considered: restoring the whole-transaction mempool bypass (rejected — its key cannot determine the height-dependent proposition, and that mismatch caused the repeated breakage that led to #10494); a submitblock validation-skipping flag (rejected as the primary mechanism — it bypasses checks rather than reusing verified work, and carries the security-review burden described in #10724).
Note: the cache capacity is a constant; whether operators need a config knob to trade memory for hit rate is deliberately deferred (see Out of Scope). Verify during review that the key-completeness test coverage includes every transaction version the verifier accepts before closing.
In Scope
zebra-consensus/src/primitives/cache.rs (new) and zebra-consensus/src/primitives/cache/tests.rs (new)
zebra-consensus/src/primitives.rs
zebra-consensus/src/primitives/sapling.rs and zebra-consensus/src/primitives/sapling/tests.rs (new)
zebra-consensus/src/primitives/halo2.rs and zebra-consensus/src/primitives/halo2/tests.rs
zebra-consensus/src/transaction.rs and zebra-consensus/src/transaction/tests.rs
.changes/ changelog fragment for zebra-consensus
Out of Scope
Transparent script verification and the remainder of the transaction verifier still re-run for block transactions; reusing script verification across mempool→block is not covered here and has no tracking issue yet.
ZIP 244 — transaction identifier non-malleability, including the authorizing-data digest.
Expertise Required
Consensus-critical caching in the verification path — needs two reviewers (extra-reviews). Zcash transaction formats (v4/v5/v6) and ZIP 244 digest structure; tower service middleware semantics (Batch, Fallback, readiness and permit handling); Sapling/Orchard proof-system verification flow. Estimate is retrospective: ~2,300 changed lines including extensive test coverage, plus a two-reviewer consensus review, sizes the work as L end to end.
Context
Every shielded proof and signature is verified twice: once when a transaction arrives over mempool gossip, and again when it arrives inside a block. Zebra used to avoid the second pass by skipping whole-transaction verification for block transactions already accepted into the mempool, but that bypass was removed in #10494 as a security fix without a replacement, so near-tip block validation now re-runs its most expensive work — Groth16/Halo2 proof and RedJubjub/RedPallas signature verification — for transactions the node has already verified.
The removed bypass kept breaking for a structural reason: it cached a height-dependent proposition —
valid(tx, height, block time, spent outputs)— under a key that did not determine it. Expiry, lock time, the consensus branch id, the Orchard soft-fork gates and the proof-size rule all move with height, and the network upgrade selects which Orchard circuit verifying key applies. Any replacement must reuse only work whose result is fully determined by its cache key.User Story
As a node operator, I want blocks whose transactions my node already verified in the mempool to validate without re-running shielded proof and signature verification, so that block validation latency near the tip — and with it propagation delay and orphan risk — drops.
As a Zebra developer, I want any verification reuse to be keyed by data that fully determines the verified statement, so that the reuse cannot repeat the height-dependent staleness bugs of the removed whole-transaction bypass.
As a mining pool operator, I want
submitblocklatency reduced without an opt-out flag that skips consensus checks, so that block submission stays fast while every check still runs.Acceptance Criteria
verify_sapling_bundleinzebra-consensus/src/primitives/sapling.rsandqueue_orchard_bundleinzebra-consensus/src/primitives/halo2.rs), not at the whole-transaction level; on a cache hit the transaction verifier inzebra-consensus/src/transaction.rsstill runs end to end at the block's height, time, and spent outputs.Okresults are cached: batch errors are never recorded as per-item verdicts, and the caching service'spoll_readydoes not surface inner-service (batch worker) errors for items whose result the cache already holds.zebra.consensus.cache.*withverifierlabels consistent withzebra.consensus.batch.duration_seconds.src/**/tests/cover: cache-key completeness for both verifiers over real mainnet bundles; hit, miss, eviction, no reuse across items, no memory of failures, cancellation, and readiness against a stub verifier; and rejection of a real Sapling bundle replayed under a different consensus branch id..changes/for the user-visible performance change.Implementation Details
The reuse is implemented as a generic caching
towerservice wrapper (new modulezebra-consensus/src/primitives/cache.rs, registered inzebra-consensus/src/primitives.rs) layered over the existing Sapling and Orchard batch verifiers.verify(bundle, sighash, vk)is a pure function, so a cache hit is bit-identical to the computation it replaces; only theoneshotinsideverify_sapling_bundle/queue_orchard_bundleis short-circuited, and all height-dependent checks inzebra-consensus/src/transaction.rsrun unconditionally.Key design points: the witnessed ID's ZIP 244 authorizing-data digest commits to the proofs and signatures (the txid alone does not — that gap was CVE-2026-34377); the sighash is a separate key component because a v5/v6 sighash also commits to spent transparent output amounts and scripts, and a v4 shielded sighash commits to the block's consensus branch id, which the v4 ID does not; the pool component separates the Orchard and Ironwood bundles of a single v6 transaction, which share an ID and sighash. Failure results are not cached because a batch error is not per-item evidence (
Fallbackre-verifies items singly, and a service error can mean the batch worker shut down rather than an invalid item); caching it would make the node reject a valid block. The miss path acquires inner readiness insidecallso hits do not holdBatchsemaphore permits. Each cache holds 20,000 keys — several blocks of history plus a full mempool.Alternatives considered: restoring the whole-transaction mempool bypass (rejected — its key cannot determine the height-dependent proposition, and that mismatch caused the repeated breakage that led to #10494); a
submitblockvalidation-skipping flag (rejected as the primary mechanism — it bypasses checks rather than reusing verified work, and carries the security-review burden described in #10724).Note: the cache capacity is a constant; whether operators need a config knob to trade memory for hit rate is deliberately deferred (see Out of Scope). Verify during review that the key-completeness test coverage includes every transaction version the verifier accepts before closing.
In Scope
zebra-consensus/src/primitives/cache.rs(new) andzebra-consensus/src/primitives/cache/tests.rs(new)zebra-consensus/src/primitives.rszebra-consensus/src/primitives/sapling.rsandzebra-consensus/src/primitives/sapling/tests.rs(new)zebra-consensus/src/primitives/halo2.rsandzebra-consensus/src/primitives/halo2/tests.rszebra-consensus/src/transaction.rsandzebra-consensus/src/transaction/tests.rs.changes/changelog fragment forzebra-consensusOut of Scope
submitblockvalidation-skipping flag for mining pools (submitblock: support skipping redundant validation for mining pool use case #10724) — this issue reduces the redundant cost without an opt-out surface;submitblock: support skipping redundant validation for mining pool use case #10724 may be rescoped once the effect is measured.Blockers
None.
Related Issues / Advisories
submitblock: support skipping redundant validation for mining pool use case #10724 —submitblockredundant-validation latency for mining pools; substantially addressed by this reuse for blocks built from the node's own mempool.Expertise Required
Consensus-critical caching in the verification path — needs two reviewers (
extra-reviews). Zcash transaction formats (v4/v5/v6) and ZIP 244 digest structure;towerservice middleware semantics (Batch,Fallback, readiness and permit handling); Sapling/Orchard proof-system verification flow. Estimate is retrospective: ~2,300 changed lines including extensive test coverage, plus a two-reviewer consensus review, sizes the work as L end to end.