-
Notifications
You must be signed in to change notification settings - Fork 37.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. |
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 |
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_cast
s ? 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! |
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 <pap.lorinc@gmail.com>
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 <cory-nospam-@coryfields.com>
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
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.
@@ -414,9 +414,9 @@ KeyPair::KeyPair(const CKey& key, const uint256* merkle_root) | |||
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.
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. |
7f4cad8 tests: add key tweak smoke test (josibake) 25c911e fixup! cmake: Build `test_bitcoin` executable (Hennadii Stepanov) Pull request description: This PR ports a single commit from bitcoin#30051 and amends the `test_bitcoin` target to fix compiling. ACKs for top commit: paplorinc: ACK 7f4cad8 m3dwards: ACK 7f4cad8 Tree-SHA512: 7494a70151345a11f03a1e6f5e90475d54498663d391ee5d4ef8e6f23e28acf6f30ae1337dc9e90e858d9237ce7cae07ec014f867c8bd6c600c3f65a4d57b503
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::SignSchnorr
method.This PR moves introduces a KeyPair class which wraps a
secp256k1_keypair
type 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
CKey
to asecp256k1_keypair
when doing anything with taproot keys, it seems generally useful to have a way to model this type in our code base.