-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add BIP352 silentpayments
module
#1519
base: master
Are you sure you want to change the base?
Add BIP352 silentpayments
module
#1519
Conversation
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.
Concept ACK
Left some initial feedback, especially around the scanning routine, will do an in-depth review round soon. Didn't look closer at the public_data
type routines and the examples yet.
3d08027
to
8b48bf1
Compare
8b48bf1
to
f5585d4
Compare
Updated 8b48bf1 -> f5585d4 (bip352-silentpayments-module-rebase -> bip352-silentpayments-module-02, compare):
For the label scanning, I looked for an example of using an invalid public key but didn't see anything except for the I also used |
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.
Second review round through, looks good so far! Left a bunch of nits, mostly about naming and missing ARG_CHECKS etc.
9d75190
to
1a3a00b
Compare
Thanks for the thorough review, @theStack ! I've addressed your feedback, along with some other changes. Update f5585d4 -> 1a3a00b (bip352-silentpayments-module-02 -> bip352-silentpayments-module-03, compare)
The sending tests now check that the generated outputs match exactly one of the possible expected output sets. Previously, the sending tests were checking that the generated outputs exist in the array of all possible outputs, but this wouldn't catch a bug where |
1a3a00b
to
92f5920
Compare
Rebased on #1518 1a3a00b -> 92f5920 (bip352-silentpayments-module-03 -> bip352-silentpayments-module-03-rebase, compare) |
92f5920
to
56ed901
Compare
Rebased on master (following #1518 merge) 92f5920 -> 56ed901 (bip352-silentpayments-module-03-rebase -> bip352-silentpayments-module-04-rebase, 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.
Went through another round. To the best of my knowledge, this PR matches the BIP352 specification and I'm close to non-cryptographer-light-ACKing it :-)
Found some nits an one open TODO that should probably be discussed though.
56ed901
to
bd66eaa
Compare
Rebased on master to fix merge conflict 56ed901 -> bd66eaa (bip352-silentpayments-module-04-rebase -> bip352-silentpayments-module-05-rebase, compare) |
CI failure seems related to not being able to install valgrind via homebrew and unrelated to my change so ignoring for now (cc @real-or-random for confirmation?). |
bd66eaa
to
2dde8f1
Compare
Thanks for the review @theStack ! Sorry for the slow response, I somehow missed the notification for your review 😅 Update bd66eaa -> 2dde8f1 (bip352-silentpayments-module-05-rebase -> bip352-silentpayments-module-06, compare)
Per #1519 (comment), I agree returning 0 is not the right thing to do, but having multiple error codes also seemed gross. I think an |
Indeed, see #1536 |
Some general notesOn error handling in generalError handling is hard, and the caller usually can't really recover from an error anyway. This is in particular true on malicious inputs: there's no reason to try to continue dealing with the attacker, and you simply want to abort. That's why, as a general rule, we try to avoid error paths as much as possible. This usually boils down to merging all errors into a single one, i.e., a) have just a single error "code" for all possible errors, b) and in the case of a multi-stage thing involving multiple function calls, have just a single place where errors are returned. Signature verification is a good example. A (signature, message, pubkey) triple is either valid or not. The caller should not care why exactly a signature fails to verify, so we don't even want to expose this to the caller. However, signature verification this is also a nice example of a case in which we stretch the rules a bit. Signature verification is implemented as two-stage process: 1. Parse the public key (which can fail). 2. Check the signature (which can fail). Purely from a "safe" API point of view, this is not great because we give the user two functions and two error paths instead of one. Ideally, there could just be one verification function which also takes care of parsing (this is how it's defined BIP340). The primary reason why we want to have a separate parsing function in this case is performance: if you check several signatures under the same key, you don't want to parse, which involves computing the y-coordinate, every time. ARG_CHECK
Line 324 in 1791f6f
What does this mean for this discussion?
So let's take a look at the two sides: On the sender side: The secret keys sum up to zero (
|
@real-or-random thanks for the response, this is super helpful.
In hindsight, I think my preference for
If we imagine an index + light client scenario, the Thinking about this a bit more:
Most of the high-level functions in our API are calling multiple lower-level functions and so far the approach has been something like:
EDIT: reading your comment again, I realize "error paths" is not really talking about branches in the code and more error paths for the user. |
Makes sense. My worry was that without an explicit error-code for this corner case, some users wouldn't even be aware of an indirect "not eligible" case and more likely interpret a return value of 0 as "only possible if there's a logic error on our side, so let's assert for success" (given the passed in data is public and already verified for consensus-validity). But in the end that's more a matter of good API documentation I guess. An example for the "input public keys sum up to point of infinity" case ( I think it would be also a good idea to add this scenario to the BIP352 test vectors, or at least a unit test in this PR? [1] created with the following Python script: https://github.com/theStack/bitcoin/blob/202405-contrib-bip352_input_pubkeys_cancelled/contrib/silentpayments/submit_input_pubkeys_infinity_tx.py |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
include/secp256k1_silentpayments.h
Outdated
/** This struct serves as an input parameter for passing the silent payment | ||
* address data. | ||
* | ||
* The index field is for when more than one address is being sent to in | ||
* a transaction. Index is set based on the original ordering of the addresses | ||
* and used to return the generated outputs matching the original ordering. | ||
* When more than one recipient is used, the recipient array will be sorted in | ||
* place as part of generating the outputs, but the generated outputs will be | ||
* returned in the original ordering specified by the index to ensure the | ||
* caller is able to match up the generated outputs to the correct silent | ||
* payment address (e.g., to be able to assign the correct amounts to the | ||
* correct generated outputs in the final transaction). |
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 struct serves as an input parameter for passing the silent payment | |
* address data. | |
* | |
* The index field is for when more than one address is being sent to in | |
* a transaction. Index is set based on the original ordering of the addresses | |
* and used to return the generated outputs matching the original ordering. | |
* When more than one recipient is used, the recipient array will be sorted in | |
* place as part of generating the outputs, but the generated outputs will be | |
* returned in the original ordering specified by the index to ensure the | |
* caller is able to match up the generated outputs to the correct silent | |
* payment address (e.g., to be able to assign the correct amounts to the | |
* correct generated outputs in the final transaction). | |
/** This struct serves as an input parameter for passing the silent payment | |
* address data to `silentpayments_sender_create_outputs`. | |
* | |
* The index field is for when more than one address is being sent to in | |
* a transaction. Index is set to the position of this recipient in the `recipients` array passed to `silentpayments_sender_create_outputs` | |
* and used to return the generated outputs matching the original ordering. |
nit: this is more clear about how to set the index and doesn't repeat what is stated in the silentpayments_sender_create_outputs
API doc.
hash->bytes = 64; | ||
} | ||
|
||
static void secp256k1_silentpayments_create_t_k(secp256k1_scalar *t_k_scalar, const unsigned char *shared_secret33, unsigned int k) { |
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 type of k
is a bit strange. In scan_outputs
it's a size_t
. When it gets passed into this function it's a unsigned int
, which is only guaranteed to be 16 bits. Then in write_be32
it gets casted to a uint32_t
. This means that when size_t
is larger than unsigned int
or size_t
is larger than uint32_t
the function will return wrong results.
I suggest to change the type of k
in this function from unsigned int
to uint32_t
and add a branch to scan_outputs
that returns 0 if k
exceeds UINT32_MAX
. Does the BIP 352 spec have a different approach to 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.
add a branch to scan_outputs that returns 0 if k exceeds UINT32_MAX.
Alternatively, we can change the type of k
in the API to uint32_t
(and perhaps do the same to m
in secp256k1_silentpayments_recipient_create_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.
Good catch - k
is underspecified in the BIP. IIRC, we did have it specified as a uint32 in an earlier version of the BIP. I'll update it here to be uint32
and make a note to update the BIP, as well.
secp256k1_write_be32(k_serialized, k); | ||
secp256k1_sha256_write(&hash, k_serialized, sizeof(k_serialized)); | ||
secp256k1_sha256_finalize(&hash, hash_ser); | ||
secp256k1_scalar_set_b32(t_k_scalar, hash_ser, 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.
How about
secp256k1_scalar_set_b32(t_k_scalar, hash_ser, overflow);
VERIFY_CHECK(!overflow);
VERIFY_CHECK(!secp256k1_scalar_is_zero(&t_k_scalar));
/* Calculate P_output = B_spend + t_k * G | ||
* This can fail if t_k overflows the curve order, but this is statistically improbable | ||
*/ |
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 comment doesn't seem to be true. This only fails if t_k = -b_spend
where b_spend*G = B_spend
.
/* Calculate P_output = B_spend + t_k * G | |
* This can fail if t_k overflows the curve order, but this is statistically improbable | |
*/ | |
/* Calculate P_output = B_spend + t_k * G | |
* This can fail if t_k equals the negation of the DLog of P_output, but t_k | |
* is the output of a hash function. | |
*/ | |
ret = secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar); |
Also, aren't we calculating B_spend
and not P_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.
The comment is indeed incorrect, since we aren't ever checking if the output of the hash function (t_k
) overflows the curve order.
I can have _create_t_k
fail if t_k_scalar
overflows, although this introduces a branch that we are unable to test (and is statistically improbable). This would, however, make the function more strictly adhere to the specification
int ret; | ||
|
||
/* Compute shared_secret = tweaked_secret_component * Public_component */ | ||
secp256k1_ecmult_const(&ss_j, public_component, secret_component); |
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.
Has the topic of using x-only ECDH come up on silent payments before like is used in BIP324? This avoids a square root which I think a small but measurable speed up on an ecmult. Perhaps someone more familiar with the x-only impl in BIP324 can comment on what the exact speed up would be.
Note that in terms of code all that has to change is to make the shared secret just the x coordinate (change it to shared_secret32
) and the actual optimization could be implemented later. It seems like it could be worth changing the spec slightly even at this stage to at least leave the door open to taking advantage of this so I thought it'd be worth mentioning.
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.
(@sipa Replying here in the thread to keep things organized in this huge PR.)
@LLFourn I've included the possibility of x-only ECDH (and x-only B_scan) in my review document of the BIP: gist.github.com/sipa/c9299811fb1f56abdcd2451a8a078d20
Note that in terms of code all that has to change is to make the shared secret just the x coordinate (change it to
shared_secret32
) and the actual optimization could be implemented later. It seems like it could be worth changing the spec slightly even at this stage
Yeah, I think x-only ECDH has fallen between the cracks. For all other protocols, there was not enough pressure to optimize our existing ECDH because it's fast enough (BIP324 is an exception in that we had to redo it anyway from scratch due to ellswift).
But I agree that the situation is different here. Scanning is the bottleneck, and it will be nice to save a few percent.
We even have draft implementations:
- input=xonly, shared_secret=xonly: Add x-only ECDH support to ecdh module #1198 (this one is in a good shape)
- input=compressed, shared_secret=xonly: x-only ECDH without sqrt #262
The big question is whether the spec can be changed at this point, and I'm afraid the answer is no. If I recall correctly, others have picked it up already? @josibake @RubenSomsen
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.
@real-or-random No implementation work is really needed, since the silentpayment
module does ECDH internally; it doesn't expect users to use the ecdh
module interface, and the internal side of things, the secp256k1_ecmult_const_xonly
function, already exists since #1118.
The big question is indeed whether breaking spec changes are still possible. Please have a look at my other questions/suggestions in the doc too, if it 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.
Out of curiosity, I tried implementing x-only ecdh using secp256k1_ecmult_const_xonly
here: josibake@e7ec39e
When I ran the benchmarks between the old and the new, I didn't see any speedup. This means:
- I didn't implement the x-only ECDH correctly
- The benchmark is not accurate
- The speedup doesn't make a noticeable difference in the context of scanning
@real-or-random @sipa others have indeed picked it up, but it's a small enough group that I think we could sneak in a breaking change and coordinate an update if there is a compelling enough reason to do so. Assuming 3) is correct, this doesn't seem like a compelling enough speedup to warrant a breaking change. However, it would be great if someone could double check my work as I'm a bit skeptical of my results.
My results:
❯ ./new/bin/bench silentpayments
Benchmark , Min(us) , Avg(us) , Max(us)
silentpayments_full_tx_scan , 98.1 , 98.2 , 98.7
silentpayments_output_scan , 74.5 , 74.6 , 74.8
❯ ./old/bin/bench silentpayments
Benchmark , Min(us) , Avg(us) , Max(us)
silentpayments_full_tx_scan , 98.3 , 98.3 , 98.3
silentpayments_output_scan , 74.5 , 74.5 , 74.6
How about this: jonasnick@7c4ecb8. It matches how we test multiple illegal callbacks in |
include/secp256k1_silentpayments.h
Outdated
* In: public_data: pointer to an initialized silentpayments_recipient_public_data | ||
* object | ||
*/ | ||
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_recipient_public_data_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.
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.
Not sure I follow. The linked PR implies I should remove the SECP256K1_WARN_UNUSED_RESULT
if the function always returns 1, but in this case _public_data_serialize
does not always 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.
The PR removes SECP256K1_WARN_UNUSED_RESULT
for functions that always return 1. The doc of _public_data_serialize
indicates that this is such a function.
Looking into _public_data_serialize
, it seems like it cannot fail unless the API is used incorrectly or if input_hash is out of range. I think that public data create should only succeed wheninput_hash
is in range.
This comment was marked as outdated.
This comment was marked as outdated.
* label1 = tx_output - P_output */ | ||
secp256k1_gej_add_ge_var(&label_gej, &tx_output_gej, &P_output_negated_ge, NULL); | ||
secp256k1_ge_set_gej(&label_ge, &label_gej); | ||
secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 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.
VERIFY_CHECK/handle return value secp256k1_ec_pubkey_serialize
* label2 = -tx_output - P_output */ | ||
secp256k1_gej_add_ge_var(&label_gej, &label_gej, &P_output_negated_ge, NULL); | ||
secp256k1_ge_set_gej(&label_ge, &label_gej); | ||
secp256k1_eckey_pubkey_serialize(&label_ge, label33, &len, 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.
VERIFY_CHECK/handle return value secp256k1_ec_pubkey_serialize
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as duplicate.
This comment was marked as duplicate.
examples/silentpayments.c
Outdated
/* To keep things simple, we cast the tx_output_ptr array to remove the | ||
* const qualifer, so that we can create the outputs. We want the const | ||
* qualifer because this same array will be passed to the scan function | ||
* later in the example. | ||
*/ | ||
ret = secp256k1_silentpayments_sender_create_outputs(ctx, | ||
(secp256k1_xonly_pubkey **)tx_output_ptrs, |
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 seems unsafe because we're casting const away for a function that is definitely violating the const. If we only want to use one array for creating and scanning, then we should just remove const from the declaration of tx_output_ptrs.
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 only want to use one array for creating and scanning, then we should just remove const from the declaration of tx_output_ptrs.
Strongly agree with this.
For the sake of education, it is safe because the pointers in tx_out_ptrs
point to the elements of tx_outputs
, which is not declared const
. So the "object" we're modifying is not declared const
. And only this would be UB: "If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined." (https://port70.net/~nsz/c/c99/n1256.html#6.7.3p5)
But yep, relying on this is probably not the kind of programming style we should encourage in an example. ^^
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 re: this being an example. I changed this to remove const from the declaration of tx_output_ptrs
. This required adding a cast later on when tx_output_ptrs
is used as the input to scan_outputs
, but since this cast is adding the const declaration, I think this is fine?
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 haven't checked the code, but is the new cast really necessary? An explicit cast shouldn't be necessary if you're casting to a more "restricted" type.
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 had thought the same, but this is the warning I get when I remove the cast:
/root/secp256k1/examples/silentpayments.c: In function ‘main’:
/root/secp256k1/examples/silentpayments.c:421:17: warning: passing argument 4 of ‘secp256k1_silentpayments_recipient_scan_outputs’ from incompatible pointer type [-Wincompatible-pointer-types]
421 | tx_output_ptrs, N_OUTPUTS,
| ^~~~~~~~~~~~~~
| |
| secp256k1_xonly_pubkey **
In file included from /root/secp256k1/examples/silentpayments.c:14:
/root/secp256k1/include/secp256k1_silentpayments.h:342:43: note: expected ‘const secp256k1_xonly_pubkey * const*’ but argument is of type ‘secp256k1_xonly_pubkey **’
342 | const secp256k1_xonly_pubkey * const *tx_outputs,
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
[100%] Linking C executable ../bin/silentpayments_example
[100%] Built target silentpayments_example
sender_keypair_ptrs, N_INPUTS, | ||
NULL, 0 | ||
); | ||
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.
Ideally, the documentation in the include file would indicate whether this can be asserted or not. In the case of create_outputs
and potentially others, it's 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.
Doing a pass on the full header to make sure the documentation is consistent and up-to-date for all of the functions, will leave this comment open for now.
examples/silentpayments.c
Outdated
ret = secp256k1_silentpayments_recipient_create_output_pubkey(ctx, | ||
&potential_output, | ||
shared_secret, | ||
&spend_pubkey, | ||
k | ||
); | ||
if (!ret) { | ||
printf("\n"); | ||
printf("This transaction is not valid for silent payments, skipping."); | ||
return 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.
If we're consistent, then I think the return value can be asserted because it fails only if the API is used in a wrong way.
hash->bytes = 64; | ||
} | ||
|
||
static void secp256k1_silentpayments_create_t_k(secp256k1_scalar *t_k_scalar, const unsigned char *shared_secret33, unsigned int k) { |
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.
add a branch to scan_outputs that returns 0 if k exceeds UINT32_MAX.
Alternatively, we can change the type of k
in the API to uint32_t
(and perhaps do the same to m
in secp256k1_silentpayments_recipient_create_label
.
*/ | ||
typedef struct { | ||
secp256k1_pubkey scan_pubkey; | ||
secp256k1_pubkey spend_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.
nit: The terminology could be more clear. The naming in this struct appears to suggest that a spend_pubkey
can either be the "actual" spend pubkey or a labelled spend pubkey. However, everywhere else in the API spend_pubkey
is the actual spend pubkey and not the labelled spend 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.
Agree, this is pretty confusing. Perhaps spend_pubkey
can be renamed to labelled_spend_pubkey
, with a comment mentioning that the label is optional? Something like:
/**
* ...
* `lablled_spend_pubkey` is the spend public key with an (optional) label tweak applied.
* If no label tweak is applied, `labelled_spend_pubkey` is the recipients spend public key.
* ...
*/
typedef struct {
secp256k1_pubkey scan_pubkey;
secp256k1_pubkey labelled_spend_pubkey;
...
* Args: ctx: pointer to a context object | ||
* Out: label: pointer to the resulting label public key | ||
* label_tweak32: pointer to the 32 byte label tweak | ||
* In: recipient_scan_key: pointer to the recipient's scan 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.
* In: recipient_scan_key: pointer to the recipient's scan key | |
* In: recipient_scan_key: pointer to the recipient's 32 byte scan key |
Maybe also consider calling this argument recipient_scan_key32
. Same in scan_outputs
and create_shared_secrets
.
return 0; | ||
} | ||
combined = (int)public_data->data[0]; | ||
if (!combined) { |
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.
Maybe best to explain the rationale of the combined flag (and tradeoffs) when the public data data structure is defined because it affects multiple functions.
secp256k1_scalar_set_b32(&input_hash_scalar, input_hash, &overflow); | ||
if (overflow) { | ||
return 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.
I think it would be best if a properly created public_data
object would only contain valid data. Then, the overflow check here would be unnecessary.
To detect uninitialized data structure from being passed to functions, we started using a 4 byte magic sequence in the musig module. See for example here,
static const unsigned char secp256k1_musig_secnonce_magic[4] = { 0x22, 0x0e, 0xdc, 0xf1 }; |
_load
functions ARG_CHECK
for the correct magic.
secp256k1_scalar_set_b32(&input_hash_scalar, input_hash, &overflow); | ||
/* TODO: consider VERIFY_CHECK ??? */ | ||
if (overflow) { | ||
return 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.
The BIP does not specify the conversion from the input hash byte array to a scalar. In particular, it doesn't specify whether to fail on overflow or not.
} | ||
} | ||
|
||
static void bench_silentpayments_full_tx_scan(void* arg, int iters) { |
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.
Wouldn't the benchmarks be more helpful if the full tx scan also included public data creation because this is effectively what wallets do when encountering a transaction?
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, will update.
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 labelled addresses and b) to populate a labels cache when scanning. Add function for creating a labelled 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 labelled silent payment address. Add tests for the label API.
f5740a4
to
c31114a
Compare
Rebased 71df073 -> c31114a (bip352-silentpayments-module-18 -> bip352-silentpayments-module-rebased, compare)
Separating the rebase from addressing feedback for my own sanity and to help make sure I haven't missed any feedback. |
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 summed input public key (A_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 b_scan * input_hash before doing ECDH. Add functions for deserializing / serializing a public_data object to and from a public key. When serializing a public_data object, the input_hash is multplied into A_sum. This is so the object can be stored as public key for wallet rescanning later, or to vend to light clients. For the light client, a `_parse` function is added which parses the compressed public key serialization into a `public_data` object. Finally, add test coverage for the recieiving 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.
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: Jonas Nick <[email protected]> Co-authored-by: Sebastian Falbesoner <[email protected]>
c31114a
to
592f251
Compare
Updated c31114a -> 592f251 (bip352-silentpayments-module-rebased -> bip352-silentpayments-module-19, compare) A few smaller changes, namely:
Bigger changes:
I'm still working through some of the feedback, namely improving the API documentation in the header and adding some comments in a few places. |
This PR adds a new Silent Payments (BIP352) module to secp256k1. It is a continuation of the work started in #1471.
The module implements the full protocol, except for transaction input filtering and silent payment address encoding / decoding as those will be the responsibility of the wallet software. It is organized with functions for sending (prefixed with
_sender
) and receiving (prefixed by_recipient
).For sending
taproot_seckeys
andplain_seckeys
Two lists are used since the
taproot_seckeys
may need negation.taproot_seckeys
are passed as keypairs to avoid the function needing to compute the public key to determine parity.plain_seckeys
are passed as just secret keys_silentpayment_recipient
objectsThese structs hold the scan and spend public key and an index for remembering the original ordering. It is expected that a caller will start with a list of silent payment addresses (with the desired amounts), convert these into an array of
recipients
and then match the generated outputs back to the original silent payment addresses. The index is used to return the generated outputs in the original ordersilentpayments_sender_create_outputs
to generate the xonly public keys for the recipientsThis function can be called with one or more recipients. The same recipient may be repeated to generate multiple outputs for the same recipient
For scanning
taproot_pubkeys
andplain_pubeys
This avoids the caller needing to convert taproot public keys into compressed public keys (and vice versa)
input_hash
This is done as a separate step to allow the caller to reuse this output if scanning for multiple scan keys. It also allows a caller to use this function for aggregating the transaction inputs and storing them in an index to vend to light clients later (or for faster rescans when recovering a wallet)
silentpayments_recipient_scan_outputs
to scan the transaction outputs and return the tweak data (and optionally label information) needed for spending laterIn addition, a few utility functions for labels are provided for the recipient for creating a label tweak and tweaked spend public key for their address. Finally, two functions are exposed in the API for supporting light clients,
_recipient_created_shared_secret
and_recipient_create_output_pubkey
. These functions enable incremental scanning for scenarios where the caller does not have access to the transaction outputs:This is done as a separate step to allow the caller to reuse the shared secret result when creating outputs and avoid needing to do a costly ECDH every time they need to check for an additional output
k = 0
)k++
See
examples/silentpayments.c
for a demonstration of how the API is expected to be used.Note for reviewers
My immediate goal is to get feedback on the API so that I can pull this module into bitcoin/bitcoin#28122 (silent payments in the bitcoin core wallet). That unblocks from finishing the bitcoin core PRs while work continues on this module.
Notable differences between this PR and the previous version
See #1427 and #1471 for discussions on the API design. This iteration of the module attempts to be much more high level and incorporate the feedback from #1471. I also added a
secp256k1_silentpayments_public_data
opaque data type, which contains the summed public key and the input_hash. My motivation here was:A_sum
andrecipient_spend_key
, which was impossible to catch withARG_CHECKS
and would result in the scanning process finishing without errors, but not finding any outputsinput_hash
from the caller, which makes for an overall simpler API IMOI also removed the need for the recipient to generate a shared secret before using the
secp256k1_silentpayments_recipient_scan_outputs
function and instead create the shared secret inside the function.Outstanding work