-
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:
4428fd3
to
5fe0d3e
Compare
Updated 4428fd3 -> 5fe0d3e (2025_24 -> 2025_25, compare)
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 |
5fe0d3e
to
1a21cf1
Compare
Updated 5fe0d3e -> 1a21cf1 (2025_25 -> 2025_26, compare)
I believe this push addresses all of the remaining outstanding feedback, except for #1698 (comment). 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! |
The |
1a21cf1
to
ac4a726
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.
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.
include/secp256k1_silentpayments.h
Outdated
* 0 if any combination of the shared secret, label and spend public keys | ||
* sum to zero. |
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.
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
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 something like this reads much better:
if hash(shared_secret || k) is an invalid scalar or the negation of
either the spend secret key or the label tweak, or if the arguments
are invalid.
and then per your later comment, we just add "if the arguments are invalid" to the rest of the API docs. WDYT?
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.
Sounds good to me 👍
include/secp256k1_silentpayments.h
Outdated
* used instead. | ||
* | ||
* Returns: 1 if shared secret creation was successful. | ||
* 0 if the recipient scan key is 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.
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.
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 adding "if the arguments are invalid" uniformly is a good approach.
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.
Similarly, recipient_create_labeled_spend_pubkey
is the only function that explicitly mentions failure if a public key is invalid.
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 by exposing the `_create_shared_secret` and `_create_output_pubkey` functions in the API. This means the light client will need to manage their own scanning state, so wherever possible it is preferrable to use the `_recipient_scan_ouputs` function. Add an opaque data type for passing around the prevout public key sum and the input hash tweak (input_hash). This data is passed to the scanner before the ECDH step as two separate elements so that the scanner can multiply the scan_key * input_hash before doing ECDH. Add functions for deserializing / serializing a prevouts_summary object to and from a public key. When serializing a prevouts_summary object, the input_hash is multplied into the prevout public key sum. This is so the object can be stored as public key for wallet rescanning later, or to send to light clients. For the light client, a `_parse` function is added which parses the compressed public key serialization into a `prevouts_summary` object. Finally, add test coverage for the receiving API.
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 <91535+thestack@users.noreply.github.com>
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>
Co-authored-by: Jonas Nick <2582071+jonasnick@users.noreply.github.com> Co-authored-by: Sebastian Falbesoner <91535+thestack@users.noreply.github.com>
Test midstate tags used in silent payments.
* Note: we can only hit this branch if tx_output != output_xonly. Thus, | ||
* we can add tx_output_gej + output_negated_ge without needing to check | ||
* whether or not the result is the point at infinity. |
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: 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?
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.
Good point, the label2 case wording also is much more clear.
hash->bytes = 64; | ||
} | ||
|
||
static int secp256k1_silentpayments_calculate_input_hash_scalar(secp256k1_scalar *input_hash_scalar, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) { |
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 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.".
4d90585 docs: Improve API docs of _context_set_illegal_callback (Tim Ruffing) 895f53d docs: Clarify that callback can be called more than once (Tim Ruffing) Pull request description: The tests in #1698 reminded me that some functions, e.g., `secp256k1_ec_pubkey_cmp`, may call the illegal callback more than once (see #1390 (comment) for more context). This PR clarifies the API docs to state explicitly that this is possible. This is the simplest solution. Any production code should crash anyway if it encounters a callback. And in debug code or in our test code, it doesn't really matter whether you see an error message once or twice. The alternative is to provide a guarantee that the callback is called only once. But that would make our code more complex for no good reason. The second commit fixes a few typos, wording, and consistency. ACKs for top commit: stratospher: ACK 4d90585. theStack: re-ACK 4d90585 Tree-SHA512: 97c31d68851e845b21e9ec2530432603917c019580feba98b62014b538f61be94ba963bf619217720d8f7331ac830e97e62c76c02e7297d3cf73dd085e6f4ca2
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:
* any further elliptic-curve operations from the wallet. | ||
*/ | ||
|
||
static const unsigned char secp256k1_silentpayments_prevouts_summary_magic[4] = { 0xa7, 0x1c, 0xd3, 0x5e }; |
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_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?
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!
* payment addresses. The same recipient can be | ||
* passed multiple times to create multiple outputs | ||
* for the same recipient. | ||
* n_recipients: the number of recipients. This is equal to the |
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.
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.
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.
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.
* 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."
* Out: label: pointer to the resulting label public key | ||
* label_tweak32: pointer to the 32 byte label tweak | ||
* In: scan_key32: pointer to the recipient's 32 byte scan key | ||
* m: label integer (0 is used for change 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.
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.
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.
Perhaps m: integer representing the m-th label
?
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 wouldn't use representing
here, because it's hiding the fact that the integer
is being used to produce the label.
* In: prevouts_summary: pointer to an initialized silentpayments_prevouts_summary | ||
* object | ||
*/ | ||
SECP256K1_API int secp256k1_silentpayments_recipient_prevouts_summary_serialize( |
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_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.
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.
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.
* incremented for each additional output created | ||
* or after each output found when scanning) | ||
*/ | ||
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_create_output_pubkey( |
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_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 usesecp256k1_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 withsecp256k1_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 ofk
. -
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 isyes
, then it will request the full blockchain chunk and extract the relevant outputs together with the tweaks usingsecp256k1_silentpayments_recipient_scan_outputs
.
Here, again, thek
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 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.
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.
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 ak=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.
Hope to have documented the the thought chain well. Here is a TLDR of the proposed changes:
|
*/ | ||
secp256k1_scalar_set_b32(input_hash_scalar, input_hash, &overflow); | ||
ret &= !secp256k1_scalar_is_zero(input_hash_scalar); | ||
return !!ret & !overflow; |
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 !!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)
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 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.
include/secp256k1_silentpayments.h
Outdated
* used instead. | ||
* | ||
* Returns: 1 if shared secret creation was successful. | ||
* 0 if the recipient scan key is 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.
Similarly, recipient_create_labeled_spend_pubkey
is the only function that explicitly mentions failure if a public key is invalid.
* `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized as a | ||
* compressed public key using | ||
* `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is |
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_silentpayments_recipient_prevouts_summary_create`. Can be serialized as a | |
* compressed public key using | |
* `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is | |
* `secp256k1_silentpayments_recipient_prevouts_summary_create`. Can be serialized with | |
* `secp256k1_silentpayments_recipient_prevouts_summary_serialize`. The serialization is |
nit: Unless I'm missing something, mentioning the compressed public key seems unnecessary?
include/secp256k1_silentpayments.h
Outdated
* 0 if the sequence is invalid (e.g., does not represent a valid | ||
* 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.
* 0 if the sequence is invalid (e.g., does not represent a valid | |
* public key). | |
* 0 if the sequence is invalid. |
* size of the found outputs array. This number | ||
* represents the number of outputs found while | ||
* scanning (0 if none are found) | ||
* In: tx_outputs: pointer to the tx's x-only public key 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.
* In: tx_outputs: pointer to the tx's x-only public key outputs | |
* 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.
* 0 if hash(shared_secret || k) is an invalid scalar or the negation of | ||
* either the spend secret key or the label tweak, 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.
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 if hash(shared_secret || k) is an invalid scalar or the negation of | |
* either the spend secret key or the label tweak, or if the arguments | |
* are invalid. | |
* 0 if the transaction is not a silent payments transaction or if the recipient's scan key is invalid. |
/* Since we've already validated the prevouts data, this shouldn't fail, but | ||
* better to be careful here since we are scanning data that could have been | ||
* maliciously created. | ||
*/ | ||
printf("Something went wrong while scanning this transaction, skipping.\n"); |
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 it'd be simpler to follow if we removed the comment (but maybe I'm missing something).
/* Since we've already validated the prevouts data, this shouldn't fail, but | |
* better to be careful here since we are scanning data that could have been | |
* maliciously created. | |
*/ | |
printf("Something went wrong while scanning this transaction, skipping.\n"); | |
printf("This transaction is not valid for silent payments, skipping.\n"); |
* 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. |
/* Since we've already validated the prevouts data, the only reason this could fail | ||
* is if we input a bad scan key or bad spend public key, which should never happen | ||
* because this is data under our control. | ||
*/ |
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.
The create_shared_secret doc doesn't mention the bad spend public key, so wouldn't it be sufficient to say something like
/* Since we've already validated the prevouts data, the only reason this could fail | |
* is if we input a bad scan key or bad spend public key, which should never happen | |
* because this is data under our control. | |
*/ | |
/* This only fails if the scan key is invalid. The recipient, Carol, | |
* created the scan key herself, so this must always succeed. */ |
/* We can only fail here if the particular set of inputs results in the | ||
* output of a hash function resulting in an invalid scalar. This is | ||
* statistically improbable for an honest execution, but if we were to | ||
* get to this point that means this is not a silent payments transaction | ||
* (or if it is, the sender was not following the protocol). | ||
*/ |
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.
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.
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.
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.
|
||
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. :)
} | ||
} | ||
if (found) { | ||
found_outputs[n_found]->output = *tx_outputs[found_idx]; |
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 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
- There's a collision in
create_output_tweak
that result in the same tweaks for different values ofk
. That is very unlikely but I think we want to protect against BOs even in that case. - The user-provided
label_lookup
function is broken. For example, iflabel_lookup
always returns thelabel_tweak
0 for any input, then every while loop iteration will "find" the firsttx_output
, leading to a buffer overflow (and an infinite loop). The same result happens with alabel_lookup
function where the adversary has the ability to insert their own chosenlabels
. I don't think the scanning function should trust the user's lookup function that much. - A transaction with
UINT32_MAX + 1
matching outputs is given. In every iterationk
will be incremented, and provided tocreate_output_tweak
which converts it to auint32_t
. Whenk = UINT32_MAX + 1
, the sameoutput_tweak
will be computed as in a previous iteration, resulting in a duplicate match.
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.
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).
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.
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?
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.
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?
### 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)
* 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
* returned, it is set to a parsed version of input33. | ||
* In: input33: pointer to a serialized silentpayments_prevouts_summary. | ||
*/ | ||
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_prevouts_summary_parse( |
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'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 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.
secp256k1_memclear_explicit(&shared_secret, sizeof(shared_secret)); | ||
return 0; | ||
} | ||
VERIFY_CHECK(k < SIZE_MAX); |
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.
Does this VERIFY_CHECK
have an effect? k
appears to be <= n_recipients
which is a size_t
.
* Returning an error here results in an untestable branch in the code, but we do this anyways to ensure strict compliance with BIP0352. | ||
*/ | ||
if (!secp256k1_silentpayments_create_output_tweak(&output_tweak_scalar, shared_secret, k)) { | ||
secp256k1_scalar_clear(&scan_key_scalar); |
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 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.
} | ||
} | ||
if (found) { | ||
found_outputs[n_found]->output = *tx_outputs[found_idx]; |
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.
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?
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. |
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.