[EXPERIMENTAL] Schnorr batch verification for blocks #29491

pull fjahr wants to merge 12 commits into bitcoin:master from fjahr:2024-02-batch-validation-updated changing 84 files +3454 −512
  1. fjahr commented at 5:06 pm on February 27, 2024: contributor

    This is a simple draft implementation of Schnorr signature batch verification in blocks, put here for conceptual discussion, and collaborative benchmarking. Most of code added here is from the secp256k1 implementation.

    The secp256k1 code is still under review itself, please spend some time giving feedback on the API etc. if you can:

    TODOs

    • Fix functional/feature_taproot.py
    • Extensive benchmarking using ConnectBlock() tracing
    • Add unit tests
    • Batch taproot tweak verification as well
    • Conceptual discussion in what form this realistically could be introduced into the code base, and later, a release
  2. DrahtBot commented at 5:06 pm on February 27, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/29491.

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32080 (OP_CHECKCONTRACTVERIFY by bigspider)
    • #32028 (Update secp256k1 subtree to latest master by hebasto)
    • #31507 ([POC] build: Use clang-cl to build on Windows natively by hebasto)
    • #31132 (validation: fetch block inputs on parallel threads 10% faster IBD by andrewtoth)
    • #29247 (CAT in Tapscript (BIP-347) by 0xBEEFCAF3)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. fjahr force-pushed on Feb 27, 2024
  4. DrahtBot added the label CI failed on Feb 27, 2024
  5. DrahtBot commented at 5:11 pm on February 27, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/22041704016

  6. Sjors commented at 9:40 am on February 28, 2024: member

    I guess this is most interesting to test against the last 50K blocks on mainnet, i.e. since early 2023? Given the high percentage of taproot transactions. Such testing could benefit from a assume utxo snapshot, saving everyone the pain of rewinding. I can bake one, though ideally I’d like #26045 to land first.

    Is the current PR already using batches? It’s unclear to me by glancing over validation.cpp if and how it’s collecting up to max_batch_size{106} signatures before calling batch.Verify().

    Maybe add a -schnorrbatchverify startup argument so more people can try it once it gets through review.

  7. fjahr commented at 12:53 pm on February 28, 2024: contributor

    I guess this is most interesting to test against the last 50K blocks on mainnet, i.e. since early 2023? Given the high percentage of taproot transactions. Such testing could benefit from a assume utxo snapshot, saving everyone the pain of rewinding. I can bake one, though ideally I’d like #26045 to land first.

    Yeah, that would be great. I have to get the tracing part to work in my setup again before I can start.

    Is the current PR already using batches? It’s unclear to me by glancing over validation.cpp if and how it’s collecting up to max_batch_size{106} signatures before calling batch.Verify().

    Yes, the rough architecture is as as follows (sorry for not writing a better explanation above but it’s still very rough and I expect it to change). So, the batch object is constructed and then handed down from ConnectBlock() to CheckInputScripts() which hands it to the CScriptCheck constructor. When the CScriptCheck instance is executed and the batch object is present BatchingCachingTransactionSignatureChecker is used which only differs from the default CachingTransactionSignatureChecker in that its implementation of VerifySchnorrSignature() and adds the Schnorr sig to the batch object instead of verifying it right there. (Here we have an issue because the function is not doing what the name says anymore but it is a much simpler change this way and I find it hard to predict where we will land with this in the end.) Then back in ConnectBlock() the batch object is verified after all transactions have been iterated over.

    The 106 is an implementation detail from secp256k1 that I am not sure needs to be really exposed here. But what matters for the question is: if there are more than 106 sigs added to the batch the verification for the already added sigs will happen when the next sig is added and if the verification fails the adding itself will fail. So, a callback to the last paragraph, every 106 sigs the naming of VerifySchnorrSignature() is actually still correct ;) and the one verify call in the end just verifies the last n % 106 sigs left for that block. I did not put any effort in documenting this properly here yet because the secp256k1 API is not finalized yet and the details on this particular topic might still change, see for example https://github.com/bitcoin-core/secp256k1/pull/1134#pullrequestreview-1083948472

    Everything should be conditional on the presence of a batch object which defaults to nullptr and if that is the case all the other functions should work as usual, for example when used outside of the context of a new block.

    Maybe add a -schnorrbatchverify startup argument so more people can try it once it gets through review.

    Yeah, I have been thinking that or maybe even a compiler argument

  8. fjahr force-pushed on Feb 28, 2024
  9. fjahr commented at 1:14 pm on February 28, 2024: contributor
    @Sjors maybe what confused you is that BatchingCachingTransactionSignatureChecker::VerifySchnorrSignatureBatch() was unused. That was actually a leftover from an earlier design spike and I just forgot to remove it. It’s gone now.
  10. Sjors commented at 1:36 pm on February 28, 2024: member

    if there are more than 106 sigs added to the batch the verification for the already added sigs will happen when the next sig is added and if the verification fails the adding itself will fail.

    That clarifies a lot, and should be documented :-)

  11. DrahtBot added the label Needs rebase on Apr 6, 2024
  12. 0xB10C commented at 8:33 am on May 13, 2024: contributor

    @willcl-ark and I ran a IBD benchmark vs master for this PR: IBD from a local node to height 840000 and currently only looking at the connect_block duration.

    In the current draft implementation:

    • master is currently faster
    • batch validation is attempted and decreases performance even before taproot activated
    • after taproot activation, batch validation is significantly slower than non-batch validation

    image

    image

    Used this bpftrace script to collect CSV data:

     0#!/usr/bin/env bpftrace
     1
     2usdt:./src/bitcoind:validation:block_connected
     3{
     4  $height = (int32) arg1;
     5  $tx = (uint64) arg2;
     6  $ins = (int32) arg3;
     7  $sigops = (int64) arg4;
     8  $duration = (int64) arg5; // in nanoseconds
     9
    10  printf("%d,%ld,%d,%ld,%ld\n", $height, $tx, $ins, $sigops, $duration);
    11}
    
  13. willcl-ark commented at 8:38 am on May 13, 2024: member

    batch validation is attempted and decreases performance even before taproot activated

    From a quick skim of the changes ISTM that we always use the BatchingCachingTransactionSignatureChecker, and there was no switch to the CachingTransactionSignatureChecker, but this could well be because this is only in WIP state. It doesnt account for why it’s currently slower in all cases though.

  14. fjahr force-pushed on Dec 5, 2024
  15. DrahtBot removed the label Needs rebase on Dec 5, 2024
  16. andrewtoth commented at 10:55 pm on December 7, 2024: contributor

    It seems in this approach the 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.

    I think a better approach would be to have a thread local batch for each worker thread and the main thread, so each thread can add all schnorr sigs without locking, and then each thread can verify their batches in parallel at the end.

  17. fjahr force-pushed on Mar 11, 2025
  18. fjahr renamed this:
    [DO NOT MERGE] Schnorr batch verification for blocks
    [EXPERIMENTAL] Schnorr batch verification for blocks
    on Mar 11, 2025
  19. in src/script/sigcache.h:81 in d333d5c6e3 outdated
    71@@ -71,6 +72,22 @@ class CachingTransactionSignatureChecker : public TransactionSignatureChecker
    72 
    73     bool VerifyECDSASignature(const std::vector<unsigned char>& vchSig, const CPubKey& vchPubKey, const uint256& sighash) const override;
    74     bool VerifySchnorrSignature(Span<const unsigned char> sig, const XOnlyPubKey& pubkey, const uint256& sighash) const override;
    75+
    76+    bool GetStore() const { return store; }
    77+    SignatureCache& GetSigCache() const { return m_signature_cache; }
    78+};
    79+
    80+class CollectingSignatureChecker : public CachingTransactionSignatureChecker {
    


    Eunovo commented at 6:47 am on March 16, 2025:
    https://github.com/bitcoin/bitcoin/pull/29491/commits/d333d5c6e3c4b454621998772ff74ff6656af691: This is nice! sets us up nicely to later do batch taptweak checking since we can “collect” it too.
  20. in src/checkqueue.h:163 in 4541038323 outdated
    160+                            break;
    161+                        }
    162+                        // Check succeeded; add signatures to shared batch
    163+                        const auto& signatures = std::get<std::vector<SchnorrSignatureToVerify>>(check_result);
    164+                        for (const auto& sig : signatures) {
    165+                            m_batch.Add(sig.sig, sig.pubkey, sig.sighash);
    


    Eunovo commented at 3:23 am on March 17, 2025:
    https://github.com/bitcoin/bitcoin/pull/29491/commits/454103832351311d11e16505ed1925bd5f1c6875: IIUC All the threads still share this batch. That means we still have the same locking problem; threads will be stuck waiting for the batch to be free. In my very rough implementation https://github.com/bitcoin-dev-tools/benchcoin/pull/41/commits/173f07dc4493de3d3a9420f6a966606913277e9d#diff-0f68d4063c2fb11000f9048e46478e26c0ea2622f7866d00b574f429c3a4db61 I opted to give each batch a thread to resolve this.

    fjahr commented at 0:17 am on March 20, 2025:

    You’re right, this wasn’t a complete solution. I have improved this now by using buckets of signatures for each thread which are all added and only then verified by the master thread at the end. This could be further improved if we could add the signatures to a batch per thread right away but this would require that we could merge batch objects which the secp api currently doesn’t make possible. I’ll add that to my wish list because I think it should be even better than the current approach but it should be a comparatively minor improvement. I considered other approaches as well but this seems to be a very simple solution that does not rely on a more complex library or code or uses thread_local (which we try to avoid).

    Your approach does do batching in each thread but also per each vChecks allocation, which are surprisingly small, especially when there are a lot of threads available. This is what caused me to look more into the nNow algo behavior. With the current approach here I try to only do one verify call per block which should be the fastest approach once pippenger is used in secp.


    andrewtoth commented at 0:45 am on March 20, 2025:

    I think @Eunovo’s approach is closer to what we want. Instead of a bucket per thread, have a batch per thread. However, instead of batch verifying after each vChecks batch, batch verify after the queue of checks is empty for the block. We need CCheckQueue::Complete to set a flag that no more checks will be added, and wake all threads again. The threads will all verify their batches once the global queue is empty. We would need to reset the flag after Complete.

    This approach of verifying on master thread at the end will not be faster even with pippinger algo. The max speedup is closer to 2x, but multi threaded is 16x faster.


    fjahr commented at 10:59 pm on March 23, 2025:

    Instead of a bucket per thread, have a batch per thread. However, instead of batch verifying after each vChecks batch, batch verify after the queue of checks is empty for the block. We need CCheckQueue::Complete to set a flag that no more checks will be added, and wake all threads again. The threads will all verify their batches once the global queue is empty. We would need to reset the flag after Complete.

    Yes, one batch per thread is the approach I mentioned above as well as preferred, if that wasn’t clear. My thinking was that the fastest approach overall would be if we would do that plus efficient merging of batches which should ideally have the same overhead as adding a single signature to a batch (to be verified that this is possible). It would need to be benchmarked across different scenarios for blocks with varying share of schnorr sigs and across different numbers of available threads, of course. I am sure there will be some scenarios where the “let each thread verify their own batch” will be faster and there will be other scenarios where “merge thread batches and verify once” is faster and we would need to decide based on that in the end. A definite upside of the merging of thread batches would be that it would be simpler code, we could save this round of waking up the threads with the flag set.

    However, in the meantime I have asked Siv how hard it would be to add the merging of batches to the secp api and it appears it might be harder than I thought because it would require merging of scratch spaces, which is currently not possible. So, while maybe theoretically possible, it’s quite far of terms of engineering effort. Siv will still look into it and check if it’s feasible at all but until then I will disregard this approach.

    So, long story short, I have now implemented it with a batch per thread using a flag set in complete as you suggested.

  21. DrahtBot added the label Needs rebase on Mar 20, 2025
  22. in src/checkqueue.h:129 in 6055f3a6f5 outdated
    123@@ -114,6 +124,12 @@ class CCheckQueue
    124                     if (fMaster && nTodo == 0) {
    125                         // All checks are done; master performs batch verification
    126                         if constexpr (std::is_same_v<R, ScriptFailureResult>) {
    127+                            for (auto& bucket : m_signature_buckets) {
    128+                                for (const auto& sig : bucket) {
    129+                                    m_batch.Add(sig.sig, sig.pubkey, sig.sighash);
    


    Eunovo commented at 12:46 pm on March 20, 2025:
    https://github.com/bitcoin/bitcoin/pull/29491/commits/6055f3a6f530818e2805d11debb1fb2ce81fc5c0: This leaves the master thread to do all the “Pubkey recovery”(I think that’s what its called?) work in each batch.Add call, which is substantial. We can call “secp256k1_xonly_pubkey_parse” when collecting the signatures so the master thread is not stuck doing all the work to parse the XOnlyPubkey.
  23. ryanofsky referenced this in commit b9c281011b on Mar 23, 2025
  24. Squashed 'src/secp256k1/' changes from 0cdc758a56..74dc25820b
    74dc25820b build: Add build option for batch module
    01f8b86de7 SQUASH ME: Fix silent merge conflicts
    991cf91e65 batch: Generate graphs for batch verification speed up
    c7b4d2c0b2 batch, extrakeys: Add benchmark for batch verify and `tweak_add_check`
    f9d09ff8d3 batch: Add tests for `batch_add_*` APIs
    d94a99bbfd batch,ecmult: Add tests for core batch APIs and `strauss_batch` refactor
    220277f062 batch: Add API usage example
    6cc9210001 batch: Add `batch_add_*` APIs
    9beee683a9 batch, ecmult: Add `batch_verify` API and refactor `strauss_batch`
    a31e017cbc batch: Add `create` and `destroy` APIs
    4feb9f72a1 batch: Initialize an experimental batch module
    70f149b9a1 Merge bitcoin-core/secp256k1#1662: bench: add ellswift to bench help output
    6b3fe51fb6 bench: add ellswift to bench help output
    d84bb83e26 Merge bitcoin-core/secp256k1#1661: configure: Show exhaustive tests in summary
    3f54ed8c1b Merge bitcoin-core/secp256k1#1659: include: remove WARN_UNUSED_RESULT for functions always returning 1
    20b05c9d3f configure: Show exhaustive tests in summary
    e56716a3bc Merge bitcoin-core/secp256k1#1660: ci: Fix exiting from ci.sh on error
    d87c3bc58f ci: Fix exiting from ci.sh on error
    1b6e081538 include: remove WARN_UNUSED_RESULT for functions always returning 1
    2abb35b034 Merge bitcoin-core/secp256k1#1657: tests: remove unused uncounting_illegal_callback_fn
    51907fa918 tests: remove unused uncounting_illegal_callback_fn
    a7a5117144 Merge bitcoin-core/secp256k1#1359: Fix symbol visibility issues, add test for it
    13ed6f65dc Merge bitcoin-core/secp256k1#1593: Remove deprecated `_ec_privkey_{negate,tweak_add,tweak_mul}` aliases from API
    d1478763a5 build: Drop no longer needed  `-fvisibility=hidden` compiler option
    8ed1d83d92 ci: Run `tools/symbol-check.py`
    41d32ab2de test: Add `tools/symbol-check.py`
    88548058b3 Introduce `SECP256K1_LOCAL_VAR` macro
    03bbe8c615 Merge bitcoin-core/secp256k1#1655: gha: Print all *.log files, in a separate action
    59860bcc24 gha: Print all *.log files, in a separate action
    4ba1ba2af9 Merge bitcoin-core/secp256k1#1647: cmake: Adjust diagnostic flags for `clang-cl`
    abd25054a1 Merge bitcoin-core/secp256k1#1656: musig: Fix clearing of pubnonces
    961ec25a83 musig: Fix clearing of pubnonces
    3186082387 Merge bitcoin-core/secp256k1#1614: Add _ge_set_all_gej and use it in musig for own public nonces
    6c2a39dafb Merge bitcoin-core/secp256k1#1639: Make static context const
    37d2c60bec Remove deprecated _ec_privkey_{negate,tweak_add,tweak_mul} aliases
    432ac57705 Make static context const
    1b1fc09341 Merge bitcoin-core/secp256k1#1642: Verify `compressed` argument in `secp256k1_eckey_pubkey_serialize`
    c0d9480fbb Merge bitcoin-core/secp256k1#1654: use `EXIT_` constants over magic numbers for indicating program execution status
    13d389629a CONTRIBUTING: mention that `EXIT_` codes should be used
    c855581728 test, bench, precompute_ecmult: use `EXIT_...` constants for `main` return values
    965393fcea examples: use `EXIT_...` constants for `main` return values
    2e3bf13653 Merge bitcoin-core/secp256k1#1646: README: add instructions for verifying GPG signatures
    b682dbcf84 README: add instructions for verifying GPG signatures
    00774d0723 Merge bitcoin-core/secp256k1#1650: schnorrsig: clear out masked secret key in BIP-340 nonce function
    a82287fb85 schnorrsig: clear out masked secret key in BIP-340 nonce function
    4c50d73dd9 ci: Add new "Windows (clang-cl)" job
    84c0bd1f72 cmake: Adjust diagnostic flags for clang-cl
    f79f46c703 Merge bitcoin-core/secp256k1#1641: doc: Improve cmake instructions in README
    2ac9f558c4 doc: Improve cmake instructions in README
    1823594761 Verify `compressed` argument in `secp256k1_eckey_pubkey_serialize`
    8deef00b33 Merge bitcoin-core/secp256k1#1634: Fix some misspellings
    39705450eb Fix some misspellings
    ec329c2501 Merge bitcoin-core/secp256k1#1633: release cleanup: bump version after 0.6.0
    c97059f594 release cleanup: bump version after 0.6.0
    64228a648f musig: Use _ge_set_all_gej for own public nonces
    300aab1c05 tests: Improve _ge_set_all_gej(_var) tests
    365f274ce3 group: Simplify secp256k1_ge_set_all_gej
    d3082ddead group: Add constant-time secp256k1_ge_set_all_gej
    
    git-subtree-dir: src/secp256k1
    git-subtree-split: 74dc25820b54684769fde919822d1290abbd69e2
    cd1404164e
  25. Merge commit 'cd1404164e0a7f56b9ebdfc7ba500b4497c3d8da' into 2024-02-batch-validation-updated-clean 9f2b4ad5e0
  26. build: Enable secp256k1 experimental batch module 1c472ca34c
  27. validation: Add BatchVerify (unused) bcd51a49f8
  28. script: Add batch validation error 2a2a2030ad
  29. sigcache: Add CollectingSignatureChecker 696bd7547e
  30. validation: Add BatchableScriptChecker ee3b1019d3
  31. validation: Use schnorr signature batch validation in parallel script verification 549c9af77c
  32. validation: Use schnorr batch validation in single thread case 516c27fb21
  33. fuzz: Add basic fuzz test for BatchSchnorrVerifier 913751f091
  34. checkqueue: Use per-thread batch object to prevent lock contention cab02b4d5c
  35. TODO: Fix Taproot functional test 4ab16a0b28
  36. fjahr force-pushed on Mar 23, 2025
  37. DrahtBot removed the label Needs rebase on Mar 23, 2025
  38. fjahr commented at 1:29 pm on March 24, 2025: contributor
    I’ve also rebased to get #31689 in here by the way.

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-03-31 21:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me