silentpayments: tweak spend pubkey using ecmult_gen_var (~25% scanning speedup for small N) #1914

pull theStack wants to merge 3 commits into bitcoin-core:master from theStack:silentpayments-use-ecmult_gen_var-for-P_k-calc changing 11 files +180 −33
  1. theStack commented at 12:49 AM on August 14, 2026: contributor

    Based on the #1883 branch, this PR takes advantage of the variable-time generator point multiplication function ecmult_gen_var for spend public key tweaking in the output public key calculation $P_k = B_{spend} + t_k \cdot G$ (used both for sending and scanning). Note that $t_k$ is not considered a long-term secret (in contrast to e.g. the scan secret key), so using variable-time functions should be fine. This leads to a ~25% scanning speedup if the number of transaction outputs is small (N <= 10).

    Results on my arm64 machine (as per $ ./build/bin/bench silentpayments_scan_nomatch, see also the commit body):

    N master PR branch speedup
    2 39.3 us 30.3 us 28.85%
    5 40.4 us 31.7 us 27.44%
    10 43.3 us 34.6 us 25.14%
    100 89.6 us 81.2 us 10.34%
    1000 1259.0 us 1244.0 us 1.99%

    This PR is similar to the previous ones #1843, #1844, but it only changes the behavior of the silentpayments module and leaves other API functions unchanged.

  2. theStack added the label performance on Aug 14, 2026
  3. theStack force-pushed on Aug 24, 2026
  4. real-or-random commented at 6:57 AM on August 25, 2026: contributor

    Concept ACK

    This was discussed in yesterday's IRC meeting:

    08:30 < real_or_random> I tend to think that's a bit of a hopeless case. When josie brought this up first, my initial reaction was "uh, let's stay safe here". But now I'm leaning towards the opposite. Any meaningful caller will branch on whether a match was found or not, with much more timing differences than we can ever generate in the raw crypto code. So getting a 25% speedup seems worth the deal [...] 08:36 < sipa> real_or_random: seems reasonable

  5. real-or-random added the label side-channel on Aug 25, 2026
  6. theStack commented at 2:23 AM on September 4, 2026: contributor

    Concept ACK

    This was discussed in yesterday's IRC meeting:

    08:30 < real_or_random> I tend to think that's a bit of a hopeless case. When josie brought this up first, my initial reaction was "uh, let's stay safe here". But now I'm leaning towards the opposite. Any meaningful caller will branch on whether a match was found or not, with much more timing differences than we can ever generate in the raw crypto code. So getting a 25% speedup seems worth the deal [...] 08:36 < sipa> real_or_random: seems reasonable

    I only notice now that the PR in its current state applies the same optimization also for the sending side, without giving much thought if that's also okay. The argument from the IRC meeting above is based solely on the scanning scenario ("caller will branch on whether a match was found or not"), for the sending side I assume there could theoretically still be (more of) a point treating shared_secret and t_k as actual secret?

    Another slightly related follow-up question would be if at places where we treat these values not as secrets, we could (or should) then consequently also remove the memory clearing efforts, like e.g. the following: https://github.com/bitcoin-core/secp256k1/blob/a9a61831bdcd97475e200a4bdd1ebed641f7268d/src/modules/silentpayments/main_impl.h#L705-L707

    I'd say yes for consistency, though in this case there is no performance upside (I haven't verified, but I'd be surprised if this makes a noticeable difference), it just make the code overall a bit simpler by reducing LOC.

  7. Yudis-bit commented at 4:56 PM on October 15, 2026: contributor

    On the sending side, t_k blinds the recipient because P_k = B_spend + t_k * G. Leaking t_k lets an observer compute B_spend = P_k - t_k * G directly from the public on-chain output and reveal the recipient's silent payment address. Saving ~9 us on 1-2 outputs per tx isn't worth the cache-timing risk on the sender, whereas for scanning (thousands of outputs, branching on matches anyway) ecmult_gen_var makes total sense. Restricting it to recipient_scan_outputs keeps the speedup where it matters.

    Also, definitely keep memclear_explicit on shared_secret. It's the raw 33-byte ECDH point, so leaving it on the stack exposes the root secret across memory dumps regardless of whether t_k is vartime.

  8. apoelstra commented at 5:04 PM on October 15, 2026: contributor

    4c3013bc3a446e6c455260984a394870ad41915b needs rebase

  9. ecmult_gen: compute `ecmult_gen_scalar_diff` at compile-time
    Having this available as a global constant allows to introduce an
    alternative `ecmult_gen_gej` function that doesn't need access to
    a context, see next commit.
    
    Note that the precomputed constant takes the name of the function that
    previously generated it at run-time (`secp256k1_ecmult_gen_scalar_diff`),
    while the function is now renamed to include the "compute" verb
    (`secp256k1_ecmult_gen_compute_scalar_diff`), to match the naming
    of the table generation function.
    
    Can be reviewed with `--color-moved=dimmed-zebra` for easier
    checking of the move-only parts.
    099bb2b456
  10. ecmult_gen: introduce `secp256k1_ecmult_gen_var_{gej,ge}`
    Add faster variable-time variants for generator point multiplication.
    This is essentially `ecmult_gen` without side-channel mitigations and
    without requiring a context object. Intended for use cases where the
    scalar is not representing sensitive data.
    
    On my arm64 machine, this is ~86% faster than the constant-time variant
    (with the default build table size, i.e. ECMULT_GEN_KB=86):
    
    ```
    $ ./build/bin/bench_ecmult
    Benchmark                     ,    Min(us)    ,    Avg(us)    ,    Max(us)
    
    ecmult_gen                    ,     9.32      ,     9.35      ,     9.60
    ecmult_gen_var                ,     5.02      ,     5.02      ,     5.02
    .....
    ```
    fee337a78c
  11. silentpayments: tweak spend pubkey using ecmult_gen_var (~25% scanning speedup for small N)
    Note that the tweak t_k is not considered a long-term secret, so using
    variable-time functions for calculating P_k = B_spend + t_k * G seems fine.
    This leads to a ~25% scanning speedup if the number of outputs is small (N <= 10):
    
    master:
    ```
    $ ./build/bin/bench silentpayments_scan_nomatch
    Benchmark                               ,    Min(us)       ,    Avg(us)       ,    Max(us)
    
    silentpayments_scan_nomatch_N=2         ,       39.3       ,       39.3       ,       39.3
    silentpayments_scan_nomatch_N=5         ,       40.4       ,       40.4       ,       40.4
    silentpayments_scan_nomatch_N=10        ,       43.3       ,       43.3       ,       43.3
    silentpayments_scan_nomatch_N=100       ,       89.0       ,       89.6       ,       91.0
    silentpayments_scan_nomatch_N=1000      ,      560.0       ,      563.0       ,      568.0
    silentpayments_scan_nomatch_N=2323      ,     1254.0       ,     1259.0       ,     1271.0
    silentpayments_scan_nomatch_N=23250     ,    12196.0       ,    12209.0       ,    12228.0
    ```
    
    PR branch (using ecmult_gen_var for calculating t_k * G):
    
    ```
    $ ./build/bin/bench silentpayments_scan_nomatch
    Benchmark                               ,    Min(us)       ,    Avg(us)       ,    Max(us)
    
    silentpayments_scan_nomatch_N=2         ,       30.5       ,       30.5       ,       30.5
    silentpayments_scan_nomatch_N=5         ,       31.7       ,       31.7       ,       31.7
    silentpayments_scan_nomatch_N=10        ,       34.6       ,       34.6       ,       34.6
    silentpayments_scan_nomatch_N=100       ,       80.0       ,       81.2       ,       85.0
    silentpayments_scan_nomatch_N=1000      ,      550.0       ,      552.0       ,      554.0
    silentpayments_scan_nomatch_N=2323      ,     1239.0       ,     1244.0       ,     1253.0
    silentpayments_scan_nomatch_N=23250     ,    12164.0       ,    12182.0       ,    12214.0
    ```
    
    Speedups:
    
    N=2: ~28.85%
    N=5: ~27.44%
    N=10: ~25.14%
    N=100: ~10.34%
    N=1000: ~1.99%
    4cb79cfc30
  12. theStack force-pushed on Oct 16, 2026
  13. theStack commented at 1:42 AM on October 16, 2026: contributor

    Rebased on #1883 (itself rebased on master) and applied the optimization to scanning only.

  14. fametrano commented at 6:43 AM on October 21, 2026: contributor

    ACK 4cb79cfc3049fce91e02466dc8ce436cb6cfd9da for the last commit; I did not review the #1883 commits it builds on. The scan key only reaches the ecmult_const ECDH, while ecmult_gen_var only sees t_k, which create_output_tweak already declassifies. So ctime_tests, clean under valgrind, cannot see this path, and the safety argument rests on that declassify. Base, head and an independent Python scanner agree on all BIP352 receiving vectors and 500 random transactions with labels. The speedup is 26% at N=2 and about 13% up to N=100, and fades at large N (medians of 5 interleaved bench silentpayments runs).


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/secp256k1. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-23 21:15 UTC

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