Skip to content

refactor(consensus)!: split transaction verifier into BlockVerifier and MempoolVerifier - #11095

Merged
mergify[bot] merged 14 commits into
ZcashFoundation:mainfrom
syszery:refactor/split-transaction-verifier-types
Aug 4, 2026
Merged

refactor(consensus)!: split transaction verifier into BlockVerifier and MempoolVerifier#11095
mergify[bot] merged 14 commits into
ZcashFoundation:mainfrom
syszery:refactor/split-transaction-verifier-types

Conversation

@syszery

@syszery syszery commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Motivation

Follow-up to #10843, closing #10950.

#10131 asked for the transaction verifier's tower::Service implementation to be split by request variant. After #10843, the verifier still exposes a single Verifier<ZS, Mempool> with one Request/Response enum pair and a single Service impl that dispatches on the request variant at runtime. This PR introduces two separate types: BlockVerifier<ZS> and MempoolVerifier<ZS, Mempool>.

Solution

  • Moved verification helpers that do not depend on ZS / Mempool into free functions. No behavior change intended; these helpers were already logically decoupled from Request and self.
  • Introduced separate transaction verifier request/response types:
    • BlockRequest / BlockResponse
    • MempoolRequest / MempoolResponse
  • Introduced separate verifier services:
    • BlockVerifier<ZS>
    • MempoolVerifier<ZS, Mempool>
  • Updated zebra-consensus to use the split verifier types end-to-end:
    • zebra-consensus/src/router.rs
    • zebra-consensus/src/block.rs
  • Updated zebrad callers to use the split verifier types end-to-end:
    • zebrad/src/components/mempool/downloads.rs
    • zebrad/src/components/mempool.rs
  • Updated affected tests in zebra-consensus and zebrad to use BlockVerifier / MempoolVerifier directly.
  • Removed the legacy unified transaction verifier compatibility layer:
    • transaction::Verifier
    • transaction::Request
    • transaction::Response
  • Added a zebra-consensus/CHANGELOG.md entry for the breaking API change.

Reviewer question

Two former legacy tests in zebra-consensus/src/transaction/tests.rs originally covered block-vs-mempool cross-context regressions (expiry-cache bypass and garbage Orchard proof handling):

  • mempool_cached_result_bypasses_expiry_check_for_block_at_next_height
  • block_with_garbage_orchard_proofs_is_rejected

After the verifier split, BlockVerifier no longer depends on mempool state directly, so that setup no longer exists in the same form. I kept direct block-verification coverage where the invariant still exists, and removed legacy scaffolding that no longer participates in the code path.

Tests

cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo nextest run all pass locally for the current commit. No behavior change intended.

AI Disclosure

  • No AI tools were used in this PR
  • AI tools were used: Claude for refactoring discussion and code review; ChatGPT for independent review and sanity checking.

PR Checklist

  • The PR title follows conventional commits format: type(scope): description
  • The PR follows the contribution guidelines.
  • This change was discussed in an issue or with the team beforehand.
  • The solution is tested.
  • The documentation and changelogs are up to date.

@syszery
syszery force-pushed the refactor/split-transaction-verifier-types branch from 25b624e to 7ccd86a Compare July 27, 2026 18:15
@syszery
syszery marked this pull request as ready for review July 29, 2026 16:10
@natalieesk

Copy link
Copy Markdown
Contributor

Thanks syszery, we'll get this reviewed!

@conradoplg conradoplg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This looks good. Claude found some stuff to improve which I agree with (including item 5; the suggested rename looks better), would you mind changing those?


Findings

  1. BlockRequest.known_outpoint_hashes is a dead public field —
    transaction.rs:177

Nothing in BlockVerifier::call reads it, and block.rs:294-295 builds a HashSet
over every created outpoint per block solely to populate it. It's residue of
the mempool-cache fast path (find_verified_unmined_tx) deleted when
CVE-2026-34377 was fixed. Since this PR is defining a brand-new public type,
this is the moment to drop the field and the per-block set construction —
leaving it invites someone to re-wire a cache bypass into the block path.

  1. BlockResponse.sigops doc contradicts the value it holds —
    transaction.rs:224-226

Doc says "The number of legacy signature operations." The constructed value is
sigops.saturating_add(cached_ffi_transaction.p2sh_sigops()) (:369), i.e.
legacy + P2SH — which is the point of GHSA-jv4h-j224-23cc, and zebra-chain's
own p2sh_sigop_count doc says it "must be added to legacy_sigop_count for the
block-level MAX_BLOCK_SIGOPS check." The wrong text was carried over verbatim
from the old enum variant (renamed from legacy_sigop_count to sigops in a
prior PR without updating the doc). This value feeds MAX_BLOCK_SIGOPS, so a
misleading doc here is worth fixing in a branch that already has a docs
commit.

  1. Stale test docs describe machinery that no longer exists — tests.rs:4250,
    4320, 4330, 4386

block_with_garbage_orchard_proofs_is_rejected (the CVE-2026-34377 regression
test) lost its mempool mock, but its doc still says "even if the mempool has a
valid version of the same transaction" — a scenario the test no longer sets
up. And mempool_cached_result_bypasses_expiry_check_for_block_at_next_height
still carries ~20 lines of doc plus an inline comment about
find_verified_unmined_tx and "the cache hit fires immediately" — that function
exists nowhere in the tree.

To be fair to the change: this is not a coverage regression. The mempool arm
was already vacuous on main — the mock was driven from an unjoined
tokio::spawn, so its expect_request(...).unwrap() panic could never fail the
test. And the refactor replaces the guarantee with a stronger structural one.
But the docs now overstate what's tested, in a consensus-critical crate, and
this PR touches exactly these tests.

  1. Changelog entry is under-specified for a library-consumer breaking change —
    zebra-consensus/CHANGELOG.md:12-15

Per book/src/dev/changelog-guidelines.md ("Name specific types, traits,
functions affected", "Focus on what code changes are needed"), the entry
should name the removed items (transaction::Verifier, transaction::Request,
transaction::Response and their accessors) and — most importantly for
consumers like Zaino/Zallet — state that the second return value of
router::init/init_test is now a mempool-only verifier that can no longer serve
block requests. The guidelines' both-breaking-and-additive rule also calls
for an ### Added entry for the new types. (Line 16 is a whitespace-only line;
it passes MD009's br_spaces: 2 allowance, so it's cosmetic only, not a CI
failure.)

  1. transaction::BlockVerifier collides with an existing name family — worth a
    maintainer decision

zebra-consensus already has BlockVerifierRouter, SemanticBlockVerifier, and
router::service_trait::BlockVerifierService, all of which verify whole blocks.
transaction::BlockVerifier verifies a transaction in block context. Fully
qualified it reads fine, but a bare use
zebra_consensus::transaction::BlockVerifier in a file that also touches block
verification will be actively confusing. BlockTxVerifier/MempoolTxVerifier
would disambiguate.

  1. Minor
  • BlockRequest.transaction_hash is also unread — call recomputes
    tx.unmined_id(). The old Request::tx_id() carried a // TODO: get the
    precalculated ID from the block verifier that was deleted in the refactor
    without the field becoming used. block.rs already computes all tx hashes for
    the merkle root, so wiring it is a cheap win (note unmined_id() != hash() for
    v5, so it can only supply the mined_id half).
  • zebra-chain/src/transaction/tests/vectors.rs:265,289 still reference
    zebra_consensus::transaction::Verifier. Plain prose, not an intra-doc link, so
    no CI failure.
  • mempool::Request::TransactionWithDepsByMinedId /
    Response::TransactionWithDeps (zebra-node-services/src/mempool.rs:67,151) now
    have a handler at zebrad/src/components/mempool.rs:891 but no sender anywhere.
    Dead since the CVE fix; this PR removes the last test that even pretended to
    exercise it. Out of scope here, but worth a follow-up issue so a cache-shaped
    hole doesn't linger in a public API.

@syszery

syszery commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author
  • Remove the unused BlockRequest.known_outpoint_hashes field (review comment 1).
  • Fix BlockResponse.sigops doc (review comment 2).
  • Update the CHANGELOG (review comment 4).
  • Rename transaction::BlockVerifier/MempoolVerifier to BlockTxVerifier/ MempoolTxVerifier (review comment 5).
  • Rename dont_skip_verification_of_block_transactions_in_mempool() to block_verification_does_not_use_mempool_verified_state() to clarify the new meaning.

@conradoplg
conradoplg dismissed their stale review July 31, 2026 22:08

comment was addressed

@syszery

syszery commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @conradoplg for cleaning up the test docs and for implementing the tx_id optimization.

I've updated the root CHANGELOG.md , which should address the changelog CI failure, and added documentation updates to block_verification_does_not_use_mempool_verified_state.

On the new changelog-gate failure: The only change to zebra-chain is the two-line doc fix in transaction/tests/vectors.rs, which is #[cfg(test)]-gated. I don't think this needs a changelog bullet, but let me know if you'd prefer one.

conradoplg
conradoplg previously approved these changes Aug 3, 2026
mpguerra
mpguerra previously approved these changes Aug 4, 2026
…refactor/split-transaction-verifier-types
@mergify mergify Bot added the queued label Aug 4, 2026
@mergify

mergify Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-04 22:51 UTC · Rule: batched · triggered by rule move to any queue if GitHub Rulesets are satisfied
  • Checks passed · on draft merge queue: checking main (485d859) and #11095 together #11183
  • Merged2026-08-04 23:21 UTC · at 6e5a79777cf45169da5c67a7e65d8fc0dc98fbb4 · merge

This pull request spent 30 minutes 40 seconds in the queue, including 29 minutes 44 seconds running CI.

Required conditions to merge

@mergify
mergify Bot merged commit 5098bdc into ZcashFoundation:main Aug 4, 2026
113 checks passed
@mergify mergify Bot removed the queued label Aug 4, 2026
jvff added a commit that referenced this pull request Aug 6, 2026
Link the split transaction verifier API to PR #11095 so consumers can
find its design and implementation context.
jvff added a commit that referenced this pull request Aug 6, 2026
Link the removed unified verifier API to PR #11095 so consumers can
find its migration and review context.
jvff added a commit that referenced this pull request Aug 6, 2026
Link the `BlockRequest::transaction_hash` contract to PR #11095 so
consumers can find its implementation and rationale.
jvff added a commit that referenced this pull request Aug 6, 2026
Link the new transaction verifier types to PR #11095 so consumers can
find their design and implementation context.
jvff added a commit that referenced this pull request Aug 6, 2026
Link the dedicated verifier request and response types to PR #11095 so
consumers can find their implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the split transaction verifier API to PR #11095 so consumers can
find its design and implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the removed unified verifier API to PR #11095 so consumers can
find its migration and review context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the `BlockRequest::transaction_hash` contract to PR #11095 so
consumers can find its implementation and rationale.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the new transaction verifier types to PR #11095 so consumers can
find their design and implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the dedicated verifier request and response types to PR #11095 so
consumers can find their implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the split transaction verifier API to PR #11095 so consumers can
find its design and implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the removed unified verifier API to PR #11095 so consumers can
find its migration and review context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the `BlockRequest::transaction_hash` contract to PR #11095 so
consumers can find its implementation and rationale.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the new transaction verifier types to PR #11095 so consumers can
find their design and implementation context.
jvff added a commit that referenced this pull request Aug 10, 2026
Link the dedicated verifier request and response types to PR #11095 so
consumers can find their implementation context.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task: split the transaction verifier into separate block and mempool request types with dedicated tower::Service impls

5 participants