Add BIP352 module (take 3) #1698

pull josibake wants to merge 12 commits into bitcoin-core:master from josibake:bip352-silentpayments-module-2025 changing 23 files +8296 −35
  1. josibake commented at 8:53 am on July 10, 2025: member

    This PR implements BIP352 - Silent payments. It is recommended to read through the BIP before reviewing this PR.

    This is a continuation of the work in #1519 and only opened as a new PR due to the comment history on #1519 becoming quite long and difficult to sift through. It is recommended reviewers go through #1519 for background context, if interested.

  2. in include/secp256k1_silentpayments.h:342 in 6264c3d093 outdated
    337+ *                               pubkey as an argument and returns a pointer to
    338+ *                               the label tweak if the label exists, otherwise
    339+ *                               returns a NULL pointer (NULL if labels are not
    340+ *                               used)
    341+ *                label_context: pointer to a label context object (NULL if
    342+ *                               labels are not used)
    


    antonilol commented at 12:37 pm on July 10, 2025:
    0 *                label_context: pointer to a label context object (NULL if
    1 *                               labels are not used or context is not needed)
    

    (was changed after #1519 (review))


    josibake commented at 9:55 am on July 14, 2025:
    Thanks, I’ve updated the comment and added a test case for the (lookup, NULL) argument combination.
  3. in include/secp256k1_silentpayments.h:369 in 6264c3d093 outdated
    335+ *                 label_lookup: pointer to a callback function for looking up
    336+ *                               a label value. This function takes a label
    337+ *                               pubkey as an argument and returns a pointer to
    338+ *                               the label tweak if the label exists, otherwise
    339+ *                               returns a NULL pointer (NULL if labels are not
    340+ *                               used)
    


    antonilol commented at 4:26 pm on July 10, 2025:
    I think it should be documented for how long the returned pointer from label_lookup should be valid. I think it is obvious it should be valid until the next call of label_lookup, but it is currently not clear (from the docs) whether it should remain valid until secp256k1_silentpayments_recipient_scan_outputs returns. The current implementation of secp256k1_silentpayments_recipient_scan_outputs does not need this (from looking at the code) and in a safe Rust abstraction for this function (code, does contain some outdated comments) suggested for https://github.com/rust-bitcoin/rust-secp256k1/pull/721 (WIP bindings to this pull request’s code) I relied on this not being a requirement (though that can be changed).

    josibake commented at 9:53 am on July 14, 2025:

    Hi @antonilol , thanks for the review and bindings code! Agreed that this should be documented. How about something like:

     0diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
     1index 2e71405..11fa596 100644
     2--- a/include/secp256k1_silentpayments.h
     3+++ b/include/secp256k1_silentpayments.h
     4@@ -311,6 +311,11 @@ typedef struct secp256k1_silentpayments_found_output {
     5  *  the recipient uses labels. This allows for checking if a label exists in
     6  *  the recipients label cache and retrieving the label tweak during scanning.
     7  *
     8+ *  If used, the `label_lookup` function must return a pointer to a 32-byte label
     9+ *  tweak if the label is found, or NULL otherwise. The returned pointer must remain
    10+ *  valid until the next call to `label_lookup` or until the function returns,
    11+ *  whichever comes first. It is not retained beyond that.
    12+ *
    13  *  For the labels cache, `secp256k1_silentpayments_recipient_create_label`
    14  *  can be used.
    15  *
    

    I think the above is the most precise requirement, but perhaps a simpler alternative would be to require that the returned pointer (and labels cache, if used) be valid until _scan_outputs returns? You mention you rely on this not being the case today, but does this make a material difference if we do require that the returned pointer and labels cache be valid for the lifetime of the _scan_outputs function?


    antonilol commented at 1:00 pm on July 14, 2025:

    Looks good!

    I think the above is the most precise requirement, but perhaps a simpler alternative would be to require that the returned pointer (and labels cache, if used) be valid until _scan_outputs returns? You mention you rely on this not being the case today, but does this make a material difference if we do require that the returned pointer and labels cache be valid for the lifetime of the _scan_outputs function?

    This depends on the function signature on the Rust side, I have considered 2 cases. The difference is in what the user has to return from label_lookup.

    First, there is the one in the proof of concept binding I mentioned here, the user has to return any byte array of length 32 or nothing (translates to NULL pointer). The C side accepts a pointer so the Rust side stores this byte array on the stack and gives the C side a pointer to that. This place on the stack is reused, so requiring that the pointer lives for longer than until the next call of label_lookup means that previous return values need to be kept and some dynamic allocation would be needed. I see this as the easiest for user to use. As far as I can tell no borrow checker issues can arise from this design.

    Second, the user has to return a reference to a byte array of length 32 or nothing. The reference is required to live for as long as the function runs. This is because in Rust it is not (yet) possible to declare the reference is only required to live until the next call of label_lookup. This is simple to implement because it is basically a pointer cast and is compatible with _scan_outputs needing pointers to be valid for the lifetime of the function. This one is harder to use, especially for label_lookups that are not simply looking up in a data structure. Returning a reference to any byte array that is not stored outside the function’s stack frame is not allowed and storing it on the fly like the first case here probably requires the user to use unsafe code (needs a data structure that allows adding elements while there exist references to its existing elements because the reference is required to live for as long as the function runs) and has the same need for dynamic allocation like the first case can have.

    The validity of the context pointer should be completely up to the users of the C library (I consider the Rust binding to also be a user of the C library).

    I will also link this discussion in the Rust bindings pull request.


    josibake commented at 2:50 pm on July 14, 2025:
    Thanks for the detailed explanation! As I was thinking through this, it also occurred to me requiring the pointers to be valid for the lifetime of _scan_outputs would require keeping around pointers to every label found, even after they had been used by _scan_outputs. This seems undesirable, e.g., imagine a transaction with ~15k labeled outputs all paying to a single recipient. Your explanation seems to confirm my understanding.
  4. josibake force-pushed on Jul 14, 2025
  5. josibake commented at 3:04 pm on July 14, 2025: member

    Updated 6264c3d -> 9e85256 (2025_00 -> 2025_01, compare)

    • Added documentation for expectations around label_lookup pointer lifetimes (h/t @antonilol)
    • Update docs to accurately reflect that label_context is optional (h/t @antonilol)
    • Added a test case for passing a lookup callback with a null context (which required some small updates to the test label lookup function)
  6. in src/ctime_tests.c:343 in de508a78ac outdated
    338+    ret = secp256k1_silentpayments_recipient_scan_outputs(ctx, found_outputs, &n_found_outputs, tx_outputs, 1, key, &public_data, &recipient.labeled_spend_pubkey, NULL, NULL);
    339+    CHECK(ret == 1);
    340+
    341+    /* TODO: this fails */
    342+    /* CHECK(secp256k1_silentpayments_recipient_create_shared_secret(ctx, shared_secret, key, &public_data)); */
    343+    /* TODO: test secp256k1_silentpayments_recipient_create_output_pubkey */
    


    theStack commented at 12:45 pm on July 21, 2025:

    in commit de508a78ac66b93b0ff83b419cc6e149950ecc25: took a look into these TODOs, I think the following patch would tackle them (note that the serialize/parse-roundtrip looks superfluous at first sight, but it is needed to set the “combined” flag in the public data, which is ARG_CHECKEDed for 1 in _recipient_create_shared_secret):

     0diff --git a/src/ctime_tests.c b/src/ctime_tests.c
     1index 17964b9..83596a3 100644
     2--- a/src/ctime_tests.c
     3+++ b/src/ctime_tests.c
     4@@ -116,6 +116,8 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) {
     5     const secp256k1_xonly_pubkey *xonly_pubkeys[1];
     6     secp256k1_pubkey plain_pubkey;
     7     const secp256k1_pubkey *plain_pubkeys[1];
     8+    unsigned char public_data_ser[33] = { 0 };
     9+    unsigned char shared_secret[33] = { 0 };
    10 #endif
    11 
    12     for (i = 0; i < 32; i++) {
    13@@ -338,9 +340,10 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) {
    14     ret = secp256k1_silentpayments_recipient_scan_outputs(ctx, found_outputs, &n_found_outputs, tx_outputs, 1, key, &public_data, &recipient.labeled_spend_pubkey, NULL, NULL);
    15     CHECK(ret == 1);
    16 
    17-    /* TODO: this fails */
    18-    /* CHECK(secp256k1_silentpayments_recipient_create_shared_secret(ctx, shared_secret, key, &public_data)); */
    19-    /* TODO: test secp256k1_silentpayments_recipient_create_output_pubkey */
    20+    CHECK(secp256k1_silentpayments_recipient_public_data_serialize(ctx, public_data_ser, &public_data));
    21+    CHECK(secp256k1_silentpayments_recipient_public_data_parse(ctx, &public_data, public_data_ser));
    22+    CHECK(secp256k1_silentpayments_recipient_create_shared_secret(ctx, shared_secret, key, &public_data));
    23+    CHECK(secp256k1_silentpayments_recipient_create_output_pubkey(ctx, &xonly_pubkey, shared_secret, &recipient.labeled_spend_pubkey, 0));
    24 
    25 #endif
    26 }
    27diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
    28index 3fc42f5..7d3ad41 100644
    29--- a/src/modules/silentpayments/main_impl.h
    30+++ b/src/modules/silentpayments/main_impl.h
    31@@ -708,6 +708,7 @@ int secp256k1_silentpayments_recipient_create_shared_secret(const secp256k1_cont
    32     ret &= secp256k1_scalar_set_b32_seckey(&rsk, recipient_scan_key32);
    33     ret &= secp256k1_silentpayments_recipient_public_data_load_pubkey(ctx, &A_tweaked_ge, public_data);
    34     /* If there are any issues with the recipient scan key or public data, return early */
    35+    secp256k1_declassify(ctx, &ret, sizeof(ret));
    36     if (!ret) {
    37         return 0;
    38     }
    

    josibake commented at 2:18 pm on July 21, 2025:
    Thanks for the patch! I’ve updated the ctime tests to use this, and also removed the remaining TODOs from the test file (with comments where appropriate).
  7. josibake force-pushed on Jul 21, 2025
  8. real-or-random commented at 2:15 pm on July 21, 2025: contributor

    Sorry, stopping CI here. We’re about to make a release and need to the CI. :)

    We’ll restart the jobs here afterwards.

  9. josibake commented at 2:21 pm on July 21, 2025: member

    Update 9e85256 -> a4db279 (2025_01 -> 2025_02, compare)

    • Update the constant time tests to cover the _recipient_created_shared_secret and _recipient_created_output_pubkey functions (h/t @theStack )
    • Remove no longer needed TODO comments and clarify why a constant time test without a label lookup function is sufficient for _recipient_scan_outputs
  10. josibake force-pushed on Jul 22, 2025
  11. josibake commented at 10:29 am on July 22, 2025: member
    Rebased on top of 0.7.0 release :tada: a4db279 -> e35bede (2025_02 -> 2025_02_rebase, compare)
  12. josibake commented at 11:13 am on July 22, 2025: member
    I did a deep dive on using (*arg)[size] in this PR and opened #1710 for discussion, since this is a broader topic than just this PR. The relevant changes for here and the downstream Bitcoin Core PRs are https://github.com/josibake/secp256k1/commit/5a1088066b2ce5e2e77b4e4bc190575d1171c374 and https://github.com/josibake/bitcoin/commit/5835d987477fc8eae391e4d1cc5033d921925ea1
  13. real-or-random added the label feature on Jul 23, 2025
  14. in src/modules/silentpayments/main_impl.h:148 in 896e0af2f8 outdated
    145+    }
    146+    ret = secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar);
    147+    /* tweak add only fails if t_k_scalar is equal to the dlog of -P_output_ge, but t_k_scalar is the output of a collision resistant hash function. */
    148+    /* TODO: consider declassify ret */
    149+    /* TODO: but we don't want to imply this can never happen */
    150+    VERIFY_CHECK(ret);
    


    theStack commented at 2:51 pm on July 23, 2025:
    in commit 896e0af2f883d21ba3540290f176dcaa7f57272d: As for those TODOs, probably the safest option is to return an error if _pubkey_tweak_add indeed fails? Initially I assumed that doing that would introduce a branch that can’t be tested in practice, but I think it is: by picking arbitrary shared_secret33 and k values, and calculating recipient_labeled_spend_pubkey as -G^(_create_t_k(shared_secret33, k)) the tweaking should fail with these three values. Will try to come up with some test code later to verify.

    theStack commented at 5:14 pm on July 23, 2025:

    Ok, managed to come up with a test that makes the VERIFY_CHECK on the _pubkey_tweak_add return value fail:

     0diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h
     1index a1acab7..2a10791 100644
     2--- a/src/modules/silentpayments/tests_impl.h
     3+++ b/src/modules/silentpayments/tests_impl.h
     4@@ -367,6 +367,23 @@ static void test_recipient_api(void) {
     5     CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkey(CTX, &t, NULL, &p, 0));
     6     CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_output_pubkey(CTX, &t, o, NULL, 0));
     7 
     8+    /* check the _recipient_create_output_pubkey cornercase where internal tweaking would fail;
     9+       this is the case if the recipient spend public key is P = -(create_t_k(shared_secret, k))*G */
    10+    {
    11+        const unsigned char *shared_secret = o;
    12+        const uint32_t k = 0;
    13+        secp256k1_scalar t_k;
    14+        unsigned char t_k_ser[32];
    15+        secp256k1_pubkey fake_spend_pubkey;
    16+        secp256k1_xonly_pubkey output_xonly;
    17+
    18+        secp256k1_silentpayments_create_t_k(&t_k, shared_secret, k);
    19+        secp256k1_scalar_get_b32(t_k_ser, &t_k);
    20+        CHECK(secp256k1_ec_pubkey_create(CTX, &fake_spend_pubkey, t_k_ser));
    21+        CHECK(secp256k1_ec_pubkey_negate(CTX, &fake_spend_pubkey));
    22+        CHECK(secp256k1_silentpayments_create_output_pubkey(CTX, &output_xonly, shared_secret, &fake_spend_pubkey, k) == 0);
    23+    }
    24+
    25     n_f = 0;
    26     labels_cache.entries_used = 0;
    27     CHECK(secp256k1_silentpayments_recipient_scan_outputs(CTX, fp, &n_f, tp, 1, ALICE_SECKEY, &pd, &p, &label_lookup, &labels_cache));
    

    I assume that in practice a user can’t be tricked into such a scenario though, as the recipient spend public key is known/generated before the shared secret is.


    josibake commented at 12:10 pm on July 24, 2025:
    Awesome! IIRC, the initial hesitance to returning a value was adding an untestable branch in the code. But considering this can be exercised in a test (which indicates its not inconceivable that someone could be tricked into calling this function with a maliciously crafted spend pubkey), I removed the VERIFY_CHECK in favour of returning the value and added your test case. Thanks for working on this!

    josibake commented at 9:23 am on July 25, 2025:
    Updated this to actually calculate the shared secret from the public data object, to make valgrind happy.
  15. josibake force-pushed on Jul 24, 2025
  16. josibake commented at 12:23 pm on July 24, 2025: member

    Updated e35bede -> 1a84908 (2025_02_rebase -> 2025_03, compare)

    • Added a test case for the _recipient_create_output_pubkey corner case (h/t @theStack)
    • Removed the VERIFY_CHECK in favour of returning an error
  17. in src/modules/silentpayments/main_impl.h:253 in 78bbd7b65f outdated
    247+    }
    248+    /* Compute input_hash = hash(outpoint_L || (a_sum * G)) */
    249+    secp256k1_ecmult_gen(&ctx->ecmult_gen_ctx, &A_sum_gej, &a_sum_scalar);
    250+    secp256k1_ge_set_gej(&A_sum_ge, &A_sum_gej);
    251+    /* TODO: comment */
    252+    secp256k1_declassify(ctx, &A_sum_ge, sizeof(A_sum_ge));
    


    theStack commented at 9:04 pm on July 24, 2025:

    Suggestion for the comment to tackle another TODO 🔫 (IIUC, the non-constant timing of group element serialization seems to ultimately be caused by variable-time normalization of field elements):

    0    /* Need to declassify the pubkey sum because serializing a group element (done in the
    1       `_calculate_input_hash` call following) is not a constant-time operation */
    2    secp256k1_declassify(ctx, &A_sum_ge, sizeof(A_sum_ge));
    

    josibake commented at 9:22 am on July 25, 2025:
    Thanks for digging into this! Added the comment with some slight formatting changes.
  18. in src/modules/silentpayments/main_impl.h:145 in 78bbd7b65f outdated
    142+        secp256k1_scalar_clear(&t_k_scalar);
    143+        return 0;
    144+    }
    145+    /* `tweak_add` only fails if t_k_scalar is equal to the dlog of -P_output_ge, but t_k_scalar is the output of a collision resistant hash function.
    146+     * This will never happen under normal usage, but we handle this error to anyways to protect against this function being called with a malicious
    147+     * B_spend argument, i.e., B_spend = -G^(_create_t_k(shared_secret33, k))
    


    theStack commented at 9:07 pm on July 24, 2025:

    only noticing now that in one of my earlier comments I was using multiplicative notation (cryptocamp leaving its marks already I guess 😁)

    0     * B_spend argument, i.e., B_spend = -(_create_t_k(shared_secret33, k))*G
    

    real-or-random commented at 7:04 am on July 25, 2025:
    I was about to ask what this strange “-” symbol in -G^(_create_t_k(shared_secret33, k)) is supposed to mean. :stuck_out_tongue:

    josibake commented at 9:21 am on July 25, 2025:
    Team additive 💪
  19. in src/modules/silentpayments/main_impl.h:308 in 78bbd7b65f outdated
    283+            secp256k1_silentpayments_create_shared_secret(ctx, shared_secret, &a_sum_scalar, &pk);
    284+            k = 0;
    285+        }
    286+        if (!secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->labeled_spend_pubkey, k)) {
    287+            secp256k1_scalar_clear(&a_sum_scalar);
    288+            return 0;
    


    theStack commented at 9:22 pm on July 24, 2025:
    should also memclear the shared secret here and in the error path a few lines above (admitedly a bit annoying though that this leads to so much code duplication :/)

    josibake commented at 9:21 am on July 25, 2025:
    Done, also slightly reworded the comment. Overall, while slightly annoying, the code duplication doesn’t seem too bad to me and I don’t see a clever way to DRY it up.
  20. in examples/silentpayments.c:151 in 72489a733c outdated
    146+
    147+    /* Before we can call actual API functions, we need to create a "context" */
    148+    secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
    149+    if (!fill_random(randomize, sizeof(randomize))) {
    150+        printf("Failed to generate randomness\n");
    151+        return 1;
    


    theStack commented at 9:32 pm on July 24, 2025:
    nit: could update the main return values in the example to use EXIT_FAILURE/EXIT_SUCCESS (done for other examples in #1654)

    josibake commented at 9:20 am on July 25, 2025:
    Done
  21. josibake force-pushed on Jul 25, 2025
  22. josibake commented at 9:20 am on July 25, 2025: member

    Update 1a84908 -> 2948a9b (2025_03 -> 2025_04, compare)

    • Fixed valgrind error in test
    • Update the example to use EXIT_SUCCESS/EXIT_FAILURE (h/t @theStack)
    • Clear shared secret variable consistently (and update comment) (h/t @theStack)
    • Add comment explaining why we declassify the pubkey sum (h/t @theStack)

    Thanks for the thorough review, @theStack !

  23. josibake force-pushed on Jul 25, 2025
  24. josibake commented at 2:33 pm on July 25, 2025: member

    Update 2948a9b -> 64ecd6c (2025_04 -> 2025_05, compare)

    • Remove no longer needed TODO comment regarding _cmov
    • Remove todo comment regarding input_hash, now that this is properly specified in the BIP

    cc @jonasnick and @real-or-random regarding the use of a VERIFY_CHECK in favour of returning an error, when returning an error results in an untestable branch. I’m happy with the approach here were we use a VERIFY_CHECK for input_hash and t_k to check for an overflow of the curve order. However, given this is something we’ve discussed a few times in the post, would be great to hear your thoughts on this and I’m happy to defer to whatever you both think is best.

    This should address all of the outstanding TODOs (at least the ones we left comments for 😅 )

  25. in src/modules/silentpayments/bench_impl.h:111 in e18e04f63c outdated
    106+            k
    107+        ));
    108+    }
    109+}
    110+
    111+static void bench_silentpayments_full_tx_scan(void* arg, int iters) {
    


    theStack commented at 1:27 pm on July 28, 2025:

    It might make sense to split this benchmark into “labeled” and “unlabeled” variants in order to also get a feeling how overhead the labels scanning causes, with something like e.g.:

     0diff --git a/src/modules/silentpayments/bench_impl.h b/src/modules/silentpayments/bench_impl.h
     1index 1dd1e5a..366d172 100644
     2--- a/src/modules/silentpayments/bench_impl.h
     3+++ b/src/modules/silentpayments/bench_impl.h
     4@@ -108,7 +108,7 @@ static void bench_silentpayments_output_scan(void* arg, int iters) {
     5     }
     6 }
     7 
     8-static void bench_silentpayments_full_tx_scan(void* arg, int iters) {
     9+static void bench_silentpayments_full_tx_scan(void* arg, int iters, int use_labels) {
    10     int i;
    11     size_t n_found = 0;
    12     secp256k1_silentpayments_found_output *found_output_ptrs[2];
    13@@ -116,6 +116,8 @@ static void bench_silentpayments_full_tx_scan(void* arg, int iters) {
    14     const secp256k1_xonly_pubkey *tx_input_ptrs[2];
    15     bench_silentpayments_data *data = (bench_silentpayments_data*)arg;
    16     secp256k1_silentpayments_recipient_public_data public_data;
    17+    const secp256k1_silentpayments_label_lookup label_lookup_fn = use_labels ? label_lookup : NULL;
    18+    const void *label_context = use_labels ? label_cache : NULL;
    19 
    20     for (i = 0; i < 2; i++) {
    21         found_output_ptrs[i] = &data->found_outputs[i];
    22@@ -135,11 +137,19 @@ static void bench_silentpayments_full_tx_scan(void* arg, int iters) {
    23             data->scan_key,
    24             &public_data,
    25             &data->spend_pubkey,
    26-            label_lookup, label_cache)
    27+            label_lookup_fn, label_context)
    28         );
    29     }
    30 }
    31 
    32+static void bench_silentpayments_full_tx_scan_unlabeled(void *arg, int iters) {
    33+    bench_silentpayments_full_tx_scan(arg, iters, 0);
    34+}
    35+
    36+static void bench_silentpayments_full_tx_scan_labeled(void *arg, int iters) {
    37+    bench_silentpayments_full_tx_scan(arg, iters, 1);
    38+}
    39+
    40 static void run_silentpayments_bench(int iters, int argc, char** argv) {
    41     bench_silentpayments_data data;
    42     int d = argc == 1;
    43@@ -147,7 +157,8 @@ static void run_silentpayments_bench(int iters, int argc, char** argv) {
    44     /* create a context with no capabilities */
    45     data.ctx = secp256k1_context_create(SECP256K1_FLAGS_TYPE_CONTEXT);
    46 
    47-    if (d || have_flag(argc, argv, "silentpayments")) run_benchmark("silentpayments_full_tx_scan", bench_silentpayments_full_tx_scan, bench_silentpayments_scan_setup, NULL, &data, 10, iters);
    48+    if (d || have_flag(argc, argv, "silentpayments")) run_benchmark("silentpayments_full_tx_scan_labeled", bench_silentpayments_full_tx_scan_labeled, bench_silentpayments_scan_setup, NULL, &data, 10, iters);
    49+    if (d || have_flag(argc, argv, "silentpayments")) run_benchmark("silentpayments_full_tx_scan_unlabeled", bench_silentpayments_full_tx_scan_unlabeled, bench_silentpayments_scan_setup, NULL, &data, 10, iters);
    50     if (d || have_flag(argc, argv, "silentpayments")) run_benchmark("silentpayments_output_scan", bench_silentpayments_output_scan, bench_silentpayments_scan_setup, NULL, &data, 10, iters);
    51 
    52     secp256k1_context_destroy(data.ctx);
    
    0Benchmark                     ,    Min(us)    ,    Avg(us)    ,    Max(us)
    1
    2silentpayments_full_tx_scan_labeled,    94.6       ,    96.6       ,   101.0
    3silentpayments_full_tx_scan_unlabeled,    82.7       ,    84.2       ,    85.9
    4silentpayments_output_scan    ,    85.3       ,    87.5       ,    90.7
    

    josibake commented at 2:11 pm on July 28, 2025:

    Agreed. I added the dummy labels originally to make sure I was reporting worst-case1 numbers for the full scan benchmark, and the idea was to add more benchmarks in a follow-up that more accurately benchmark labels. However, I think what you have here is nicer and we can still leave a TODO comment to add a more representative benchmark for scanning with labels.


    1. worst case in the sense that all of the scan-with-labels code paths are hit during the benchmark. Its not an actual worst case in that you could have a very large, poorly implemented labels cache which would necessarily slow down scanning ↩︎


    josibake commented at 6:09 pm on July 28, 2025:
    Added your diff with a few minor naming differences and left a TODO comment for filling out the labels benchmark in a follow-up. In particular, I would be interested to see a benchmark that demonstrates that scan time is largely unaffected by label cache size, e.g., scanning with 10 labels vs 100,000 labels shouldn’t make a difference. I think this should be a follow-up, however, since it’s not strictly necessary and will likely require dedicated review of its own to get things right.

    theStack commented at 8:08 pm on July 29, 2025:
    Seems fine to limit the scanning-with-labels benchmark to the cryptography-related parts for now. The planned extended benchmark doesn’t even necessarily have to live in the secp repo IMHO, it could as well be in a project using the module (i.e. Bitcoin Core in our case), where a labels cache has to be implemented anyways and we wouldn’t have to fake it (which might be non-trivial in C to achieve something representative, as I guess we’d need to implement some hashmap-like data structure a la std::unordered_map?). But no hard feelings on the details of where/how, I agree that seeing such a followup would be very interesting indeed.

    josibake commented at 10:48 am on July 30, 2025:
    Excellent point regarding the hashmap. I’ll leave the TODO comment for now, but it might be that the follow-up is simply to remove the comment in favour of, as you suggest, benchmarking this in a more realistic setting like Bitcoin Core.
  26. josibake force-pushed on Jul 28, 2025
  27. josibake commented at 6:06 pm on July 28, 2025: member

    Updated 64ecd6c -> 3c4af8f (2025_05 -> 2025_06, compare)

    • Updates the benchmarks per @theStack ’s suggestion to have separate benchmarks for _full_scan and full_scan_with_labels
    • Leave a TODO comment for a follow-up to make the labels benchmark more representative of real world usage
    • Cleans up the benchmark arguments and formatting
  28. in examples/silentpayments.c:35 in 070ea00233 outdated
    30+        int diff = p1[i] - p2[i];
    31+        if (diff != 0) {
    32+            return diff;
    33+        }
    34+    }
    35+    return EXIT_SUCCESS;
    


    theStack commented at 11:05 pm on July 30, 2025:

    in 070ea002339bf10603da5dd772795c7ccae3af51, function my_memcmp_var

    0    return 0;
    

    as this doesn’t indicate a process execution status


    josibake commented at 9:33 am on July 31, 2025:
    Ah, good catch!
  29. theStack commented at 11:49 pm on July 30, 2025: contributor

    In the CI commit, could add the silent payments module also to the native macOS arm64 job (as done for musig recently in #1699), e.g.

     0diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
     1index 8ee13ce..f612a84 100644
     2--- a/.github/workflows/ci.yml
     3+++ b/.github/workflows/ci.yml
     4@@ -583,13 +583,13 @@ jobs:
     5       fail-fast: false
     6       matrix:
     7         env_vars:
     8-          - { WIDEMUL: 'int64',  RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
     9+          - { WIDEMUL: 'int64',  RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
    10           - { WIDEMUL: 'int128_struct', ECMULTGENPRECISION: 2, ECMULTWINDOW: 4 }
    11-          - { WIDEMUL: 'int128',                  ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
    12+          - { WIDEMUL: 'int128',                  ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
    13           - { WIDEMUL: 'int128', RECOVERY: 'yes' }
    14-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
    15-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc' }
    16-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CPPFLAGS: '-DVERIFY' }
    17+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
    18+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CC: 'gcc' }
    19+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CPPFLAGS: '-DVERIFY' }
    20           - BUILD: 'distcheck'
    21 
    22     steps:
    
  30. josibake force-pushed on Jul 31, 2025
  31. josibake commented at 10:04 am on July 31, 2025: member

    Updated 3c4af8f -> eb32d06 (2025_06 -> 2025_07, compare)

    • Enable macOS arm64 jobs in the CI (h/t @theStack)
    • Fix return value in the example (h/t @theStack)
    • Improve test coverage (100% function and branch coverage 💪)
    • Simplify public data functions

    While working through the test coverage, I realised the two public data internal functions public_data_load_pubkey and public_data_load_input_hash are unnecessary, as they were just thin wrappers around secp256k1 function calls that never return an error.

    I’ve removed those functions and instead ensured we always check a public data object for correctness before accessing the internals.

  32. in include/secp256k1_silentpayments.h:53 in eeed5671de outdated
    48+
    49+/** Create Silent Payment outputs for recipient(s).
    50+ *
    51+ *  Given a list of n secret keys a_1...a_n (one for each silent payment
    52+ *  eligible input to spend), a serialized outpoint, and a list of recipients,
    53+ *  create the taproot outputs.
    


    Sjors commented at 1:23 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: it may be worth reminding the reader why there’s never more than one secret key per input; BIP352 excludes inputs “with conditional branches or multiple public keys”.

    josibake commented at 10:53 am on August 1, 2025:
    Added a reminder, with a prompt to review the BIP for more details.
  33. in include/secp256k1_silentpayments.h:64 in eeed5671de outdated
    59+ *  payment. Determining the smallest outpoint from the list of transaction
    60+ *  inputs is the responsibility of the caller. It is strongly recommended
    61+ *  that implementations ensure they are doing this correctly by using the
    62+ *  test vectors from BIP352.
    63+ *
    64+ *  If necessary, the secret keys are negated to enforce the right y-parity.
    


    Sjors commented at 1:33 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: the taproot secret keys

    josibake commented at 10:53 am on August 1, 2025:
    Done.
  34. in include/secp256k1_silentpayments.h:66 in eeed5671de outdated
    57+ *  inputs). This value MUST be the smallest outpoint out of all of the
    58+ *  transaction inputs, otherwise the recipient will be unable to find the
    59+ *  payment. Determining the smallest outpoint from the list of transaction
    60+ *  inputs is the responsibility of the caller. It is strongly recommended
    61+ *  that implementations ensure they are doing this correctly by using the
    62+ *  test vectors from BIP352.
    


    Sjors commented at 1:35 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: I still think it would be nice to have a helper for this, but it can be introduced in a later PR.

    josibake commented at 10:39 am on August 1, 2025:
    I don’t think this module should include a helper function for this, but we can debate this in a later PR 😉
  35. in src/modules/silentpayments/main_impl.h:14 in eeed5671de outdated
     6@@ -7,10 +7,299 @@
     7 #define SECP256K1_MODULE_SILENTPAYMENTS_MAIN_H
     8 
     9 #include "../../../include/secp256k1.h"
    10+#include "../../../include/secp256k1_extrakeys.h"
    11 #include "../../../include/secp256k1_silentpayments.h"
    12 
    13-/* TODO: implement functions for sender side. */
    14+/** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to
    15+ *  ensure the correct values of k are used when creating multiple outputs for a recipient. */
    


    Sjors commented at 2:00 pm on July 31, 2025:

    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: from offline conversation my understanding is as follows:

    We don’t add a tie breaker here (e.g. ->index) because:

    1. The protocol doesn’t require it
    2. It would break the constant time property of heap sort

    If so, it would be good to say this in a comment so nobody is a) confused b) tempted later to introduce a footgun.

    (the temptation might come from a user of this library who finds that their tests are breaking half of the time because they had an ordering related bug - or just natural programmer desire for deterministic sort )


    josibake commented at 9:35 am on August 1, 2025:

    Following up on this, I misspoke regarding 2). Its not about being constant time (heapsort is $O(n\log{}n)$ time, $O(1)$ space), but rather heapsort is unstable so there isn’t a straightforward way to do a stable, e.g., multi-key, sort.

    Even if there were a straightforward way, I’d still push back since the protocol is designed to not rely on ordering. Rather, the BIP only specifies grouping by scan key and sorting is just our way of implementing “grouping.” I’ll flesh out the comment to make this more clear.


    josibake commented at 10:37 am on August 1, 2025:
    Updated the comment to indicate developers cannot and should not rely on deterministic sorting of the recipients.
  36. in src/modules/silentpayments/main_impl.h:51 in eeed5671de outdated
    34+    #if defined(_MSC_VER) && (_MSC_VER < 1933)
    35+    #pragma warning(pop)
    36+    #endif
    37+}
    38+
    39+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Inputs". */
    


    Sjors commented at 2:07 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: anyone have a nice Python / Ruby incantation to derive the midstates of BIP0352/...?

    theStack commented at 3:57 pm on July 31, 2025:
    Wrote one in C using secp256k1 itself a while ago, see #1653 (regarding Python, the thread suggests using the https://github.com/cloudtools/sha256 library, but didn’t try that yet; with the standard library implementation in hashlib it’s unfortunately not possible to access the internal state :/).

    josibake commented at 9:45 am on August 1, 2025:
    I’ve had this thought a few times in the past. Opened https://github.com/secp256k1lab/secp256k1lab/issues/10 to discuss further, as that seems like the most natural place to me for an example/script to live.

    real-or-random commented at 10:45 am on August 12, 2025:

    see https://gist.github.com/real-or-random/a4aaae5d04eee9f63e7a2e43d25bc2b1#file-sha256_midstate-py

    But I think the “proper"TM way is that josie adds a test that checks if the finalized hash is the same as when recomputed from scratch, see https://github.com/bitcoin-core/secp256k1/blob/e523e4f90e1b1c0fba49cd8a08016e1a8dff9232/src/modules/musig/tests_impl.h#L551-L586. Then the reviewer only needs to review this check.


    josibake commented at 3:46 pm on August 13, 2025:
    Agree on the “proper” approach, thanks for the pointer to the musig test. I’ll add the same here.

    josibake commented at 10:07 am on August 15, 2025:
    Considering we are using optimised tagged hashes in a majority of the modules, this seemed like a good opportunity to DRY up the code and propose a convention for testing tagged hash midstates: https://github.com/bitcoin-core/secp256k1/pull/1725
  37. in src/modules/silentpayments/main_impl.h:311 in eeed5671de outdated
    294+        if (!secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->labeled_spend_pubkey, k)) {
    295+            secp256k1_scalar_clear(&a_sum_scalar);
    296+            secp256k1_memclear(&shared_secret, sizeof(shared_secret));
    297+            return 0;
    298+        }
    299+        k++;
    


    Sjors commented at 2:34 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: it’s worth noting somewhere, maybe in the BIP, that removing (omitting) any output k later will cause the recipient to not find outputs > k

    josibake commented at 9:46 am on August 1, 2025:
    Great point! I’ll add a footnote to the BIP.

    josibake commented at 10:53 am on August 1, 2025:
  38. in include/secp256k1_silentpayments.h:33 in eeed5671de outdated
    25@@ -25,6 +26,93 @@ extern "C" {
    26  *  any further elliptic-curve operations from the wallet.
    27  */
    28 
    29+/** This struct serves as an input parameter for passing the silent payment
    30+ *  address data to `silentpayments_sender_create_outputs`.
    31+ *
    32+ *  The index field is for when more than one address is being sent to in
    33+ *  a transaction. Index is set to the position of this recipient in the
    


    Sjors commented at 2:54 pm on July 31, 2025:
    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: “is set to” -> “must be set to … starting with 0” (since it’s checked).

    josibake commented at 10:36 am on August 1, 2025:
    Done. I also took out the first sentence, as it seemed to imply we only need to set the index field in certain situations (the index field must always be set, even if only sending to one silent payment recipient).
  39. in src/modules/silentpayments/main_impl.h:269 in eeed5671de outdated
    266+     *
    267+     * More specifically, this ensures `k` is incremented from 0 to the number of requested outputs for each recipient group,
    268+     * where a recipient group is all addresses with the same scan public key.
    269+     */
    270+    secp256k1_silentpayments_recipient_sort(ctx, recipients, n_recipients);
    271+    last_recipient = *recipients[0];
    


    Sjors commented at 3:10 pm on July 31, 2025:

    In eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending: so the last shall be first, and the first last :-)

    I think it would be more clear to store recipients[0]->scan_pubkey instead and call it current_scan_pubkey.


    josibake commented at 10:35 am on August 1, 2025:
    Done.
  40. Sjors commented at 3:24 pm on July 31, 2025: member

    Reviewed eeed5671de059d5812571dc0bfcaee12f10f71e2 silentpayments: sending again and it looks good to me (not a C expert). Comments mainly about documentation and one variable name suggestion.

    It could make sense to split 993d34b7ae3d4a8b6d3a8957b309b38e7e50e796 tests: add BIP-352 test vectors and move the send-side tests directly after this commit. Ditto for the send example in ce7cb982d4aceefd8bb8591fd069c3a0b81d1ef2 silentpayments: add examples/silentpayments.c.

  41. josibake force-pushed on Aug 1, 2025
  42. josibake commented at 10:34 am on August 1, 2025: member

    Updated eb32d06 -> b26c87d (2025_07 -> 2025_08, compare)

    • Updated sending API docs (h/t @Sjors)
    • Updated comments and improved variable naming for sending (h/t @Sjors)

    Thanks for the review, @Sjors !

  43. josibake commented at 11:10 am on August 1, 2025: member

    It could make sense to split https://github.com/bitcoin-core/secp256k1/commit/993d34b7ae3d4a8b6d3a8957b309b38e7e50e796 tests: add BIP-352 test vectors and move the send-side tests directly after this commit. Ditto for the send example in https://github.com/bitcoin-core/secp256k1/commit/ce7cb982d4aceefd8bb8591fd069c3a0b81d1ef2 silentpayments: add examples/silentpayments.c.

    By splitting the test vectors commit into sending and receiving commits, it would be possible to break out just sending into its own PR (recall that sending can be implemented without implementing receiving). If this makes it easier to review and merge sending, I’m open to it. This would allow downstream work on the Bitcoin Core sending PR to proceed, while continued review on receiving happens here.

    On the flipside, I do feel like the module in its current state is close to finished so I’m not sure how much benefit splitting at this stage would bring. I dropped a note in the #secp256k1 IRC; will wait to hear from other reviewers/maintainers before making a decision here.

  44. in examples/silentpayments.c:252 in e0c225c047 outdated
    247+        }
    248+        /* To keep things simple, we cast the tx_output_ptr array to remove the
    249+         * const qualifer, so that we can create the outputs. We want the const
    250+         * qualifer because this same array will be passed to the scan function
    251+         * later in the example.
    252+         */
    


    theStack commented at 11:47 am on August 5, 2025:
    in e0c225c047645ae92db877f661ede385f4f8603d: this comment seems outdated? the cast for tx_output_ptrs is now happening at the scan function below (from non-const to const rather than the other way round)
  45. in examples/silentpayments.c:439 in e0c225c047 outdated
    454+                for (i = 0; i < n_found_outputs; i++) {
    455+                    printf("    ");
    456+                    secp256k1_xonly_pubkey_serialize(ctx,
    457+                        serialized_xonly,
    458+                        &found_outputs[i].output
    459+                    );
    


    theStack commented at 12:30 pm on August 5, 2025:

    in e0c225c047645ae92db877f661ede385f4f8603d:

    0                    ret = secp256k1_xonly_pubkey_serialize(ctx,
    1                        serialized_xonly,
    2                        &found_outputs[i].output
    3                    );
    4                    assert(ret);
    

    just to evaluate all API call return values for consistency (here and in the light client scanning loop below)

  46. in include/secp256k1_silentpayments.h:92 in e0c7a09415 outdated
    87+ *                            multiple outputs for the same recipient.
    88+ *              n_recipients: the number of recipients. This is equal to the
    89+ *                            total number of outputs to be generated as each
    90+ *                            recipient may passed multiple times to generate
    91+ *                            multiple outputs for the same recipient
    92+ *         outpoint_smallest: serialized (36-byte) smallest outpoint
    


    theStack commented at 12:53 pm on August 5, 2025:

    in e0c7a09415de00e94e8a8c8fc3c5154d00cc0739:

    0 *       outpoint_smallest36: serialized (36-byte) smallest outpoint
    

    (also in the description above)

  47. in include/secp256k1_silentpayments.h:415 in 0a79d3a2e5 outdated
    410+ *                               incremented for each additional output created
    411+ *                               or after each output found when scanning)
    412+ */
    413+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_output_pubkey(
    414+    const secp256k1_context *ctx,
    415+    secp256k1_xonly_pubkey *P_output_xonly,
    


    theStack commented at 1:12 pm on August 5, 2025:
    nit: could maybe consider removing the P_ prefix as we don’t use this naming scheme anywhere else in this or other modules (I think I introduced that in the first take of the BIP352 module for some reason)
  48. theStack commented at 1:33 pm on August 5, 2025: contributor

    Left some comments with minor suggestions regarding API header and the example, I’m planning to finish the full review this week.

    No strong opinion on whether a split-up into sending/receiving PRs makes sense, from a reviewing point of view it likely doesn’t make a difference for me at this point. I could imagine it’s potentially brittle to only release the sending part if there is no strong test coverage provided that the generated outputs can also be found by receivers, so I’d slightly lean to keep as-is, I guess.

  49. josibake force-pushed on Aug 6, 2025
  50. josibake commented at 9:15 am on August 6, 2025: member

    Updated b26c87d -> 393a2a2 (2025_08 -> 2025_09, compare)

    • Various example fix-ups (h/t @theStack)
    • s/outpoint_smallest/outpoint_smallest36/ (h/t @theStack )
    • s/P_output_xonly/output_xonly/ (h/t @theStack ) @theStack thanks for the continued review!

    I could imagine it’s potentially brittle to only release the sending part if there is no strong test coverage provided that the generated outputs can also be found by receivers

    For this, I think it would be sufficient to show that the same inputs/outputs from the test vectors used in the sending PR are the same ones used in the receiving PR. But overall, I’m also leaning towards keeping everything together unless its expected that the sending commit is largely good to go but more review is required for the benchmarks, example, and receiving commits.

  51. josibake force-pushed on Aug 6, 2025
  52. josibake commented at 11:27 am on August 6, 2025: member

    Updated 393a2a2 -> 96b2362 (2025_09 -> 2025_10, compare)

    • Add asserts in example (h/t @theStack )
    • Update header to better document how functions can fail

    cc @real-or-random @jonasnick on the header documentation. A majority of the error cases are can only happen if the user provides malformed inputs, or the output of a hash function results in something greater than the curve order. I opted to document these, but wanted to confirm to make sure I am doing something consistent with the rest of the library.

  53. in include/secp256k1_silentpayments.h:61 in 32d8077cb7 outdated
    55@@ -53,7 +56,7 @@ typedef struct secp256k1_silentpayments_recipient {
    56  *  public keys are excluded from silent payments eligible inputs; see BIP352
    57  *  for more information.
    58  *
    59- *  `outpoint_smallest` refers to the smallest outpoint lexicographically
    60+ *  `outpoint_smallest36` refers to the smallest outpoint lexicographically
    


    theStack commented at 3:09 pm on August 8, 2025:
    in commit 32d8077cb7d288c7114dcf8d7b831d7ada590bd0: minimum-diff pedantic nit: this change, as well as all other single-line changes in the same commit below should already be part of an earlier commit
  54. in include/secp256k1_silentpayments.h:73 in d176c1e5d8 outdated
    68+ *  (taproot) outputs or not.
    69+ *
    70+ *  Returns: 1 if creation of outputs was successful.
    71+ *           0 if the input private keys sum to zero,
    72+ *             or input_hash or hash(shared_secret || k) are invalid scalars
    73+ *             (stastically improbable).
    


    theStack commented at 3:30 pm on August 8, 2025:
    0 *             (statistically improbable).
    

    (here and in many other instances)


    josibake commented at 8:07 am on August 11, 2025:
    Turns out I am not as good a typist as I thought I was 😅
  55. in include/secp256k1_silentpayments.h:65 in d176c1e5d8 outdated
    60+ *  payment. Determining the smallest outpoint from the list of transaction
    61+ *  inputs is the responsibility of the caller. It is strongly recommended
    62+ *  that implementations ensure they are doing this correctly by using the
    63+ *  test vectors from BIP352.
    64+ *
    65+ *  If necessary, the taproot secret keys are negated to enforce even y-parity.
    


    theStack commented at 3:41 pm on August 8, 2025:
    Not knowing the answer myself, but I’m wondering now whether internal details like this should be part of the header API documentation (that’s slightly related to the questions brought up at #1698 (comment) about the proper detail level of error case documentation), or if it’s just sufficient to say that there are two secret key types that are somehow treated differently. Or even just remove the sentence completely (and the “For that reason” part). Maybe it’s not a big deal to keep it, but I’d assume the user of the module likely doesn’t care (and shouldn’t) what exactly is going on under the hood at this detail level. That sentence was already part of the first take at #1471, so all the blame goes to me anyways :sweat_smile:

    josibake commented at 8:06 am on August 11, 2025:
    Good catch, I think keeping the API documentation focused on usage makes sense. In this case, I think we can safely drop line 65 to the end. libsecp256k1 already has different data types for plain keys and taproot keys, which I think is explanation enough for a user as to why we have two different arguments for accepting secret keys in this function.
  56. theStack commented at 3:58 pm on August 8, 2025: contributor

    I’ve convinced myself that the implementation in this PR matches the BIP352 specification and the usual quality requirements, especially regarding treatment of secret data (constant-time ops, clearing out the memory) are met from what I can tell.

    The remaining comments are just nits for typos/minimizing-diff and a general question about the detail level of header API documentation. Other than that, I think I’d be ready to give a “take it with a grain of salt, i’m not a cryptographer”-ACK at this point :detective:

  57. josibake force-pushed on Aug 11, 2025
  58. josibake commented at 8:31 am on August 11, 2025: member

    Updated 96b2362 -> 142f07b (2025_10 -> 2025_11, compare)

    • Spelling and documentation fixes, (h/t @theStack)

    Thanks again, @theStack, for the thorough review! Much appreciated.

  59. in src/modules/silentpayments/tests_impl.h:280 in 051ad33942 outdated
    232+    memset(&r[1].labeled_spend_pubkey.data, 0, sizeof(secp256k1_pubkey));
    233+    CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
    234+    {
    235+         secp256k1_pubkey tmp = r[1].labeled_spend_pubkey;
    236+         memset(&r[1].labeled_spend_pubkey, 0, sizeof(r[1].labeled_spend_pubkey));
    237+         CHECK_ILLEGAL(CTX, secp256k1_silentpayments_sender_create_outputs(CTX, op, rp, 2, SMALLEST_OUTPOINT, NULL, 0, p, 1));
    


    theStack commented at 10:43 pm on August 11, 2025:
    Seems like this memset is a no-op and the test a repetition of the one four lines above. Was the idea here to run the previous test without damaging the spend pubkey of r[1] first? (would make sense considering that it is saved and restored via tmp)

    josibake commented at 9:33 am on August 12, 2025:
    I believe the lines above were a leftover. The idea, as you mention, was to add coverage for a damaged spend pubkey and a damaged scan pubkey. I removed the old lines in favour of the two test cases using the tmp values.
  60. in src/modules/silentpayments/tests_impl.h:19 in 051ad33942 outdated
    14+ *             Seckey: secret key for Alice
    15+ *            Outputs: generated outputs from Alice's secret key and Bob/Carol's
    16+ *                     scan public keys
    17+ *  Smallest Outpoint: smallest outpoint lexicographically from the transaction
    18+ *             orderc: a scalar which overflows the secp256k1 group order
    19+ *   Malformed Seckey: a seckey that is all zeros
    


    theStack commented at 10:44 pm on August 11, 2025:
    nitty nit: for easier matching for the reader, could sort these constant descriptions in the same order as they appear in the array definitions below
  61. in include/secp256k1_silentpayments.h:275 in b107203694 outdated
    270+/** Callback function for label lookups
    271+ *
    272+ *  This function is implemented by the recipient to check if a value exists in
    273+ *  the recipients label cache during scanning.
    274+ *
    275+ *  For creating the labels cache,
    


    theStack commented at 11:20 pm on August 11, 2025:

    nit: slightly more specific (as the current wording implies that the actual cache data structure can be created with this module)

    0 *  For creating the labels cache data (label public key and label tweak),
    

    or just

    0 *  For creating the labels cache data,
    

    josibake commented at 8:59 am on August 12, 2025:
    I think “data” should be sufficient, considering the data is better explained in the documentation for the _create_label function.
  62. in src/modules/silentpayments/main_impl.h:134 in b107203694 outdated
    130@@ -131,28 +131,28 @@ static void secp256k1_silentpayments_create_t_k(secp256k1_scalar *t_k_scalar, co
    131     secp256k1_sha256_clear(&hash);
    132 }
    133 
    134-static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *P_output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *recipient_labeled_spend_pubkey, uint32_t k) {
    135-    secp256k1_ge P_output_ge;
    136+static int secp256k1_silentpayments_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *recipient_labeled_spend_pubkey, uint32_t k) {
    


    theStack commented at 11:37 pm on August 11, 2025:
    reviewer-friendliness nit: the P_ prefix removal should be part of an earlier commit already to not create extra diff lines
  63. in src/modules/silentpayments/main_impl.h:505 in b107203694 outdated
    500+     */
    501+    secp256k1_ge_from_bytes(&ge, &public_data->data[5]);
    502+    secp256k1_scalar_set_b32(&input_hash_scalar, &public_data->data[5 + 64], NULL);
    503+    secp256k1_eckey_pubkey_tweak_mul(&ge, &input_hash_scalar);
    504+    secp256k1_eckey_pubkey_serialize(&ge, output33, &pubkeylen, 1);
    505+    VERIFY_CHECK(pubkeylen == 33);
    


    theStack commented at 11:41 pm on August 11, 2025:
    for consistency, could still VERIFY_CHECK the return values of these two function calls
  64. in src/modules/silentpayments/main_impl.h:666 in b107203694 outdated
    661+                VERIFY_CHECK(ret);
    662+                secp256k1_pubkey_save(&found_outputs[n_found]->label, &label_ge);
    663+            } else {
    664+                found_outputs[n_found]->found_with_label = 0;
    665+                /* Set the label public key with an invalid public key value */
    666+                secp256k1_memclear(&found_outputs[n_found]->label, sizeof(secp256k1_pubkey));
    


    theStack commented at 11:56 pm on August 11, 2025:
    nit: a regular memset is sufficient here, as this is no stack data (and not secret data either)
  65. in src/modules/silentpayments/main_impl.h:711 in b107203694 outdated
    706+    }
    707+    secp256k1_silentpayments_create_shared_secret(ctx, shared_secret33, &rsk, &A_tweaked_ge);
    708+
    709+    /* Explicitly clear secrets */
    710+    secp256k1_scalar_clear(&rsk);
    711+    return ret;
    


    theStack commented at 0:00 am on August 12, 2025:

    equivalent but more explicit (ret can only be 1 at this point)

    0    return 1;
    
  66. in Makefile.am:283 in b1138b20a4 outdated
    273@@ -273,6 +274,10 @@ src/wycheproof/ecdh_secp256k1_test.h:
    274 	mkdir -p $(@D)
    275 	python3 $(top_srcdir)/tools/tests_wycheproof_generate_ecdh.py $(top_srcdir)/src/wycheproof/ecdh_secp256k1_test.json > $@
    276 
    277+src/modules/silentpayments/vectors.h:
    278+	mkdir -p $(@D)
    279+	python3 $(top_srcdir)/tools/tests_silentpayments_generate.py $(top_srcdir)/src/modules/silentpayments/bip352_send_and_receive_test_vectors.json > $@
    


    theStack commented at 0:23 am on August 12, 2025:
    unrelated nit: noted that there is no CMake-equivalent for that, but it’s not for any other generated test vectors either, so probably a topic for a different PR (if at all, I assume test vectors are usually just updated by manual script runs rather than involving the build system)

    josibake commented at 9:22 am on August 12, 2025:
    Would consider this a follow-up as a more general build system improvement.

    real-or-random commented at 9:29 am on August 12, 2025:

    (if at all, I assume test vectors are usually just updated by manual script runs rather than involving the build system)

    The current idea is that you can manually delete the files, and the build system will then regenerate them (and you don’t need to worry about how this is done). And yes, that’s restricted to autotools, which is fine because it’s a developer-only thing. Decoupling this from the build system entirely would also be an option.

    Anyway, none of this is related to this PR.


    josibake commented at 10:44 am on August 12, 2025:
    I opened #1723 as a follow-up. Mostly because I think it would be really nice to keep the build systems consistent, and I got curious on what this would look like for CMake.
  67. theStack commented at 0:32 am on August 12, 2025: contributor
    Left some more nitty findings below.
  68. josibake force-pushed on Aug 12, 2025
  69. in configure.ac:413 in ca2538a878 outdated
    408+  enable_module_schnorrsig=yes
    409+
    410+  if test x"$enable_module_extrakeys" = x"no"; then
    411+    AC_MSG_ERROR([Module dependency error: You have disabled the extrakeys module explicitly, but it is required by the silentpayments module.])
    412+  fi
    413+  enable_module_extrakeys=yes
    


    real-or-random commented at 9:38 am on August 12, 2025:

    https://github.com/bitcoin-core/secp256k1/pull/1698/commits/ca2538a8782628437488ca32c49f52957aff5f69 I’d drop this check for consistency. (But nothing bad happens if we keep it.)

    • Musig also depends on schnorrsig and thus transitively on extrakeys, but we don’t check for this.
    • You don’t add an equivalent check to CMake

    josibake commented at 12:54 pm on August 14, 2025:
    Removed.
  70. josibake commented at 9:40 am on August 12, 2025: member

    Updated 142f07b -> 2b57d2a (2025_11 -> 2025_12, compare)

    • Documentation fixes (h/t @theStack)
    • Added VERIFY_CHECK and more consistent ret (h/t @theStack)
    • Removed dead code from tests and improved legibility (h/t @theStack)
  71. in src/modules/silentpayments/main_impl.h:234 in 1a5f53f2cf outdated
    204+    }
    205+    ARG_CHECK(outpoint_smallest36 != NULL);
    206+    /* ensure the index field is set correctly */
    207+    for (i = 0; i < n_recipients; i++) {
    208+        ARG_CHECK(recipients[i]->index == i);
    209+    }
    


    real-or-random commented at 10:28 am on August 12, 2025:
    https://github.com/bitcoin-core/secp256k1/commit/1a5f53f2cf7d3228b7ed147282ea5bd796957cbf Is there a reason why we require the user to set this instead of setting it ourselves? This would make the API simpler and easier to explain.

    josibake commented at 12:18 pm on August 14, 2025:

    I vaguely remember discussing this with you and @jonasnick previously where we were debating back and forth on whether the caller should set this or we should set it internally. The preference was to have the caller set the value as this seemed the least confusing of our options, but I can’t recall the reasons why 😅 .

    If we keep the index field in the struct, then I do think we should have the caller set it, as it seems even more confusing to have the caller create the recipient structs without fully initialising them. That leaves removing the field entirely from the struct, but I don’t think there is a (sane) way to keep track of the original ordering internally so that we can ensure we map the generated outputs back to the original ordering.

    So I’d prefer to leave it as-is, but perhaps we can better explain this in the API documentation?


    theStack commented at 5:02 pm on August 18, 2025:
    Fwiw, I remember thinking about this idea while reviewing at least once, but always ended up on dropping the suggestion as I found it strange that users would pass in input parameters that can be partly uninitialized (haven’t checked, but I assume that none of the existing API functions allow this). Users have to fill in the recipients in a loop anyway, so this should usually need one extra line of code. I agree though that the current solution is not ideal either, so no strong opinion. As a compromise, to not allow uninitialized values, one could require that the index field is always zero (maybe renaming it to something like _internal, to express that the user doesn’t have to care what it is used for), and then set it internally, but not sure if that is better.

    real-or-random commented at 8:29 am on August 19, 2025:
    Okay, yes. My thinking was that I prefer an API in which the user passes a data structure which doesn’t even have an index field, but this seems hard in C (or at least, it comes with a bunch of other disadvantages). So yes, let’s keep this unless someone has a great idea to get rid of this field without introducing any other overhead. In any case, having the field is safe because we check that the value is set correctly. It’s just a minor annoyance for the caller.

    josibake commented at 9:46 am on August 20, 2025:

    API in which the user passes a data structure which doesn’t even have an index field, but this seems hard in C (or at least, it comes with a bunch of other disadvantages).

    Just wanted to confirm this is where I landed as well, i.e., its annoying to have the field but trying to work around the field is even more annoying.

  72. in include/secp256k1_silentpayments.h:46 in 1a5f53f2cf outdated
    36+ *  The spend public key field is named `labeled_spend_pubkey` to indicate this
    37+ *  spend public key may be tweaked with an optional label. This is not relevant
    38+ *  for the sender and is purely a documentation convention to differentiate
    39+ *  between other uses of `spend_pubkey` in this API, where it is meant to refer
    40+ *  to the unlabeled spend public key.
    41+ */
    


    real-or-random commented at 10:32 am on August 12, 2025:

    https://github.com/bitcoin-core/secp256k1/commit/1a5f53f2cf7d3228b7ed147282ea5bd796957cbf some writing improvements. if you take them, please reformat

     0/** The data from a single recipient address
     1 *
     2 *  This struct serves as an input argument to `silentpayments_sender_create_outputs`.
     3 *
     4 *  `index` must be set to the position (starting with 0) of this recipient in the
     5 *  `recipients` array passed to `silentpayments_sender_create_outputs`. It is
     6 *  used to map the returned generated outputs back to the original recipient.
     7 *
     8 *  Note:
     9 *  The spend public key field is named `labeled_spend_pubkey` to indicate that this
    10 *  spend public key may have been optionally tweaked with a label by the sender.  
    11 *  This is purely a documentation convention to differentiate 
    12 *  between other uses of `spend_pubkey` in this API, where it is meant to refer
    13 *  to the unlabeled spend public key.
    14 *  Whether `labeled_spend_pubkey` has actually been tagged is irrelevant
    15 *  for the sender and
    16 */
    
    • Would it be nicer for the user if we had spend_pubkey and unlabeled_spend_pubkey or (raw_ or whatever) instead?

    josibake commented at 3:51 pm on August 13, 2025:
    I think what you’re suggesting makes more sense. As the comment already mentions, from the senders perspective it is always just a spend public key. Using this naming convention, we could simplify the docs here and instead move the explainer to the label API docs, where it is actually important that the user of the API understand the distinction.

    josibake commented at 12:53 pm on August 14, 2025:
    Updated to use spend_pubkey and unlabeled_spend_pubkey, which I think is much better than what we had before.
  73. in src/modules/silentpayments/tests_impl.h:14 in 1a5f53f2cf outdated
     8+
     9+#include "../../../include/secp256k1_silentpayments.h"
    10+
    11+/** Constants
    12+ *
    13+ *             orderc: a scalar which overflows the secp256k1 group order
    


    real-or-random commented at 10:33 am on August 12, 2025:
    https://github.com/bitcoin-core/secp256k1/commit/1a5f53f2cf7d3228b7ed147282ea5bd796957cbf want to add a test for this property? (this also makes review easier)

    josibake commented at 3:44 pm on August 13, 2025:
    I copied this value from https://github.com/bitcoin-core/secp256k1/blob/d599714147b20dda092ec4af44ef4174d584bb7d/src/tests.c#L6031-L6037 , where it is comprehensively tested. Would a comment pointing the reviewer to the edge case test be sufficient? Also happy to duplicate a subset of the edge case test over here if that’s preferred.

    real-or-random commented at 9:49 am on August 14, 2025:
    Ah, I see. Then I suggest copying the line CHECK(secp256k1_ec_seckey_verify(CTX, orderc) == 0); . This saves the reviewer from looking up the group order and doing the comparison manually.

    josibake commented at 12:52 pm on August 14, 2025:
    Added!
  74. in src/modules/silentpayments/tests_impl.h:35 in 1a5f53f2cf outdated
    30+static unsigned char BOB_ADDRESS[2][33] = {
    31+    {
    32+        0x02,0x15,0x40,0xae,0xa8,0x97,0x54,0x7a,
    33+        0xd4,0x39,0xb4,0xe0,0xf6,0x09,0xe5,0xf0,
    34+        0xfa,0x63,0xde,0x89,0xab,0x11,0xed,0xe3,
    35+        0x1e,0x8c,0xde,0x4b,0xe2,0x19,0x42,0x5f,0x23
    



    josibake commented at 3:34 pm on August 13, 2025:
    Took a moment to look how secret keys and public keys are formatted in the other tests, will update these to match the rest of the library.
  75. in src/modules/silentpayments/tests_impl.h:114 in 1a5f53f2cf outdated
    109+        recipient_ptrs, 3,
    110+        SMALLEST_OUTPOINT,
    111+        NULL, 0,
    112+        seckey_ptrs, 1
    113+    );
    114+    CHECK(ret);
    


    real-or-random commented at 10:46 am on August 12, 2025:
  76. in README.md:25 in 2b57d2ad89 outdated
    21@@ -22,6 +22,7 @@ Features:
    22 * Optional module for Schnorr signatures according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
    23 * Optional module for ElligatorSwift key exchange according to [BIP-324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki).
    24 * Optional module for MuSig2 Schnorr multi-signatures according to [BIP-327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).
    25+* Optional module for Silent Payments send and receive according to [BIP-352](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki).
    


    real-or-random commented at 11:01 am on August 12, 2025:
  77. in include/secp256k1_silentpayments.h:78 in 1a5f53f2cf outdated
    73+ *                            provided in the recipient objects.
    74+ *  In:           recipients: pointer to an array of pointers to silent payment
    75+ *                            recipients, where each recipient is a scan public
    76+ *                            key, a spend public key, and an index indicating
    77+ *                            its position in the original ordering. The
    78+ *                            recipient array will be sorted in place, but
    


    real-or-random commented at 3:57 pm on August 12, 2025:
    https://github.com/bitcoin-core/secp256k1/commit/1a5f53f2cf7d3228b7ed147282ea5bd796957cbf sorted by what? And do we want to make this guarantee in the API?

    josibake commented at 12:21 pm on August 14, 2025:
    “Sorting” is an implementation detail, since sorting is simply the method we are using to “group” the addresses by scan key. Given that, its probably better to remove all references to sorting from the API docs and instead use “grouping” to match the language in the BIP?

    josibake commented at 12:27 pm on August 14, 2025:

    What about:

     0diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
     1index f547207..108c61c 100644
     2--- a/include/secp256k1_silentpayments.h
     3+++ b/include/secp256k1_silentpayments.h
     4@@ -80,14 +80,15 @@ typedef struct secp256k1_silentpayments_recipient {
     5  *                            recipients, where each recipient is a scan public
     6  *                            key, a spend public key, and an index indicating
     7  *                            its position in the original ordering. The
     8- *                            recipient array will be sorted in place, but
     9- *                            generated outputs are saved in the
    10- *                            `generated_outputs` array to match the ordering
    11- *                            from the index field. This ensures the caller is
    12- *                            able to match the generated outputs to the
    13- *                            correct silent payment addresses. The same
    14- *                            recipient can be passed multiple times to create
    15- *                            multiple outputs for the same recipient.
    16+ *                            recipient array will be grouped by scan public key
    17+ *                            in place (as specified in BIP0352), but generated
    18+ *                            outputs are saved in the `generated_outputs` array
    19+ *                            to match the original ordering (using the index
    20+ *                            field). This ensures the caller is able to match
    21+ *                            the generated outputs to the correct silent
    22+ *                            payment addresses. The same recipient can be
    23+ *                            passed multiple times to create multiple outputs
    24+ *                            for the same recipient.
    25  *              n_recipients: the number of recipients. This is equal to the
    26  *                            total number of outputs to be generated as each
    27  *                            recipient may passed multiple times to generate
    

    josibake commented at 12:52 pm on August 14, 2025:
    Pushed a slight variant of this, lmk what you think.

    real-or-random commented at 8:25 am on August 19, 2025:
    looks good. I think I’ll do an entire review pass over the API docs once I’ve finished looking at the actual code
  78. in src/modules/silentpayments/main_impl.h:17 in 1a5f53f2cf outdated
    13-/* TODO: implement functions for sender side. */
    14+/** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to
    15+ *  ensure the correct values of k are used when creating multiple outputs for a recipient.
    16+ *
    17+ *  Note: only grouping, not deterministic ordering, of the scan public keys is required by the protocol.
    18+ *  As such, users of the library cannot and should not rely on deterministic sorting of the recipients.
    


    real-or-random commented at 3:59 pm on August 12, 2025:
    https://github.com/bitcoin-core/secp256k1/commit/1a5f53f2cf7d3228b7ed147282ea5bd796957cbf related to the other comment in this review: Are you saying here that the user should not rely on int secp256k1_silentpayments_sender_create_outputs sorting the recipients? If yes, this contradicts the API docs. (And the implementation file is the wrong place to make such a statement.)

    josibake commented at 12:32 pm on August 14, 2025:

    Looking at this with fresh eyes, I think the need for this comment arose from the fact the API docs were perhaps giving too much information about the sorting implementation and creating confusion.

    I think it would be better to remove any mentions of sorting from the API, e.g., #1698 (review), and then remove the “Note: " clarifying comment.


    real-or-random commented at 8:25 am on August 19, 2025:
    Agreed
  79. josibake force-pushed on Aug 14, 2025
  80. josibake commented at 12:51 pm on August 14, 2025: member

    Updated 2b57d2a -> a428424 (2025_12 -> 2025_13, compare)

    • Remove extra check to make the autotools build system consistent with other modules (h/t @real-or-random )
    • Various wording fix-ups (h/t @real-or-random)
    • Use spend_pubkey to indicate a spend public key that might also have a tweak and unlabeled_spend_pubkey we mean to refer the unlabeled spend public key (vs labeled_spend_pubkey and spend_pubkey) (h/t @real-or-random )
    • Reword the sending API docs to avoid mentioning “sorting”

    Thanks for the review @real-or-random , much prefer your wording suggestions. I pushed a version of the docs that I think helps minimise confusion about sorting, but curious to hear your thoughts as I imagine these docs will still need a bit of refining.

  81. josibake force-pushed on Aug 15, 2025
  82. josibake commented at 10:26 am on August 15, 2025: member

    Updated a428424 -> 99aef92 (2025_13 -> 2025_14, compare)

    • Add tests for the tagged hash midstates used in silent payments (h/t @real-or-random )

    The first commit moves the convenience function for verifying tagged hashes out of the musig module and is pulled in from #1725.

  83. real-or-random commented at 8:32 am on August 19, 2025: contributor
    I’ve made my way through sending (ignoring the public header for now) and I pushed some suggestions for fixups here: https://github.com/bitcoin-core/secp256k1/compare/master...real-or-random:secp256k1:bip352-silentpayments-module-2025-sending-fixups (and some fixups also to receiving, I hope I always picked the right commit to fixup). See commit messages for rationale. Some of these may be opinionated, so feel free to reject, disagree, or adjust.
  84. in src/modules/silentpayments/main_impl.h:594 in 99aef92046 outdated
    593+
    594+        /* Calculate P_output = B_spend + t_k * G
    595+         * This can fail if t_k is the negation of B_spend, but this is statistically
    596+         * improbable as t_k is the output of a hash function. */
    597+        ret = secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar);
    598+        VERIFY_CHECK(ret);
    


    real-or-random commented at 8:35 am on August 19, 2025:
    Couldn’t we also in this case (like on the sending side) construct inputs to the functions that hit this? (Did we discuss this on the BIP? I vaguely remember having thought about this before.)

    josibake commented at 8:54 am on August 20, 2025:

    We did discuss this and decided it was improbable due to t_k_scalar being the output of a hash function, where the allowable hash preimage values can only come from the senders spendable UTXOs.

    That being said, you’re right that we can create a test case for this by first picking a t_k_scalar value and then choosing a B_spend that is the negation of t_k_scalar. In practice, I don’t think this could ever happen since the sender is not in control of B_spend, but you could have a malicious receiver that puts out silent payment addresses with maliciously crafted B_spends. Based on that, I think it’s worth adding a test case and handling the error in this function.


    real-or-random commented at 9:41 am on August 20, 2025:

    Yes, we use VERIFY_CHECK like an assertion, i.e., there should be no API calls that make the asserted condition false. Now, we make an exception for inputs that are “cryptographically hard to find” (i.e., a computationally limited adversary will find them only with negligible probability under hardness assumptions that we accept). This is justified because noone will actually be able to hit the assertion in the real world (unless our assumptions turn out to be wrong).

    But I think we should not extend our set of assumptions to things like “the API function is called on inputs created from spendable UTXOs in some existing blockchain” or similar. Or in other words: if you can come up with a test case against the public API that triggers that VERIFY_CHECK, then it should not be a VERIFY_CHECK.


    josibake commented at 10:23 am on August 20, 2025:

    Or in other words: if you can come up with a test case against the public API that triggers that VERIFY_CHECK, then it should not be a VERIFY_CHECK.

    Thanks for the explanation, I’ve always been a bit hazy on when/how to use VERIFY_CHECK appropriately; this cleared it up for me! Will add a test for this and remove the VERIFY_CHECK in my next push.

  85. in src/modules/silentpayments/tests_impl.h:727 in 090f383963 outdated
    286+        CHECK(secp256k1_ec_pubkey_negate(CTX, &neg_spend_pubkey));
    287+        CHECK(secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &neg_spend_pubkey) == 0);
    288+        memset(&l, 0, sizeof(l));
    289+        CHECK_ILLEGAL(CTX, secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(CTX, &ls, &s, &l));
    290+    }
    291+}
    


    real-or-random commented at 9:41 am on August 20, 2025:
    This could also test with an illegal s, I think.

    josibake commented at 11:32 am on September 4, 2025:
    Updated the tests to cover both the negation case and malformed public keys cases.
  86. josibake force-pushed on Aug 20, 2025
  87. josibake commented at 10:09 am on August 20, 2025: member

    Updated 99aef92 -> 2164d5c (2025_14 -> 2025_15, compare)

    Primarily updates the sending commit, with a few changes to the receiving commit:

    • Rebased on #1725
    • IWYU (h/t @real-or-random )
    • Improve comments regarding the scan key, i.e., stop referring to it as “secret-but-not-really” and explicitly mention what happens if the scan key is leaked (h/t @real-or-random )
    • Use the more precise terminology “negligible probability” (h/t @real-or-random and crypto camp, where this has been explained! 😄 )
    • General wording, clarity improvements for the code/comments

    Thanks for the thorough review, @real-or-random ! I reviewed each fixup commit and ended up taking all of them, as I agree with the rational and much prefer the wording(s) you suggested. Also, thanks for the fixup! branch; the branch made it super simple to integrate your changes.

  88. real-or-random referenced this in commit f36afb8b3d on Aug 21, 2025
  89. in examples/silentpayments.c:23 in 2164d5cfe1 outdated
    18+
    19+/* Use my_memcmp_var instead of memcmp.
    20+ *
    21+ * Normally, memcmp should be fine, but we use my_memcmp_var
    22+ * here to avoid a false positive from valgrind on macOS.
    23+ * TODO: remove this in the event the bug is fixed with valgrind in the future.
    


    real-or-random commented at 7:45 am on August 25, 2025:
    The Valgrind bug has been fixed: https://github.com/LouisBrunner/valgrind-macos#135

    josibake commented at 11:32 am on September 4, 2025:
    Amazing! Thanks for digging into that.
  90. in include/secp256k1_silentpayments.h:189 in 2164d5cfe1 outdated
    184+ *  can use this serialization with
    185+ *  `secp256k1_silentpayments_recipient_public_data_parse`.
    186+ */
    187+typedef struct secp256k1_silentpayments_recipient_public_data {
    188+    unsigned char data[101];
    189+} secp256k1_silentpayments_recipient_public_data;
    


    real-or-random commented at 8:51 am on August 25, 2025:

    I suggest calling this a _transaction_summary. This is more descriptive.

    Perhaps then public can be dropped because it’s a bit more implied from the name. And this is also implied because the function that creates it doesn’t get secret data. I think I slightly tend to drop public then. (My thinking is that, if it was secret data, then secret should totally be an explicit part of the name to warn the user, but it’s okay to drop public.) It should still be mentioned in the docs that this is just public data.

    • secp256k1_silentpayments_recipient_transaction_summary (my favorite)
    • secp256k1_silentpayments_recipient_public_transaction_summary
    • secp256k1_silentpayments_recipient_public_tx_summary
    • secp256k1_silentpayments_recipient_public_tx_summary

    josibake commented at 11:48 am on September 1, 2025:

    Agree with dropping “public.” My only nit with calling this a transaction summary is that it contains data not found in a transaction, i.e., the prevout scriptPubKeys. The prevouts are referenced in a transaction (by outpoint). I think this is important because calling it transaction summary to me implies that I can create this with only the transaction itself (which is not true).

    What about _recipient_inputs_summary? , or _prevouts_summary?


    real-or-random commented at 12:11 pm on September 1, 2025:
    Ok, good point! I think then recipient_prevouts_summary is the right name. Technically speaking, what references a prevout is a transaction input, so _recipient_inputs_summary is similarly imprecise (though already much better than transaction_summary).

    josibake commented at 11:30 am on September 4, 2025:
    Renamed to prevout_summary.

    josibake commented at 11:31 am on September 4, 2025:
    Just realised prevouts_summary is probably more correct 😅 Not sure why I didn’t make it plural. Can update in a later push to prevouts_summary if we feel that would be better.

    josibake commented at 9:13 am on September 5, 2025:
    Fixed in the latest push; now using prevouts_summary everywhere.
  91. in src/modules/silentpayments/main_impl.h:416 in 2164d5cfe1 outdated
    392+ *  - `_recipient_public_data_serialize` only accepts a public_data object with combined = false
    393+ *    and then performs an EC mult before serializing the resulting public key as a compressed
    394+ *    public key
    395+ *  - `_recpient_public_data_parse` assumes the input represents a previously serialized
    396+ *    public_data object and always deserializes into a public_data object with combined = true
    397+ *    (and the input_hash portion zeroed out).
    


    real-or-random commented at 10:10 am on August 25, 2025:

    I find this unexpected. What I expect from any data structure that comes with serialize and parse methods is that serializing and re-parsing it doesn’t make a functional difference.

    The need for boolean indicates that there should perhaps be two different data structures. Or, simpler: declare the serialized thing to be something else. This means that one way to get this cleaner is to split the scan function into two, one that takes recipient_public_data and one that takes serialized public data. But wait, we already have it like that: secp256k1_silentpayments_recipient_create_shared_secret is purely for light clients.

    That suggests dropping _recpient_public_data_parse (and perhaps renaming _recipient_public_data_serialize to _recipient_public_data_export).

    There’s a slight loss of functionality: The docs of _recipient_public_data_create say: “If calling this function for simply aggregating the public transaction data for later use, the caller can save the result with silentpayments_recipient_public_data_serialize”. Is this really a use case we’d like to offer? It’s not great in terms of performance. It will double the amount of necessary EC mults. Note that the caller could simply store the opaque data structure in memory for later use. Only if the caller wants to write it to permanent storage, then serialization will be necessary.

    My thinking is that if this is not an important use case, then we should just drop it. But if we want this use case, then we suggest just that there should be a serialization function that serializes the “uncombined” variant for saving EC mults at the cost of increased storage requirements.


    Alternative way: I can see why you want just one data structure. This is simply about lazy evaluation of the EC mult, and whether it has already happened or not is merely an implementation detail and should not make an observable difference to the API caller (except in terms of performance). But then I suggest implementing it like a real lazy evaluation:

    • _recipient_public_data_create always creates a public_data object with combined = false
    • _recipient_public_data_serialize accepts any public_data object. If combined = false, it performs the EC mult before serializing the resulting public key as a compressed public key. If combined = true, it simply serializes
    • _recpient_public_data_parse assumes the input represents a previously serialized public_data object and always deserializes into a public_data object with combined = true (and the input_hash portion zeroed out).
    • _recipient_create_shared_secret can also accept both combined = true and combined = false. But if we think that calling _create_shared_secret latter necessarily implies the caller is doing something wrong, then my first suggestion above is probably better.

    josibake commented at 12:02 pm on September 1, 2025:

    Agree that having _serialize and _parse methods that do not roundtrip is a bit code smelly.

    If calling this function for simply aggregating the public transaction data for later use … Only if the caller wants to write it to permanent storage, then serialization will be necessary.

    The comment in the code is a bit confusing. The only reason a caller would want to “aggregate for later use” is when building a silent payments index, for personal use (e.g., fast wallet rescans), or for serving the data to external light clients.

    I much prefer your alternative suggestion, as this seems simpler than having multiple data structures and easier to document. I also think full nodes keeping their own silent payments index for fast wallet rescans and for supporting light clients will be a very common (and important!) use case to support.


    real-or-random commented at 12:12 pm on September 1, 2025:
    Makes sense to me. We should then make sure to document the performance implications of the API calls.

    josibake commented at 11:30 am on September 4, 2025:
    I updated this per your suggestion to make this a proper lazy evaluation. Also documented that calling _serialize can incur an EC mult in some cases. I don’t think there are any other performance implications that need documenting, since calling _recipient_create_shared_secret will always involve one EC mult, and sometimes a scalar mult, depending on the value of combined. But a scalar mult seems negligible enough that it shouldn’t warrant documenting.
  92. real-or-random commented at 10:33 am on August 25, 2025: contributor

    I attached some high-level comments on the receiver API. I think these should be addressed before I dig deeper into the receiving code.

    Some style things:

    Full sentences in the comments should end with a period for readability (with the exception that short imperatives are fine). In some sentences my brain really expects more input after the end of the comment, e.g., “Additionally, we zero out the 32 bytes where the input_hash would be” (What would the hash be? :D)

    And I’d prefer to avoid “formula-style” variable names like A_sum, B_m, P_output, and t_k in code. (The latter is borderline, I guess; at least it’s not capital.) Sometimes these appear only in the comments, but that can be confusing then because there are two names for the same thing, e.g., recipient_spend_pubkey vs B_spend. I think most (all?) of these can be resolved by replacing “A” with “input” or “sender,” as appropriate, and “B” with “receiver.”

    On the other hand, having a “normal” variable name in the code but also the formula-style name in the comments or API docs may be a good idea if you assume that the reader/user is familiar with the BIP. But then I’d make it explicit in the comments, e.g., have a translation table somewhere (could even be a general comment at the top of the file) or say something like “recipient_spending_key (“B_spend” in the BIP)” whenever a variable occurs first in a comment.

    To be honest, I’m not sure if it’s better to keep this connection to the BIP or avoid it entirely. An ideal API would be understandable without the BIP, but since the API covers only the cryptographic core of SP, any user will necessarily need to know some details from the BIP that go beyond what the API will do.

  93. real-or-random commented at 6:30 pm on August 29, 2025: contributor

    I pushed some (totally non-essential) fixup suggestions for the Python code to https://github.com/real-or-random/secp256k1/tree/bip352-silentpayments-module-2025-vector-fixups .


    Related to this: I see why we need the ripemd160 and bech32m Python modules to parse addresses from the test vectors JSON file. This feels a bit wrong. This SP module covers some cryptographic core of SP, which does not deal with address encoding issues. So these files belong to full wallet implementations such as Bitcoin Core (where you got them from, I assume).

    The only way around this that I see is that the intermediate results computed from the addresses (i.e., the extracted input pubkeys) would additionally be included in the JSON file. Do you think that would be a reasonable solution and acceptable from your point of view as a BIP author? Are there any drawbacks to this? I don’t see any, but I might be missing something.

    edit: Oh, or we could compute the input pubkeys from the seckeys in the JSON. Preferably in the C tests because this will require no Python EC code.

    (It’s not the end of the world to have this in our repo. I think this is my tendency as a maintainer to avoid code. The more code we have, the more we’ll need to maintain.)

  94. josibake commented at 12:14 pm on September 1, 2025: member

    Some style things:

    Thanks for the feedback! Agree with all of your points and will implement. Regarding keeping notation in line with the BIP or removing entirely, I’d prefer to avoid the BIP as much as possible. As you said, the API should be understandable without the BIP.

    How the BIP and module are written today is more of a historic “oopsie.” Ideally, the protocol should have been specified in a more module way, i.e., a cryptographic primitive (lets call it “non-interactive-payment”, or “multiparty ECDH”) and then a Bitcoin specific BIP that specifies how to use this NIP/MPECDH crypto primitive in a bitcoin specific context.

    TLDR; I think we should keep this module more aligned with how things “should” have been done.

    intermediate results computed from the addresses (i.e., the extracted input pubkeys) would additionally be included in the JSON file. Do you think that would be a reasonable solution and acceptable from your point of view as a BIP author? Are there any drawbacks to this? I don’t see any, but I might be missing something.

    Regarding the tests, I’d love to rip out all the Bitcoin specific python code. I dont see any drawbacks toward including intermediary results in the test files, and this would also be useful for index builders wishing to reuse the same set of test vectors. I’ve had a TODO for awhile now to update the BIP test vectors; I’ll prioritise that work and update the PR here.


    As always, thanks for the thorough review! I had one question that I’d like your input on, otherwise I’ll work on implementing the rest of your feedback over the next couple days.

  95. josibake force-pushed on Sep 3, 2025
  96. josibake commented at 11:33 am on September 3, 2025: member
  97. in src/modules/silentpayments/main_impl.h:69 in bd745e1a06 outdated
    64+    int ret;
    65+
    66+    secp256k1_silentpayments_sha256_init_inputs(&hash);
    67+    secp256k1_sha256_write(&hash, outpoint_smallest36, 36);
    68+    ret = secp256k1_eckey_pubkey_serialize(pubkey_sum, pubkey_sum_ser, &len, 1);
    69+#ifdef VERFIY
    


    w0xlt commented at 10:58 pm on September 3, 2025:

    Just a minor typo, yet it causes a sanity check for pubkey serialization to be skipped without warning.

    0#ifdef VERIFY
    

    josibake commented at 11:27 am on September 4, 2025:
    Great catch!
  98. in src/modules/silentpayments/main_impl.h:438 in bd745e1a06 outdated
    433+
    434+    /* Compute input public keys sum: A_sum = A_1 + A_2 + ... + A_n */
    435+    secp256k1_gej_set_infinity(&A_sum_gej);
    436+    for (i = 0; i < n_plain_pubkeys; i++) {
    437+        ret &= secp256k1_pubkey_load(ctx, &addend, plain_pubkeys[i]);
    438+        secp256k1_gej_add_ge_var(&A_sum_gej, &A_sum_gej, &addend, NULL);
    


    w0xlt commented at 11:12 pm on September 3, 2025:
    What happens if parsing fails and an addend other than expected (maybe invalid) is supplied to secp256k1_gej_add_ge_var?

    josibake commented at 9:52 am on September 4, 2025:
    From what I can tell, nothing. If any single public key fails to parse, we will catch it a few lines down. Arguably, we could return early to avoid doing extra work when a single public key is bad, which seems preferable.

    josibake commented at 11:27 am on September 4, 2025:
    Update to return as soon as a bad pubkey is encountered. This avoids calling _gej_add_ge_var with a bad addend and fails early on the first bad key, instead of wasting the callers time with additional work. Also updated the tests to explicitly test both bad plain pubkeys and bad taproot x-only pubkeys.

    real-or-random commented at 8:34 am on September 5, 2025:

    From what I can tell, nothing.

    Indeed. secp256k1_gej_add_ge_var will compute garbage (garbage in, garbage out), but there won’t be any crashes or UB. But when looking deeper, there’s an interesting question; see #1736.

  99. in src/modules/silentpayments/main_impl.h:370 in bd745e1a06 outdated
    328+    secp256k1_silentpayments_sha256_init_label(&hash);
    329+    secp256k1_sha256_write(&hash, recipient_scan_key32, 32);
    330+    secp256k1_write_be32(m_serialized, m);
    331+    secp256k1_sha256_write(&hash, m_serialized, sizeof(m_serialized));
    332+    secp256k1_sha256_finalize(&hash, label_tweak32);
    333+
    


    w0xlt commented at 11:43 pm on September 3, 2025:

    nit (in int secp256k1_silentpayments_recipient_create_label):

    0secp256k1_memclear(m_serialized, sizeof(m_serialized));
    1secp256k1_sha256_clear(&hash);
    

    josibake commented at 11:26 am on September 4, 2025:
    Done.
  100. in src/modules/silentpayments/main_impl.h:311 in bd745e1a06 outdated
    289+        if (!secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->spend_pubkey, k)) {
    290+            secp256k1_scalar_clear(&a_sum_scalar);
    291+            secp256k1_memclear(&shared_secret, sizeof(shared_secret));
    292+            return 0;
    293+        }
    294+        k++;
    


    w0xlt commented at 0:11 am on September 4, 2025:

    nit: ensure there’s room for the increment

    0        VERIFY_CHECK(k < SIZE_MAX);
    1        k++;
    2        
    

    josibake commented at 11:25 am on September 4, 2025:
    Done.
  101. josibake force-pushed on Sep 4, 2025
  102. josibake commented at 11:25 am on September 4, 2025: member

    Updated bd745e1 -> d7281b9 (2025_15-rebased -> 2025_16, compare)

    • Removed the workaround for the valgrind false positive :tada:
    • s/public_data/prevout_summary/ (h/t @real-or-random)
    • Update functions that either output or take a prevout_summary object as input to be a proper lazy evaluation (h/t @real-or-random)
    • Avoid using BIP specific variable names, e.g., s/B_m/labeled_spend_pubkey/ etc (h/t @real-or-random)
    • Clean up comments to use proper punctuation and full sentences (h/t @real-or-random)
    • Improve error handling for malformed public keys and update test coverage (h/t @w0xlt)
    • Explicitly clear intermediate hash variables when creating labels (h/t @w0xlt)
    • Improve test coverage by explicitly testing more corner cases (h/t @real-or-random)
    • Fix VERIFY typo (h/t @w0xlt)

    In addition to what is explicitly mentioned above, I also slightly reworded a few comments and removed comments that I felt were simply restating what the code does without adding any extra information.


    Thanks for the continued review @real-or-random . I think your suggestions for variable names and comments have improved the readability quite a bit. Also much happier with the prevout_summary naming and treating the prevout_summary object as a lazy evaluation.

    Also, thanks @w0xlt for the thorough review! Much happier with the error handling and glad you caught the VERIFY typo.

  103. josibake commented at 11:35 am on September 4, 2025: member

    @real-or-random

    The only way around this that I see is that the intermediate results computed from the addresses

    Working on an update to the BIP test vectors to include intermediate outputs. Once I have that pull request open, I’ll update the test code here to remove the bech32m and ripemd160 python code.

  104. in include/secp256k1_silentpayments.h:151 in 163dc82627 outdated
    146+/** Create Silent Payment labeled spend public key.
    147+ *
    148+ *  Given a recipient's spend public key and a label, calculate the
    149+ *  corresponding labeled spend public key:
    150+ *
    151+ *      labeled_spend_pubkey = recipient_spend_pubkey + label * G
    


    theStack commented at 2:58 pm on September 4, 2025:

    in 163dc82627e864b202b1e919ee39d69df89f12f8:

    0 *      labeled_spend_pubkey = recipient_spend_pubkey + label
    

    (as label is already a pubkey, not a scalar)

  105. in src/modules/silentpayments/main_impl.h:346 in 163dc82627 outdated
    341+    VERIFY_CHECK(ctx != NULL);
    342+    ARG_CHECK(labeled_spend_pubkey != NULL);
    343+    ARG_CHECK(recipient_spend_pubkey != NULL);
    344+    ARG_CHECK(label != NULL);
    345+
    346+    /* Calculate labeled_spend_pubkey = recipient_spend_pubkey + label * G
    


    theStack commented at 3:00 pm on September 4, 2025:
    0    /* Calculate labeled_spend_pubkey = recipient_spend_pubkey + label
    
  106. in src/modules/silentpayments/main_impl.h:98 in a7017e766a outdated
     95+    VERIFY_CHECK(ret && len == 33);
     96+#else
     97+    (void)ret;
     98+#endif
     99+
    100+    /* Leaking these values would break indistiguishability of the transaction, so clear them. */
    


    theStack commented at 3:03 pm on September 4, 2025:
    0   /* Leaking these values would break indistinguishability of the transaction, so clear them. */
    

    (here and in 5 other instances, in both the sending and receiving commits)

  107. in include/secp256k1_silentpayments.h:269 in cb53ab5d05 outdated
    264+ *  Returns: 1 if the data was able to be parsed.
    265+ *           0 if the sequence is invalid (e.g., does not represent a valid
    266+ *           public key).
    267+ *
    268+ *  Args:         ctx: pointer to a context object.
    269+ *  Out:  public_data: pointer to a silentpayments_recipient_prevout_summary object. If 1 is
    


    theStack commented at 3:18 pm on September 4, 2025:
    0 *  Out:  prevout_summary: pointer to a silentpayments_recipient_prevout_summary object. If 1 is
    
  108. in include/secp256k1_silentpayments.h:393 in cb53ab5d05 outdated
    388+ *           0 if the recipient scan key is invalid.
    389+ *  Args:                  ctx: pointer to a context object
    390+ *  Out:       shared_secret33: pointer to the resulting 33-byte shared secret
    391+ *  In:   recipient_scan_key32: pointer to the recipient's 32 byte scan key
    392+ *                 public_data: pointer to the public_data object, loaded using
    393+ *                              `_recipient_public_data_parse`
    


    theStack commented at 3:20 pm on September 4, 2025:
    0 *             prevout_summary: pointer to the public_data object, loaded using
    1 *                              `_recipient_prevout_summary_parse`
    
  109. in src/modules/silentpayments/main_impl.h:392 in cb53ab5d05 outdated
    387+ *
    388+ *  - `_recipient_prevout_summary_create` always creates a prevout_summary object with combined = false
    389+ *  - `_recipient_prevout_summary_serialize` only accepts a prevout_summary object with combined = false
    390+ *    and then performs an EC mult before serializing the resulting public key as a compressed
    391+ *    public key
    392+ *  - `_recpient_prevout_summary_parse` assumes the input represents a previously serialized
    


    theStack commented at 3:22 pm on September 4, 2025:
    0 *  - `_recipient_prevout_summary_parse` assumes the input represents a previously serialized
    
  110. theStack commented at 3:35 pm on September 4, 2025: contributor
    Re-reviewed up to the receiving commit cb53ab5d05049527ff3d6be30144c188263b1192, left a few suggestions only regarding code comments (i.e. typos, outdated API names/references). I agree that the new naming _prevout_summary is an improvement. :+1:
  111. josibake force-pushed on Sep 5, 2025
  112. josibake commented at 9:10 am on September 5, 2025: member

    Updated d7281b9 -> f3c0ad6 (2025_16 -> 2025_17, compare)

    • s/prevout_summary/prevouts_summary/ - should have done this in the last push, apologies for the churn on this
    • Updated tests to use the new structure introduced in https://github.com/bitcoin/bips/pull/1953 (h/t @macgyver13)
    • Various spelling/comment cleanups (h/t @theStack , I promise I’ll eventually learn how to spell recipient 😅 )

    The main change in this push is being able to remove the ripemd160.py and bech32m.py files from the reference code! Thanks @macgyver13 for making these changes , both here and in the BIP repo!

  113. josibake force-pushed on Sep 5, 2025
  114. josibake commented at 10:49 am on September 5, 2025: member

    Updated f3c0ad6 -> aa85bfb (2025_17 -> 2025_18, compare)

    • Fix tests_silentpayments_generate.py to not skip test case (h/t @real-or-random )
    • Refactor tests_silentpayments_generate.py to be more idiomatic python/more readable (h/t @real-or-random )
    • Fix up tests_impl.h to handle extra test case (h/t @real-or-random)

    Thanks for the fixup! branch for the test vectors, @real-or-random ! I added you as a coauthor.

  115. in src/modules/silentpayments/main_impl.h:389 in 22b20fd617 outdated
    384+ *  the resulting point serialized as a compressed public key, i.e., input_hash * prevout_pubkey_sum.
    385+ *
    386+ *  For the each function:
    387+ *
    388+ *  - `_recipient_prevouts_summary_create` always creates a prevouts_summary object with combined = false
    389+ *  - `_recipient_prevouts_summary_serialize` only accepts a prevouts_summary object with combined = false
    


    theStack commented at 3:38 pm on September 5, 2025:
    with the recent change to use lazy evaluation (see #1698 (review) ff.), this comment needs to be updated (the ones for _create and _parse are still correct, I think)

    josibake commented at 9:48 am on September 8, 2025:
    Updated, and also updated the ctime_tests, where we were doing a serialization roundtrip to get the prevouts summary object into a combined = true state.
  116. in src/modules/silentpayments/main_impl.h:334 in df1de93765 outdated
    329+    secp256k1_memclear(m_serialized, sizeof(m_serialized));
    330+    secp256k1_sha256_clear(&hash);
    331+    return secp256k1_ec_pubkey_create(ctx, label, label_tweak32);
    332+}
    333+
    334+int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256k1_context *ctx, secp256k1_pubkey *labeled_spend_pubkey, const secp256k1_pubkey *recipient_spend_pubkey, const secp256k1_pubkey *label) {
    


    theStack commented at 3:41 pm on September 5, 2025:
    0int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256k1_context *ctx, secp256k1_pubkey *labeled_spend_pubkey, const secp256k1_pubkey *unlabeled_spend_pubkey, const secp256k1_pubkey *label) {
    

    to match the parameter name in the API header

  117. in src/modules/silentpayments/main_impl.h:710 in 22b20fd617 outdated
    705+
    706+    secp256k1_scalar_clear(&rsk);
    707+    return 1;
    708+}
    709+
    710+int secp256k1_silentpayments_recipient_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *recipient_spend_pubkey, const uint32_t k)
    


    theStack commented at 3:47 pm on September 5, 2025:
    0int secp256k1_silentpayments_recipient_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *spend_pubkey, const uint32_t k)
    

    to match the parameter name in the API header

  118. in include/secp256k1_silentpayments.h:151 in df1de93765 outdated
    146+/** Create Silent Payment labeled spend public key.
    147+ *
    148+ *  Given a recipient's spend public key and a label, calculate the
    149+ *  corresponding labeled spend public key:
    150+ *
    151+ *      labeled_spend_pubkey = recipient_spend_pubkey + label
    


    theStack commented at 3:52 pm on September 5, 2025:
    0 *      labeled_spend_pubkey = unlabeled_spend_pubkey + label
    

    sorry to dig into the same line once again, missed this in the last review round :sweat_smile: might be worth it to grep for recipient_spend occurences in general, if the plan is to get rid of that terminology completely (and only use {labeled_,unlabeled_,}spend_pubkey)


    josibake commented at 9:47 am on September 8, 2025:
    No worries! I went ahead and updated all instances to use {labeled_,unlabeled_,}spend_pubkey and also update the scan_key32 argument to remove the recipient_ prefix. Much cleaner, imo.
  119. theStack commented at 4:03 pm on September 5, 2025: contributor
    Just noting that the “public data” terminology is still used a lot, as shown by $ git grep -i public.*data. I think it’s not entirely wrong in all cases, as the prevouts summary is indeed public data :-) but instances that say “public data object” should probably be changed to “prevouts_summary object” for consistency.
  120. in include/secp256k1_silentpayments.h:82 in 76a0451c76 outdated
    67+ *
    68+ *  Returns: 1 if creation of outputs was successful.
    69+ *           0 if the input private keys sum to zero,
    70+ *             or input_hash or hash(shared_secret || k) are invalid scalars
    71+ *             (statistically improbable).
    72+ *  Args:                ctx: pointer to a context object
    


    theStack commented at 5:32 pm on September 5, 2025:
    0 *  Args:                ctx: pointer to a context object (not secp256k1_context_static)
    

    as the sending function needs the generator point multiplication context (for calculating the pubkey sum from the seckey sum), and this is how we usually denote that in the API header (see also #1737)

  121. josibake force-pushed on Sep 8, 2025
  122. josibake commented at 9:46 am on September 8, 2025: member

    Updated aa85bfb -> 09d1aee (2025_18 -> 2025_19, compare)

    • s/public data/prevouts data/ (h/t @theStack)
    • Use {labeled_, unlabeled_,}spend_pubkey consistently (h/t @theStack)
    • Drop recipient_ prefix from scan_key32 arg - this better matches the convention we are using for scan_pubkey, spend_pubkey
    • Remove unnecessary serialization roundtrip from the constant time tests, now that prevouts_summary is a proper lazy evaulation
    • Document the sending function to mention ctx cannot be static (h/t @theStack)
  123. real-or-random referenced this in commit 4985ac0f89 on Sep 8, 2025
  124. jonasnick added this to the milestone 0.7.1 on Sep 8, 2025
  125. in src/modules/silentpayments/main_impl.h:226 in b802ecfd6c outdated
    223+        if (!ret) {
    224+            secp256k1_scalar_clear(&addend);
    225+            secp256k1_scalar_clear(&seckey_sum_scalar);
    226+            return 0;
    227+        }
    228+        secp256k1_declassify(ctx, &ret, sizeof(ret));
    


    theStack commented at 4:29 pm on September 8, 2025:

    seems that this declassify call is a duplicate from the one a few lines above and can be removed

    (ctime tests still pass for me locally)

  126. in include/secp256k1_silentpayments.h:141 in afe3aa01cb outdated
    128+ *      label_tweak = hash(scan_key || m)
    129+ *            label = label_tweak * G
    130+ *
    131+ *  Returns: 1 if label tweak and label creation was successful.
    132+ *           0 if label_tweak32 is an invalid scalar (statistically improbable).
    133+ *  Args:                ctx: pointer to a context object
    


    theStack commented at 4:34 pm on September 8, 2025:

    in the _create_label API header:

    0 *  Args:                ctx: pointer to a context object (not secp256k1_context_static)
    

    compared to the sending function it’s a bit less obvious that a static context can’t be used, as we don’t check that ecmult_gen_ctx is built manually, but call another API function ec_pubkey_create which requires this

  127. in src/modules/silentpayments/main_impl.h:679 in 91789906aa outdated
    674+
    675+    /* Leaking these values would break indistinguishability of the transaction, so clear them. */
    676+    secp256k1_scalar_clear(&rsk_scalar);
    677+    secp256k1_scalar_clear(&t_k_scalar);
    678+    secp256k1_memclear(shared_secret, sizeof(shared_secret));
    679+    return ret;
    


    theStack commented at 4:38 pm on September 8, 2025:

    in _recipient_scan_outputs:

    0    return 1;
    

    as the “single point of return” approach isn’t followed anymore and any failures return early

  128. in src/modules/silentpayments/main_impl.h:574 in 91789906aa outdated
    569+    if (!combined) {
    570+        secp256k1_scalar input_hash_scalar;
    571+        secp256k1_scalar_set_b32(&input_hash_scalar, &prevouts_summary->data[5 + 64], NULL);
    572+        secp256k1_scalar_mul(&rsk_scalar, &rsk_scalar, &input_hash_scalar);
    573+    }
    574+    ret &= secp256k1_pubkey_load(ctx, &spend_pubkey_ge, spend_pubkey);
    


    theStack commented at 4:40 pm on September 8, 2025:
    0    ret = secp256k1_pubkey_load(ctx, &spend_pubkey_ge, spend_pubkey);
    

    (as the previous failure would return early)

  129. in src/modules/silentpayments/main_impl.h:620 in 91789906aa outdated
    588+        /* Calculate output = spend_pubkey + t_k * G.
    589+         * This can fail if t_k is the negation of spend_pubkey, but this happens only
    590+         * with negligible probability as t_k is the output of a hash function. */
    591+        if (!secp256k1_eckey_pubkey_tweak_add(&output_ge, &t_k_scalar)) {
    592+            secp256k1_scalar_clear(&rsk_scalar);
    593+            return 0;
    


    theStack commented at 4:41 pm on September 8, 2025:
    should securely erase the shared_secret and t_k_scalar here

    real-or-random commented at 6:44 am on September 9, 2025:
    And maybe we can find a better name for t_k_scalar. Not sure if it appears in the BIP but it’s “BIP-style”

    josibake commented at 11:06 am on September 9, 2025:
    It is “BIP-style,” in that its the terminology used in the BIP. I think the most accurate thing to call it would be “output_tweak,” since this is the value that is created from the hash of the shared secret (with counter k), and then used as an additive tweak with the spend public key / spend private key.
  130. in src/modules/silentpayments/main_impl.h:353 in afe3aa01cb outdated
    348+     * return early
    349+     */
    350+    ret = secp256k1_pubkey_load(ctx, &labeled_spend_pubkey_ge, unlabeled_spend_pubkey);
    351+    ret &= secp256k1_pubkey_load(ctx, &label_addend, label);
    352+    if (!ret) {
    353+        return ret;
    


    theStack commented at 4:43 pm on September 8, 2025:

    nit, ret is always zero here:

    0        return 0;
    
  131. in include/secp256k1_silentpayments.h:188 in 91789906aa outdated
    183+ *  `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is
    184+ *  intended for sending the prevout summary data to light clients. Light clients
    185+ *  can use this serialization with
    186+ *  `secp256k1_silentpayments_recipient_prevouts_summary_parse`.
    187+ */
    188+typedef struct secp256k1_silentpayments_recipient_prevouts_summary {
    


    theStack commented at 4:47 pm on September 8, 2025:

    naming nit: I wonder if we could drop the recipient part of the structure (especially considering that the prevout information actually represents key/input material from the sender side, so it could be even potentially confusing?)

    0typedef struct secp256k1_silentpayments_prevouts_summary {
    

    real-or-random commented at 6:47 am on September 9, 2025:
    I guess either is fine. It’s a data structure only used on the recipient side, so having recipient is consistent. On the other hand, it suffices that only function names have the sender and recipient infixes. If you believe it could be confusing in this case, then it may be better to drop it.

    josibake commented at 9:07 am on September 9, 2025:
    I think having the sender and recipient infixes should be sufficient and would prefer to keep the names simple, wherever possible. Its also a good point that including recipient in the struct name is at best redundant and at worst confusing.
  132. in src/modules/silentpayments/main_impl.h:589 in 91789906aa outdated
    584+    while (1) {
    585+        secp256k1_ge output_ge = spend_pubkey_ge;
    586+        secp256k1_silentpayments_create_t_k(&t_k_scalar, shared_secret, k);
    587+
    588+        /* Calculate output = spend_pubkey + t_k * G.
    589+         * This can fail if t_k is the negation of spend_pubkey, but this happens only
    


    theStack commented at 4:57 pm on September 8, 2025:
    0         * This can fail if t_k * G is the negation of spend_pubkey, but this happens only
    

    (or alternatively, s/spend_pubkey/spend_seckey/, though that would more abstractly refer to a scalar that is not in the scope of this function)

  133. in src/modules/silentpayments/main_impl.h:174 in b802ecfd6c outdated
    171+    const unsigned char * const *plain_seckeys,
    172+    size_t n_plain_seckeys
    173+) {
    174+    size_t i, k;
    175+    secp256k1_scalar seckey_sum_scalar, addend, input_hash_scalar;
    176+    secp256k1_ge input_pubkey_sum_ge;
    


    theStack commented at 5:06 pm on September 8, 2025:
    consistent naming nit: the prevouts_summary struct description above uses prevout_pubkey_sum
  134. in src/modules/silentpayments/main_impl.h:60 in b802ecfd6c outdated
    57+    hash->s[7] = 0xcd06903eul;
    58+
    59+    hash->bytes = 64;
    60+}
    61+
    62+static void secp256k1_silentpayments_calculate_input_hash(unsigned char *input_hash, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) {
    


    theStack commented at 5:14 pm on September 8, 2025:
    potential follow-up deduplication idea: could include the hash -> scalar conversion in this function (including the VERIFY_CHECK and comments), so the two call-sites in sending/receiving don’t have to do it

    josibake commented at 10:14 am on September 9, 2025:
    I went ahead and took this suggestion now. I think its much cleaner and makes the call sites of _calculate_input_hash_scalar easier to understand without the the duplicated overflow checking and comment explaining why.
  135. theStack commented at 5:27 pm on September 8, 2025: contributor
    Left some more comments below, again mostly about naming and tiny refactoring nits, but also one memory clearing finding.
  136. in examples/silentpayments.c:84 in 09d1aee830 outdated
    79+/** Labels
    80+ *
    81+ *  The structs and call back function are implemented here as a demonstration
    82+ *  of how the label lookup callback is meant to query a label cache and return
    83+ *  the label tweak when a match is found. This is for demonstration purposes
    84+ *  only and not optimized. In a production usecase, it is expected that the
    


    real-or-random commented at 7:25 am on September 9, 2025:
    0 *  only and not optimized. In a production use case, it is expected that the
    

    Or just “in production”

  137. in examples/silentpayments.c:119 in 09d1aee830 outdated
    114+    }
    115+    return NULL;
    116+}
    117+
    118+int main(void) {
    119+    enum { N_INPUTS = 2, N_OUTPUTS = 3 };
    


    real-or-random commented at 7:32 am on September 9, 2025:
    nit: I see why you use an enum here, but this is advanced C stuff that we could try to avoid in the examples. This is conceptually not an enumeration, so I’d prefer #define.
  138. in examples/silentpayments.c:213 in 09d1aee830 outdated
    208+                (*(sp_addresses[i]))[1],
    209+                33
    210+            );
    211+            if (!ret) {
    212+                printf("\n");
    213+                printf("Something went wrong, this is not a valid silent payments address.");
    


    real-or-random commented at 7:34 am on September 9, 2025:
    nit: Why print \n before the message? In the other error outputs, the \n comes after the message.

    josibake commented at 10:12 am on September 9, 2025:
    No reason, good catch!
  139. in examples/silentpayments.c:212 in 09d1aee830 outdated
    142+    {
    143+        secp256k1_keypair sender_keypairs[N_INPUTS];
    144+        const secp256k1_keypair *sender_keypair_ptrs[N_INPUTS];
    145+        secp256k1_silentpayments_recipient recipients[N_OUTPUTS];
    146+        const secp256k1_silentpayments_recipient *recipient_ptrs[N_OUTPUTS];
    147+        unsigned char (*sp_addresses[N_OUTPUTS])[2][33];
    


    real-or-random commented at 7:37 am on September 9, 2025:
    Could add a comment here to explain the structure of this array, e.g., that the [2] is for scan/spend.
  140. in examples/silentpayments.c:141 in 09d1aee830 outdated
    136+     * leakage. See `secp256k1_context_randomize` in secp256k1.h for more
    137+     * information about it. This should never fail. */
    138+    ret = secp256k1_context_randomize(ctx, randomize);
    139+    assert(ret);
    140+
    141+    /*** Sending ***/
    


    real-or-random commented at 7:39 am on September 9, 2025:
    0    /*** Sending (Alice) ***/
    
  141. in examples/silentpayments.c:279 in 09d1aee830 outdated
    274+             *
    275+             * Since Bob has access to the full transaction, scanning is simple:
    276+             *
    277+             *     1. Collect the relevant prevouts data from the transaction
    278+             *        and call `_silentpayments_recipient_prevouts_summary_create`
    279+             *     2. Call `_silentpayments_recipient_scan_outputs`
    


    real-or-random commented at 7:42 am on September 9, 2025:
    nit: We shouldn’t omit the secp256k1 prefix in user-facing docs.
  142. in examples/silentpayments.c:302 in 09d1aee830 outdated
    297+                 *  These steps are only necessary if the recipient is using the
    298+                 *  optional labels feature. Recipients not using labels can
    299+                 *  ignore these steps and simply pass `NULL` for the
    300+                 *  label_lookup and label_context arguments:
    301+                 *
    302+                 *      _silentpayments_recipient_scan_outputs(..., NULL, NULL);
    


    real-or-random commented at 7:42 am on September 9, 2025:
    0                 *      secp256k1_silentpayments_recipient_scan_outputs(..., NULL, NULL);
    
  143. in examples/silentpayments.c:304 in 09d1aee830 outdated
    299+                 *  ignore these steps and simply pass `NULL` for the
    300+                 *  label_lookup and label_context arguments:
    301+                 *
    302+                 *      _silentpayments_recipient_scan_outputs(..., NULL, NULL);
    303+                 *
    304+                 *  In this case, since Bob has access to the full transaction
    


    real-or-random commented at 7:43 am on September 9, 2025:
    0                 *  In this example, since Bob has access to the full transaction
    

    In this “case” could refer to the previous paragraph (no labels used at all)

  144. in examples/silentpayments.c:237 in 09d1aee830 outdated
    232+            smallest_outpoint,
    233+            sender_keypair_ptrs, N_INPUTS,
    234+            NULL, 0
    235+        );
    236+        assert(ret);
    237+        printf("Alice created the following outputs for Bob and Carol: \n");
    


    real-or-random commented at 7:44 am on September 9, 2025:
    0        printf("Alice created the following outputs for Bob and Carol:\n");
    
  145. in examples/silentpayments.c:311 in 09d1aee830 outdated
    306+                 *  as demonstrated below. For efficient scanning, Bob keeps a
    307+                 *  cache of every label he has previously used and uses a
    308+                 *  callback to check if a potential label exists in his cache.
    309+                 *  Since the labels are created using an incremental integer
    310+                 *  `m`, if Bob ever forgets how many labels he has previously
    311+                 *  used, he can pregenerate a large number of labels e.g.,
    


    real-or-random commented at 7:45 am on September 9, 2025:
    0                 *  used, he can pregenerate a large number of labels, e.g.,
    
  146. in examples/silentpayments.c:445 in 09d1aee830 outdated
    440+                    /* Verify that this output is spendable by Bob by reconstructing the full
    441+                     * secret key for the xonly output.
    442+                     *
    443+                     * This is done by adding the tweak from the transaction to Bob's spend key.
    444+                     * If the output was sent to a labeled address, the label tweak has
    445+                     * already been added to the tweak returned in `_silentpayments_found_output`.
    


    real-or-random commented at 7:47 am on September 9, 2025:
    prefix
  147. in examples/silentpayments.c:449 in 09d1aee830 outdated
    444+                     * If the output was sent to a labeled address, the label tweak has
    445+                     * already been added to the tweak returned in `_silentpayments_found_output`.
    446+                     *
    447+                     * To verify that we are able to sign for this output, it is sufficient to
    448+                     * check that the public key generated from `full_seckey` matches the output
    449+                     * in the transaction. For a full example on signing for a schnorr ouput,
    


    real-or-random commented at 7:47 am on September 9, 2025:
    0                     * in the transaction. For a full example on signing for a Schnorr signature ouput,
    

    or do you want to call this “taproot” to match the terms above?


    josibake commented at 10:12 am on September 9, 2025:
    I think taproot is better here.
  148. in examples/silentpayments.c:494 in 09d1aee830 outdated
    489+             *
    490+             * Additionally, Carol likely does not have access to the
    491+             * transaction inputs and prevout information, so she uses the
    492+             * `prevouts_summary` object created by Bob's full node earlier. This
    493+             * serialized `prevouts_summary` object contains everything she needs for
    494+             * generating the shared secret, i.e., `input_hash * A_sum`.
    


    real-or-random commented at 7:49 am on September 9, 2025:
    A_sum is still BIP-style
  149. in examples/silentpayments.c:501 in 09d1aee830 outdated
    492+             * `prevouts_summary` object created by Bob's full node earlier. This
    493+             * serialized `prevouts_summary` object contains everything she needs for
    494+             * generating the shared secret, i.e., `input_hash * A_sum`.
    495+             *
    496+             * In practice, Carol wouldn't know the number of outputs ahead of
    497+             * time but we are cheating here to keep the example simple.
    


    real-or-random commented at 7:50 am on September 9, 2025:
    The number of found outputs?

    josibake commented at 10:11 am on September 9, 2025:
    Technically, Carol wouldn’t know anything about the transaction, i.e., the total number of inputs, outputs, or found outputs. But I think its more correct to be explicit here and say “found outputs” since we are only using this information to initialize the ser_found_outputs array.
  150. in examples/silentpayments.c:413 in 09d1aee830 outdated
    446+                     *
    447+                     * To verify that we are able to sign for this output, it is sufficient to
    448+                     * check that the public key generated from `full_seckey` matches the output
    449+                     * in the transaction. For a full example on signing for a schnorr ouput,
    450+                     * see `examples/schnorr.c` */
    451+                    memcpy(&full_seckey, &bob_spend_key, 32);
    


    real-or-random commented at 7:52 am on September 9, 2025:
    We should clean full_seckey at some point.
  151. real-or-random commented at 7:53 am on September 9, 2025: contributor
    Some nits in the example file :)
  152. josibake force-pushed on Sep 9, 2025
  153. josibake commented at 11:32 am on September 9, 2025: member

    Updated 09d1aee -> 9bf5073 (2025_19 -> 2025_20, compare)

    • Update examples/silentpayments.c per @real-or-random ’s review
    • Rename the prevouts_summary struct to drop the recipients_ infix (h/t @theStack)
    • Move the conversion of input hash into a scalar inside the _calculate_input_hash function (h/t @theStack)
      • Update the function signature to name
      • This allows for deduplicating the VERIFY_CHECK and explanatory comment
    • s/t_k/output_tweak/ (h/t @real-or-random)
    • Use prevouts_pubkey_sum everywhere (as opposed to input_pubkey_sum)
  154. in examples/silentpayments.c:84 in a9d8fce0a3 outdated
    79+    }
    80+};
    81+
    82+/** Labels
    83+ *
    84+ *  The structs and call back function are implemented here as a demonstration
    


    theStack commented at 11:51 am on September 10, 2025:
    0 *  The structs and callback function are implemented here as a demonstration
    
  155. in examples/silentpayments.c:282 in a9d8fce0a3 outdated
    277+             *
    278+             * Since Bob has access to the full transaction, scanning is simple:
    279+             *
    280+             *     1. Collect the relevant prevouts from the transaction and call
    281+             *        `secp256k1_silentpayments_recipient_prevouts_summary_create`
    282+             *     2. Call `secp256k1__silentpayments_recipient_scan_outputs`
    


    theStack commented at 11:51 am on September 10, 2025:
    0             *     2. Call `secp256k1_silentpayments_recipient_scan_outputs`
    
  156. in include/secp256k1_silentpayments.h:167 in faebc5972b outdated
    162+ *  Args:                    ctx: pointer to a context object
    163+ *  Out:    labeled_spend_pubkey: pointer to the resulting labeled spend
    164+ *                                public key
    165+ *  In:   unlabeled_spend_pubkey: pointer to the recipient's unlabeled spend pubkey
    166+ *                         label: pointer to the recipient's label public
    167+ *                                key
    


    theStack commented at 11:53 am on September 10, 2025:

    formatting nit

    0 *  Out:    labeled_spend_pubkey: pointer to the resulting labeled spend public key
    1 *  In:   unlabeled_spend_pubkey: pointer to the recipient's unlabeled spend pubkey
    2 *                         label: pointer to the recipient's label public key
    
  157. in src/bench.c:260 in 65a4967f55 outdated
    251@@ -236,6 +252,15 @@ int main(int argc, char** argv) {
    252     }
    253 #endif
    254 
    255+#ifndef ENABLE_MODULE_SILENTPAYMENTS
    256+    if (have_flag(argc, argv, "silentpayments") || have_flag(argc, argv, "silentpayments_full_scan") ||
    257+        have_flag(argc, argv, "silentpayments_full_scan_with_labels") || have_flag(argc, argv, "silentpayments_output_scan")) {
    258+        fprintf(stderr, "./bench: silentpayments module not enabled.\n");
    259+        fprintf(stderr, "Use ./configure --enable-module-silentpayments.\n\n");
    260+        return 1;
    


    theStack commented at 11:54 am on September 10, 2025:
    0        return EXIT_FAILURE;
    
  158. in src/modules/silentpayments/main_impl.h:368 in afdb7f7724 outdated
    364@@ -365,4 +365,349 @@ int secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(const secp256
    365     return 1;
    366 }
    367 
    368+/** A explanation of the prevouts_summary object and its usage:
    


    theStack commented at 11:55 am on September 10, 2025:
    0/** An explanation of the prevouts_summary object and its usage:
    
  159. in src/modules/silentpayments/main_impl.h:408 in afdb7f7724 outdated
    403+    size_t n_xonly_pubkeys,
    404+    const secp256k1_pubkey * const *plain_pubkeys,
    405+    size_t n_plain_pubkeys
    406+) {
    407+    size_t i;
    408+    size_t pubkeylen = 64;
    


    theStack commented at 12:05 pm on September 10, 2025:
    nit: other functions that access the _prevouts_summary.data array (e.g. _prevouts_summary_{serialize,parse}) don’t use this constant and directly access at offset 5 + 64 instead, so for consistency, could either remove it here (my preference) or introduce it in the other functions as well
  160. in src/modules/silentpayments/main_impl.h:528 in afdb7f7724 outdated
    523+    const secp256k1_silentpayments_prevouts_summary *prevouts_summary,
    524+    const secp256k1_pubkey *spend_pubkey,
    525+    const secp256k1_silentpayments_label_lookup label_lookup,
    526+    const void *label_context
    527+) {
    528+    secp256k1_scalar output_tweak_scalar, rsk_scalar;
    


    theStack commented at 12:08 pm on September 10, 2025:
    nit, here and in the _create_shared_secret function: could rename rsk_scalar to something more explicit like scan_key_scalar (even after many review rounds, I tend to repeatedly forget that rsk is short for “recipient scan key”)
  161. in include/secp256k1_silentpayments.h:273 in afdb7f7724 outdated
    268+ *           public key).
    269+ *
    270+ *  Args:              ctx: pointer to a context object.
    271+ *  Out:  prevouts_summary: pointer to a silentpayments_prevouts_summary object. If 1 is
    272+ *                     returned, it is set to a parsed version of input33.
    273+ *  In:       input33: pointer to a serialized silentpayments_prevouts_summary.
    


    theStack commented at 12:28 pm on September 10, 2025:

    formatting nit

    0 *  Out:  prevouts_summary: pointer to a silentpayments_prevouts_summary object. If 1 is
    1 *                          returned, it is set to a parsed version of input33.
    2 *  In:            input33: pointer to a serialized silentpayments_prevouts_summary.
    
  162. in src/modules/silentpayments/main_impl.h:385 in afdb7f7724 outdated
    380+ *  curve multiplication can be avoided when creating the shared secret, i.e.,
    381+ *  (recipient_scan_key * input_hash) * prevout_pubkey_sum.
    382+ *
    383+ *  But when storing the prevouts_summary object, either to send to light clients or for
    384+ *  wallet rescans, we can save 32-bytes by combining the input_hash and prevout_pubkey_sum and saving
    385+ *  the resulting point serialized as a compressed public key, i.e., input_hash * prevout_pubkey_sum.
    


    theStack commented at 12:29 pm on September 10, 2025:
    s/prevout_pubkey_sum/prevouts_pubkey_sum/
  163. in src/modules/silentpayments/bench_impl.h:107 in 65a4967f55 outdated
    102+        CHECK(secp256k1_silentpayments_recipient_create_output_pubkey(data->ctx,
    103+            &xonly_output,
    104+            shared_secret,
    105+            &data->spend_pubkey,
    106+            k
    107+        ));
    


    theStack commented at 12:32 pm on September 10, 2025:
    nit: k is initialized to zero and never changes in this benchmark, was the plan to increment it in the loop in order to have changing input data? (didn’t check if it leads to significantly different results, I presume it doesn’t)

    josibake commented at 3:25 pm on September 10, 2025:
    It wasn’t the plan, and I’m not sure it makes a difference? FWIW, the scan key in the full tx benchmark is the same on every run, as well. So if we are going to update it so the benchmarks run on different data each iteration, I’d be inclined to update both benchmarks. I’d recommend this as a follow up.
  164. theStack commented at 12:55 pm on September 10, 2025: contributor

    One more “final pedantic nit” review round :-)

    Two non-code-comment findings:

    • Noted that this PR directly includes the test vectors .json file in the repository, while the musig module doesn’t. I’m not sure what is the preferred way (and don’t have a strong opinion about it), I guess it’s ultimately up to the maintainers.
    • The commit message of the receiving commit (afdb7f77249ca7a83cc86c2d2a21309079264c2a) could be updated to reflect the most recent naming changes (s/public_data/prevouts_summary/, s/b_scan/scan_key/, s/A_sum/prevouts_pubkey_sum/ etc.)
  165. josibake force-pushed on Sep 10, 2025
  166. josibake commented at 3:43 pm on September 10, 2025: member

    Updated 9bf5073 -> 64633f4 (2025_20 -> 2025_21, compare)

    • Use “public keys” in header documentation consistently (h/t @theStack)
    • Rename rsk_scalar to scan_key_scalar (h/t @theStack)
    • Typo and formatting fix-ups (h/t @theStack)

    Thanks again for catching all the little things, @theStack ! Much appreciated.

    Noted that this PR directly includes the test vectors .json file in the repository, while the musig module doesn’t. I’m not sure what is the preferred way (and don’t have a strong opinion about it), I guess it’s ultimately up to the maintainers

    cc @jonasnick as the author of the musig module and @real-or-random on this one. I prefer to have the data committed in the repo, but happy to go with whatever the project prefers.

    The commit message of the receiving commit (https://github.com/bitcoin-core/secp256k1/commit/afdb7f77249ca7a83cc86c2d2a21309079264c2a) could be updated to reflect the most recent naming changes

    Apologies! Missed this, will fix in my next push.

  167. in include/secp256k1_silentpayments.h:73 in 307df90a62 outdated
    66+ *  test vectors from BIP352.
    67+ *
    68+ *  Returns: 1 if creation of outputs was successful.
    69+ *           0 if the input private keys sum to zero,
    70+ *             or input_hash or hash(shared_secret || k) are invalid scalars
    71+ *             (statistically improbable).
    


    theStack commented at 5:31 pm on September 11, 2025:

    in the _sender_create_outputs API docs: looking closer at the statistically improbable (and thus untestable) failure cases, I think strictly speaking this documentation is currently incorrect, as we would not return 0 if any of these hash result sclars were indeed larger than or equal than the curve order, as _scalar_set_b32 would just reduce them (mod n) and the function would still succeed and create some outputs (which might or might not be found by receivers, depending on if their implementation reduces these hash values as well or fails on overflow); the overflow condition check is only VERIFY_CHECKed and hence not active in production. According to BIP352 we should fail, but then we would introduce code branches that can’t be tested, which as far as I understood we want to avoid in general in libsecp.

    Not sure what to do about this, but if we keep the code as-is, I tend to think the mentioning of statistically improbable cases resulting from hash values could be removed from the API docs (it’s not something the user has direct control of or could avoid, so it seems to provide not much value, especially if it refers to internal variables).


    josibake commented at 11:42 am on September 12, 2025:

    I’ve gone back and forth on this in my own head so many times 😅 You’re correct that the documentation is incorrect. Looking at this with fresh eyes, I do think the best thing to do here is return an error to ensure strict compliance with the BIP. This does introduce a few untestable branches, but I think a comment explaining why we have an untestable branch is sufficient.

    Its also worth mentioning we aren’t really adding extra cases the callers would need to handle: the function outputs 0 or 1, and in all cases the caller is expected to try again with different inputs.

    cc @real-or-random , iirc you’ve made some compelling arguments in the past against returning an error here, curious to hear your thoughts.


    theStack commented at 5:54 pm on September 12, 2025:

    I’ve gone back and forth on this in my own head so many times 😅 You’re correct that the documentation is incorrect. Looking at this with fresh eyes, I do think the best thing to do here is return an error to ensure strict compliance with the BIP. This does introduce a few untestable branches, but I think a comment explaining why we have an untestable branch is sufficient.

    I tend to agree, and it seems a very reasonable choice to strictly follow the BIP [1] when in doubt. My current stance on this is that either approach should be fine (as long as the API docs are accurate), and this is much more of a theoretical/philosophical question, rather than one that would ever be relevant in practice, but happy to hear more arguments for either side.

    [1] in theory one option would be to change the BIP to reduce these hash scalars modulo n (like e.g. done in BIP340), but I assume it’s a bit too late for that, and that still wouldn’t eliminate the zero-scalar failure case (though that one is again orders of magnitude less likely than the equal-or-above-group-order case :P).


    josibake commented at 10:29 am on September 15, 2025:

    I’ve updated this to handle the error explicitly, while also commenting this is for strict compliance with the BIP and results in an untestable branch of the code.

    Regarding reducing hash scalars modulo n, I do think its too late for that and I also pushed back against that when we were designing the protocol. I wish I had a better reason for not liking it besides “it makes me uncomfortable.” 😅 But this is a topic I plan to dig into at some point in the future more generally, e.g., when is it appropriate to reduce a hash scalar modulo the curve and when should we be concerned about it.

  168. in src/modules/silentpayments/main_impl.h:84 in 307df90a62 outdated
    81+     * This can only fail if the output of the hash function is greater than the curve order, which
    82+     * happens with negligible probability. We use a VERIFY_CHECK as opposed to returning an error,
    83+     * since returning an error here would result in an untestable branch in the code.
    84+     */
    85+    secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
    86+    VERIFY_CHECK(!overflow);
    


    theStack commented at 6:17 pm on September 11, 2025:

    should VERIFY_CHECK here for non-zero as well (same as in _create_output_tweak below)

    0    secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
    1    VERIFY_CHECK(!overflow);
    2    VERIFY_CHECK(!secp256k1_scalar_is_zero(input_hash_scalar));
    
  169. in src/modules/silentpayments/main_impl.h:206 in 307df90a62 outdated
    203+    if (plain_seckeys != NULL) {
    204+        ARG_CHECK(n_plain_seckeys > 0);
    205+    } else {
    206+        ARG_CHECK(n_plain_seckeys == 0);
    207+    }
    208+    ARG_CHECK(outpoint_smallest36 != NULL);
    


    theStack commented at 6:27 pm on September 11, 2025:
    yocto-nit: could move this ARG_CHECK up to after n_recipients to match the order of the input parameters
  170. theStack approved
  171. theStack commented at 6:36 pm on September 11, 2025: contributor

    ACK 64633f42d2e9a26d785115cb2712636cb9d6227e 🤫

    With one comment regarding statistically improbable failure cases that we VERIFY_CHECK. I initially thought of it as small API docs nit, but I guess it’s potentially more than that – depending on how you look at it, there is big range between “this is just a documentation inaccuracy” to “this is horrible, if the unlikely case ever happens, people could actually lose money”.

  172. real-or-random commented at 11:11 am on September 12, 2025: contributor

    Noted that this PR directly includes the test vectors .json file in the repository, while the musig module doesn’t. I’m not sure what is the preferred way (and don’t have a strong opinion about it), I guess it’s ultimately up to the maintainers

    cc @jonasnick as the author of the musig module and @real-or-random on this one. I prefer to have the data committed in the repo, but happy to go with whatever the project prefers.

    My thinking is that having the input JSON makes sense to be able to (re-)create the test vectors in the build system without further dependencies. It also documents which version of JSON we use in the library. Perhaps we should have this in the musig module, too.

  173. josibake force-pushed on Sep 12, 2025
  174. josibake force-pushed on Sep 15, 2025
  175. josibake commented at 10:26 am on September 15, 2025: member

    Rebased 64633f4 -> ff0bb11 (2025_21 -> 2025_21-rebased, compare)

    • Rebased to incorporate s/secp256k1_memclear/secp256k1_memclear_explicit/
  176. josibake force-pushed on Sep 15, 2025
  177. josibake commented at 10:39 am on September 15, 2025: member

    Updated ff0bb11 -> d8d062e (2025_21-rebased -> 2025_22, compare)

    • Check and return an error if the output of a hash function is greater than the curve order. Specifically, this updates the _calculate_input_hash_scalar and _create_output_tweak functions (h/t @theStack)
    • Add comments explaining the newly introduced untestable branches
    • Have ARG_CHECKS match argument order (h/t @theStack)

    Thanks for digging into the API docs @theStack , I think this last push brings the code inline with the documentation, and overall I’m happier with where we landed w.r.t. spec compliance with the BIP.

  178. in src/modules/silentpayments/main_impl.h:86 in d8d062eabb
    81+     * since returning an error here would result in an untestable branch in the code. But in this case, we return
    82+     * an error to ensure strict compliance with BIP0352.
    83+     */
    84+    secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
    85+    VERIFY_CHECK(!secp256k1_scalar_is_zero(input_hash_scalar));
    86+    return !overflow;
    


    jonasnick commented at 12:07 pm on September 15, 2025:
    Is there a reason we handle overflow different to 0 (same in create_output_tweak)?

    josibake commented at 12:16 pm on September 16, 2025:
    Great catch. We should treat both the same, i.e., return an error. BIP352 does specify failing if input_hash_scalar is zero or greater than the curve order, we must fail. I had forgotten that zero was included as a failure mode.

    josibake commented at 1:37 pm on September 17, 2025:
    Fixed.
  179. in include/secp256k1_silentpayments.h:73 in d8d062eabb
    68+ *  test vectors from BIP352.
    69+ *
    70+ *  Returns: 1 if creation of outputs was successful.
    71+ *           0 if the input private keys sum to zero,
    72+ *             or input_hash or hash(shared_secret || k) are invalid scalars
    73+ *             (statistically improbable).
    


    jonasnick commented at 12:59 pm on September 15, 2025:
    It’s not clear if “statistically improbable” applies to the function returning 0 or just to the scalars being invalid. I guess it only applies to the scalars being invalid because the function calls create_output_pubkey which fails if output_tweak_scalar*G = -spend_pubkey. The comment says that this has only “negligible probability for honestly created spend_pubkey”. Couldn’t a dishonest party set their spend_pubkey to output_tweak_scalar*G? If yes, then we should also not assert in the example. Same applies to recipient_create_output_pubkey.

    josibake commented at 1:36 pm on September 17, 2025:
    Removed “statistically improbable,” but reading your comment again I realise I may have missed this assert in the example. If so, will fix in my next push.

    josibake commented at 11:34 am on September 22, 2025:
    Removed the inappropriate asserts from the example.
  180. in include/secp256k1_silentpayments.h:218 in d8d062eabb
    213+ *  for later use, the caller can save the result with
    214+ *  `silentpayments_recipient_prevouts_summary_serialize`.
    215+ *
    216+ *  Returns: 1 if prevout summary creation was successful.
    217+ *           0 if the input public keys sum to zero,
    218+ *             or the input_hash is an invalid scalar (statistically improbable).
    


    jonasnick commented at 1:02 pm on September 15, 2025:
    It’s not clear to the reader if “statistically improbable” applies to the function returning 0 or just to the scalar being invalid.

    josibake commented at 12:20 pm on September 16, 2025:

    Someone once told me “statistics mean nothing to individual experience,” which feels appropriate here? What I mean is, I’m warming up to removing all references to statistically improbably from the API documentation. For the user, if they hit a failure case, they don’t really care if it was the input keys summing to zero, or the specific set of inputs resulting in an invalid scalar because for them the next step is the same: they need to try again with different inputs.

    How do you feel about simply removing “statistically improbable” from the API documentation? I do feel its still worth having comments in the code, especially when it adds an explanation for why we don’t have 100% test coverage.


    jonasnick commented at 6:30 pm on September 16, 2025:

    I’d be in favor of removing “statistically improbable” because it doesn’t help the user deciding whether to assert that the return value is 1 or to handle it in some way. We can see this in the examples, where we currently do both even if “statistically improbable”. I think if we want to be consistent, we only say “Returns 1 always” when it really always returns 1 if the API is used correctly, which allows the caller to assert the return value. In cases where the failure probability is really negligible, we’d not specify that it “Returns 1 always”.

    Additionally, “statistically improbable” is also an API guarantee that we may not want to make.


    josibake commented at 1:35 pm on September 17, 2025:
    Updated.
  181. in include/secp256k1_silentpayments.h:163 in d8d062eabb
    158+ *  consisting of the serialized and concatenated scan public key and
    159+ *  (labeled) spend public key.
    160+ *
    161+ *  Returns: 1 if labeled spend public key creation was successful.
    162+ *           0 if the input public keys are invalid,
    163+ *             or the spend pubkey + label sum to zero (statistically improbable).
    


    jonasnick commented at 1:08 pm on September 15, 2025:
    That spend pubkey + label sum to zero is only statistically improbable for a honestly created label. Can you imagine that this function is called with untrusted inputs? The example asserts that this function returns 1.

    josibake commented at 12:21 pm on September 16, 2025:
    Same argument as #1698 (review), I’m no longer convinced “statistically improbable” is adding anything for the user.

    josibake commented at 1:35 pm on September 17, 2025:
    Removed all references to “statistically improbable,” which is a bit unfortunate given how many times @theStack had to correct my spelling of “statistically” during review, all for nothing 😅
  182. in include/secp256k1_silentpayments.h:335 in d8d062eabb
    330+ *  For creating the labels cache, `secp256k1_silentpayments_recipient_create_label`
    331+ *  can be used.
    332+ *
    333+ *  Returns: 1 if output scanning was successful.
    334+ *           0 if any combination of the shared secret, label and spend public keys
    335+ *             sum to zero (statistically improbable).
    


    jonasnick commented at 1:13 pm on September 15, 2025:
    Doesn’t this only fail if “output_tweak * G is the negation of spend_pubkey”? This doesn’t seem to be statistically improbable, because a malicious party could have set their spend_key to -output_tweak * G. In the example we also don’t trust that this is statistically improbable and explicitly check the return value.

    josibake commented at 12:49 pm on September 16, 2025:

    It’s not only if output_tweak is the negation of the spend pubkey. This is because when calling this function, the caller passes in an “unlabeled_spend_pubkey” and a label_lookup function. If the silent payment was sent to one of their labeled addresses, i.e., spend_pubkey = unlabeled_spend_pubkey + label_tweak * G, then the final output is of the form unlabeled_spend_pubkey + label_tweak * G + output_tweak * G.

    And agree that a malicious party can choose specific values for spend_pubkey to trigger this, especially given that the set of all possible inputs in silent payments is already known, i.e., the UTXO set.


    jonasnick commented at 7:00 pm on September 16, 2025:
    Maybe I’m blind, but I only see a return 0 when “output_tweak * G is the negation of spend_pubkey”.

    josibake commented at 7:24 am on September 17, 2025:

    Apologies, I’m blind! We only do a VERIFY_CHECK when applying the label tweak here:

    https://github.com/bitcoin-core/secp256k1/blob/d8d062eabbf3a98f717ea7779dddcc9eeea80ef6/src/modules/silentpayments/main_impl.h#L684-L685

    .. but given that the label value is returned from a callback function that might be retrieving label values not controlled by the caller (and thus, potentially maliciously created), we should also return here. Also, this is consistent with the principle “if we can trigger it with a test case, don’t do a verify check.”


    josibake commented at 1:38 pm on September 17, 2025:
    Removed the VERIFY_CHECK, will add a test case that covers this branch in the next push.

    josibake commented at 11:01 am on September 22, 2025:
    Added a test which covers this branch.
  183. in src/modules/silentpayments/main_impl.h:614 in d8d062eabb
    609+            return 0;
    610+        }
    611+
    612+        /* Calculate output = spend_pubkey + output_tweak * G.
    613+         * This can fail if output_tweak * G is the negation of spend_pubkey, but this happens only
    614+         * with negligible probability as output_tweak is the output of a hash function. */
    


    jonasnick commented at 1:17 pm on September 15, 2025:
    As mentioned in the comment in create_output_pubkey, this only holds for honestly created spend_pubkeys.

    josibake commented at 1:34 pm on September 17, 2025:
    Updated the comment to clarify.
  184. in src/modules/silentpayments/tests_impl.h:28 in fcd2443665 outdated
    23+static unsigned char ORDERC[32] = {
    24+    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    25+    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
    26+    0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b,
    27+    0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41
    28+};
    


    theStack commented at 0:29 am on September 16, 2025:
    this constant is now available as secp256k1_group_order_bytes in util.h (#1745)

    josibake commented at 1:34 pm on September 17, 2025:
    Fixed, good catch!
  185. in src/modules/silentpayments/bench_impl.h:162 in d5fd1871bd outdated
    157+static void run_silentpayments_bench(int iters, int argc, char** argv) {
    158+    bench_silentpayments_data data;
    159+    int d = argc == 1;
    160+
    161+    /* create a context with no capabilities */
    162+    data.ctx = secp256k1_context_create(SECP256K1_FLAGS_TYPE_CONTEXT);
    


    theStack commented at 0:40 am on September 16, 2025:
    0    data.ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
    

    That’s logically the same, but for consistency with other benchmarks and _context_create call-sites. I guess these lines origin from the ECDH benchmark, where it should be fixed too (if anyone wants to open a PR; for that one we could probably even use the static context, as a “no capabilities” context is indeed sufficient).


    josibake commented at 1:34 pm on September 17, 2025:
    Fixed.
  186. in src/modules/silentpayments/main_impl.h:646 in d8d062eabb
    641+                secp256k1_ge_neg(&output_negated_ge, &output_ge);
    642+                /* Negate the generated output and calculate first scan label candidate:
    643+                 *     label1 = tx_output - generated_output
    644+                 */
    645+                secp256k1_gej_add_ge_var(&label_gej, &tx_output_gej, &output_negated_ge, NULL);
    646+                secp256k1_ge_set_gej(&label_ge, &label_gej);
    


    jonasnick commented at 1:25 pm on September 16, 2025:

    Is there a reason not to use secp256k1_ge_set_gej_var here and few lines below in this function? On my machine, this saves n_tx_outputs microseconds per scan_outputs call.

    It seems like we can also use ge_set_gej_var in create_labeled_spend_pubkey and prevouts_summary_create. As an aside, if we also replace it in create_shared_secret or sender_create_outputs, our ctime tests complain. So seems like that is working!


    josibake commented at 7:38 am on September 17, 2025:
    No reason, great suggestion!

    josibake commented at 1:33 pm on September 17, 2025:
    Added.
  187. in src/modules/silentpayments/bench_impl.h:110 in d8d062eabb outdated
    106+            k
    107+        ));
    108+    }
    109+}
    110+
    111+static void bench_silentpayments_full_tx_scan(void* arg, int iters, int use_labels) {
    


    jonasnick commented at 1:31 pm on September 16, 2025:
    I printed n_found to confirm what the function is benchmarking. Maybe it make sense to mention somewhere that scanning will not find an output or add CHECK(n_found == 0).

    josibake commented at 1:33 pm on September 17, 2025:
    Added.
  188. in src/modules/silentpayments/bench_impl.h:152 in d8d062eabb
    147+}
    148+
    149+/* TODO: currently, the with_labels benchmark ensures all of the labels code paths
    150+ * are hit during scanning, but should be extended to measure scanning for labels
    151+ * with a sizable labels cache.
    152+ */
    


    jonasnick commented at 1:34 pm on September 16, 2025:
    Unless I’m missing something this would only benchmark the speed of the labels cache, which we’re not planning to implement.

    josibake commented at 7:43 am on September 17, 2025:
    This is a good point. My thinking initially was to create something that confirms that the size of the labels cache won’t affect scanning, but that seems like more effort than its worth to implement in C. I think it’s best to remove the comment.

    josibake commented at 1:33 pm on September 17, 2025:
    Removed.
  189. in src/modules/silentpayments/main_impl.h:641 in d8d062eabb
    605+         * Note: _create_output_tweak can only fail if the output of the hash function is greater than the curver order, which is statistically improbable.
    606+         * Returning an error here results in an untestable branch in the code, but we do this anyways to ensure strict compliance with BIP0352.
    607+         */
    608+        if (!secp256k1_silentpayments_create_output_tweak(&output_tweak_scalar, shared_secret, k)) {
    609+            return 0;
    610+        }
    


    jonasnick commented at 1:53 pm on September 16, 2025:
    Is there a reason why we clear secret values everywhere else when this function returns 0 but not here?

    josibake commented at 7:39 am on September 17, 2025:
    Nope, was likely missed when I changed this from a VERIFY_CHECK to a return.

    josibake commented at 1:33 pm on September 17, 2025:
    Fixed.
  190. jonasnick commented at 2:01 pm on September 16, 2025: contributor

    I was able to confirm on my machine that the benchmark numbers are not lower than the benchmarks of the individual components despite the benchmarks using static data (*):

    • silentpayments_output_scan 48us
      • recipient_prevouts_summary_parse
        • 1 pubkey parse (square root): 3us
      • recipient_create_shared_secret
        • 1 ECDH: 28 us
      • recipient_create_output_pubkey
        • pubkey_tweak_add (ecmult (var) with generator): 13us
    • silentpayments_full_scan: 47
      • prevouts_summary_create
        • ge_set_gej_var: 1us
      • recipient_scan_outputs
        • ECDH + pubkey_tweak_add: 41us
    • silentpayments_full_scan_with_labels: 53
      • prevouts_summary_create
        • ge_set_gej_var: 1us
      • recipient_scan_outputs
        • ECDH + pubkey_tweak_add + 2 * 2 * ge_set_gej_var: 45us

    By the way, I find it interesting that when scanning outputs without labels, the number of outputs doesn’t really matter for scanning performance. However, when scanning with labels, each transaction output requires 2 ge_set_gej(_var) and scanning performance is independent of the number of labels (assuming constant time label lookup).

    (*) Tested this with and without changing ge_set_gej to ge_set_gej_var where possible in main_impl.h.

  191. in src/modules/silentpayments/main_impl.h:655 in d8d062eabb outdated
    643+                 *     label1 = tx_output - generated_output
    644+                 */
    645+                secp256k1_gej_add_ge_var(&label_gej, &tx_output_gej, &output_negated_ge, NULL);
    646+                secp256k1_ge_set_gej(&label_ge, &label_gej);
    647+                ret = secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 1);
    648+                /* Serialize must succeed because the point was just loaded. */
    


    jonasnick commented at 7:02 pm on September 16, 2025:
    This argument appears to be oversimplifying because serialize also fails if label_ge is the point at infinity. This cannot happen, however, because we only land in this branch if x(tx_output) != x(generated_output) (same below).

    josibake commented at 12:59 pm on September 17, 2025:
    Agreed, and the justification of tx_output != generated_output is not obvious. Will add a comment.

    josibake commented at 1:32 pm on September 17, 2025:
    Updated comments.
  192. in src/modules/silentpayments/main_impl.h:67 in d8d062eabb outdated
    55+    hash->s[7] = 0xcd06903eul;
    56+
    57+    hash->bytes = 64;
    58+}
    59+
    60+static int secp256k1_silentpayments_calculate_input_hash_scalar(secp256k1_scalar *input_hash_scalar, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) {
    


    jonasnick commented at 7:08 pm on September 16, 2025:
    This function requires that pubkey_sum is not the point at infinity. We check this explicitly in recipient_prevouts_summary_create but not in sender_create_outputs. I think we should do that and document the requirement.

    josibake commented at 12:58 pm on September 17, 2025:
    In create_outputs, we generate the pubkey_sum from the seckey_sum. We do explicitly check that the seckey_sum is not zero before generating the corresponding public point. My assumption was checking the seckey_sum would be sufficient, but it sounds like you’re saying we can still end up at the point at infinity from a valid, non-zero scalar? It was my understanding was this is impossible, but happy to add the extra check if my intuition on this is wrong. Otherwise, the extra check feels redundant to me.

    theStack commented at 9:42 pm on September 20, 2025:
    It’s also my understanding that this can’t happen currently in sender_create_outputs due to early return if seckey_sum is zero. I’d agree that adding a run-time check is redundant, but I guess adding a VERIFY_CHECK line mostly for documentation purposes could make sense.

    jonasnick commented at 3:06 pm on September 24, 2025:
    I missed the seckey_sum != 0 check. nit: I would still document the requirement above calculate_input_hash_scalar. Just something like “pubkey_sum must not be the point at infinity.”.

    josibake commented at 2:01 pm on October 7, 2025:
    Updated.
  193. josibake force-pushed on Sep 17, 2025
  194. josibake commented at 1:32 pm on September 17, 2025: member

    Updated d8d062e -> fe2cec5 (2025_22 -> 2025_23, compare)

    • Update example to use newlines at the end of printed statements
    • Don’t assert in example on function that can fail (albeit with negligible probability)
    • Remove “stastically improbable” wording from API documentation (h/t @jonasnick)
    • Clean up benchmark:
    • Explicitly verify that scalars produced as a result of hash functions are not zero and return an error otherwise (h/t @jonasnick)
    • Prefer gej_var where appropriate (h/t @jonasnick)
    • Explicitly clear sensitive data everywhere (h/t @jonasnick)
    • Various comment updates (h/t @jonasnick)
    • Replace custom ORDERC value with global constant (h/t @theStack)

    Thanks all for the continued review! I have one remaining todo to add a test case for a maliciously created label tweak value, but wanted to go ahead and push the updates completed so far.

  195. in src/modules/silentpayments/main_impl.h:723 in 23c19b896e outdated
    718+    secp256k1_memclear_explicit(shared_secret, sizeof(shared_secret));
    719+    return 1;
    720+}
    721+
    722+int secp256k1_silentpayments_recipient_create_shared_secret(const secp256k1_context *ctx, unsigned char *shared_secret33, const unsigned char *scan_key32, const secp256k1_silentpayments_prevouts_summary *prevouts_summary) {
    723+    secp256k1_scalar rsk;
    


    theStack commented at 9:07 pm on September 17, 2025:
    naming nit: s/rsk/scan_key_scalar/ (after this, I’m pretty confident that all of the outdated or confusing scan/spend key variable names are fixed)

    josibake commented at 3:27 pm on September 18, 2025:
    One of these days, I will learn how to use find and replace all 😅
  196. in src/modules/silentpayments/main_impl.h:287 in 8044d3770c outdated
    270+     * Note: _input_hash_scalar can only fail if the output of the hash function is greater than the curver order, which is statistically improbable.
    271+     * Returning an error here results in an untestable branch in the code, but we do this anyways to ensure strict compliance with BIP0352.
    272+     */
    273+    if (!secp256k1_silentpayments_calculate_input_hash_scalar(&input_hash_scalar, outpoint_smallest36, &prevouts_pubkey_sum_ge)) {
    274+        return 0;
    275+    }
    


    theStack commented at 9:13 pm on September 17, 2025:
    in the newly introduced failure branch of the sending function: missing clearing of seckey_sum_scalar detected :detective:

    josibake commented at 3:26 pm on September 18, 2025:
    One of these days, I promise: I will learn how to spell statiscaly , and I will remember to clear my scalars 😅
  197. in src/modules/silentpayments/bench_impl.h:158 in 1a1c7f9ff4 outdated
    153+
    154+static void run_silentpayments_bench(int iters, int argc, char** argv) {
    155+    bench_silentpayments_data data;
    156+    int d = argc == 1;
    157+
    158+    /* create a context with no capabilities */
    


    theStack commented at 9:19 pm on September 17, 2025:
    nit: the context has indeed capabilities, so could just remove the comment (or s/no capabilities/signing capabilities/, as done in the EllSwift benchmarks)

    josibake commented at 3:31 pm on September 18, 2025:
    I opted to remove the comment. It didn’t feel like it was adding any new information, beyond just describing what the code is doing.
  198. in src/modules/silentpayments/main_impl.h:25 in 8044d3770c outdated
    19+#include "../../hash.h"
    20+#include "../../hsort.h"
    21 
    22-/* TODO: implement functions for receiver side. */
    23+/** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to
    24+ *  ensure the correct values of k are used when creating multiple outputs for a recipient.
    


    Sjors commented at 11:40 am on September 18, 2025:

    In 8044d3770cb64392ad9b93ad7f44da66e92cf159 silentpayments: sending: in an earlier discussion you said you planned to update this comment, in order to clarify that callers should not assume this is deterministic (and it currently isn’t because of the use of heap sort, but that’s perhaps an implementation detail).

    Looks like that update went missing.

    secp256k1_silentpayments_recipient_sort might be a better place for that comment.

    #1698 (review)


    josibake commented at 11:00 am on September 22, 2025:
    Added, not sure how this was dropped 😅
  199. josibake force-pushed on Sep 18, 2025
  200. josibake commented at 3:44 pm on September 18, 2025: member

    Updated fe2cec5 -> 4428fd3 (2025_23 -> 2025_24, compare)

    • Clear secret sum in early return (h/t @theStack)
    • Fix missed rename of rsk (h/t @theStack)
    • Remove comment from benchmarks (h/t @theStack)

    As always, thank you for the incredibly thorough review, @theStack ❤️

  201. in include/secp256k1_silentpayments.h:90 in e9350ffd23 outdated
    74+ *                            one per recipient.
    75+ *                            The outputs are ordered to match the original
    76+ *                            ordering of the recipient objects, i.e.,
    77+ *                            `generated_outputs[0]` is the generated output
    78+ *                            for the `_silentpayments_recipient` object with
    79+ *                            index = 0.
    


    Sjors commented at 3:50 pm on September 18, 2025:

    In 8044d3770cb64392ad9b93ad7f44da66e92cf159 silentpayments: sending: in line with our earlier discussion, it might be worth warning the user to not drop outputs (at least not for transactions with more than one recipient).

    https://github.com/bitcoin-core/secp256k1/pull/1698/commits/8044d3770cb64392ad9b93ad7f44da66e92cf159#r2245582298


    josibake commented at 11:00 am on September 22, 2025:
    Added!
  202. Sjors commented at 3:56 pm on September 18, 2025: member
    Reviewed sending support again in 8c5d3e343feaffcdb902f611d1356ad37de807ac (missed todays push) as well as the new tag test in 8c5d3e343feaffcdb902f611d1356ad37de807ac.
  203. in src/modules/silentpayments/main_impl.h:75 in e9350ffd23 outdated
    72+    VERIFY_CHECK(ret && len == sizeof(pubkey_sum_ser));
    73+    secp256k1_sha256_write(&hash, pubkey_sum_ser, sizeof(pubkey_sum_ser));
    74+    secp256k1_sha256_finalize(&hash, input_hash);
    75+    /* Convert input_hash to a scalar to ensure the value is less than the curve order.
    76+     *
    77+     * This can only fail if the output of the hash function is zero or greater than the curve order, which
    


    theStack commented at 9:07 pm on September 20, 2025:
    pedantic-nit: s/greater than/greater than or equal/ (here and in many other instances)

    josibake commented at 9:20 am on September 22, 2025:
    Good point, initially I was thinking this is correct since a scalar equal to the curve order is zero when reduced module p. But this comment refers to the output of the hash function, meaning the modulus step has not happened yet. So, indeed, >= is correct.

    josibake commented at 10:59 am on September 22, 2025:
    Updated.
  204. theStack commented at 9:21 pm on September 20, 2025: contributor

    Now that there are a good number of practically unreachable code branches (entered if scalars converted from hash results are not in range [1,N-1]), I tried verifying that hitting those would indeed result in API function failures by introducing fake hash values, controlled by global test flags that are selectively turned on/off in the tests: https://github.com/theStack/secp256k1/commit/b64cbb0619b3f2c46a504ff0fbe3aa88e593dc05. This is very ugly and obviously not meant to land in the code base, but seeing that this commit passes the tests gives me at least a one-time confidence that the error propagation works as intended, if any of the statistically improbable cases happened.

    Summary of API functions that can “untestably” fail, depending on which hash values are out-of-range (hope I didn’t miss any):

    API function returning 0 in unreachable branches input_hash output_tweak label_tweak
    secp256k1_sender_create_outputs X X
    secp256k1_recipient_create_label X
    secp256k1_prevouts_summary_create X
    secp256k1_recipient_scan_outputs X
    secp256k1_recipient_create_output_pubkey X

    That might be helpful to double-check if the API docs for return value 0 are complete. The code looks good to me, happy to re-ACK once the outstanding comments are addressed. :shushing_face:

  205. real-or-random referenced this in commit de6af6ae35 on Sep 22, 2025
  206. josibake force-pushed on Sep 22, 2025
  207. josibake commented at 10:59 am on September 22, 2025: member

    Updated 4428fd3 -> 5fe0d3e (2025_24 -> 2025_25, compare)

    • Added comment regarding unstable sort (h/t @Sjors)
    • Added documentation to the header regarding using all generated outputs (h/t @Sjors)
    • s/greater than curve order/greater than or equal to the curve order/ (h/t @theStack)
    • Added a test case for a maliciously created label tweak

    Thanks @Sjors for catching the missed comments; I’ve added both back in. @theStack thanks for the untestable branch checker! After adding the last edge case test, I also re-ran gcovr and confirmed only the documented untestable branches are the ones not covered in the coverage report.

  208. josibake force-pushed on Sep 22, 2025
  209. josibake commented at 11:32 am on September 22, 2025: member

    Updated 5fe0d3e -> 1a21cf1 (2025_25 -> 2025_26, compare)

    • Update example to no longer assert on functions that can return errors (h/t @jonasnick)
    • Update send API docs to document all possible failure cases

    I believe this push addresses all of the remaining outstanding feedback, except for #1698 (review). It is my belief that the check for the secret key sum being zero is sufficient, but waiting on @jonasnick to confirm. If there is any other outstanding feedback I have missed, please let me know!

  210. josibake commented at 4:30 pm on September 22, 2025: member
    The UBSan failure seems to be happening due to the newly introduced test case; will troubleshoot in the morning. Just wanted to mention that its caused by the tests, in case anyone is willing to review the other changes but dissuaded by the red CI 😅
  211. josibake force-pushed on Sep 23, 2025
  212. josibake commented at 8:29 am on September 23, 2025: member

    Updated 1a21cf1 -> ac4a726 (2025_26 -> 2025_27, compare)

    • Fix UBsan error in newly introduced test
  213. in include/secp256k1_silentpayments.h:340 in a03b9e835c outdated
    335+ *  For creating the labels cache, `secp256k1_silentpayments_recipient_create_label`
    336+ *  can be used.
    337+ *
    338+ *  Returns: 1 if output scanning was successful.
    339+ *           0 if any combination of the shared secret, label and spend public keys
    340+ *             sum to zero.
    


    theStack commented at 2:26 pm on September 23, 2025:

    The “output_tweak invalid” case is currently missing here, documented on the sender side (and for _recipient_create_output_pubkey) as “if hash(shared_secret || k) is an invalid scalar”. The other cases could be mentioned explicitly for consistency with other API headers, as it’s (at least for me) not clear what “any combination” exactly means, e.g. if that also includes the sum of all three of the mentioned elements or not. IIUC the two other cases are:

    • output tweak is the negation of spend secret key
    • label tweak is the negation of output tweak

    josibake commented at 3:18 pm on September 24, 2025:

    I think something like this reads much better:

    0if hash(shared_secret || k) is an invalid scalar or the negation of
    1either the spend secret key or the label tweak, or if the arguments
    2are invalid.
    

    and then per your later comment, we just add “if the arguments are invalid” to the rest of the API docs. WDYT?


    theStack commented at 3:33 pm on September 24, 2025:
    Sounds good to me :+1:

    josibake commented at 1:59 pm on October 7, 2025:
    Updated based on @jonasnick ’s most recent feedback.
  214. in include/secp256k1_silentpayments.h:393 in a03b9e835c outdated
    388+ *  access to the full transaction. If the caller does have access to the full
    389+ *  transaction, `secp256k1_silentpayments_recipient_scan_outputs` should be
    390+ *  used instead.
    391+ *
    392+ *  Returns: 1 if shared secret creation was successful.
    393+ *           0 if the recipient scan key is invalid.
    


    theStack commented at 2:44 pm on September 23, 2025:

    Noticed only now that this is currently the only function that explicitly mentions failure if a secret key input is invalid. Other functions that get (plain) secret keys as inputs are:

    • _sender_create_outputs
    • _recipient_create_label (this one currently doesn’t check if the scan key is valid internally, I guess it should though to be on the safe side, preventing users from creating unspendable labels?)
    • _recipient_scan_outputs

    Not sure if it’s better to always mention failure for invalid secret key inputs explicitly or never in the API docs; maybe something generic like “if the arguments are invalid” could be used to keep it simple, as e.g. done in the musig module.


    josibake commented at 3:18 pm on September 24, 2025:
    I think adding “if the arguments are invalid” uniformly is a good approach.

    jonasnick commented at 7:54 am on September 25, 2025:
    Similarly, recipient_create_labeled_spend_pubkey is the only function that explicitly mentions failure if a public key is invalid.
  215. theStack commented at 2:56 pm on September 23, 2025: contributor
    Left two more comments only on the API docs, with one small potential secret key verification proposal for the labels creation function. Planning to take a final look at the tests and the example tomorrow.
  216. in Makefile.am:263 in e868d11158 outdated
    259@@ -260,6 +260,7 @@ maintainer-clean-local: clean-precomp
    260 ### Pregenerated test vectors
    261 ### (see the comments in the previous section for detailed rationale)
    262 TESTVECTORS = src/wycheproof/ecdsa_secp256k1_sha256_bitcoin_test.h
    263+TESTVECTORS += src/modules/silentpayments/vectors.h
    


    stratospher commented at 7:38 am on September 24, 2025:
    60f209c: we could wrap this in if ENABLE_MODULE_SILENTPAYMENTS as well. (similar to ECDH test vectors)
  217. in src/modules/silentpayments/main_impl.h:650 in ac4a726a2a outdated
    645+                /* Negate the generated output and calculate first scan label candidate:
    646+                 *     label1 = tx_output - generated_output
    647+                 *
    648+                 * Note: we can only hit this branch if tx_output != output_xonly. Thus,
    649+                 * we can add tx_output_gej + output_negated_ge without needing to check
    650+                 * whether or not the result is the point at infinity.
    


    jonasnick commented at 2:44 pm on September 24, 2025:
    nit: potentially confusing because we can always add without needing to check whether or not the result is the point at infinity. This misses the serialization context. Why not move this note to below serialize and use the same phrasing as in the label2 case?

    josibake commented at 2:02 pm on September 25, 2025:
    Good point, the label2 case wording also is much more clear.
  218. in examples/silentpayments.c:199 in a59a668e2e outdated
    194+         *
    195+         *     1. One output to Bob's labeled address
    196+         *     2. Two outputs for Carol
    197+         *
    198+         * To create multiple outputs for Carol, Alice simply passes Carol's
    199+         * silent payment address mutltiple times.
    


    stratospher commented at 2:56 pm on September 24, 2025:
    0e94f2d: typo nit: multiple
  219. real-or-random referenced this in commit baa265429f on Sep 24, 2025
  220. in src/modules/silentpayments/main_impl.h:535 in a03b9e835c outdated
    530+    ARG_CHECK(input33 != NULL);
    531+    /* Since an attacker can send us malicious data that looks like a serialized public key but is not, fail early. */
    532+    if (!secp256k1_eckey_pubkey_parse(&pk, input33, inputlen)) {
    533+        return 0;
    534+    }
    535+    /* A serialized prevouts_summary will always have have the input_hash multiplied in, so we set combined = true.
    


    stratospher commented at 5:14 am on September 25, 2025:
    da3120e: typo nit: have have
  221. in src/modules/silentpayments/main_impl.h:85 in ac4a726a2a outdated
    80+     * since returning an error here would result in an untestable branch in the code. But in this case, we return
    81+     * an error to ensure strict compliance with BIP0352.
    82+     */
    83+    secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow);
    84+    ret &= !secp256k1_scalar_is_zero(input_hash_scalar);
    85+    return !!ret & !overflow;
    


    jonasnick commented at 7:51 am on September 25, 2025:
    Why !!ret? I don’t think we use this pattern anywhere except for the return value of user-provided functions. (same in secp256k1_silentpayments_create_output_tweak)

    josibake commented at 12:22 pm on September 26, 2025:
    I was following the ecdh/elligator swift modules, but you are correct: this pattern is only used when evaluating the output of a user provided function, which is not the case here. In our case, we know the return values will be {0, 1}, so ret & !overflow is sufficient.

    josibake commented at 2:04 pm on October 7, 2025:
    Fixed.
  222. in include/secp256k1_silentpayments.h:192 in ac4a726a2a outdated
    182+ *
    183+ *  This structure does not contain secret data. Guaranteed to be 101 bytes in
    184+ *  size. It can be safely copied/moved. Created with
    185+ *  `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized as a
    186+ *  compressed public key using
    187+ *  `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is
    


    jonasnick commented at 7:56 am on September 25, 2025:
    0 *  `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized with
    1 *  `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is
    

    nit: Unless I’m missing something, mentioning the compressed public key seems unnecessary?

  223. in include/secp256k1_silentpayments.h:271 in ac4a726a2a
    266+
    267+/** Parse a 33-byte sequence into a silentpayments_recipients_prevouts_summary object.
    268+ *
    269+ *  Returns: 1 if the data was able to be parsed.
    270+ *           0 if the sequence is invalid (e.g., does not represent a valid
    271+ *           public key).
    


    jonasnick commented at 7:57 am on September 25, 2025:
    0 *           0 if the sequence is invalid.
    
  224. in include/secp256k1_silentpayments.h:358 in ac4a726a2a outdated
    346+ *                               tx_outputs array
    347+ *              n_found_outputs: pointer to an integer indicating the final
    348+ *                               size of the found outputs array. This number
    349+ *                               represents the number of outputs found while
    350+ *                               scanning (0 if none are found)
    351+ *  In:              tx_outputs: pointer to the tx's x-only public key outputs
    


    jonasnick commented at 7:58 am on September 25, 2025:
    0 *  In:              tx_outputs: pointer to the transaction's x-only public key outputs
    

    nit: this would be the only time we mention “tx” in the text.

  225. in include/secp256k1_silentpayments.h:421 in ac4a726a2a outdated
    409+ *  Given a shared_secret, a recipient spend public key, and an output counter k,
    410+ *  create an output public key.
    411+ *
    412+ *  This function is used by the recipient when scanning for outputs without
    413+ *  access to the transaction outputs (e.g., using BIP158 block filters). When
    414+ *  scanning with this function, it is the scanners responsibility to determine
    


    jonasnick commented at 7:58 am on September 25, 2025:
    0 *  scanning with this function, it is the scanner's responsibility to determine
    

    nit

  226. in include/secp256k1_silentpayments.h:353 in ac4a726a2a outdated
    341+ *
    342+ *  Args:                   ctx: pointer to a context object
    343+ *  Out:          found_outputs: pointer to an array of pointers to found
    344+ *                               output objects. The found outputs array MUST
    345+ *                               be initialized to be the same length as the
    346+ *                               tx_outputs array
    


    jonasnick commented at 8:11 am on September 25, 2025:
    0 *  Out:          found_outputs: pointer to an array of pointers to found
    1 *                               output objects. The found outputs array MUST
    2 *                               have the same length as the tx_outputs array.                       
    

    “initialized” seems like a strange term for an array. Also, afaik we don’t have to initialize the individual elements of the array, just provide the memory.

  227. in include/secp256k1_silentpayments.h:30 in ac4a726a2a outdated
    25+ *  any wallet software already using libsecp256k1, this API should provide all
    26+ *  the functions needed for a Silent Payments implementation without requiring
    27+ *  any further elliptic-curve operations from the wallet.
    28+ */
    29+
    30+static const unsigned char secp256k1_silentpayments_prevouts_summary_magic[4] = { 0xa7, 0x1c, 0xd3, 0x5e };
    


    nymius commented at 10:46 am on September 25, 2025:
    secp256k1_silentpayments_prevouts_summary_magic isn’t mentioned in any documentation string, nor relevant for the API users. Shouldn’t be placed in the implementation file?

    josibake commented at 12:10 pm on September 26, 2025:
    Yes!
  228. in include/secp256k1_silentpayments.h:103 in ac4a726a2a outdated
     96+ *                            field). This ensures the caller is able to match
     97+ *                            the generated outputs to the correct silent
     98+ *                            payment addresses. The same recipient can be
     99+ *                            passed multiple times to create multiple outputs
    100+ *                            for the same recipient.
    101+ *              n_recipients: the number of recipients. This is equal to the
    


    nymius commented at 10:48 am on September 25, 2025:

    Reading the secp256k1_silentpayments_sender_create_outputs function docs, I have to re-read multiple times the n_recipients doc, because it says the number of recipients, I quote:

    “…is equal to the total number of outputs to be generated as each recipient may passed multiple times to generate multiple outputs for the same recipient.”

    This make me question if the docs where mentioning this again because there was a difference between the number of recipients and the recipients length. Like: if the n_recipients is equal to the total number of outputs, and docs are making this statement, then there is another statement that doesn’t hold for the n_recipients. The most probable other statement for me was: n_recipients is not equal to recipients’s length. Convoluted, but any “maybe though” may be enough.

    Then I had to re-read again. What I mean here is the redundancy may cause issues here instead of clarify the meaning, and maybe, n_recipients: the number of recipients. is enough.


    josibake commented at 9:34 am on September 26, 2025:

    Good point, I think explaining twice here does indeed cause some confusion. It should also be fairly obvious to the caller that n_recipients is meant to represent the size of the recipients array (where duplicates are allowed).

    EDIT: perhaps the size of the recipients array? This feels less ambiguous to me.


    josibake commented at 2:02 pm on October 7, 2025:
    Updated this and other places to use size of the array.
  229. in include/secp256k1_silentpayments.h:105 in ac4a726a2a outdated
     98+ *                            payment addresses. The same recipient can be
     99+ *                            passed multiple times to create multiple outputs
    100+ *                            for the same recipient.
    101+ *              n_recipients: the number of recipients. This is equal to the
    102+ *                            total number of outputs to be generated as each
    103+ *                            recipient may passed multiple times to generate
    


    nymius commented at 10:49 am on September 25, 2025:

    nit:

    0 *                            recipient may be passed multiple times to generate
    

    josibake commented at 12:10 pm on September 26, 2025:
    Following your earlier suggestion, I removed the redundant explanation in favour of “the size of the recipients array.”
  230. in examples/silentpayments.c:380 in ac4a726a2a outdated
    375+                );
    376+                assert(ret);
    377+                if (secp256k1_ec_pubkey_cmp(ctx, &labeled_spend_pubkey, &address_labeled_spend_pubkey) != 0) {
    378+                    printf("Something went wrong, the labeled spend public key does not match Bob's address.\n");
    379+                    return EXIT_FAILURE;
    380+                };
    


    jonasnick commented at 12:03 pm on September 25, 2025:
    Is it clear enough that we’re only doing this here because it is easier to hardcode the addresses and that this is supposed to show how the addresses were generated in the first place and that no one would implement this in practice?

    josibake commented at 12:37 pm on September 26, 2025:
    This is a good question. I don’t think its super clear, and generally speaking, it seems the example should mirror “real world” expected usage. Looking at this with fresh eyes, I think it might be better to simplify and just demonstrate how to create the labeled address (without any sanity checks against a hard coded value).

    jonasnick commented at 2:19 pm on September 30, 2025:
    Yes, I think a dedicated “key gen” step at the start of the example would help. IIRC you mentioned before that you tried this, but it introduced too much complexity.

    josibake commented at 9:50 am on October 2, 2025:
    Yes, but I think the feeling was less that it was too much complexity and more that the complexity didn’t seem justified. But given the recent feedback, I do think the example full demonstrating usage of the API does justify the complexity.

    josibake commented at 2:06 pm on October 7, 2025:
    Ive updated the example to demonstrate Bob creating his labeled address. Overall, I am much happier with how the example flows with the latest round of changes.
  231. in include/secp256k1_silentpayments.h:149 in ac4a726a2a outdated
    140+ *  Args:                ctx: pointer to a context object
    141+ *                            (not secp256k1_context_static)
    142+ *  Out:               label: pointer to the resulting label public key
    143+ *             label_tweak32: pointer to the 32 byte label tweak
    144+ *  In:           scan_key32: pointer to the recipient's 32 byte scan key
    145+ *                         m: label integer (0 is used for change outputs)
    


    nymius commented at 12:29 pm on September 25, 2025:

    In secp256k1_silentpayments_recipient_create_label docs, m is said to be a label integer. I know the intention is to not induce the user to think that arbitrary strings can be used as labels.

    So I guess label integer is better than integer label (a label that happens to be an integer). But at the same time, the API uses label to refer to the public key produced by tweaking the spend public key with the label_tweak32. Maybe m: the integer from which the label is derived or something along this line is better.


    josibake commented at 9:39 am on September 26, 2025:
    Perhaps m: integer representing the m-th label ?

    nymius commented at 9:19 am on September 27, 2025:
    I wouldn’t use representing here, because it’s hiding the fact that the integer is being used to produce the label.

    josibake commented at 2:02 pm on October 7, 2025:
    Fixed.
  232. josibake force-pushed on Sep 25, 2025
  233. in include/secp256k1_silentpayments.h:266 in c4942d3646 outdated
    263+ *  Out:        output33: pointer to a 33-byte array to place the serialized
    264+ *                        `silentpayments_prevouts_summary` in
    265+ *  In: prevouts_summary: pointer to an initialized silentpayments_prevouts_summary
    266+ *                        object
    267+ */
    268+SECP256K1_API int secp256k1_silentpayments_recipient_prevouts_summary_serialize(
    


    nymius commented at 12:35 pm on September 25, 2025:

    secp256k1_silentpayments_recipient_prevouts_summary_serialize performs evaluation and serialization atomically. Now it only returns a 33 bytes compressed prevout summary. This is useful to retrieve the prevout summary from tweak servers to light clients in a bandwidth conscious. However, it is also useful to have the 64 uncompressed serialization for large clients doing scanning in behalf of users, as the computation of the square root is costly and performance is key for these clients, which will be probably serving multiple users at the same time.

    A flag parameter which takes SECP256K1_EC_COMPRESSED or SECP256K1_EC_UNCOMPRESSED would be useful here.


    josibake commented at 9:52 am on September 26, 2025:
    Great point. Without this option, a callers only other option would be to store the full 101 byte object, or try to copy out only the uncompressed public key bytes by accessing the internals. Both of these are sub-optimal so I think adding the flag makes sense.

    josibake commented at 2:04 pm on October 7, 2025:
    Added an argument for serializing/parsing both 33-byte and 65-byte representations.
  234. in include/secp256k1_silentpayments.h:439 in c4942d3646
    434+ *                               (labeled or unlabeled)
    435+ *                            k: output counter (initially set to 0, must be
    436+ *                               incremented for each additional output created
    437+ *                               or after each output found when scanning)
    438+ */
    439+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_output_pubkey(
    


    nymius commented at 12:39 pm on September 25, 2025:

    secp256k1_silentpayments_recipient_create_output_pubkey has a k parameter. I’m questioning if this parameter is useful at all. The scenarios that come to my mind where it may be considered are:

    • large servers: the server has the tweaks, the script pubkeys and the scanning secret key together with the spending public key and uses this function to derive all script pubkeys for a transaction and find matches against the script pubkey database. This would be like using the inefficient lookup every time. The server would have to derive ahead of time all possible script pubkeys for the bare spend key, plus any additional label, times k ranging from 0 to the length of the vout. Additionally, as the function doesn’t produce tweaks, it would have to also use secp256k1_silentpayments_recipient_scan_outputs on the transaction to get the tweaks, as without them, outputs cannot be spent. Fixing k to 0 would make an improvement here, because the only match should be against the bare keys or the labeled ones, and all possible remaining ones would be scanned with secp256k1_silentpayments_recipient_scan_outputs, using the lookup table. In order to do this the full tx should be obtained too for each match. That reduces the multiplicative factor of k.

    • light clients: for these kind of clients, local label scanning is out of discussion because the data burden is very high. The envisioned use for these clients is more of the style of compact block filter scanning (not necessarily using Golomb filters). The client would be connected to tweak provider, and use this function in combination with the tweaks to create an oracle with a binary output answering if a blockchain chunk contains any transaction relevant for the client. If the answer is yes, then it will request the full blockchain chunk and extract the relevant outputs together with the tweaks using secp256k1_silentpayments_recipient_scan_outputs. Here, again, the k wouldn’t change from 0 as the output of the oracle is binary, and the full transaction has to be scanned anyways to get the tweaks. One may also argue that at least the change output script should be generated too, but that’s not the case because change outputs are always generated from transactions spending outputs already known by the client, so it’s more efficient to look up for spent outputs rather than created outputs in this case. This holds even in the case of wallet recovery, because this is performed in order, and the creation of outputs always precedes its destruction, so not a problem.

    If k is hidden from the API, and made an internal parameter, the underlying flow above will still be working and without incurring in any performance downgrade to the previous state.

    At the same time, k is only used to get $t_k$ which is the tweak added to the spending public key to get the final script pubkey, in combination with the shared secret obtained presumably with secp256k1_silentpayments_recipient_create_shared_secret (already recommended for light clients for this use case in the docs). If this is the only use case for secp256k1_silentpayments_recipient_create_shared_secret, the removal of the k parameter would allow the merge of both functions into one, just taking spend public key, scan secret key, and the prevout_summary, and returning the x-only public key of the first possible derived silent payment output from that prevout summary, which together with the tweak server, would form the oracle for the client.


    josibake commented at 12:20 pm on September 26, 2025:

    Additionally, as the function doesn’t produce tweaks, it would have to also use secp256k1_silentpayments_recipient_scan_outputs

    Great catch. As implemented, the _create_output_pubkey function is useless on its own for a light client. We could keep k and add an out param for the tweak, but this feels like the wrong direction. I much prefer what you described where we can instead remove k, and potentially even get rid of the recipient_created_shared_secret function.

    One may also argue that at least the change output script should be generated too, but that’s not the case because change outputs are always generated from transactions spending outputs already known by the client, so it’s more efficient to look up for spent outputs rather than created outputs in this case. This holds even in the case of wallet recovery, because this is performed in order, and the creation of outputs always precedes its destruction, so not a problem.

    I don’t think we can assume the client will always recover by processing blocks in order. Also, imagine a client that wants to quickly recover its unspent balance and doesn’t care about the transaction history, e.g., recovering from only the UTXO set. In this case, I think we do need a way for a light client to easily check for both the unlabeled spend public key and the change spend public key. We could do this by:

    • s/create_output_pubkey/create_output_pubkeys/, where now the caller passes in a list of spend public keys and gets in return a k=0 output for each spend public key
    • Remove the _recipient_created_shared_secret function

    This would allow a light client to check for at a minimum the k=0 output for both the spend public key and change public key. If a match is found, the caller would then request the transaction (or the block containing the transaction), and then run the _scan_outputs function on the transaction. This would result in a simpler and less footgunny API, while still providing enough flexibility in that we are not assuming all light clients would behave the same way.


    josibake commented at 2:03 pm on October 7, 2025:
    Implemented this based on your feedback; much happier with the API!
  235. nymius commented at 12:39 pm on September 25, 2025: none

    I’m focusing on the integration on silent payments with BDK. Have been working the last couple of months rolling my own crypto to implement things on top of it. I’ve shaped off many of the changes required by BDK to work with BIP 352. Now I’m looking to start reimplementing this changes on top of more solid bases, that’ s what I’ve started looking at this PR, interested in the bindings for rust-secp256k1, and probably rewiring the project I’ve been working on to that library.

    Looking at the headers file (include/secp256k1_silentpayments.h), and after discussing with Josie, we come up with the following points:

  236. nymius commented at 12:46 pm on September 25, 2025: none

    Hope to have documented the the thought chain well. Here is a TLDR of the proposed changes:

    • move secp256k1_silentpayments_prevouts_summary_magic to the implementation file, src/modules/silentpayments/main_impl.h.
    • n_recipients docs in secp256k1_silentpayments_sender_create_outputs may be improved.
    • m parameter description in secp256k1_silentpayments_recipient_create_label may have a better description.
    • adding a new flag parameter secp256k1_silentpayments_recipient_prevouts_summary_serialize to allow compressed and uncompressed serialization.
    • refactor secp256k1_silentpayments_recipient_create_output_pubkey to only take ctx, spend_pubkey, scan_key and prevout_summary to not expose k parameter.
    • remove secp256k1_silentpayments_recipient_create_shared_secret function if the above point is done.
  237. in src/modules/silentpayments/main_impl.h:732 in c4942d3646
    697+                if (!secp256k1_ec_seckey_tweak_add(ctx, found_outputs[n_found]->tweak, label_tweak)) {
    698+                    secp256k1_scalar_clear(&scan_key_scalar);
    699+                    secp256k1_scalar_clear(&output_tweak_scalar);
    700+                    secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
    701+                    return 0;
    702+                }
    


    jonasnick commented at 12:47 pm on September 25, 2025:
    Is this branch necessary? secp256k1_ec_seckey_tweak_add fails if the resulting tweak is 0. But why is that a problem? When we try to tweak the spend secret key with secp256k1_ec_seckey_tweak_add(ctx, full_seckey, found_outputs[i].tweak), it’s fine if the tweak is 0.

    josibake commented at 1:06 pm on September 26, 2025:

    Hm, this is an unfortunate edge case. The sender cannot tell whether or not the spend public key has been tweaked with a label or not. If the tweak ends up being the negation of the label tweak, e.g., $P = B_{spend} + label_m \cdot G+ (-label_m \cdot G)$, then the resulting output is just the spend public key. Since the sender is unaware that this is just the spend public key for the recipient, I don’t think we can do any checks to prevent this.

    If the recipient has published their unlabeled silent payments address somewhere, an outside observer would be able to link this particular UTXO sent to the labeled address back to the recipient’s unlabeled address.

    Given that the UTXO can already be linked back to the recipient address even if the recipient hasn’t scanned for or spent the UTXO, seems better to not fail here? Failing isn’t protecting the recipients privacy in any way, and instead just makes the UTXO unfindable/unspendable.


    jonasnick commented at 2:20 pm on September 30, 2025:
    If I’m not mistaken, this failure condition only happens when the sender/receiver found a second preimage of the label tweak. This should never happen if the sender follows the protocol correctly. Were you trying to protect against incorrect API usage or against this negligible event happening? Protecting against privacy leaks when the protocol is not followed correctly seems difficult because there are potentially many failure scenarios. And I don’t think we need to consider the possibility of this negligible event occuring to be a downgrade of privacy (especially because the sender is considered to be honest for privacy as well).

    josibake commented at 3:07 pm on October 1, 2025:
    Correct! Since the label tweak is produced with a salt of the recipients scan key, the sender can only hit this if they 1) know the recipients scan key or 2) have found a second preimage. I was more thinking of how to protect against a privacy leak, even if it happens with a negligible probability. I agree that we shouldn’t consider a negligible event a downgrade in privacy, rather, if there is a way we can guard against even a negligible event that seems preferable (if possible).

    josibake commented at 2:06 pm on October 7, 2025:
    Added a test case for this.
  238. in examples/silentpayments.c:159 in c4942d3646
    154+        /*** Generate private keys for the sender ***
    155+         *
    156+         * In this example, only taproot inputs are used but the function can be
    157+         * called with a mix of taproot seckeys and plain seckeys. Taproot
    158+         * seckeys are passed as keypairs to allow the sending function to check
    159+         * if the private keys need to be negated without needing to do an
    


    jonasnick commented at 12:47 pm on September 25, 2025:
    private -> secret
  239. in include/secp256k1_silentpayments.h:79 in c4942d3646
    74+ *
    75+ *  Returns: 1 if creation of outputs was successful.
    76+ *           0 if the input private keys sum to zero, if the input private key
    77+ *             sum is the negation of the spend secret key, if the input_hash
    78+ *             or hash(shared_secret || k) are invalid scalars,
    79+ *             or if the arguments are invalid.
    


    jonasnick commented at 1:26 pm on September 25, 2025:

    I had another look at error handling in the example code and got the impression that we can improve the docs with respect to error handling for a few functions in the module.

    As for create_outputs, right now, if it fails we print the string “try with different inputs”. But I don’t think we would want a wallet dev to actually implement this because it seems overly complicated as it should never happen (and if it does, then “most likely” the recipient keys were chosen maliciously. So how about this:

    0 *  Returns: 1 if creation of outputs was successful.
    1 *           0 on failure. This is expected only with an adversarially chosen
    2 *           recipient spend key. Specifically, failure occurs when:
    3 *             - Input secret keys sum to 0 or the negation of a spend key
    4 *               (negligible probability if at least one of the input secret
    5 *               keys is uniformly random and independent of all other keys)
    6 *             - A hash output is not a valid scalar (negligible probability
    7 *               per hash evaluation)
    

    Yes, this reintroduces the notion of “negligible probability”/“improbable”, but after looking at the examples again, this might be important information for users of the library in order to do error handling appropriately.


    josibake commented at 3:09 pm on October 1, 2025:
    I think the docs get better with each iteration 😄 I prefer your latest suggestions because they avoid leaking internal implementation details, while also being educational to the user as to how likely it is they would ever hit some of the handled error cases.
  240. in examples/silentpayments.c:240 in c4942d3646
    235+            smallest_outpoint,
    236+            sender_keypair_ptrs, N_INPUTS,
    237+            NULL, 0
    238+        );
    239+        if (!ret) {
    240+            printf("Something went wrong, try again with different inputs.\n");
    


    jonasnick commented at 1:27 pm on September 25, 2025:
    0            printf("Something went wrong, a recipient provided an invalid address.\n");
    
  241. in include/secp256k1_silentpayments.h:141 in c4942d3646
    136+ *
    137+ *      label_tweak = hash(scan_key || m)
    138+ *            label = label_tweak * G
    139+ *
    140+ *  Returns: 1 if label tweak and label creation was successful.
    141+ *           0 if label_tweak32 is an invalid scalar,
    


    jonasnick commented at 1:28 pm on September 25, 2025:
    0 *           0 if hash output label_tweak32 is not valid scalar (negligible
    1 *             probability per hash evaluation)
    

    would be more consistent with my other suggested changes regarding docs and implies simpler error handling (because this should never happen).

  242. in examples/silentpayments.c:345 in c4942d3646
    340+                    labels_cache.entries[0].label_tweak,
    341+                    bob_scan_key,
    342+                    m
    343+                );
    344+                if (!ret) {
    345+                    printf("Something went wrong, the resulting label_tweak is not a valid scalar. Try again with m++.\n");
    


    jonasnick commented at 1:29 pm on September 25, 2025:
    0                    printf("Something went wrong, event with negligible probability happened.\n");
    
  243. in include/secp256k1_silentpayments.h:171 in c4942d3646
    166+ *  The result is used by the recipient to create a Silent Payment address,
    167+ *  consisting of the serialized and concatenated scan public key and
    168+ *  (labeled) spend public key.
    169+ *
    170+ *  Returns: 1 if labeled spend public key creation was successful.
    171+ *           0 if the spend pubkey + label sum to zero,
    


    jonasnick commented at 1:29 pm on September 25, 2025:
    0 *           0 if spend pubkey and label sum to zero (negligible probability for
    1 *             labels created according to BIP352).
    

    I think, again, knowing that this has negligible probability, it leads to simpler error handling.

  244. in examples/silentpayments.c:366 in c4942d3646
    361+                    &labeled_spend_pubkey,
    362+                    &spend_pubkey,
    363+                    &label
    364+                );
    365+                if (!ret) {
    366+                    printf("Something went wrong, the choice of label and spend public key is invalid.\n");
    


    jonasnick commented at 1:31 pm on September 25, 2025:
    0                    printf("Something went wrong, event with negligible probability happened.\n");
    
  245. in include/secp256k1_silentpayments.h:229 in c4942d3646
    224+ *  `silentpayments_recipient_prevouts_summary_serialize`.
    225+ *
    226+ *  Returns: 1 if prevout summary creation was successful.
    227+ *           0 if the input public keys sum to zero,
    228+ *             if the input_hash is an invalid scalar,
    229+ *             or the arguments are invalid.
    


    jonasnick commented at 1:32 pm on September 25, 2025:

    For a legitimate silent payments transaction this should never be the case. So why not just say

    0 *           0 if the transaction is not a silent payments transaction.
    
  246. in examples/silentpayments.c:357 in c4942d3646
    401+                /* We need to always check that the prevouts data object is valid
    402+                 * before proceeding, since a malicious actor could create a transaction
    403+                 * such that the input public keys sum to the point at infinity, which
    404+                 * could cause our node to crash if, e.g., we assume that prevouts_summary_create
    405+                 * will always succeed."
    406+                 */
    


    jonasnick commented at 1:34 pm on September 25, 2025:
    0                /* We need to always check that the prevouts data object is valid
    1                 * before proceeding.
    2                 */
    
  247. in include/secp256k1_silentpayments.h:347 in c4942d3646
    342+ *  can be used.
    343+ *
    344+ *  Returns: 1 if output scanning was successful.
    345+ *           0 if hash(shared_secret || k) is an invalid scalar or the negation of
    346+ *             either the spend secret key or the label tweak, or if the arguments
    347+ *             are invalid.
    


    jonasnick commented at 1:38 pm on September 25, 2025:

    If we can remove the tweak = 0 failure branch as I suggested in another comment, then it seems like this function can never fail for a legitimately created silent payments transaction.

    0 *           0 if the transaction is not a silent payments transaction or if the recipient's scan key is invalid.
    

    josibake commented at 10:02 am on October 2, 2025:

    Do you feel its important to mention the invalid scan key here? Alternatively, we could check the validity of the scan key with an ARG_CHECK and have this say “if the transaction is not a silent payments transaction.”

    EDIT: checking the scan_key with an arg check means we wont be able to clear the _scalar variable in the event of overflow, but I’d argue this is okay since this is an invalid scan key and can’t be used by the recipient anyways. Curious what others think, though.


    jonasnick commented at 1:02 pm on October 2, 2025:

    I think it would make sense to mention what makes a scan_key valid, perhaps similar to the musig docs:

    The scan key is valid if it passes secp256k1_ec_seckey_verify.

    In that case, I don’t think it’s too important to mention the invalid scan key as a reason for the function returning 0.

    ARG_CHECK could be overkill because it’d crash the program on an invalid value, which is categorically different from (all?) other ARG_CHECKs in the library. On the other hand, one could argue that would be invalid API usage as well, because the key is not uniformly distributed.


    josibake commented at 1:53 pm on October 2, 2025:
    I convinced myself ARG_CHECK’ing is a bad idea trying to implement it, and you also raise a good point re: crashing the program with a runtime argument.

    josibake commented at 2:07 pm on October 7, 2025:
    Updated to include a check for valid_scan_key in the create outputs and scan outputs functions.
  248. in examples/silentpayments.c:432 in c4942d3646
    427+            if (!ret) {
    428+                /* Since we've already validated the prevouts data, this shouldn't fail, but
    429+                 * better to be careful here since we are scanning data that could have been
    430+                 * maliciously created.
    431+                 */
    432+                printf("Something went wrong while scanning this transaction, skipping.\n");
    


    jonasnick commented at 1:40 pm on September 25, 2025:

    I think it’d be simpler to follow if we removed the comment (but maybe I’m missing something).

    0                printf("This transaction is not valid for silent payments, skipping.\n");
    
  249. in include/secp256k1_silentpayments.h:427 in c4942d3646
    422+ *  if the generated output exists in a block before proceeding to the next
    423+ *  value of `k`.
    424+ *
    425+ *  Returns: 1 if output creation was successful.
    426+ *           0 if hash(shared secret || k) results in an invalid scalar,
    427+ *             or if the arguments are invalid.
    


    jonasnick commented at 1:43 pm on September 25, 2025:

    I think this can never happen for a legitimate silent payments transaction, so maybe we can just say

    0 *           0 if the transaction is not a silent payments transaction.
    
  250. in examples/silentpayments.c:545 in c4942d3646
    540+                &prevouts_summary
    541+            );
    542+            /* Since we've already validated the prevouts data, the only reason this could fail
    543+             * is if we input a bad scan key or bad spend public key, which should never happen
    544+             * because this is data under our control.
    545+             */
    


    jonasnick commented at 1:48 pm on September 25, 2025:

    The create_shared_secret doc doesn’t mention the bad spend public key, so wouldn’t it be sufficient to say something like

    0            /* This only fails if the scan key is invalid. The recipient, Carol,
    1             * created the scan key herself, so this must always succeed. */
    
  251. in examples/silentpayments.c:565 in c4942d3646
    560+                    /* We can only fail here if the particular set of inputs results in the
    561+                     * output of a hash function resulting in an invalid scalar. This is
    562+                     * statistically improbable for an honest execution, but if we were to
    563+                     * get to this point that means this is not a silent payments transaction
    564+                     * (or if it is, the sender was not following the protocol).
    565+                     */
    


    jonasnick commented at 1:51 pm on September 25, 2025:
    Isn’t the output of the hash function resulting in an invalid scalar impossible (instead of improbable) for an honest execution? If so (unless I’m missing something), it seems like we can remove this comment. It seems sufficient for the reader to know that “This transaction is not valid for silent payments”, which is what we print below.

    josibake commented at 1:31 pm on September 26, 2025:

    Isn’t the output of the hash function resulting in an invalid scalar impossible

    Its not impossible for the sender to hit this case when creating the output, but indeed, since we specify that the sender should fail if this happens it would be impossible for the recipient to hit this case.

  252. in src/modules/silentpayments/main_impl.h:101 in c4942d3646 outdated
    91+    size_t len;
    92+    int ret;
    93+
    94+    secp256k1_ecmult_const(&ss_j, public_component, secret_component);
    95+    secp256k1_ge_set_gej(&ss, &ss_j);
    96+    secp256k1_declassify(ctx, &ss, sizeof(ss));
    


    jonasnick commented at 7:45 am on September 26, 2025:
    Why do we declassify the shared secret? Isn’t this secret data? If we do declassify the secret, I think a comment is needed here to justify this.

    josibake commented at 1:38 pm on September 26, 2025:
    IIRC, we declassify because serializing a group element is a non-constant time operation. So I think it’s appropriate to declassify here (with a comment).

    jonasnick commented at 2:20 pm on September 30, 2025:

    secp256k1_eckey_pubkey_serialize is not constant time, but it’s possible to serialize in constant time:

    0    secp256k1_fe_normalize(&ss.x);
    1    secp256k1_fe_normalize(&ss.y);
    2    secp256k1_fe_get_b32(&shared_secret33[1], &ss.x);
    3    shared_secret33[0] = 2 | secp256k1_fe_is_odd(&ss.y);
    

    This has some cost: secp256k1_fe_normalize instead of secp256k1_fe_normalize_var and we have to replace ecmult in pubkey_tweak_add with ecmult_const. Given that the library is not constant time with respect to the tweaks used for BIP 32 derivation, not being constant time with respect to the shared secret would be consistent. If we’re not protecting privacy against side channels, we could also not be constant time with respect to the scan key, but I don’t see how this would result in a speedup, so it’s probably worthwhile to keep that. But ideally we would document these choices.


    real-or-random commented at 10:17 am on October 1, 2025:

    Your code suggestion is similar to what the ECDH module does: https://github.com/bitcoin-core/secp256k1/blob/baa265429fa8f1686138380e52a75c25b0344719/src/modules/ecdh/main_impl.h#L57-L63

    Of course, we could call the ECDH module here and pass a custom hash function. This sounds clean (as clean as your suggestion), but as you say that wouldn’t give us much because the tweak functions are still variable time. The tweak functions are generic, and only the caller knows whether the data is secret or not. :/ I’d like the idea of making the tweak functions constant-time for that reason.

    and we have to replace ecmult in pubkey_tweak_add with ecmult_const.

    In this case, we could replace it with the constant-time ecmult_gen instead. And it turns out that our current ecmult_gen is faster than ecmult for generator multiplications. (cc @sipa who pointed that out recently). This could be done in a separate PR.

    The only drawback is that this doesn’t work in pubkey_tweak_mul. Here we’d need to use ecmult_const which is indeed slower. (We could still offer a pubkey_tweak_mul_var).

    I think the root of the problem is that we use the same type for public group elements and private group elements. Most group elements are public (pubkeys), but some are not (ECDH shared secret). The same is true for scalars. Some are secret, some are not. We could have different types for them, and this would make things clearer. Of course, this will be a huge change.


    That the handling of secret data is a bit inconsistent had also occurred to me previously (only later after my last review round) but I forgot to point it out. We now have some code comments like this:

    0/* Leaking this value would break indistinguishability of the transaction, so clear it. */
    1secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
    

    (see https://github.com/bitcoin-core/secp256k1/pull/1698/files/c4942d3646a58bb9fd8a51e60defe1571f8ee595#diff-4d053be8d1f6d948b412f26ae89711a9dcd2a2683da581cb52b9f6757480361bR298-R299)

    So we clear stack values in the case of the shared secret and other data secret with respect to unlinkability. This is fine because it’s cheap to do, but I agree we should document these choices.

    I think what we should do is roughly this:

    • We always clear unlinkability secrets from the stack because that’s cheap.
    • We use constant-time operations when the scan key is involved or data derived from it (or equivalently, secret keys on the sender side), but we declassify the final derived pubkeys, i.e., pubkeys to send coins to (as derived by the sender) and candidate pubkeys to receive on (as derived by the scanner).

    The last thing sounds like reasonable middle ground to me. We don’t want full protection here. ~Making scanning constant-time means you need to read the entire blockchain before you can return. :) ~ edit: That’s wrong, but here’s a better rationale that it’s okay to declassify the derived pubkeys: On the side of the sender, the sender can (and probably will) reveal them on the blockchain, so this is public data. On the side of the scanner, the code wants to branch when a payment is found: we can’t meaningfully hold up constant-timeness and hide how many payments have been found etc.


    jonasnick commented at 12:24 pm on October 2, 2025:

    That’s wrong, but here’s a better rationale that it’s okay to declassify the derived pubkeys

    That’s an interesting observation. But, wouldn’t this also justify declassifying the shared secret (and keep the current implementation)?


    josibake commented at 2:14 pm on October 7, 2025:

    we could also not be constant time with respect to the scan key, but I don’t see how this would result in a speedup, so it’s probably worthwhile to keep that

    IIRC, using non-constant time multiplication with the scan key resulted in a ~ 9% - 15% speedup. Overall, I agree we should have a consistent strategy and document our choices. Curious to hear @real-or-random ’s response and then happy to implement whatever makes the most sense here.


    real-or-random commented at 10:31 am on October 13, 2025:

    @jonasnick and I discussed this. Our conclusion is that we should also serialize the shared secret in constant time and only declassify hash(shared_secret || k). Leaking raw (=unhashed) ECDH shared secrets can, in theory, make Cheon’s attack on ECDH possible. Not sure if it’s really a concern here, but serializing in constant time is essentially for free, so let’s just do it.

    So we think that the general rules for secrets whose leakage would break indistinguishability should be as follows. Perhaps these can be added as a file-level comment.

    • We always clear them from the stack because that’s cheap.
    • We use constant-time operations when ECDH (input) secret keys are involved, because leaking the sender secret key is obviously bad, and leaking the entire scan key is also bad. But we declassify the hashed ECDH shared secret. As opposed to the scan key, this value allows the attacker to recognize a single incoming payment but not all incoming payments.

    The declassify call should then get a code comment along these lines: The only thing that the attacker can do with the hashed secret is derive the final pubkeys (TODO Is there a term that is more precise than “final pubkeys”). On the side of the sender, we assume that will reveal those on the blockchain anyway. On the side of the scanner, we assume that the caller wants to branch when a payment is found.

  253. in src/modules/silentpayments/main_impl.h:714 in c4942d3646 outdated
    683+                    break;
    684+                }
    685+            }
    686+        }
    687+        if (found) {
    688+            found_outputs[n_found]->output = *tx_outputs[found_idx];
    


    jonasnick commented at 8:51 am on September 26, 2025:

    This assumes assumes that there are no two iterations of the while loop that will find the same tx_output. Otherwise, this would result in a buffer overflow. I can see three scenarios where that assumption is broken

    1. There’s a collision in create_output_tweak that result in the same tweaks for different values of k. That is very unlikely but I think we want to protect against BOs even in that case.
    2. The user-provided label_lookup function is broken. For example, if label_lookup always returns the label_tweak 0 for any input, then every while loop iteration will “find” the first tx_output, leading to a buffer overflow (and an infinite loop). The same result happens with a label_lookup function where the adversary has the ability to insert their own chosen labels. I don’t think the scanning function should trust the user’s lookup function that much.
    3. A transaction with UINT32_MAX + 1 matching outputs is given. In every iteration k will be incremented, and provided to create_output_tweak which converts it to a uint32_t. When k = UINT32_MAX + 1, the same output_tweak will be computed as in a previous iteration, resulting in a duplicate match.

    theStack commented at 11:18 am on September 26, 2025:
    Thinking out loud: to avoid both the potential buffer overflow and infinite loop without introducing new branches, would it be an option to simply limit the (outer) while loop to a maximum of n_tx_outputs iterations? That assumes that a transaction with n outputs never contains Silent Payments outputs that were created with values k >= n, which I think holds, if the sender includes all generated outputs in the transaction (as suggested in the sender API docs).

    josibake commented at 2:55 pm on September 26, 2025:
    Great catch! I will need to think a bit longer on how to handle these cases elegantly, but one thought for 3) is we could add an ARG_CHECK to make sure this function is not called with n_tx_outputs > UINT32_MAX. This is a reasonable check for Bitcoin (given todays constraints), but the downside is these constraints could change in the future and it assumes this library is only used on Bitcoin, and not another chain or a side chain with different constraints. Thoughts?

    jonasnick commented at 2:18 pm on September 30, 2025:

    would it be an option to simply limit the (outer) while loop to a maximum of n_tx_outputs iterations

    That seems to address the first two conditions for the bufer overflow.

    an ARG_CHECK to make sure this function is not called with n_tx_outputs > UINT32_MAX

    Possible, but since this is a deviation from the spec (strictly speaking), seems a bit necessary. Maybe we can just return 0 if k reaches UINT32_MAX in create_outputs and scan?


    josibake commented at 11:23 am on October 7, 2025:

    @theStack great suggestion, I replaced the while loop with a for loop that only goes up to the total number of transaction outputs. I also took @jonasnick ’s suggestion of returning 0 if k is equal to the max value.

    One remaining question I had is whether or not its worth mentioning the k failure case in the API docs. Technically, its impossible to hit that limit on Bitcoin today, and mentioning it in the API docs would reveal some internal implementation details of the function.


    josibake commented at 2:14 pm on October 7, 2025:
    I opted to leave this out of the API docs, for now.
  254. in include/secp256k1_silentpayments.h:284 in c4942d3646 outdated
    279+ *  Args:              ctx: pointer to a context object.
    280+ *  Out:  prevouts_summary: pointer to a silentpayments_prevouts_summary object. If 1 is
    281+ *                          returned, it is set to a parsed version of input33.
    282+ *  In:            input33: pointer to a serialized silentpayments_prevouts_summary.
    283+ */
    284+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_prevouts_summary_parse(
    


    nymius commented at 10:49 am on September 30, 2025:

    I’ve started building some bindings on top of PrevoutSummary, and I’ve got a panic because I was trying to assert a prevout summary was equal to its serialized and parsed (again) version. I’ve just assumed $\text{serialize} = \text{parse}^{-1}$. Dumb assumption (parse would be finding the inverse of a EC multiplication), but natural in other contexts. This doc in secp256k1_silentpayments_recipient_prevouts_summary_serialize may be enough:

    Serializing a prevouts_summary object created with _recipent_prevouts_summary_create will result in an EC multiplication. This allows for a more compact serialization.

    but also commenting here and in secp256k1_silentpayments_recipient_prevouts_summary_serialize the assumption I made does not hold wouldn’t hurt and it wouldn’t go further than mentioning the EC multiplication above in terms of internals leakage.


    josibake commented at 2:16 pm on October 7, 2025:
    Great callout, this was also pointed out by @real-or-random in an earlier review. I’ve added a comment to (hopefully) explain to the caller that they cannot expect a perfect roundtrip between serialization and parsing.
  255. in src/modules/silentpayments/main_impl.h:310 in c4942d3646
    305+        if (!secp256k1_silentpayments_create_output_pubkey(ctx, generated_outputs[recipients[i]->index], shared_secret, &recipients[i]->spend_pubkey, k)) {
    306+            secp256k1_scalar_clear(&seckey_sum_scalar);
    307+            secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
    308+            return 0;
    309+        }
    310+        VERIFY_CHECK(k < SIZE_MAX);
    


    jonasnick commented at 2:07 pm on September 30, 2025:
    Does this VERIFY_CHECK have an effect? k appears to be <= n_recipients which is a size_t.

    josibake commented at 2:17 pm on October 7, 2025:
    Removed. It likely wasnt needed before but especially not now, considering we return 0 if k exceeds UINT32_MAX.
  256. in src/modules/silentpayments/main_impl.h:609 in c4942d3646
    604+         *
    605+         * Note: _create_output_tweak can only fail if the output of the hash function is greater than or equal to the curve order, which is statistically improbable.
    606+         * Returning an error here results in an untestable branch in the code, but we do this anyways to ensure strict compliance with BIP0352.
    607+         */
    608+        if (!secp256k1_silentpayments_create_output_tweak(&output_tweak_scalar, shared_secret, k)) {
    609+            secp256k1_scalar_clear(&scan_key_scalar);
    


    jonasnick commented at 2:11 pm on September 30, 2025:
    nit: not sure if you’ve considered this, but you could also clear scan_key_scalar right after it is used for the last time. Then we’d have fewer lines for clearing and there wouldn’t be the possibility of forgetting to clear when the function fails. There are other secret values in this file that could follow the same pattern.

    josibake commented at 10:52 am on October 2, 2025:

    Good catch! I fixed this for the _scan_outputs function, but didnt see any opportunities to DRY this up in the _create_outputs function, which is the other function that deals with secret keys.

    There is an opportunity to clear the _scan_key_scalar early in the _recipient_create_shared_secret function, but I am working on removing that function from the API, based on feedback from @nymius.


    josibake commented at 2:16 pm on October 7, 2025:
    Updated.
  257. josibake commented at 3:13 pm on October 1, 2025: member
    Thanks all for the ongoing review! I’m working on implementing the feedback so far and expect to have an updated branch by Friday, at the latest.
  258. build: add skeleton for new silentpayments (BIP352) module bb2c5bff15
  259. 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
    6fe3a7ae94
  260. 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.
    28917fa162
  261. 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.
    
    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.
    55dce8c6a2
  262. silentpayments: light client scanning
    Add function for creating k=0 outputs for multiple spend public keys.
    These keys can then be checked for existance against the UTXO set/blockchain.
    
    If a match is found, the client needs to download the full transaction and
    rescan with `_scan_outputs`.
    14b1bf04ff
  263. silentpayments: add examples/silentpayments.c
    Demonstrate sending, scanning, and light client scanning.
    a0b77dcf2f
  264. 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>
    9c6d949438
  265. 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>
    e759094a55
  266. 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>
    568e3aeb82
  267. tests: add sha256 tag test
    Test midstate tags used in silent payments.
    c7d3827e4f
  268. ci: enable silentpayments module 29bf593441
  269. docs: update README a0d2a33690
  270. josibake force-pushed on Oct 7, 2025
  271. josibake commented at 1:56 pm on October 7, 2025: member

    Updated ac4a726 -> a0d2a33 (2025_27 -> 2025_28, compare)

    • Makefile and typo fixes (h/t @stratospher)
    • examples/silentpayments.c rewrite
      • Various typo and wording fixes (h/t @jonasnick)
      • Demonstrates usage of the new _recipient_create_output_pubkeys function
      • Includes an address creation example, demonstrating usage of the label functions (h/t @jonasnick)
      • Various other simplifications
    • Move magic bytes out of header into implementation file (h/t @nymius)
    • Remove _recipient_{create_shared_secret, create_output_pubkey} functions in favour of a _recipient_create_output_pubkeys function (h/t @nymius)
      • This removes all references to k and provides a simpler API for light clients, without exposing internal details of the protocol
      • This allows for clients to check for a k=0 output, and then proceed to downloading the full transaction if a match is found
    • Various doc fixes in the header file (h/t @jonasnick , @nymius )
    • Update _prevouts_summary_{parse, serialize} functions to work with both 33 and 65 byte serializations
      • For indexes that are not storage constrained, being able to save the prevouts_summary object as a 65 byte uncompressed key saves the index from needing to calculate the y value when scanning, e.g., in an outsourced scanning use case
    • Update benchmarks, tests to use new _create_output_pubkeys function
    • Update test coverage
    • Fix the buffer overflow cases in the _scan_outputs function (h/t @jonasnick , @theStack )

    This is quite an extensive push based on a lot of excellent review. Its likely I’ve missed some feedback and will be polishing over the next few days, but wanted to get the bulk of the changes up. Thanks again for all the continued review!

  272. in include/secp256k1_silentpayments.h:421 in a0d2a33690
    416+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_output_pubkeys(
    417+    const secp256k1_context *ctx,
    418+    secp256k1_xonly_pubkey **outputs_xonly,
    419+    const unsigned char *scan_key32,
    420+    const secp256k1_silentpayments_prevouts_summary *prevouts_summary,
    421+    const secp256k1_pubkey **spend_pubkeys,
    


    nymius commented at 7:58 am on October 8, 2025:
    Any reason for this to not be const secp256k1_pubkey * const *spend_pubkeys? like _pubkeys in _silentpayments_recipient_prevouts_summary_create or _seckeys in _silentpayments_sender_create_outputs

    josibake commented at 8:29 am on October 8, 2025:
    Yes, outputs_xonly is the out-param where we will write the generated xonly public keys. This is the same as found_outputs in the scanning function and generated_outputs in the _create_outputs function.

    nymius commented at 8:34 am on October 8, 2025:
    Are we referring to the same line? I was talking about line 421: const secp256k1_pubkey **spend_pubkeys.

    nymius commented at 8:38 am on October 8, 2025:
    Sorry, I missed a word in the original comment. I edited it. tldr: Why not const secp256k1_pubkey **spend_pubkeys -> const secp256k1_pubkey * const *spend_pubkeys?

    josibake commented at 11:40 am on October 13, 2025:
    Ah, gotcha! Sorry, I misunderstood your original comment. This should be const secp256k1_pubkey * const *spend_pubkeys, as you suggest.
  273. nymius commented at 8:00 am on October 8, 2025: none

    All my previous concerns have been addressed. From a first read I extracted the some nits. Let me know if the following patch is easier to apply:

     0diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
     1index b8357b8..da40acf 100644
     2--- a/include/secp256k1_silentpayments.h
     3+++ b/include/secp256k1_silentpayments.h
     4@@ -187,7 +187,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
     5  *  `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized with
     6  *  `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is
     7  *  intended for storing the object or sending the prevout summary data to light clients.
     8- *  The serialization is is parsed with
     9+ *  The serialization is parsed with
    10  *  `secp256k1_silentpayments_recipient_prevouts_summary_parse`.
    11  */
    12 typedef struct secp256k1_silentpayments_prevouts_summary {
    13@@ -250,7 +250,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
    14  *
    15  *  Serializing a prevouts_summary object created with `_recipent_prevouts_summary_create` will result in
    16  *  an EC multiplication. This allows for a more compact serialization, but also means a serialized
    17- *  prevouts_summary will not parse back to a the same prevouts_summary object (due to the EC multiplication).
    18+ *  prevouts_summary will not parse back to the same prevouts_summary object (due to the EC multiplication).
    19  *
    20  *  Returns: 1 always.
    21  *
    22@@ -386,7 +386,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
    23 
    24 /** Create Silent Payment output public keys.
    25  *
    26- *  Given a scan key, a prevouts_summary, and array of recipient spend public keys,
    27+ *  Given a scan key, a prevouts_summary, and an array of recipient spend public keys,
    28  *  create the silent payments output public keys.
    29  *
    30  *  This function is used by the recipient when scanning for outputs without
    31diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
    32index c0f4822..52cfa1f 100644
    33--- a/src/modules/silentpayments/main_impl.h
    34+++ b/src/modules/silentpayments/main_impl.h
    35@@ -97,7 +97,7 @@ static void secp256k1_silentpayments_create_shared_secret(const secp256k1_contex
    36 
    37     secp256k1_ecmult_const(&ss_j, public_component, secret_component);
    38     secp256k1_ge_set_gej(&ss, &ss_j);
    39-    /* We declassify the shared secret group elemement because serializing a group element is a non-constant time operation. */
    40+    /* We declassify the shared secret group element because serializing a group element is a non-constant time operation. */
    41     secp256k1_declassify(ctx, &ss, sizeof(ss));
    42     /* This can only fail if the shared secret is the point at infinity, which should be
    43      * impossible at this point considering we have already validated the public key and
    
  274. josibake commented at 8:30 am on October 8, 2025: member
    Thanks for the review @nymius , let me know if you uncover anything else while working with the new API! Also, thanks for the patch; very easy to apply and much appreciated!
  275. in src/modules/silentpayments/main_impl.h:24 in 6fe3a7ae94 outdated
    21 
    22-/* TODO: implement functions for receiver side. */
    23+/** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to
    24+ *  ensure the correct values of k are used when creating multiple outputs for a recipient.
    25+ *
    26+ *  Note: secp256k1_ec_pubkey_cmp uses heap sort, which is unstable. Developers cannot and should not
    


    theStack commented at 3:37 pm on October 8, 2025:

    _ec_pubkey_cmp itself doesn’t do any sorting, should that be

    0 *  Note: secp256k1_silentpayments_recipient_sort uses heap sort, which is unstable. Developers cannot and should not
    

    instead? (Or maybe just “note that we use heap sort, …”)

  276. in include/secp256k1_silentpayments.h:51 in 6fe3a7ae94 outdated
    46+    secp256k1_pubkey scan_pubkey;
    47+    secp256k1_pubkey spend_pubkey;
    48+    size_t index;
    49+} secp256k1_silentpayments_recipient;
    50+
    51+/** Create Silent Payment outputs for recipient(s).
    


    theStack commented at 3:39 pm on October 8, 2025:
    case-sensitivity-yocto-nit: noticed that sometimes in the API header the “Silent Payment” is used, sometimes “silent payment” is, each also in singular and plural, might be worth it to use only one variant for consistency.
  277. in src/modules/silentpayments/main_impl.h:359 in 28917fa162 outdated
    354+
    355+    /* Sanity check inputs. */
    356+    VERIFY_CHECK(ctx != NULL);
    357+    ARG_CHECK(label != NULL);
    358+    ARG_CHECK(label_tweak32 != NULL);
    359+    ARG_CHECK(scan_key32 != NULL);
    


    theStack commented at 3:45 pm on October 8, 2025:
    in _recipient_create_label: I think it would make sense to also error if the passed scan key is invalid, to prevent the user of creating unspendable labels
  278. in include/secp256k1_silentpayments.h:153 in 28917fa162 outdated
    148+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_label(
    149+    const secp256k1_context *ctx,
    150+    secp256k1_pubkey *label,
    151+    unsigned char *label_tweak32,
    152+    const unsigned char *scan_key32,
    153+    const uint32_t m
    


    theStack commented at 3:51 pm on October 8, 2025:
    nit: not saying it’s wrong, but it seems unnecessary and at least very unusual to use const for parameters that are passed by value (feel free to ignore if that was discussed before, I suspect there was a reason for introducing it as I can’t remember seeing it from earlier review rounds)
  279. in include/secp256k1_silentpayments.h:264 in 55dce8c6a2 outdated
    259+ *                        `silentpayments_prevouts_summary` in.
    260+ *                  size: size of the byte array. Must be either 33 or 65.
    261+ *  In: prevouts_summary: pointer to an initialized silentpayments_prevouts_summary
    262+ *                        object
    263+ *                 flags: SECP256K1_EC_COMPRESSED if serialization should be in
    264+ *                        compressed format, otherwise SECP256K1_EC_UNCOMPRESSED.
    


    theStack commented at 3:55 pm on October 8, 2025:
    nit: seems that having only one of the size and flags parameters would be sufficient. I suspect it’s done like that to be more in-line with existing API functions (thinking of _ec_pubkey_serialize)?
  280. in include/secp256k1_silentpayments.h:282 in 55dce8c6a2 outdated
    277+ *           0 if the arguments are invalid.
    278+ *
    279+ *  Args:              ctx: pointer to a context object.
    280+ *  Out:  prevouts_summary: pointer to a silentpayments_prevouts_summary object. If 1 is
    281+ *                          returned, it is set to a parsed version of input33.
    282+ *  In:            input33: pointer to a serialized silentpayments_prevouts_summary.
    


    theStack commented at 3:57 pm on October 8, 2025:
    0 *                          returned, it is set to a parsed version of input.
    1 *  In:              input: pointer to a serialized silentpayments_prevouts_summary.
    

    also, the description of the inputlen parameter is missing

  281. in src/modules/silentpayments/main_impl.h:433 in 55dce8c6a2 outdated
    428+ *  For each function:
    429+ *
    430+ *  - `_recipient_prevouts_summary_create` always creates a prevouts_summary object with combined = false
    431+ *  - `_recipient_prevouts_summary_serialize` multiplies the input_hash into the summed public key before
    432+ *     serializing the resulting point as a compressed public key, if combined = false. If combined = true,
    433+ *     the point is serialized back into a compressed public key.
    


    theStack commented at 4:01 pm on October 8, 2025:
    needs update, now that serialization in both compressed and uncompressed public key format is possible
  282. in src/modules/silentpayments/main_impl.h:630 in 55dce8c6a2 outdated
    625+    secp256k1_scalar_clear(&scan_key_scalar);
    626+
    627+    found_idx = 0;
    628+    n_found = 0;
    629+    k = 0;
    630+    for (i = 0; i <= n_tx_outputs; i++) {
    


    theStack commented at 4:02 pm on October 8, 2025:
    0    for (i = 0; i < n_tx_outputs; i++) {
    

    off-by-one, IIUC

  283. in src/modules/silentpayments/main_impl.h:794 in 14b1bf04ff outdated
    789+        secp256k1_scalar_set_b32(&input_hash_scalar, &prevouts_summary->data[5 + 64], NULL);
    790+        secp256k1_scalar_mul(&scan_key_scalar, &scan_key_scalar, &input_hash_scalar);
    791+    }
    792+    secp256k1_silentpayments_create_shared_secret(ctx, shared_secret, &input_pubkey_ge, &scan_key_scalar);
    793+    secp256k1_scalar_clear(&scan_key_scalar);
    794+    return secp256k1_silentpayments_create_output_pubkeys(ctx, outputs_xonly, shared_secret, spend_pubkeys, n_spend_pubkeys, 0);
    


    theStack commented at 4:04 pm on October 8, 2025:
    in _recipient_create_output_pubkeys: should also clear out the shared_secret here
  284. in src/modules/silentpayments/main_impl.h:142 in 6fe3a7ae94 outdated
    139+    secp256k1_silentpayments_sha256_init_sharedsecret(&hash);
    140+    secp256k1_sha256_write(&hash, shared_secret33, 33);
    141+    secp256k1_write_be32(k_serialized, k);
    142+    secp256k1_sha256_write(&hash, k_serialized, sizeof(k_serialized));
    143+    secp256k1_sha256_finalize(&hash, hash_ser);
    144+    /* Convert output_tweak to a scalar to ensure the value is less than the curve order.
    


    theStack commented at 4:08 pm on October 8, 2025:
    nit, here and in some other similar comments before _scalar_set_b32 calls: could probably just drop the “to ensure the value is less than the curve order” part, as that sounds like that’s the only reason we do the conversion (maybe it was in the past, when that function returned the result as byte-array, but I don’t remember).
  285. theStack commented at 4:15 pm on October 8, 2025: contributor
    Glad to see the updated API, I agree that the light client scanning interface makes much more sense this way, and that supporting prevouts summary (de)serialization in both compressed and uncompressed pubkey format is useful. Left some mostly nitty comments below, some of them are likely not related to the latest changes, but I just missed them in earlier review rounds. Haven’t looked at tests and examples of the updated API functions yet.

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