-
Couldn't load subscription status.
- Fork 1.1k
Add BIP352 module (take 3) #1698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add BIP352 module (take 3) #1698
Conversation
6264c3d to
9e85256
Compare
|
Updated 6264c3d -> 9e85256 (2025_00 -> 2025_01, compare)
|
9e85256 to
a4db279
Compare
|
Sorry, stopping CI here. We're about to make a release and need to the CI. :) We'll restart the jobs here afterwards. |
|
Update 9e85256 -> a4db279 (2025_01 -> 2025_02, compare)
|
a4db279 to
e35bede
Compare
|
Rebased on top of 0.7.0 release 🎉 a4db279 -> e35bede (2025_02 -> 2025_02_rebase, compare) |
|
I did a deep dive on using |
e35bede to
1a84908
Compare
1a84908 to
2948a9b
Compare
|
Update 1a84908 -> 2948a9b (2025_03 -> 2025_04, compare)
Thanks for the thorough review, @theStack ! |
2948a9b to
64ecd6c
Compare
|
Update 2948a9b -> 64ecd6c (2025_04 -> 2025_05, compare)
cc @jonasnick and @real-or-random regarding the use of a This should address all of the outstanding TODOs (at least the ones we left comments for 😅 ) |
64ecd6c to
3c4af8f
Compare
|
Updated 64ecd6c -> 3c4af8f (2025_05 -> 2025_06, compare)
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
diff
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8ee13ce..f612a84 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -583,13 +583,13 @@ jobs:
fail-fast: false
matrix:
env_vars:
- - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
+ - { WIDEMUL: 'int64', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
- { WIDEMUL: 'int128_struct', ECMULTGENPRECISION: 2, ECMULTWINDOW: 4 }
- - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
+ - { WIDEMUL: 'int128', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
- { WIDEMUL: 'int128', RECOVERY: 'yes' }
- - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes' }
- - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc' }
- - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', CPPFLAGS: '-DVERIFY' }
+ - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
+ - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CC: 'gcc' }
+ - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', EXTRAKEYS: 'yes', SCHNORRSIG: 'yes', MUSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CPPFLAGS: '-DVERIFY' }
- BUILD: 'distcheck'
steps:
|
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. |
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
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.
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.
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`.
Demonstrate sending, scanning, and light client 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 <[email protected]>
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 <[email protected]> Co-authored-by: Sebastian Falbesoner <[email protected]> Co-authored-by: Tim Ruffing <[email protected]>
Co-authored-by: Jonas Nick <[email protected]> Co-authored-by: Sebastian Falbesoner <[email protected]>
Test midstate tags used in silent payments.
c4942d3 to
a0d2a33
Compare
|
Updated ac4a726 -> a0d2a33 (2025_27 -> 2025_28, compare)
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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index b8357b8..da40acf 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -187,7 +187,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
* `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized with
* `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is
* intended for storing the object or sending the prevout summary data to light clients.
- * The serialization is is parsed with
+ * The serialization is parsed with
* `secp256k1_silentpayments_recipient_prevouts_summary_parse`.
*/
typedef struct secp256k1_silentpayments_prevouts_summary {
@@ -250,7 +250,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
*
* 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 means a serialized
- * prevouts_summary will not parse back to a the same prevouts_summary object (due to the EC multiplication).
+ * prevouts_summary will not parse back to the same prevouts_summary object (due to the EC multiplication).
*
* Returns: 1 always.
*
@@ -386,7 +386,7 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipien
/** Create Silent Payment output public keys.
*
- * Given a scan key, a prevouts_summary, and array of recipient spend public keys,
+ * Given a scan key, a prevouts_summary, and an array of recipient spend public keys,
* create the silent payments output public keys.
*
* This function is used by the recipient when scanning for outputs without
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index c0f4822..52cfa1f 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -97,7 +97,7 @@ static void secp256k1_silentpayments_create_shared_secret(const secp256k1_contex
secp256k1_ecmult_const(&ss_j, public_component, secret_component);
secp256k1_ge_set_gej(&ss, &ss_j);
- /* We declassify the shared secret group elemement because serializing a group element is a non-constant time operation. */
+ /* We declassify the shared secret group element because serializing a group element is a non-constant time operation. */
secp256k1_declassify(ctx, &ss, sizeof(ss));
/* This can only fail if the shared secret is the point at infinity, which should be
* impossible at this point considering we have already validated the public key and| secp256k1_xonly_pubkey **outputs_xonly, | ||
| const unsigned char *scan_key32, | ||
| const secp256k1_silentpayments_prevouts_summary *prevouts_summary, | ||
| const secp256k1_pubkey **spend_pubkeys, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we referring to the same line? I was talking about line 421: const secp256k1_pubkey **spend_pubkeys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, gotcha! Sorry, I misunderstood your original comment. This should be const secp256k1_pubkey * const *spend_pubkeys, as you suggest.
|
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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| /** Sort an array of silent payment recipients. This is used to group recipients by scan pubkey to | ||
| * ensure the correct values of k are used when creating multiple outputs for a recipient. | ||
| * | ||
| * Note: secp256k1_ec_pubkey_cmp uses heap sort, which is unstable. Developers cannot and should not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_ec_pubkey_cmp itself doesn't do any sorting, should that be
| * Note: secp256k1_ec_pubkey_cmp uses heap sort, which is unstable. Developers cannot and should not | |
| * 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, ...")
| size_t index; | ||
| } secp256k1_silentpayments_recipient; | ||
|
|
||
| /** Create Silent Payment outputs for recipient(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| VERIFY_CHECK(ctx != NULL); | ||
| ARG_CHECK(label != NULL); | ||
| ARG_CHECK(label_tweak32 != NULL); | ||
| ARG_CHECK(scan_key32 != NULL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| secp256k1_pubkey *label, | ||
| unsigned char *label_tweak32, | ||
| const unsigned char *scan_key32, | ||
| const uint32_t m |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
| * flags: SECP256K1_EC_COMPRESSED if serialization should be in | ||
| * compressed format, otherwise SECP256K1_EC_UNCOMPRESSED. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)?
| * returned, it is set to a parsed version of input33. | ||
| * In: input33: pointer to a serialized silentpayments_prevouts_summary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * returned, it is set to a parsed version of input33. | |
| * In: input33: pointer to a serialized silentpayments_prevouts_summary. | |
| * returned, it is set to a parsed version of input. | |
| * In: input: pointer to a serialized silentpayments_prevouts_summary. |
also, the description of the inputlen parameter is missing
| * serializing the resulting point as a compressed public key, if combined = false. If combined = true, | ||
| * the point is serialized back into a compressed public key. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs update, now that serialization in both compressed and uncompressed public key format is possible
| found_idx = 0; | ||
| n_found = 0; | ||
| k = 0; | ||
| for (i = 0; i <= n_tx_outputs; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for (i = 0; i <= n_tx_outputs; i++) { | |
| for (i = 0; i < n_tx_outputs; i++) { |
off-by-one, IIUC
| secp256k1_scalar_clear(&scan_key_scalar); | ||
| return secp256k1_silentpayments_create_output_pubkeys(ctx, outputs_xonly, shared_secret, spend_pubkeys, n_spend_pubkeys, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in _recipient_create_output_pubkeys: should also clear out the shared_secret here
| secp256k1_write_be32(k_serialized, k); | ||
| secp256k1_sha256_write(&hash, k_serialized, sizeof(k_serialized)); | ||
| secp256k1_sha256_finalize(&hash, hash_ser); | ||
| /* Convert output_tweak to a scalar to ensure the value is less than the curve order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a little help for the new test framework changes (will need to rebase this PR as it conflicts with master). The changes are small, I think you’ll like what we came up with.
| void run_silentpayments_tests(void) { | ||
| test_recipient_sort(); | ||
| test_send_api(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6fe3a7a:
This now should be:
static const struct tf_test_entry tests_silentpayment[] = {
CASE1(test_recipient_sort),
CASE1(test_send_api),
};And also need to include the framework:
#include "../../unit_test.h"| #ifdef ENABLE_MODULE_SILENTPAYMENTS | ||
| run_silentpayments_tests(); | ||
| #endif | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 6fe3a7a:
On the modules registry, this should be
#ifdef ENABLE_MODULE_SILENTPAYMENTS
MAKE_TEST_MODULE(silentpayment),
#endifThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that scanning performance is quite poor in the worst case. Consider a maliciously crafted 1mB transaction where all outputs are SP outputs to the recipient, who uses labels. There are about 1mB/32B tx outputs. Thus the outer loop runs n_tx_outputs = 31250 times and the inner loop 1 + 2 + ... + (n_tx_outputs - 1) + n_tx_outputs = n_tx_outputs * (n_tx_outputs + 1)/2 times. Each inner loop calls ge_set_gej at most twice, which takes 2us on my machine. So it would take me 16 minutes to scan such a transaction. I believe if the BIP mandated that the transaction outputs need to be ordered by k (i.e., smaller k needs to have a lower output index), scanning could be implemented in linear time. But requiring such an order may put too many constraints on transaction creation.
EDIT: I previously assumed 4mB worth of outputs which is of course not possible. This would have led to 4.3 hours of scanning.
| ret &= secp256k1_eckey_pubkey_serialize(&ge, output, &size, compressed); | ||
| (void)ret; | ||
| return 1; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ret is unused.
| ret = secp256k1_ec_pubkey_serialize(ctx, | ||
| bob_address[1], | ||
| &len, | ||
| &labeled_spend_pubkey, | ||
| SECP256K1_EC_COMPRESSED | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ret is unchecked
| ret = secp256k1_silentpayments_recipient_create_labeled_spend_pubkey(ctx, &labeled_spend_pubkey, &unlabeled_spend_pubkey, &label); | ||
| assert(ret); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we assert that a negligible event didn't happen but in the case of recipient_create_label we handle the negligible event?
| /* If not found, negate the tx_output and calculate second scan label candidate: | ||
| * label2 = -tx_output - generated_output | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useful and we had a similar line for label1 (label1 = tx_output - generated_output). Did you remove that intentionally?
| found_idx = 0; | ||
| n_found = 0; | ||
| k = 0; | ||
| for (i = 0; i <= n_tx_outputs; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove i and use n_found in it's place. Or even going one step further, we can remove n_found and replace it with k unless you think it hurts readability. I think it helps with understanding what is going on.
|
The following patch seems to optimize recipient scanning from O(n²) to O(n log n): Performance Impact (macOS M3, 128GB running the modified
The above N_OUTPUTS uses N different labels. |
What exactly is optimized here? It looks like the example uses hash based lookup now |
The commit is: w0xlt@8d68d64 P.S.: I'm not suggesting that these approaches be adopted in this PR. They could be explored to address any scanning time concerns, if they turn out to be valid. |
|
Thank you @woxlt for your suggestions. I played around with w0xlt@1ea5fa2 and particularly liked the As for your benchmarks, I believe the main reason for the speedup here is that your code tracks already found outputs and skips them in subsequent iterations. The example file you used for benchmarks scans through a transaction where all outputs go the same receiver, who uses labels, and the outputs are ordered by k. With a simple patch to the current scanning code (jonasnick@311b4eb) that just keeps track of the found outputs, I get about the same speedup as your commit. Of course, this is just one particular benchmark. Thus, I think in order to evaluate potential improvements, we need to select the benchmarks we care about.
I have a branch (jonasnick@47a4681) with a couple of simplifications to your code that, for example, avoid redundant serializations, simplify tracking of found outputs (only one way to track instead of two that are needed to be kept in-sync), and uses the same code for scanning of "candidate family #1" and "#2" to avoid code duplication. Lastly, your code uses malloc which has some practical issues we'd like to sort out but they certainly aren't insurmountable. |
To my best understanding, this is a very unlikely case. A wallet with a single label, the change label, as the base case, would be more realistic.
I think wallet with single change label is the best candidate for this. |
|
Hi @jonasnick Thank you very much for the feedback and for simplifying the code — I also believe that's essential. The simplified branch (https://github.com/jonasnick/secp256k1/tree/bip352-optimized-simplified) runs fine for all files except
The slowdown seems to come from removing the stateful label‑scan that walks the transaction outputs exactly once with two moving cursors (“heads”) and reuses decoded points. As a result it restarts from output 0 for every new k, re‑decodes points over and over, and ends up doing ~O(n²) label checks instead of ~O(n). The latest commit is w0xlt@8d68d64 (code is more organized), which might be a better base. |
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.