experiment: batch verification#1872
Draft
ozgb wants to merge 12 commits into
Draft
Conversation
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…gress Verify transaction ZK proofs once, in an aggregate batch, instead of one tx at a time. This lands the ledger-crate foundation, the native-direct invocation machinery, and the block-import ingress point; all gated by node config (MidnightCfg) and defaulting OFF. Ledger crate: - New process-global proof-verification cache (tx_hash -> bool), keyed by the state-independent tx_validation_cache_key, with insert/get accessors. - get_verified_transaction consults the cache: defer crypto on a hit, reject on a cached-false, error-log + full inline verify on a miss. - Bridge::batch_verify_transactions: real aggregate verification using the ledger-9 collect_proof_evidence/batch_proof_verify API. The v9-only crypto is isolated in a new per-version batch_verify module so the shared code still compiles against ledger 7/8. Only the mempool isolation fallback remains a todo!(). - Native (non-WASM) entry point host_api::ledger_9::batch_verify_transactions (no new runtime API / host function). Node crate: - MidnightCfg: batch_verify_block_import / batch_verify_mempool (default false) plus queue-tuning params, via serde defaults. - batch_verify.rs: BatchVerifier builds the native inputs (state_key, BlockContext, runtime_version) from the client backend and calls the ledger natively; BatchVerifyMetrics. No backend.state_at trie view is needed - the ledger arena is process-global, so a BasicExternalities + LedgerStorageExt suffices. - batch_block_import.rs: BatchVerifyBlockImport wraps only the import-queue block import (received blocks). Rejects a block only on a genuine invalid proof; on any setup/availability failure it delegates so downstream inline verification still runs (never wrongly halts sync). Remaining (follow-up): mempool custom ChainApi + worker pool, and the per-tx isolation fallback. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Follow-up to the batch ZK-proof verification core (b731346), delivering the two deferred pieces so the mempool ingress path is real and safe to enable. Component A (ledger): implement the isolation fallback in Bridge::batch_verify_transactions (was todo!()). On aggregate-batch failure it verifies each ready transaction individually to isolate the offender(s), caching false for bad proofs and warming caches for good ones. Extracts a shared warm_verified_tx helper used by both the success loop and the fallback. Component B (node): add MidnightChainApi plus a bounded queue and blocking worker pool that batch-verify external Midnight submissions natively, warming the proof/soft/strict caches and building the same validity tags the runtime would (delegating invalid/unavailable cases to the runtime). Reworks the transaction pool to BasicPool<MidnightChainApi> (SingleState), reconstructs pool limits from the CLI (--pool-limit/--pool-kbytes/--tx-ban-seconds), and reuses one BatchVerifier for the mempool and block-import paths. Both flags (batch_verify_block_import, batch_verify_mempool) default off. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Move the aggregate-verification duration timer inside BatchVerifier::batch_verify (around the batch_verify_transactions crypto call) so midnight_batch_verify_duration_seconds is recorded for the block-import path as well as the mempool path, which previously recorded nothing. Remove the mempool's now-redundant external timer to avoid double-counting; both paths funnel through this method. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Two-phase Docker A/B benchmark for block-import batch proof verification. prime.sh builds a proof-heavy dev chain (fan-out of shielded coins, then chunked batch-single-tx working around the single funder's DUST-output limit) and archives the node's base_path; benchmark.sh restores it into a non-authoring producer and full-syncs a fresh node with BATCH_VERIFY_BLOCK_IMPORT off vs on, comparing sync time (min of N repeats) and scraping the syncer's batch-verify metrics with a coverage/engagement verdict. Adds 'just batch-verify-perf-{prime,bench}'. See scripts/tests/batch-verify-perf/{README,FINDINGS}.md.
Assisted-by: Claude:claude-4.8-opus
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Add ledger_proof_verify_duration_seconds / _txs_total (labelled by mode) to LedgerMetrics so ZK proof-verification crypto is recorded at transaction granularity on BOTH the inline (OFF) and batch (ON) block-import paths, which already share one LedgerMetricsExt registry: - mode="inline": per-tx well_formed WITH proofs in get_verified_transaction (the OFF / cold-proof-cache path). - mode="batch": the aggregate batch_verify_proofs call (the ON crypto). - mode="batch_prep": per-tx well_formed WITHOUT proofs on the batch path (the non-crypto work both paths pay). get_verified_transaction now returns the inline crypto duration (Some only when it actually verified proofs inline); apply_transaction records it. The OFF verification path is otherwise unchanged -- well_formed is only wrapped in a timer. The existing midnight_batch_verify_duration_seconds records only on the ON path (per-batch) and had nothing to diff against; these per-tx metrics give a clean OFF-vs-ON crypto speedup that sidesteps the block/DB/sync wall-clock noise. benchmark.sh scrapes them from both runs and reports full-verify and crypto-only per-tx speedups (per-tx = _sum / _txs_total); README/FINDINGS updated. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
The OFF (inline) block-import path never emitted the ledger_proof_verify_* mode="inline" metric, so the batch-verify perf harness reported "insufficient samples" and could not compute the OFF-vs-ON per-tx crypto speedup. Root cause: send_mn_transaction is unsigned, so during execute_block FRAME runs ValidateUnsigned::pre_dispatch (validate_guaranteed_execution) BEFORE dispatching the call. That pre_dispatch runs the inline ZK crypto and warms the STRICT cache but discarded its timing; by the time apply_transaction (the only place recording mode="inline") called get_verified_transaction, it hit the warm cache and got None, so nothing was recorded. Record the inline duration where the crypto actually runs on the OFF block-import path: do_validate_guaranteed_execution now returns the inline duration and validate_guaranteed_execution observes it. apply_transaction's recording stays as a guarded fallback for paths with no preceding pre_dispatch (e.g. tests). On the ON path the batch verifier pre-warms the STRICT/proof caches, so this records no false inline samples. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
benchmark.sh now picks its run mode like toolkit-tokens-minter-e2e.sh: pass a node image and it runs containers (unchanged); pass nothing and it runs a locally-built binary (NODE_BIN, default target/release/midnight-node) as host processes against the existing archive. This is the fast inner loop for node-side changes -- build once and benchmark without waiting on a CI image. Local mode uses host processes rather than a layered image because the image base (amazonlinux 2023, glibc 2.34) is older than a typical dev host, so a freshly built host binary can't run inside it. The local producer re-supplies the dev preset's authoring args (the CLI replaces the preset's args array) and runs with the repo root as CWD so the preset's relative res/ paths resolve; BASE_PATH points it at the restored archive dir. Docker and local modes share the polling, metrics-scrape and reporting logic via mode-aware node-runner helpers (start_producer/start_syncer/syncer_alive/...); README documents the new mode and its genesis-match caveat. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Experimental / draft. Batch ZK-proof verification for Midnight transactions, so the expensive aggregate crypto runs once per block/batch instead of once per transaction. Everything is gated behind node config and defaults OFF, so with the flags unset the node behaves as today.
Two ingress points feed a process-global proof cache that downstream
get_verified_transactionreads to skip (defer) the ZK crypto:batch_verify_block_import): received blocks are batch-verified up front (fail-fast; a bad proof rejects the block). (landed earlier on this branch)batch_verify_mempool): external submissions are batched natively via a customMidnightChainApi+ bounded queue + blocking worker pool.This PR adds the two pieces that make the mempool path real and safe to enable:
falsefor bad proofs and warming caches for good ones — instead of panicking. Extracts a sharedwarm_verified_txhelper.MidnightChainApiwraps the stockFullChainApi, delegating everything exceptvalidate_transaction; external Midnightsend_mn_transactionextrinsics are queued and drained in batches by N blocking workers, which warm the caches and build the same validity tag the runtime would (with_tag_prefix("Midnight").longevity(600).and_provides(tx_hash)). Invalid/unavailable cases delegate to the runtime for the authoritative result.The transaction pool is now always the SingleState
BasicPool<MidnightChainApi>(batch verification requires the single-state ChainApi seam). Pool limits are reconstructed from the CLI (--pool-limit/--pool-kbytes/--tx-ban-seconds);--pool-typeis intentionally not applied.⚙️ Configuration (new
MidnightCfgfields, for perf testing)All new fields live in
node/src/cfg/midnight_cfg/mod.rs, default OFF/unset, and are optional inres/cfgpresets — existing presets need no changes to keep today's behavior.batch_verify_block_importfalsebatch_verify_mempoolfalsesend_mn_transactionsubmissions). A bad proof is isolated per-tx, not panicked.batch_verify_max_batch_size64batch_verify_target_batch_size16batch_verify_max_age_ms50k_target.batch_verify_workers4batch_verify_queue_capacity4096ImmediatelyDroppederror instead of being queued.Tuning notes (see
scripts/tests/batch-verify-perf/FINDINGS.mdfor the block-import harness, its methodology, and open caveats):batch_verify_max_batch_size/batch_verify_target_batch_sizeare the levers to explore the favourable regime; workload needs enough proof-txs per block/window to actually fill a batch (see the FINDINGS.md note on the DUST-output ceiling that limited the current harness).batch_verify_target_batch_sizeandbatch_verify_max_age_msjointly set the mempool latency/throughput trade-off: raising either trades added tail latency for larger, more efficient batches.batch_verify_workersis CPU-bound — sizing it above available cores just adds contention, not throughput.batch_verify_queue_capacityis a burst-absorption/shedding valve, not a throughput knob: too small drops legitimate traffic under burst (watchqueue_rejected_total), too large just grows tail latency for queued txs.midnight_batch_verify_batch_size(histogram),midnight_batch_verify_duration_seconds(per-batch aggregate crypto time, both ingress paths),midnight_batch_verify_batches_total{outcome=...},midnight_batch_verify_txs_total,midnight_batch_verify_queue_depth,midnight_batch_verify_queue_rejected_total,midnight_batch_verify_dispatch_reason_total{trigger=...}(k_targetvstau),midnight_batch_verify_fallback_total(mempool-only).justtargets for the block-import A/B harness:just batch-verify-perf-prime <NODE_IMAGE> <TOOLKIT_IMAGE>(prime + archive a proof-heavy chain, once) andjust batch-verify-perf-bench <NODE_IMAGE>(A/B benchmark off vs on against the primed archive) — seescripts/tests/batch-verify-perf/README.md.🗹 TODO before merging
--pool-type fork-aware(currently ignored; SingleState is forced)📌 Submission Checklist
git commit -s) for the DCO--pool-type fork-awarerun now builds SingleState)todo!()fallback is now implemented)🧪 Testing Evidence
cargo check -p midnight-node-ledger -p midnight-node— cleancargo fmt --check— cleancargo clippy— no new findings (only pre-existing, workspace-allowed lints on touched files)Unit tests:
fallback_isolates_bad_proof_and_caches_outcomes(mixed good/bad batch isolates the offender, cachestrue/falsecorrectly, correct per-tx results) — passes on all 3 ledger versionsk_targetandtau, bounded-queue shedding (ImmediatelyDropped), oneshot resolution, invalid/unavailable → delegate, and native validity-tag shapeAdditional tests are provided (if possible)
🔱 Fork Strategy
Runtime/pallets are unchanged; the ledger crate is invoked natively by the node. No metadata or genesis rebuild required.
Links
N/A (experimental)
🤖 Generated with Claude Code