Add “silentpayments” module implementing BIP352 (take 4, limited to full-node scanning) #1765

pull theStack wants to merge 11 commits into bitcoin-core:master from theStack:silentpayments_module_fullnode_only changing 23 files +7938 −35
  1. theStack commented at 12:06 pm on October 31, 2025: contributor

    Description

    This PR implements BIP352 with scanning limited to full-nodes. Light-client scanning is planned to be added in a separate PR in the future. The following 5 API functions are currently introduced:

    Sender side [BIP description]:

    • secp256k1_silentpayments_sender_create_outputs: given a list of $n$ secret keys $a_1 … a_n$, a serialized outpoint, and a list of recipients (each consisting of silent payments scan pubkey and spend pubkey), create the corresponding transaction outputs (x-only public keys) for the sending transaction

    Receiver side, label creation [BIP description]:

    • secp256k1_recipient_create_label: given a scan secret key and label integer, calculate the corresponding label_tweak and label public key
    • secp256k1_recipient_create_labeled_spend_pubkey: given a spend public key and a label public key, create the corresponding labeled spend public key

    Receiver side, scanning [BIP description]:

    • secp256k1_recipient_prevouts_summary_create: given a list of $n$ public keys $A_1 … A_n$ and a serialized outpoint, create a prevouts_summary object needed for scanning
    • secp256k1_recipient_scan_outputs: given a prevouts_summary object, a recipients scan secret key and spend public key, and the relevant transaction outputs (x-only public keys), scan for outputs belonging to the recipients and and return the tweak(s) needed for spending the output(s). Optionally, a label_lookup callback function can be provided to also scan for labels.

    For a higher-level overview on what these functions exactly do, it’s suggested to look at a corresponding Python implementation that was created based on the secp256k1lab project (it passes the test vectors, so this “executable pseudo-code” should be correct).

    Changes to the previous take

    Based on the latest state of the previous PR #1698 (take 3), the following changes have been made:

    The scope reduction isn’t immediately visible in commit count (only one commit was only introducing light-client relevant functionality and could be completely removed), but the review burden compared #1698 is still significantly lower in terms of LOC, especially in the receiving commit.

    Open questions / TODOs

    • Recent proposals of reducing the worst-case scanning time (see posts by w0xlt and jonasnick, #1698 (comment) ff.) are not taken into account yet.
    • Not providing prevouts_summary (de)serialization functionality yet in the API poses the risk that users try to do it anyway by treating the opaque object as “serialized”. How to cope with that? Is adding a “don’t do this” comment in API header sufficient?
  2. real-or-random added the label feature on Nov 6, 2025
  3. w0xlt commented at 9:27 pm on November 6, 2025: none

    Added the optimized version on top of this PR: https://github.com/w0xlt/secp256k1/commit/8d16914cad57ba07da09d104f0c605ae6284462f

    For more context: #1698 (comment)

  4. theStack commented at 1:58 am on November 7, 2025: contributor

    Small supplementary update: I’ve created a corresponding Python implementation of the provided API functions based on secp256k1lab (https://github.com/theStack/secp256k1lab/blob/add_bip352_module_review_helper/src/secp256k1lab/bip352.py) (also linked in the PR description). The hope is that this makes reviewing this PR a bit easier by having a less noisy, “executable pseudo-code”-like description on what happens under the hood. The code passes the BIP352 test vectors and hence should be correct.

    Added the optimized version on top of this PR: w0xlt@8d16914

    For more context: #1698 (comment)

    Thanks for rebasing on top of this PR, much appreciated! I will take a closer look within the next days.

  5. w0xlt commented at 2:04 am on November 14, 2025: none

    Nit: Not related to optimization, but the diff below removes some redundant public-key serialization code:

      0diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
      1index 106da20..922433d 100644
      2--- a/src/modules/silentpayments/main_impl.h
      3+++ b/src/modules/silentpayments/main_impl.h
      4@@ -21,6 +21,19 @@
      5 /** magic bytes for ensuring prevouts_summary objects were initialized correctly. */
      6 static const unsigned char secp256k1_silentpayments_prevouts_summary_magic[4] = { 0xa7, 0x1c, 0xd3, 0x5e };
      7 
      8+/* Serialize a ge to compressed 33 bytes. Keeps eckey_pubkey_serialize usage uniform
      9+ * (expects non-const ge*), and centralizes the VERIFY_CHECK. */
     10+static SECP256K1_INLINE void secp256k1_sp_ge_serialize33(const secp256k1_ge* in, unsigned char out33[33]) {
     11+    size_t len = 33;
     12+    secp256k1_ge tmp = *in;
     13+    int ok = secp256k1_eckey_pubkey_serialize(&tmp, out33, &len, 1);
     14+#ifdef VERIFY
     15+    VERIFY_CHECK(ok && len == 33);
     16+#else
     17+    (void)ok;
     18+#endif
     19+}
     20+
     21 /** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to
     22  *  ensure the correct values of k are used when creating multiple outputs for a recipient.
     23  *
     24@@ -68,13 +81,11 @@ static int secp256k1_silentpayments_calculate_input_hash_scalar(secp256k1_scalar
     25     secp256k1_sha256 hash;
     26     unsigned char pubkey_sum_ser[33];
     27     unsigned char input_hash[32];
     28-    size_t len;
     29     int ret, overflow;
     30 
     31     secp256k1_silentpayments_sha256_init_inputs(&hash);
     32     secp256k1_sha256_write(&hash, outpoint_smallest36, 36);
     33-    ret = secp256k1_eckey_pubkey_serialize(pubkey_sum, pubkey_sum_ser, &len, 1);
     34-    VERIFY_CHECK(ret && len == sizeof(pubkey_sum_ser));
     35+    secp256k1_sp_ge_serialize33(pubkey_sum, pubkey_sum_ser);
     36     secp256k1_sha256_write(&hash, pubkey_sum_ser, sizeof(pubkey_sum_ser));
     37     secp256k1_sha256_finalize(&hash, input_hash);
     38     /* Convert input_hash to a scalar.
     39@@ -85,15 +96,13 @@ static int secp256k1_silentpayments_calculate_input_hash_scalar(secp256k1_scalar
     40      * an error to ensure strict compliance with BIP0352.
     41      */
     42     secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
     43-    ret &= !secp256k1_scalar_is_zero(input_hash_scalar);
     44+    ret = !secp256k1_scalar_is_zero(input_hash_scalar);
     45     return ret & !overflow;
     46 }
     47 
     48 static void secp256k1_silentpayments_create_shared_secret(const secp256k1_context *ctx, unsigned char *shared_secret33, const secp256k1_ge *public_component, const secp256k1_scalar *secret_component) {
     49     secp256k1_gej ss_j;
     50     secp256k1_ge ss;
     51-    size_t len;
     52-    int ret;
     53 
     54     secp256k1_ecmult_const(&ss_j, public_component, secret_component);
     55     secp256k1_ge_set_gej(&ss, &ss_j);
     56@@ -103,12 +112,7 @@ static void secp256k1_silentpayments_create_shared_secret(const secp256k1_contex
     57      * impossible at this point considering we have already validated the public key and
     58      * the secret key.
     59      */
     60-    ret = secp256k1_eckey_pubkey_serialize(&ss, shared_secret33, &len, 1);
     61-#ifdef VERIFY
     62-    VERIFY_CHECK(ret && len == 33);
     63-#else
     64-    (void)ret;
     65-#endif
     66+    secp256k1_sp_ge_serialize33(&ss, shared_secret33);
     67 
     68     /* Leaking these values would break indistinguishability of the transaction, so clear them. */
     69     secp256k1_ge_clear(&ss);
     70@@ -585,7 +589,6 @@ int secp256k1_silentpayments_recipient_scan_outputs(
     71                 secp256k1_ge output_negated_ge, tx_output_ge;
     72                 secp256k1_gej tx_output_gej, label_gej;
     73                 unsigned char label33[33];
     74-                size_t len;
     75 
     76                 secp256k1_xonly_pubkey_load(ctx, &tx_output_ge, tx_outputs[j]);
     77                 secp256k1_gej_set_ge(&tx_output_gej, &tx_output_ge);
     78@@ -595,7 +598,6 @@ int secp256k1_silentpayments_recipient_scan_outputs(
     79                 secp256k1_ge_neg(&output_negated_ge, &output_ge);
     80                 secp256k1_gej_add_ge_var(&label_gej, &tx_output_gej, &output_negated_ge, NULL);
     81                 secp256k1_ge_set_gej_var(&label_ge, &label_gej);
     82-                ret = secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 1);
     83                 /* Serialize must succeed because the point was just loaded.
     84                  *
     85                  * Note: serialize will also fail if label_ge is the point at infinity, but we know
     86@@ -603,7 +605,7 @@ int secp256k1_silentpayments_recipient_scan_outputs(
     87                  * Thus, we know that label_ge = tx_output_gej + output_negated_ge cannot be the
     88                  * point at infinity.
     89                  */
     90-                VERIFY_CHECK(ret && len == 33);
     91+                secp256k1_sp_ge_serialize33(&label_ge, label33);
     92                 label_tweak = label_lookup(label33, label_context);
     93                 if (label_tweak != NULL) {
     94                     found = 1;
     95@@ -617,7 +619,6 @@ int secp256k1_silentpayments_recipient_scan_outputs(
     96                 secp256k1_gej_neg(&label_gej, &tx_output_gej);
     97                 secp256k1_gej_add_ge_var(&label_gej, &label_gej, &output_negated_ge, NULL);
     98                 secp256k1_ge_set_gej_var(&label_ge, &label_gej);
     99-                ret = secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 1);
    100                 /* Serialize must succeed because the point was just loaded.
    101                  *
    102                  * Note: serialize will also fail if label_ge is the point at infinity, but we know
    103@@ -625,7 +626,7 @@ int secp256k1_silentpayments_recipient_scan_outputs(
    104                  * Thus, we know that label_ge = tx_output_gej + output_negated_ge cannot be the
    105                  * point at infinity.
    106                  */
    107-                VERIFY_CHECK(ret && len == 33);
    108+                secp256k1_sp_ge_serialize33(&label_ge, label33);
    109                 label_tweak = label_lookup(label33, label_context);
    110                 if (label_tweak != NULL) {
    111                     found = 1;
    
  6. w0xlt commented at 2:25 am on November 14, 2025: none

    nit: The following diff removes the implicit cast and clarifies that k is 4 bytes

     0diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
     1index 922433d..d94aed6 100644
     2--- a/src/modules/silentpayments/main_impl.h
     3+++ b/src/modules/silentpayments/main_impl.h
     4@@ -512,7 +512,8 @@ int secp256k1_silentpayments_recipient_scan_outputs(
     5     secp256k1_xonly_pubkey output_xonly;
     6     unsigned char shared_secret[33];
     7     const unsigned char *label_tweak = NULL;
     8-    size_t j, k, found_idx;
     9+    size_t j, found_idx;
    10+    uint32_t k;
    11     int found, combined, valid_scan_key, ret;
    12 
    13     /* Sanity check inputs */
    
  7. in src/modules/silentpayments/main_impl.h:625 in c11d30c25f
    620+                ret = secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 1);
    621+                /* Serialize must succeed because the point was just loaded.
    622+                 *
    623+                 * Note: serialize will also fail if label_ge is the point at infinity, but we know
    624+                 * this cannot happen since we only hit this branch if tx_output != output_xonly.
    625+                 * Thus, we know that label_ge = tx_output_gej + output_negated_ge cannot be the
    


    jonasnick commented at 4:21 pm on November 14, 2025:
    Should be - tx_output_gej?
  8. jonasnick commented at 4:25 pm on November 14, 2025: contributor

    Thanks @theStack for the new PR. I can confirm that this PR is a rebased version of #1698, with the light client functionality removed and comments addressed, except for:

  9. jonasnick commented at 4:33 pm on November 14, 2025: contributor

    Not providing prevouts_summary (de)serialization functionality yet in the API poses the risk that users try to do it anyway by treating the opaque object as “serialized”. How to cope with that? Is adding a “don’t do this” comment in API header sufficient?

    Is there a reason for serializing prevouts_summary without light client functionality? If not, I think the don’t do this comment is sufficient. Right now, in contrast to the docs of all other opaque objects, this is missing, however:

    The exact representation of data inside the opaque data structures is implementation defined and not guaranteed to be portable between different platforms or versions.

  10. build: add skeleton for new silentpayments (BIP352) module 62b752f578
  11. silentpayments: sending
    Add a routine for the entire sending flow which takes a set of private keys,
    the smallest outpoint, and list of recipients and returns a list of
    x-only public keys by performing the following steps:
    
    1. Sum up the private keys
    2. Calculate the input_hash
    3. For each recipient group:
        3a. Calculate a shared secret
        3b. Create the requested number of outputs
    
    This function assumes a single sender context in that it requires the
    sender to have access to all of the private keys. In the future, this
    API may be expanded to allow for a multiple senders or for a single
    sender who does not have access to all private keys at any given time,
    but for now these modes are considered out of scope / unsafe.
    
    Internal to the library, add:
    
    1. A function for creating shared secrets (i.e., a*B or b*A)
    2. A function for generating the "SharedSecret" tagged hash
    3. A function for creating a single output public key
    2c854b4771
  12. theStack force-pushed on Nov 15, 2025
  13. theStack commented at 0:48 am on November 15, 2025: contributor

    @w0xlt, @jonasnick: Thanks for the reviews! I’ve addressed the suggested changes:

    • in _recpient_scan_outputs: changed the type of k to uint32_t (comment above)
    • in _recipient_create_label: added a scan key validity check (+added a test for that) (#1698 - comment)
    • unified all mentions of “Silent Payments” to title case in the header API and example (#1698 - comment)
    • fixed typo s/elemement/element/ (#1698 - review)
    • in _recipient_scan_outputs: fixed comment in second label candidate (review above)
    • extended the API header comment for the _prevouts_summary opaque data structure, to point out that the data structure is implementation defined (like docs of all other opaque structs) (comment above)

    Nit: Not related to optimization, but the diff below removes some redundant public-key serialization code:

    Given that this compressed-pubkey-serialization pattern shows up repeatedly also in other modules (ellswift, musig), I think it would make the most sense to add a general helper (e.g. in eckey{,_impl}.h), which could be done in an independent PR. I’ve opened issue #1773 to see if there is conceptual support for doing this.

    Not providing prevouts_summary (de)serialization functionality yet in the API poses the risk that users try to do it anyway by treating the opaque object as “serialized”. How to cope with that? Is adding a “don’t do this” comment in API header sufficient?

    Is there a reason for serializing prevouts_summary without light client functionality? If not, I think the don’t do this comment is sufficient.

    Good point, I can’t think of a good reason for full nodes wanting to serialize prevouts_summary.

  14. silentpayments: recipient label support
    Add function for creating a label tweak. This requires a tagged hash
    function for labels. This function is used by the receiver for creating
    labels to be used for a) creating labeled addresses and b) to populate
    a labels cache when scanning.
    
    Add function for creating a labeled spend pubkey. This involves taking
    a label tweak, turning it into a public key and adding it to the spend
    public key. This function is used by the receiver to create a labeled
    silent payment address.
    
    Add tests for the label API.
    d81c6d530d
  15. silentpayments: receiving
    Add routine for scanning a transaction and returning the necessary
    spending data for any found outputs. This function works with labels via
    a lookup callback and requires access to the transaction outputs.
    Requiring access to the transaction outputs is not suitable for light
    clients, but light client support is enabled in the next commit.
    
    Add an opaque data type for passing around the prevout public key sum
    and the input hash tweak (input_hash). This data is passed to the scanner
    before the ECDH step as two separate elements so that the scanner can
    multiply the scan_key * input_hash before doing ECDH.
    
    Finally, add test coverage for the receiving API.
    f749a71735
  16. silentpayments: add examples/silentpayments.c
    Demonstrate sending and scanning on full nodes.
    d9b3b44065
  17. silentpayments: add benchmarks for scanning
    Add a benchmark for a full transaction scan.
    Only benchmarks for scanning are added as this is the most
    performance critical portion of the protocol.
    
    Co-authored-by: Sebastian Falbesoner <91535+thestack@users.noreply.github.com>
    6e8252b3d2
  18. tests: add BIP-352 test vectors
    Add the BIP-352 test vectors. The vectors are generated with a Python script
    that converts the .json file from the BIP to C code:
    
    $ ./tools/tests_silentpayments_generate.py test_vectors.json > ./src/modules/silentpayments/vectors.h
    
    Co-authored-by: Ron <4712150+macgyver13@users.noreply.github.com>
    Co-authored-by: Sebastian Falbesoner <91535+thestack@users.noreply.github.com>
    Co-authored-by: Tim Ruffing <1071625+real-or-random@users.noreply.github.com>
    95eda37e19
  19. tests: add constant time tests
    Co-authored-by: Jonas Nick <2582071+jonasnick@users.noreply.github.com>
    Co-authored-by: Sebastian Falbesoner <91535+thestack@users.noreply.github.com>
    796bedccfd
  20. tests: add sha256 tag test
    Test midstate tags used in silent payments.
    e406b931e8
  21. ci: enable silentpayments module 9f761c9288
  22. docs: update README 9103229d27
  23. theStack force-pushed on Nov 15, 2025

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: 2025-11-15 08:15 UTC

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