Skip to content

refactor(consensus): separate block and mempool transaction verification#10843

Open
syszery wants to merge 4 commits into
ZcashFoundation:mainfrom
syszery:refactor/split-tx-verifier-block-mempool
Open

refactor(consensus): separate block and mempool transaction verification#10843
syszery wants to merge 4 commits into
ZcashFoundation:mainfrom
syszery:refactor/split-tx-verifier-block-mempool

Conversation

@syszery

@syszery syszery commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Motivation

This PR makes progress toward #10131 by separating the block and mempool transaction verification paths inside Verifier<ZS, Mempool>.

The existing Service::call() implementation interleaved block and mempool verification logic throughout a single async block, branching on req.is_mempool() at multiple points. Separating the two paths makes each one easier to follow independently and should make any future architectural split simpler.

Solution

This PR consists of three related refactors:

  1. Split spent_utxos into block_spent_utxos and mempool_spent_utxos

  2. Split Service::call into verify_block and verify_mempool

    call() now dispatches immediately to one of two private methods. Each method contains only the logic for its respective verification path, eliminating the internal is_mempool() branching. Shared validation steps remain the same, while block- and mempool-specific behavior is handled directly within each method.

  3. Replace &Request in check_maturity_height with explicit arguments

    check_maturity_height previously accepted &Request only to extract the transaction, height, and known_utxos. Since the function is only used during mempool verification, where Request::known_utxos() always returns an empty map, it now accepts only the values it actually needs (tx, height, and spent_utxos).

These refactors are intended to preserve the existing verification behavior while making the two verification paths easier to understand and modify independently.

Tests

  • Existing transaction verification tests pass for both block and mempool verification paths.
  • The refactor is intended to preserve behavior; no new functionality is introduced.

Follow-up Work

This PR intentionally keeps the existing Verifier<ZS, Mempool> type and public API unchanged. With the verification logic now isolated into verify_block and verify_mempool, a future refactor could split these into separate verifier types if that is still the desired direction for #10131.

Question for reviewers: Is the intended resolution of #10131 to introduce separate verifier types, or is the current separation of the internal verification paths sufficient? I'm happy to continue with a follow-up refactor—or extend this PR—depending on the preferred direction.

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.

Copilot AI review requested due to automatic review settings June 27, 2026 21:35
@syszery syszery marked this pull request as ready for review June 27, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is an internal refactor of the Zcash transaction verifier in zebra-consensus. It splits the single Service::call implementation of Verifier<ZS, Mempool> — which previously interleaved block and mempool logic and branched on req.is_mempool() throughout one async block — into two dedicated private methods, verify_block and verify_mempool. It makes progress toward #10131 (eventually splitting the verifier into two service implementations) without changing the public Verifier type, request/response API, or verification behavior.

Changes:

  • call() now dispatches by request variant to verify_block / verify_mempool, each containing only its path's logic (block uses block time + AwaitUtxo; mempool uses median-time-past, UnspentBestChainUtxo + AwaitOutput, maturity check, and the nullifier/anchor check).
  • spent_utxos is split into block_spent_utxos (known_utxos + AwaitUtxo) and mempool_spent_utxos (best-chain + mempool fallback).
  • check_maturity_height now takes explicit tx/height arguments instead of &Request, hardcoding an empty known_utxos map (equivalent for the mempool-only call site).

Review notes (per Zebra guidelines)

  • NITPICK: A relocated TODO in verify_mempool still references the old spent_utxos() function name (now mempool_spent_utxos()).
  • Process: This touches consensus-critical verification. Behavior appears faithfully preserved (check ordering, coinbase handling, block vs. mempool lock-time, omission of maturity/anchor checks in the block path, mempool UTXO fallback, and spent_mempool_outpoints propagation all match the prior logic). The PR description's "discussed in an issue/with the team" and "changelog/docs up to date" checkboxes are unchecked; for a behavior-preserving internal refactor a changelog entry is likely unnecessary, but the consensus surface warrants careful human verification.


// TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction
// spends both chain and mempool UTXOs (mempool outputs are appended last by
// `spent_utxos()`), causing policy checks to pair the wrong input with
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.

2 participants