#29491 [EXPERIMENTAL] Schnorr batch verification for blocks

full analysis

https://github.com/bitcoin/bitcoin/pull/29491 · fjahr · +2739/-182 in 56 files, 11 commits · labels: none · draft

Goal

  • Speed up initial block download and block validation on Taproot-heavy blocks
  • Batch-verify Schnorr signatures during block connection across worker threads

Implements Schnorr signature batch verification for block validation in Bitcoin Core. Caching script checkers collect Schnorr signatures during script execution, which worker threads in CCheckQueue then batch verify in parallel per thread before completing block processing. The PR also includes an experimental vendored snapshot of the secp256k1 batch verification module.

Problem: Every Schnorr signature in a Taproot spend is currently verified independently using scalar point multiplications. Node operators running initial block download or validating new Taproot-heavy blocks pay full CPU cost for each signature rather than taking advantage of BIP 340 batch verification speedups.

Category: Validation (#10 of 48)

P2 · speedup

  • P2 because accelerating block connection and initial block download directly helps node operators
  • Substantially increases throughput on synthetic Schnorr blocks once queue contention is resolved

P2 because Schnorr batch verification delivers a major algorithmic and practical speedup for block validation and initial block download on Taproot-heavy blocks. Enabling batch validation was a core design goal of BIP 340, and benchmarks in this thread showed parallel block connection throughput jumping to over 120 blocks/sec compared to around 50 blocks/sec on master.

Membership: Directly alters block script validation in ConnectBlock, CScriptCheck, and CCheckQueue to batch verify Schnorr signatures.

Factors: security/stability 0, bug 0, performance 3, user value 1, leverage 2

Reviewability: Ready

  • Ready for code review and benchmarking, though upstream secp256k1 dependency is not yet merged

The check queue refactoring and script checker changes are stable for review, although final integration depends on secp256k1 upstream PR #1134.

Author status: active, frequently rebasing and incorporating upstream secp256k1 API changes

Resolved concerns:

  • Initial benchmark by 0xB10C and willcl-ark showed batch validation was slower than master across IBD due to lock contention.
  • andrewtoth noted a single shared batch mutex blocked worker threads and forced master-thread verification, negating multithreading benefits.
  • Eunovo identified high pubkey parsing overhead and verification inside critical sections, resolving both via per-thread batches verified outside locks.
  • Discarding the return value of xonly pubkey parsing was fixed in a recent push.

Agreement: Strong

  • Collaborative support focused on threading architecture and benchmarking (andrewtoth, Eunovo)
  • Recommended per-thread batches to avoid worker mutex contention (andrewtoth)
  • Co-authored queue optimizations and provided comparative speedup benchmarks (Eunovo)
  • Identified performance bottlenecks where earlier iterations were slower than master (0xB10C, willcl-ark)

Strong collaborative support and benchmarking; all initial architectural bottlenecks have been resolved with demonstrable speedups.

Multiple contributors actively tested and redesigned the batching mechanism to ensure real performance gains, resolving all performance regressions.

  • Eunovo co-authored commits and confirmed throughput gains over master.
  • andrewtoth guided the successful per-thread batch architecture.
  • 0xB10C provided early empirical benchmark scripts that steered the redesign.

Objections:

ReviewerKindHarmStatusBlockingAuthor repliedQuote
0xB10CapproachEarly implementation suffered severe performance regression during IBD compared to master.resolvednono2024-05-13: 'master is currently faster... after taproot activation, batch validation is significantly slower than non-batch validation'
Settled: 2025-05-18: fjahr showed ConnectBlockAllSchnorr microbenchmarks reaching 125 block/s vs master 52 block/s after checkqueue rework.
andrewtothapproachHolding a shared batch mutex during addition caused severe thread contention and single-threaded verification bottleneck.resolvednoyes2024-12-07: 'm_batch_mutex is held for the entirety of adding, which will cause lock contention. Verify is then a blocking call on the main thread, which negates the multithreading.'
Settled: 2025-03-23: fjahr: 'I have now implemented it with a batch per thread using a flag set in complete as you suggested.'
EunovocorrectnessDiscarding the return value of secp256k1_xonly_pubkey_parse risked ignoring malformed keys.resolvednoyes2025-06-15: 'why discard the return value of secp256k1_xonly_pubkey_parse()?'
Settled: 2026-01-26: fjahr: 'Not discarding it anymore in the latest push.'

Support:

  • Eunovo: Showed significant validation speedups and co-developed the multithreaded batch architecture.
  • Sjors: Anticipated strong performance gains for recent Taproot-dense transaction volume.

Participants: Sjors (support), 0xB10C (objection), willcl-ark (objection), andrewtoth (objection), Eunovo (objection)

State derived from the lists: substantive support, no open objection (Eunovo, Sjors)

Review verdicts (DrahtBot): 0

Dependencies

Enables:

  • Batch taproot tweak verification
  • Substantial initial sync and block connection speedups for Taproot blocks

Files

356 lines under test/bench/ci.

  • src/secp256k1/src/modules/schnorrsig/batch_add_tests_impl.h +245/-0
  • src/secp256k1/src/modules/batch/main_impl.h +202/-0
  • src/secp256k1/src/tests.c +137/-35
  • src/checkqueue.h +125/-29
  • src/secp256k1/src/modules/schnorrsig/batch_add_impl.h +154/-0
  • src/secp256k1/examples/batch.c +152/-0
  • src/secp256k1/src/modules/extrakeys/batch_add_impl.h +147/-0
  • src/secp256k1/src/modules/extrakeys/bench_impl.h +138/-0
  • src/secp256k1/src/modules/extrakeys/batch_add_tests_impl.h +134/-0
  • src/secp256k1/src/modules/batch/tests_impl.h +126/-0
  • src/secp256k1/doc/speedup-batch/bench_output.txt +119/-0
  • src/test/key_tests.cpp +85/-18
  • test/functional/feature_taproot_invalid_output_key.py +93/-0
  • src/secp256k1/include/secp256k1_batch.h +88/-0
  • test/functional/feature_taproot.py +38/-37
  • src/secp256k1/src/ecmult_impl.h +54/-19
  • src/validation.cpp +52/-8
  • src/test/fuzz/batchverify.cpp +52/-0
  • src/secp256k1/src/modules/schnorrsig/tests_impl.h +47/-3
  • src/batchverify.h +49/-0
  • src/batchverify.cpp +48/-0
  • src/secp256k1/src/modules/schnorrsig/bench_impl.h +46/-0
  • src/secp256k1/include/secp256k1_tweak_check_batch.h +45/-0
  • src/secp256k1/doc/speedup-batch/plot.gp +41/-0
  • src/secp256k1/include/secp256k1_schnorrsig_batch.h +39/-0
  • src/secp256k1/src/bench.c +37/-2
  • src/secp256k1/.github/workflows/ci.yml +25/-13
  • src/test/txvalidationcache_tests.cpp +15/-14
  • src/secp256k1/doc/speedup-batch/Makefile +27/-0
  • src/validation.h +21/-3
  • src/script/sigcache.cpp +22/-0
  • src/script/sigcache.h +20/-0
  • src/secp256k1/doc/speedup-batch.md +17/-0
  • src/secp256k1/Makefile.am +15/-0
  • src/secp256k1/doc/speedup-batch/bench.sh +13/-0
  • src/secp256k1/src/CMakeLists.txt +11/-0
  • src/secp256k1/configure.ac +10/-0
  • src/secp256k1/src/secp256k1.c +10/-0
  • src/secp256k1/src/modules/extrakeys/Makefile.am.include +8/-0
  • src/secp256k1/src/modules/schnorrsig/Makefile.am.include +7/-0
  • src/secp256k1/examples/CMakeLists.txt +4/-0
  • src/script/script_error.h +3/-0
  • src/secp256k1/ci/ci.sh +2/-1
  • src/secp256k1/src/modules/batch/Makefile.am.include +3/-0
  • src/script/script_error.cpp +2/-0
  • src/secp256k1/CMakeLists.txt +2/-0
  • src/test/fuzz/checkqueue.cpp +2/-0
  • src/CMakeLists.txt +1/-0
  • src/kernel/CMakeLists.txt +1/-0
  • src/secp256k1/.gitignore +1/-0
  • src/secp256k1/README.md +1/-0
  • src/secp256k1/doc/speedup-batch/.gitignore +1/-0
  • src/test/fuzz/CMakeLists.txt +1/-0
  • test/functional/test_runner.py +1/-0
  • src/secp256k1/doc/speedup-batch/schnorrsig-speedup-batch.png +None/-None
  • src/secp256k1/doc/speedup-batch/tweakcheck-speedup-batch.png +None/-None

Card

Implements Schnorr signature batch verification for block validation by collecting signatures during script evaluation and verifying them in per-thread batches in CCheckQueue. Resolves the single-signature CPU verification burden during IBD and block connection for Taproot transactions. Benchmarks by contributors show more than a 2x throughput speedup on Taproot-heavy blocks once multithreaded batching was implemented. The CCheckQueue and validation architecture has seen deep collaborative review, though merging into Bitcoin Core is pending upstream merge of the batch module in secp256k1.

Data

dossier JSON · extract JSON · model openrouter/google/gemini-3.8-flash, generated 2026-09-17T21:21, confidence high, input hash c33a72850bec43aa