-
Notifications
You must be signed in to change notification settings - 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:
ac4a726
to
c4942d3
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.
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:
include/secp256k1_silentpayments.h
Outdated
* for the same recipient. | ||
* n_recipients: the number of recipients. This is equal to the | ||
* total number of outputs to be generated as each | ||
* recipient may passed multiple times to generate |
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:
* recipient may passed multiple times to generate | |
* recipient may be passed multiple times to generate |
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.
Following your earlier suggestion, I removed the redundant explanation in favour of "the size of the recipients array."
Hope to have documented the the thought chain well. Here is a TLDR of the proposed changes:
|
include/secp256k1_silentpayments.h
Outdated
* 0 if hash(shared secret || k) results in an invalid scalar, | ||
* or if the arguments are invalid. |
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.
I think this can never happen for a legitimate silent payments transaction, so maybe we can just say
* 0 if hash(shared secret || k) results in an invalid scalar, | |
* or if the arguments are invalid. | |
* 0 if the transaction is not a silent payments transaction. |
|
||
secp256k1_ecmult_const(&ss_j, public_component, secret_component); | ||
secp256k1_ge_set_gej(&ss, &ss_j); | ||
secp256k1_declassify(ctx, &ss, sizeof(ss)); |
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.
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.
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.
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).
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.
secp256k1_eckey_pubkey_serialize
is not constant time, but it's possible to serialize in constant time:
secp256k1_fe_normalize(&ss.x);
secp256k1_fe_normalize(&ss.y);
secp256k1_fe_get_b32(&shared_secret33[1], &ss.x);
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.
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.
Your code suggestion is similar to what the ECDH module does:
secp256k1/src/modules/ecdh/main_impl.h
Lines 57 to 63 in baa2654
/* Compute a hash of the point */ | |
secp256k1_fe_normalize(&pt.x); | |
secp256k1_fe_normalize(&pt.y); | |
secp256k1_fe_get_b32(x, &pt.x); | |
secp256k1_fe_get_b32(y, &pt.y); | |
ret = hashfp(output, x, y, data); |
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
inpubkey_tweak_add
withecmult_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:
/* Leaking this value would break indistinguishability of the transaction, so clear it. */
secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret));
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.
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.
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)?
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 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.
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.
@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.
### Pregenerated test vectors | ||
### (see the comments in the previous section for detailed rationale) | ||
TESTVECTORS = src/wycheproof/ecdsa_secp256k1_sha256_bitcoin_test.h | ||
TESTVECTORS += src/modules/silentpayments/vectors.h |
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.
60f209c: we could wrap this in if ENABLE_MODULE_SILENTPAYMENTS
as well. (similar to ECDH test vectors)
examples/silentpayments.c
Outdated
* 2. Two outputs for Carol | ||
* | ||
* To create multiple outputs for Carol, Alice simply passes Carol's | ||
* silent payment address mutltiple times. |
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.
0e94f2d: typo nit: multiple
if (!secp256k1_eckey_pubkey_parse(&pk, input33, inputlen)) { | ||
return 0; | ||
} | ||
/* A serialized prevouts_summary will always have have the input_hash multiplied in, so we set combined = true. |
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.
da3120e: typo nit: have have
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),
#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.
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.
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.