-
Notifications
You must be signed in to change notification settings - Fork 38.7k
crypto, refactor: add new KeyPair class #30051
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
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
theuni
left 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.
This function feels kinda weird. As it's named ApplyTapTweak, I would expect it to modify *this. It also takes in a uint256* merkle_root but doesn't check for null, a reference would make more sense.
Perhaps this would make more sense as a static utility function that takes input/output keys?
Utility function might make more sense here: could do a utility function with and use that inside WDYT? |
|
|
e8d1b22 to
6cc72ce
Compare
|
Updated to use a utility function (instead of a method on |
f92dde3 to
0caa324
Compare
|
Updated to use the new |
0caa324 to
8d33daf
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.
It's weird to see this hooked up but unused. It could also use some simple test vectors.
Mind killing both birds by adding some tests, at least 1 for each merkle_root state?
03d98c0 to
e82c2c7
Compare
|
@theuni Updated with a comment and added |
e82c2c7 to
c2c5e72
Compare
|
Updated to add a move constructor to the KeyPair class. I noticed this was missing while trying to use the new class in #28201 (i.e. creating a std::vector of KeyPairs). @theuni assuming this was just missed, but if not curious if you have any objections to adding a move constructor on KeyPair? EDIT: also rebased on master to fix conflicts |
theuni
left 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.
Mind changing the dumb c-style casts to reinterpret_cast so it's clear that they can't be static_casts ? Sorry, I know that's my code.
utACK after that.
|
PR description needs an update too :) |
c2c5e72 to
bdc2a65
Compare
|
@theuni title, description, and dumb c-style casts updated! |
theuni
left 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.
ACK bdc2a65
Sanity check that using CKey/CPubKey directly vs using secp256k1_keypair objects returns the same results for BIP341 key tweaking. Co-authored-by: l0rinc <[email protected]>
Add a `KeyPair` class which wraps the `secp256k1_keypair`. This keeps the secret data in secure memory and enables passing the `KeyPair` object directly to libsecp256k1 functions expecting a `secp256k1_keypair`. Motivation: when passing `CKeys` for taproot outputs to libsecp256k1 functions, the first step is to create a `secp256k1_keypair` data type and use that instead. This is so the libsecp256k1 function can determine if the key needs to be negated, e.g., when signing. This is a bit clunky in that it creates an extra step when using a `CKey` for a taproot output and also involves copying the secret data into a temporary object, which the caller must then take care to cleanse. In addition, the logic for applying the merkle_root tweak currently only exists in the `SignSchnorr` function. In a later commit, we will add the merkle_root tweaking logic to this function, which will make the merkle_root logic reusable outside of signing by using the `KeyPair` class directly. Co-authored-by: Cory Fields <[email protected]>
34e7014 to
bfb2e6b
Compare
|
Update 9afa2c9 -> bfb2e6b (apply-taptweak-method-06-rebase -> apply-taptweak-method-07 (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.
utACK bfb2e6b
Thanks for your perseverance!
src/key.cpp
Outdated
| bool ret = secp256k1_schnorrsig_sign32(secp256k1_context_sign, sig.data(), hash.data(), &keypair, aux.data()); | ||
| KeyPair kp = ComputeKeyPair(merkle_root); | ||
| if (!kp.IsValid()) return false; | ||
| auto keypair = reinterpret_cast<const secp256k1_keypair*>(kp.data()); |
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 fails on the commit-by-commit CI task, we still need the getter in this commit - can be removed in the next where we'll access it directly
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, oversight on my part. I meant to combine these two commits in the last push but forgot 😅 The combined commit is still easy enough to review and I think this is more clear than adding a getter and then removing it in the next commit.
bfb2e6b to
8a878f5
Compare
Move `SignSchnorr` to `KeyPair`. This makes `CKey::SignSchnorr` now
compute a `KeyPair` object and then call `KeyPair::SignSchorr`. The
notable changes are:
* Move the merkle_root tweaking out of the sign function and into
the KeyPair constructor
* Remove the temporary secp256k1_keypair object and have the
functions access m_keypair->data() directly
Reuse existing BIP340 tests, as there should be no behavior change between the two
Replace early returns in KeyPair::KeyPair() with asserts. The if statements imply there is an error we are handling, but keypair_xonly_pub and xonly_pubkey_serialize can only fail if the keypair object is malformed, i.e., it was created with a bad secret key. Since we check that the keypair was created successfully before attempting to extract the public key, using asserts more accurately documents what we expect here and removes untested branches from the code.
8a878f5 to
ec973dd
Compare
ismaelsadeeq
left 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.
Code review ACK ec973dd
| BOOST_AUTO_TEST_CASE(key_schnorr_tweak_smoke_test) | ||
| { | ||
| // Sanity check to ensure we get the same tweak using CPubKey vs secp256k1 functions | ||
| secp256k1_context* secp256k1_context_sign = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); |
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:
secp256k1_context_create docs says
The only valid non-deprecated flag in recent library versions is
* SECP256K1_CONTEXT_NONE, which will create a context sufficient for all functionality
* offered by the library. All other (deprecated) flags will be treated as equivalent
* to the SECP256K1_CONTEXT_NONE flag.
| secp256k1_context* secp256k1_context_sign = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); | |
| secp256k1_context* secp256k1_context_sign = secp256k1_context_create(SECP256K1_CONTEXT_NONE); |
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, nice! Good catch. Will change if I end up retouching.
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 you do that, please change other usages as well
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.
Yeah, did a quick grep and it looks there is at least one other place in the fuzz tests where _SIGN is used, so this one is probably best as a follow up PR to replace all instances with _NONE in the codebase and perhaps add a mention to the developer notes.
src/key.cpp
Outdated
| secp256k1_xonly_pubkey pubkey; | ||
| if (!secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey, nullptr, keypair)) return; | ||
| unsigned char pubkey_bytes[32]; | ||
| if (!secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes, &pubkey)) return; |
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: Will be nice to have a wrapper for this that generates a serialized x-only public key.
their is a dup of this same code in the test.
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 if this is still applicable after the change in the last commit to replace the if branches with asserts, but will take a look and updated if I end up needing to retouch.
| static_assert(std::tuple_size<KeyType>() == sizeof(secp256k1_keypair)); | ||
| MakeKeyPairData(); | ||
| auto keypair = reinterpret_cast<secp256k1_keypair*>(m_keypair->data()); | ||
| bool success = secp256k1_keypair_create(secp256k1_context_sign, keypair, UCharCast(key.data())); |
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.
nice adding ec973dd
src/test/key_tests.cpp
Outdated
| BOOST_CHECK(secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey_old, nullptr, &keypair)); | ||
| unsigned char pubkey_bytes_old[32]; | ||
| BOOST_CHECK(secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes_old, &pubkey_old)); | ||
| uint256 tweak_old = XOnlyPubKey(pubkey_bytes_old).ComputeTapTweakHash(merkle_root.IsNull() ? nullptr : &merkle_root); |
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 other XOnlyPubKey constructor is just stripping the 2...32 bytes of the CPubKey and calling the same constructor called with xonly_bytes.
|
ACK ec973dd - will happily reack if you decide to apply @ismaelsadeeq's suggestions |
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.
trACK ec973dd
Rebuilt the commit, run all unit, functional and extended tests and all of them pass.
theStack
left 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.
Code-review ACK ec973dd
Left a few non-critical nits below, feel free to ignore.
| CKey key; | ||
| key.MakeNewKey(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.
nit, if you have to retouch:
| CKey key; | |
| key.MakeNewKey(true); | |
| CKey key = GenerateRandomKey(); |
| uint256 tweak_old = XOnlyPubKey(xonly_bytes).ComputeTapTweakHash(&merkle_root); | ||
|
|
||
| // CPubKey | ||
| CPubKey pubkey = key.GetPubKey(); | ||
| uint256 tweak_new = XOnlyPubKey(pubkey).ComputeTapTweakHash(&merkle_root); |
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 effectively only tests that given a certain private key, the same XOnlyPubKey objects result with both used methods (once via the secp256k1 keypair functions, once with .GetPubKey()). So strictly speaking computing and comparing the tweak hashes on top of that isn't needed and could be removed, but no strong feelings about that, as it also doesn't hurt. (Seems like this was already discussed in #30051 (comment) ff., so feel free to ignore).
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 was the result of a change that was reverted since, where it was important to test the exact before/after structure.
| assert(secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey, nullptr, keypair)); | ||
| assert(secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes, &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: AFAIR, for a long time we preferred to store the return value in a ret boolean and assert only on that (see e.g. $ git grep ret.*secp256k1 vs $ git grep assert.*secp256k1), but not sure if we still have a developer guideline like "don't use assert with side-effects" in place (I think we once had, and removed it, so this way seems to be 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.
We've had something like that before: #30051 (comment)
But it was ambiguous, as to whether it can fail or not during execution as we can see here:
https://github.com/hebasto/bitcoin/blob/master/src/pubkey.cpp#L345-L350 or https://github.com/hebasto/bitcoin/blob/master/src/secp256k1/src/secp256k1.c#L685
But @josibake argued that they cannot fail at that point - if they do it's a bug, but we didn't want to ignore the return values either. So we ended up asserting, like we did in GetPubKey already.
|
The buildsystem related stuff has been ported to the CMake-based buildsystem in hebasto#317. |
Broken out from #28201
The wallet returns an untweaked internal key for taproot outputs. If the output commits to a tree of scripts, this key needs to be tweaked with the merkle root. Even if the output does not commit to a tree of scripts, BIP341/342 recommend commiting to a hash of the public key.
Previously, this logic for applying the taptweak was implemented in the
CKey::SignSchnorrmethod.This PR moves introduces a KeyPair class which wraps a
secp256k1_keypairtype and refactors SignSchnorr to use this new KeyPair. The KeyPair class is created with an optional merkle_root argument and the logic from BIP341 is applied depending on the state of the merkle_root argument.The motivation for this refactor is to be able to use the tap tweak logic outside of signing, e.g. in silent payments when retrieving the private key (see #28201).
Outside of silent payments, since we almost always convert a
CKeyto asecp256k1_keypairwhen doing anything with taproot keys, it seems generally useful to have a way to model this type in our code base.