add DLEQ proof based on BIP374 and use it for silent payments #1651

pull stratospher wants to merge 18 commits into bitcoin-core:master from stratospher:dleq-sp changing 26 files +9380 −35
  1. stratospher commented at 5:32 pm on February 3, 2025: contributor

    built on top of c4942d36.

    BIP352 requires senders to compute output scripts using ECDH shared secrets from the same secret keys used to sign the inputs. Generating an incorrect signature will produce an invalid transaction that will be rejected by consensus. An incorrectly generated output script can still be consensus-valid, meaning funds may be lost if it gets broadcast. By producing a DLEQ proof for the generated ECDH shared secrets, the signing entity can prove to other entities that the output scripts have been generated correctly without revealing the private keys.

    (from BIP 374)

    This PR:

    • adds support for DLEQ proof generation and verification using secp256k1_dleq_prove, secp256k1_dleq_verify based on implementation in secp256k1-zkp
    • introduces a new structure for storing proofs in silent payment module - secp256k1_silentpayments_dleq_data which stores the shared secret computed using ECDH, DLEQ proof and also the index of which recipient in the original unsorted recipient array the proof refers to.
    • adds 4 new APIs in silent payments module to create, verify, serialize and parse proof
      1. sender can create proof using secp256k1_silentpayments_sender_create_outputs_with_proof
        • in the existing secp256k1_silentpayments_sender_create_outputs API, output pubkey is created by iterating over all the recipients and computing ECDH for each unique recipient.
        • since creating the proof also requites iterating over all recipients and computing ECDH for each unique recipient, compute both proof(s) and output pubkey(s) in a new function - secp256k1_silentpayments_sender_create_outputs_with_proof.
        • secp256k1_silentpayments_sender_create_outputs can use secp256k1_silentpayments_sender_create_outputs_with_proofinternally.
      2. both sender and receiver can verify proof using secp256k1_silentpayments_verify_proof
      3. Serialisation function (secp256k1_silentpayments_dleq_data_serialize) serialises secp256k1_silentpayments_dleq_data structure into bytes. secp256k1_silentpayments_dleq_data_parse parses the bytes back into secp256k1_silentpayments_dleq_data structure.
        1. useful if proof creation is done on 1 sender side device (ex: hardware wallet) and proof verification is done on another sender side device (ex: software wallet). secp256k1_silentpayments_dleq_data structure can be serialised into bytes using secp256k1_silentpayments_dleq_data_serialize and then the bytes can be sent from 1 device to another.
        2. secp256k1_silentpayments_dleq_data_parse can reconstruct the bytes back into secp256k1_silentpayments_dleq_data structure which can then be used for proof verification.

    open questions:

    1. is the API design ok?
      1. see examples/silentpayments.c for 2 scenarios in which proof verification could be useful. (Alice sends bitcoins to Bob and Carol in 1 silent payment transaction)
        1. useful situation - On Alice’s side
          1. Alice’s hardware wallet creates proof
          2. Alice’s hardware wallet sends proof to Alice’s software wallet
          3. Alice’s software wallet verifies proof and makes sure ECDH shared secrets for generating SP output is computed correctly. Alice can get additional safety guarantee about computed SP output before sending funds.
        2. not sure if this situation is useful - On Bob’s/Carol’s side
          1. Alice’s software wallet sends proof to Bob’s/Carol’s software wallet
          2. Bob’s/Carol’s software wallet verifies proof and makes sure ECDH shared secrets for generating SP output is computed correctly.
    2. should this be shipped with SP module or separately? only problem with separate release would be redundant APIs like secp256k1_silentpayments_sender_create_outputs and secp256k1_silentpayments_sender_create_outputs_with_proof for backward compatibility.

    Huge thanks to @ theStack for brainstorming and discussing this with me! ❤️

  2. in src/modules/silentpayments/tests_impl.h:606 in cec4d3553a outdated
    598@@ -598,12 +599,135 @@ void run_silentpayments_test_vectors(void) {
    599     }
    600 }
    601 
    602+static void rand_scalar(secp256k1_scalar *scalar) {
    603+    unsigned char buf32[32];
    604+    testrand256(buf32);
    605+    secp256k1_scalar_set_b32(scalar, buf32, NULL);
    606+}
    


    theStack commented at 6:49 am on February 9, 2025:
    could use testutil_random_scalar_order from testutil.h instead

    stratospher commented at 7:21 am on February 11, 2025:
    oh nice! done.
  3. in src/modules/silentpayments/tests_impl.h:615 in cec4d3553a outdated
    610+    secp256k1_gej pointj;
    611+    rand_scalar(&x);
    612+
    613+    secp256k1_ecmult_gen(&CTX->ecmult_gen_ctx, &pointj, &x);
    614+    secp256k1_ge_set_gej(point, &pointj);
    615+}
    


    theStack commented at 6:50 am on February 9, 2025:
    could use testutil_random_ge_test from testutil.h instead

    stratospher commented at 7:21 am on February 11, 2025:
    done.
  4. in src/modules/silentpayments/dleq_impl.h:57 in cec4d3553a outdated
    55+static int secp256k1_dleq_hash_point(secp256k1_sha256 *sha, secp256k1_ge *p) {
    56+    unsigned char buf[33];
    57+    size_t size = 33;
    58+    if (!secp256k1_eckey_pubkey_serialize(p, buf, &size, 1)) {
    59+        return 0;
    60+    }
    


    theStack commented at 7:01 am on February 9, 2025:
    pubkey serialization should always succeed here (as it would only fail if p is point at inifinity), so could VERIFY_CHECK that instead, like e.g. https://github.com/bitcoin-core/secp256k1/blob/00774d0723af1974e2a113db4adc479bfc47e20f/src/modules/musig/keyagg_impl.h#L131-L138

    stratospher commented at 5:33 am on February 11, 2025:
    hmm true. but since input to this function is secp256k1_ge and not secp256k1_pubkey, unsure if it makes sense to restrict possible values of secp256k1_ge here? (in the musig example, input to the function is secp256k1_pubkey)
  5. in src/modules/silentpayments/dleq_impl.h:71 in cec4d3553a outdated
    66+    secp256k1_sha256 sha;
    67+    unsigned char masked_key[32];
    68+    int i;
    69+    if (xonly_pk32 != NULL) {
    70+        return 0;
    71+    }
    


    theStack commented at 7:06 am on February 9, 2025:
    is the xonly_pk32 parameter actually needed or can it be dropped?

    stratospher commented at 6:30 am on February 11, 2025:
    ah good point. I’ve dropped it. was following the approach in schnorr module but it’s not needed.
  6. in src/modules/silentpayments/dleq_impl.h:97 in cec4d3553a outdated
    92+
    93+    if (algolen == sizeof(dleq_algo) && secp256k1_memcmp_var(algo, dleq_algo, algolen) == 0) {
    94+        secp256k1_nonce_function_bip374_sha256_tagged(&sha);
    95+    } else {
    96+        secp256k1_sha256_initialize_tagged(&sha, algo, algolen);
    97+    }
    


    theStack commented at 7:25 am on February 9, 2025:
    do we need to support creating nonces with other tags than “BIP0374/nonce”? currently the else-path is not reachable, as far as I can see

    stratospher commented at 7:19 am on February 11, 2025:
    oh true, I’ve removed it.
  7. in src/modules/silentpayments/dleq_impl.h:118 in cec4d3553a outdated
    113+    if (noncefp == NULL) {
    114+        noncefp = secp256k1_nonce_function_dleq;
    115+    }
    116+    memcpy(buf, A_33, 33);
    117+    memcpy(buf + 33, C_33, 33);
    118+    if (!noncefp(nonce, buf, 66, a32, NULL, dleq_algo, 4, ndata)) {
    


    theStack commented at 7:28 am on February 9, 2025:
    0    if (!noncefp(nonce, buf, 66, a32, NULL, dleq_algo, sizeof(dleq_algo), ndata)) {
    

    stratospher commented at 7:21 am on February 11, 2025:
    removed it to call secp256k1_nonce_function_dleq directly.
  8. in src/modules/silentpayments/dleq_impl.h:172 in cec4d3553a outdated
    167+ *  In:     a : scalar a to be proven that both A and C were generated from
    168+ *          B : point on the curve
    169+ *          A : point on the curve(a⋅G) generated from a
    170+ *          C : point on the curve(a⋅B) generated from a
    171+ *    noncefp : pointer to a nonce generation function. If NULL, secp256k1_nonce_function_dleq is used.
    172+ *      ndata : pointer to arbitrary data used by the nonce generation function (can be NULL). If it is non-NULL and
    


    theStack commented at 7:32 am on February 9, 2025:
    outdated parameters, at these positions are now aux_rand32 and m

    stratospher commented at 7:21 am on February 11, 2025:
    updated.
  9. in src/modules/silentpayments/tests_impl.h:758 in 9948bf1bee outdated
    720@@ -721,13 +721,187 @@ static void dleq_tests(void) {
    721     CHECK(secp256k1_dleq_nonce(&k, a32, A_33, C_33, NULL, NULL) == 1);
    722 }
    723 
    724+/* Test vectors according to BIP-374 ("Discrete Log Equality Proofs"). See
    725+ * https://github.com/bitcoin/bips/blob/master/bip-0374/test_vectors_(generate|verify)_proof.csv.
    726+ * Contains 32 bytes a + 33 bytes B + 32 bytes auxrand + 32 bytes optional msg + 64 bytes proof
    727+ * */
    728+
    729+static const unsigned char a_bytes[6][32] = {
    


    theStack commented at 7:52 am on February 9, 2025:
    ideally, the test vector data is created from a script that is included as well (see e.g. https://github.com/bitcoin-core/secp256k1/blob/master/tools/test_vectors_musig2_generate.py for musig2)

    stratospher commented at 5:45 pm on February 11, 2025:
    good point! done in 208352b.
  10. theStack commented at 7:53 am on February 9, 2025: contributor
    Concept ACK, nice! Left some initial comments for the BIP374-only parts (first two commits).
  11. stratospher force-pushed on Feb 11, 2025
  12. stratospher force-pushed on Feb 11, 2025
  13. jonatack commented at 7:29 pm on February 27, 2025: member
    Concept ACK
  14. real-or-random added the label feature on Mar 13, 2025
  15. real-or-random commented at 4:36 pm on March 13, 2025: contributor
    2. should this be shipped with SP module or separately? only problem with separate release would be redundant APIs like `secp256k1_silentpayments_sender_create_outputs` and `secp256k1_silentpayments_sender_create_outputs_with_proof` for backward compatibility.
    

    @josibake Do you have an opinion on this?

  16. real-or-random commented at 10:22 am on March 25, 2025: contributor
    02. should this be shipped with SP module or separately? only problem with separate release would be redundant APIs like `secp256k1_silentpayments_sender_create_outputs` and `secp256k1_silentpayments_sender_create_outputs_with_proof` for backward compatibility.
    

    @josibake Do you have an opinion on this?

    friendly ping :)

  17. josibake commented at 11:16 am on March 31, 2025: member

    Concept ACK

    02. should this be shipped with SP module or separately? only problem with separate release would be redundant APIs like `secp256k1_silentpayments_sender_create_outputs` and `secp256k1_silentpayments_sender_create_outputs_with_proof` for backward compatibility.
    

    @josibake Do you have an opinion on this?

    friendly ping :)

    Ayo, late to the party here 😅 First off, thanks for picking this up @stratospher ! I only briefly looked at the module, so all commentary here is very high level. Regarding shipping with the silent payments module, I don’t see any reason to couple the two. Rather, I would consider the silent payments module a client of the DLEQ module.

    only problem with separate release would be redundant APIs like secp256k1_silentpayments_sender_create_outputs and secp256k1_silentpayments_sender_create_outputs_with_proof

    I don’t see this as a problem. An alternative to this would be to extend create_outputs with an unused out param in anticipation of the proof, but I much prefer what you have here with create_outputs_with_proof. There will also still be use cases where computing the proof is unnecessary (e.g., single party sender where sender has full access to the private keys) and I think two separate functions in the API is clearer than a single function with an optional output parameter.

    not sure if this situation is useful - On Bob’s/Carol’s side Alice’s software wallet sends proof to Bob’s/Carol’s software wallet Bob’s/Carol’s software wallet verifies proof and makes sure ECDH shared secrets for generating SP output is computed correctly.

    I haven’t thought this all the way through, but I believe this could function as a proof of payment. Imagine Alice has paid Bob and wants to prove this to a 3rd party. Alice could compute the DLEQ proof and then pass this with the along with the shared secret to the 3rd party, who can then verify the DLEQ proof and use the shared secret to verify that the the SP output is present in the transaction. All this to say, I think having an example of this would be helpful!

  18. build: add skeleton for new silentpayments (BIP352) module f6eff121a6
  19. 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
    bd9f310b5e
  20. 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.
    7c5b1689a3
  21. 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 by exposing the
    `_create_shared_secret` and `_create_output_pubkey` functions in the
    API. This means the light client will need to manage their own scanning
    state, so wherever possible it is preferrable to use the
    `_recipient_scan_ouputs` function.
    
    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.
    
    Add functions for deserializing / serializing a prevouts_summary object to
    and from a public key. When serializing a prevouts_summary object, the
    input_hash is multplied into the prevout public key sum. This is so the
    object can be stored as public key for wallet rescanning later, or to send
    to light clients. For the light client, a `_parse` function is added which
    parses the compressed public key serialization into a `prevouts_summary`
    object.
    
    Finally, add test coverage for the receiving API.
    da3120e9b7
  22. silentpayments: add examples/silentpayments.c
    Demonstrate sending, scanning, and light client scanning.
    0e94f2dd05
  23. silentpayments: add benchmarks for scanning
    Add a benchmark for a full transaction scan and for scanning a single
    output. 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>
    a47037242d
  24. 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>
    60f209cc23
  25. 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>
    eba792a5be
  26. tests: add sha256 tag test
    Test midstate tags used in silent payments.
    f5c861610d
  27. ci: enable silentpayments module 4301cdcc6b
  28. docs: update README c4942d3646
  29. add dleq implementation
    - modify secp256k1-zkp's dleq implementation to be consistent with
      BIP 374.
    - use BIP374 notations.
    - add DLEQ tests
    90630fb284
  30. add BIP374 test vectors
    Add BIP374 test vectors. The vectors are generated with a Python script
    that converts the 2 csv files from the BIP - test_vectors_generate_proof.csv
    and test_vectors_verify_proof.csv to C code:
    
    $ ./tools/test_vectors_dleq_generate.py bips/bip-0374 > ./src/modules/silentpayments/dleq_vectors.h
    518c459a41
  31. add secp256k1_silentpayments_create_shared_secret_with_proof
    - add new internal function which returns both DLEQ proof and
      shared secret.
    - the existing secp256k1_silentpayments_create_shared_secret API
    is refactored to use
    secp256k1_silentpayments_create_shared_secret_with_proof.
    1f42784c58
  32. introduce struct to hold DLEQ proof related data for silent payments
    - structure contains 33-byte shared secret point + 64-byte DLEQ proof
      + index of recipient in original unsorted array of silent payment
        recipients
    - add functions to serialise and parse the structure to/from bytes
    fb1fd4e065
  33. add secp256k1_silentpayments_sender_create_outputs_with_proof
    - add new API which returns DLEQ proofs along with outputs for recipients
    - the existing API secp256k1_silentpayments_sender_create_outputs simply
      calls secp256k1_silentpayments_sender_create_outputs_with_proof
      internally.
    c5db9222b3
  34. add secp256k1_silentpayments_verify_proof c70d0e5a38
  35. add tests for creating and verifying proofs
    - in examples/silentpayments.c
        - sender now generates outputs with proof and verifies the proof
        - proof can be serialised to bytes and sent to recipient
        - bytes can be parsed back to proof by recipient as well
        - recipient can verify proof
    - in tests_recipients_helper, run_silentpayments_test_vector_send
        - along with output checks, generate DLEQ proofs and verify them
    ed1094896e
  36. stratospher force-pushed on Sep 29, 2025
  37. in src/modules/silentpayments/dleq_impl.h:175 in 90630fb284 outdated
    170+        return 0;
    171+    }
    172+    if (!secp256k1_eckey_pubkey_serialize(C, C_33, &pubkey_size, 1)) {
    173+        return 0;
    174+    }
    175+    ret &= secp256k1_dleq_nonce(&k, a32, A_33, C_33, aux_rand32);
    


    theStack commented at 4:18 pm on October 9, 2025:
    as per the latest version of the BIP (0.2.0), the nonce should also include the message, see https://github.com/bitcoin/bips/blob/3d0bab3cc211be9d40c0029f62c7e4eebd27ea21/bip-0374.mediawiki?plain=1#L77, I guess the test vectors are outdated and would fail if re-generated with the latest .csv file
  38. in src/modules/silentpayments/dleq_impl.h:91 in 90630fb284 outdated
    86+
    87+    secp256k1_nonce_function_bip374_sha256_tagged(&sha);
    88+    /* Hash masked-key||msg using the tagged hash as per the spec */
    89+    secp256k1_sha256_write(&sha, masked_key, 32);
    90+    secp256k1_sha256_write(&sha, msg, msglen);
    91+    secp256k1_sha256_finalize(&sha, nonce32);
    


    theStack commented at 4:21 pm on October 9, 2025:
    should memclear the sha and masked_key instances here (as also done in the BIP340 nonce function)
  39. in src/modules/silentpayments/dleq_impl.h:190 in 90630fb284 outdated
    185+    /* s = k + e * a */
    186+    secp256k1_dleq_challenge(e, B, &R1, &R2, A, C, m);
    187+    secp256k1_scalar_mul(s, e, a);
    188+    secp256k1_scalar_add(s, s, &k);
    189+
    190+    secp256k1_scalar_clear(&k);
    


    theStack commented at 4:22 pm on October 9, 2025:
    should also clear out the secret key serialization a32 here
  40. in include/secp256k1_silentpayments.h:468 in fb1fd4e065 outdated
    463+ *
    464+ *  Out:       output: pointer to a 101-byte array to place the serialized `secp256k1_silentpayments_dleq_data` in
    465+ *  In:    dleq_data: pointer to an initialized secp256k1_silentpayments_dleq_data object
    466+ */
    467+SECP256K1_API void secp256k1_silentpayments_dleq_data_serialize(
    468+    unsigned char *output33,
    


    theStack commented at 4:25 pm on October 9, 2025:
    0    unsigned char *output101,
    

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-10-13 19:15 UTC

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