Skip to content

Commit b38fb19

Browse files
committed
Merge bitcoin#30051: crypto, refactor: add new KeyPair class
ec973dd refactor: remove un-tested early returns (josibake) 72a5822 tests: add tests for KeyPair (josibake) cebb08b refactor: move SignSchnorr to KeyPair (josibake) c39fd39 crypto: add KeyPair wrapper class (josibake) 5d507a0 tests: add key tweak smoke test (josibake) f14900b bench: add benchmark for signing with a taptweak (josibake) Pull request description: Broken out from bitcoin#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 bitcoin#28201). Outside of silent payments, since we almost always convert a `CKey` to a `secp256k1_keypair` when doing anything with taproot keys, it seems generally useful to have a way to model this type in our code base. ACKs for top commit: paplorinc: ACK ec973dd - will happily reack if you decide to apply @ismaelsadeeq's suggestions ismaelsadeeq: Code review ACK ec973dd itornaza: trACK ec973dd theStack: Code-review ACK ec973dd Tree-SHA512: 34947e3eac39bd959807fa21b6045191fc80113bd650f6f08606e4bcd89aa17d6afd48dd034f6741ac4ff304b104fa8c1c1898e297467edcf262d5f97425da7b
2 parents ce1c881 + ec973dd commit b38fb19

File tree

4 files changed

+183
-21
lines changed

4 files changed

+183
-21
lines changed

src/bench/sign_transaction.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <script/script.h>
1313
#include <script/sign.h>
1414
#include <uint256.h>
15+
#include <test/util/random.h>
1516
#include <util/translation.h>
1617

1718
enum class InputType {
@@ -66,5 +67,33 @@ static void SignTransactionSingleInput(benchmark::Bench& bench, InputType input_
6667
static void SignTransactionECDSA(benchmark::Bench& bench) { SignTransactionSingleInput(bench, InputType::P2WPKH); }
6768
static void SignTransactionSchnorr(benchmark::Bench& bench) { SignTransactionSingleInput(bench, InputType::P2TR); }
6869

70+
static void SignSchnorrTapTweakBenchmark(benchmark::Bench& bench, bool use_null_merkle_root)
71+
{
72+
ECC_Context ecc_context{};
73+
74+
auto key = GenerateRandomKey();
75+
auto msg = InsecureRand256();
76+
auto merkle_root = use_null_merkle_root ? uint256() : InsecureRand256();
77+
auto aux = InsecureRand256();
78+
std::vector<unsigned char> sig(64);
79+
80+
bench.minEpochIterations(100).run([&] {
81+
bool success = key.SignSchnorr(msg, sig, &merkle_root, aux);
82+
assert(success);
83+
});
84+
}
85+
86+
static void SignSchnorrWithMerkleRoot(benchmark::Bench& bench)
87+
{
88+
SignSchnorrTapTweakBenchmark(bench, /*use_null_merkle_root=*/false);
89+
}
90+
91+
static void SignSchnorrWithNullMerkleRoot(benchmark::Bench& bench)
92+
{
93+
SignSchnorrTapTweakBenchmark(bench, /*use_null_merkle_root=*/true);
94+
}
95+
6996
BENCHMARK(SignTransactionECDSA, benchmark::PriorityLevel::HIGH);
7097
BENCHMARK(SignTransactionSchnorr, benchmark::PriorityLevel::HIGH);
98+
BENCHMARK(SignSchnorrWithMerkleRoot, benchmark::PriorityLevel::HIGH);
99+
BENCHMARK(SignSchnorrWithNullMerkleRoot, benchmark::PriorityLevel::HIGH);

src/key.cpp

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -271,27 +271,8 @@ bool CKey::SignCompact(const uint256 &hash, std::vector<unsigned char>& vchSig)
271271

272272
bool CKey::SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256* merkle_root, const uint256& aux) const
273273
{
274-
assert(sig.size() == 64);
275-
secp256k1_keypair keypair;
276-
if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, UCharCast(begin()))) return false;
277-
if (merkle_root) {
278-
secp256k1_xonly_pubkey pubkey;
279-
if (!secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey, nullptr, &keypair)) return false;
280-
unsigned char pubkey_bytes[32];
281-
if (!secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes, &pubkey)) return false;
282-
uint256 tweak = XOnlyPubKey(pubkey_bytes).ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root);
283-
if (!secp256k1_keypair_xonly_tweak_add(secp256k1_context_static, &keypair, tweak.data())) return false;
284-
}
285-
bool ret = secp256k1_schnorrsig_sign32(secp256k1_context_sign, sig.data(), hash.data(), &keypair, aux.data());
286-
if (ret) {
287-
// Additional verification step to prevent using a potentially corrupted signature
288-
secp256k1_xonly_pubkey pubkey_verify;
289-
ret = secp256k1_keypair_xonly_pub(secp256k1_context_static, &pubkey_verify, nullptr, &keypair);
290-
ret &= secp256k1_schnorrsig_verify(secp256k1_context_static, sig.data(), hash.begin(), 32, &pubkey_verify);
291-
}
292-
if (!ret) memory_cleanse(sig.data(), sig.size());
293-
memory_cleanse(&keypair, sizeof(keypair));
294-
return ret;
274+
KeyPair kp = ComputeKeyPair(merkle_root);
275+
return kp.SignSchnorr(hash, sig, aux);
295276
}
296277

297278
bool CKey::Load(const CPrivKey &seckey, const CPubKey &vchPubKey, bool fSkipCheck=false) {
@@ -363,6 +344,11 @@ ECDHSecret CKey::ComputeBIP324ECDHSecret(const EllSwiftPubKey& their_ellswift, c
363344
return output;
364345
}
365346

347+
KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const
348+
{
349+
return KeyPair(*this, merkle_root);
350+
}
351+
366352
CKey GenerateRandomKey(bool compressed) noexcept
367353
{
368354
CKey key;
@@ -420,6 +406,39 @@ void CExtKey::Decode(const unsigned char code[BIP32_EXTKEY_SIZE]) {
420406
if ((nDepth == 0 && (nChild != 0 || ReadLE32(vchFingerprint) != 0)) || code[41] != 0) key = CKey();
421407
}
422408

409+
KeyPair::KeyPair(const CKey& key, const uint256* merkle_root)
410+
{
411+
static_assert(std::tuple_size<KeyType>() == sizeof(secp256k1_keypair));
412+
MakeKeyPairData();
413+
auto keypair = reinterpret_cast<secp256k1_keypair*>(m_keypair->data());
414+
bool success = secp256k1_keypair_create(secp256k1_context_sign, keypair, UCharCast(key.data()));
415+
if (success && merkle_root) {
416+
secp256k1_xonly_pubkey pubkey;
417+
unsigned char pubkey_bytes[32];
418+
assert(secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey, nullptr, keypair));
419+
assert(secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes, &pubkey));
420+
uint256 tweak = XOnlyPubKey(pubkey_bytes).ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root);
421+
success = secp256k1_keypair_xonly_tweak_add(secp256k1_context_static, keypair, tweak.data());
422+
}
423+
if (!success) ClearKeyPairData();
424+
}
425+
426+
bool KeyPair::SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256& aux) const
427+
{
428+
assert(sig.size() == 64);
429+
if (!IsValid()) return false;
430+
auto keypair = reinterpret_cast<const secp256k1_keypair*>(m_keypair->data());
431+
bool ret = secp256k1_schnorrsig_sign32(secp256k1_context_sign, sig.data(), hash.data(), keypair, aux.data());
432+
if (ret) {
433+
// Additional verification step to prevent using a potentially corrupted signature
434+
secp256k1_xonly_pubkey pubkey_verify;
435+
ret = secp256k1_keypair_xonly_pub(secp256k1_context_static, &pubkey_verify, nullptr, keypair);
436+
ret &= secp256k1_schnorrsig_verify(secp256k1_context_static, sig.data(), hash.begin(), 32, &pubkey_verify);
437+
}
438+
if (!ret) memory_cleanse(sig.data(), sig.size());
439+
return ret;
440+
}
441+
423442
bool ECC_InitSanityCheck() {
424443
CKey key = GenerateRandomKey();
425444
CPubKey pubkey = key.GetPubKey();

src/key.h

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ constexpr static size_t ECDH_SECRET_SIZE = CSHA256::OUTPUT_SIZE;
2828
// Used to represent ECDH shared secret (ECDH_SECRET_SIZE bytes)
2929
using ECDHSecret = std::array<std::byte, ECDH_SECRET_SIZE>;
3030

31+
class KeyPair;
32+
3133
/** An encapsulated private key. */
3234
class CKey
3335
{
@@ -202,6 +204,22 @@ class CKey
202204
ECDHSecret ComputeBIP324ECDHSecret(const EllSwiftPubKey& their_ellswift,
203205
const EllSwiftPubKey& our_ellswift,
204206
bool initiating) const;
207+
/** Compute a KeyPair
208+
*
209+
* Wraps a `secp256k1_keypair` type.
210+
*
211+
* `merkle_root` is used to optionally perform tweaking of
212+
* the internal key, as specified in BIP341:
213+
*
214+
* - If merkle_root == nullptr: no tweaking is done, use the internal key directly (this is
215+
* used for signatures in BIP342 script).
216+
* - If merkle_root->IsNull(): tweak the internal key with H_TapTweak(pubkey) (this is used for
217+
* key path spending when no scripts are present).
218+
* - Otherwise: tweak the internal key with H_TapTweak(pubkey || *merkle_root)
219+
* (this is used for key path spending with the
220+
* Merkle root of the script tree).
221+
*/
222+
KeyPair ComputeKeyPair(const uint256* merkle_root) const;
205223
};
206224

207225
CKey GenerateRandomKey(bool compressed = true) noexcept;
@@ -235,6 +253,61 @@ struct CExtKey {
235253
void SetSeed(Span<const std::byte> seed);
236254
};
237255

256+
/** KeyPair
257+
*
258+
* Wraps a `secp256k1_keypair` type, an opaque data structure for holding a secret and public key.
259+
* This is intended for BIP340 keys and allows us to easily determine if the secret key needs to
260+
* be negated by checking the parity of the public key. This class primarily intended for passing
261+
* secret keys to libsecp256k1 functions expecting a `secp256k1_keypair`. For all other cases,
262+
* CKey should be preferred.
263+
*
264+
* A KeyPair can be created from a CKey with an optional merkle_root tweak (per BIP342). See
265+
* CKey::ComputeKeyPair for more details.
266+
*/
267+
class KeyPair
268+
{
269+
public:
270+
KeyPair() noexcept = default;
271+
KeyPair(KeyPair&&) noexcept = default;
272+
KeyPair& operator=(KeyPair&&) noexcept = default;
273+
KeyPair& operator=(const KeyPair& other)
274+
{
275+
if (this != &other) {
276+
if (other.m_keypair) {
277+
MakeKeyPairData();
278+
*m_keypair = *other.m_keypair;
279+
} else {
280+
ClearKeyPairData();
281+
}
282+
}
283+
return *this;
284+
}
285+
286+
KeyPair(const KeyPair& other) { *this = other; }
287+
288+
friend KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const;
289+
[[nodiscard]] bool SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256& aux) const;
290+
291+
//! Check whether this keypair is valid.
292+
bool IsValid() const { return !!m_keypair; }
293+
294+
private:
295+
KeyPair(const CKey& key, const uint256* merkle_root);
296+
297+
using KeyType = std::array<unsigned char, 96>;
298+
secure_unique_ptr<KeyType> m_keypair;
299+
300+
void MakeKeyPairData()
301+
{
302+
if (!m_keypair) m_keypair = make_secure_unique<KeyType>();
303+
}
304+
305+
void ClearKeyPairData()
306+
{
307+
m_keypair.reset();
308+
}
309+
};
310+
238311
/** Check that required EC support is available at runtime. */
239312
bool ECC_InitSanityCheck();
240313

src/test/key_tests.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <key_io.h>
99
#include <span.h>
1010
#include <streams.h>
11+
#include <secp256k1_extrakeys.h>
1112
#include <test/util/random.h>
1213
#include <test/util/setup_common.h>
1314
#include <uint256.h>
@@ -299,6 +300,13 @@ BOOST_AUTO_TEST_CASE(bip340_test_vectors)
299300
// Verify those signatures for good measure.
300301
BOOST_CHECK(pubkey.VerifySchnorr(msg256, sig64));
301302

303+
// Repeat the same check, but use the KeyPair directly without any merkle tweak
304+
KeyPair keypair = key.ComputeKeyPair(/*merkle_root=*/nullptr);
305+
bool kp_ok = keypair.SignSchnorr(msg256, sig64, aux256);
306+
BOOST_CHECK(kp_ok);
307+
BOOST_CHECK(pubkey.VerifySchnorr(msg256, sig64));
308+
BOOST_CHECK(std::vector<unsigned char>(sig64, sig64 + 64) == sig);
309+
302310
// Do 10 iterations where we sign with a random Merkle root to tweak,
303311
// and compare against the resulting tweaked keys, with random aux.
304312
// In iteration i=0 we tweak with empty Merkle tree.
@@ -312,6 +320,12 @@ BOOST_AUTO_TEST_CASE(bip340_test_vectors)
312320
bool ok = key.SignSchnorr(msg256, sig64, &merkle_root, aux256);
313321
BOOST_CHECK(ok);
314322
BOOST_CHECK(tweaked_key.VerifySchnorr(msg256, sig64));
323+
324+
// Repeat the same check, but use the KeyPair class directly
325+
KeyPair keypair = key.ComputeKeyPair(&merkle_root);
326+
bool kp_ok = keypair.SignSchnorr(msg256, sig64, aux256);
327+
BOOST_CHECK(kp_ok);
328+
BOOST_CHECK(tweaked_key.VerifySchnorr(msg256, sig64));
315329
}
316330
}
317331
}
@@ -345,4 +359,31 @@ BOOST_AUTO_TEST_CASE(bip341_test_h)
345359
BOOST_CHECK(XOnlyPubKey::NUMS_H == H);
346360
}
347361

362+
BOOST_AUTO_TEST_CASE(key_schnorr_tweak_smoke_test)
363+
{
364+
// Sanity check to ensure we get the same tweak using CPubKey vs secp256k1 functions
365+
secp256k1_context* secp256k1_context_sign = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
366+
367+
CKey key;
368+
key.MakeNewKey(true);
369+
uint256 merkle_root = InsecureRand256();
370+
371+
// secp256k1 functions
372+
secp256k1_keypair keypair;
373+
BOOST_CHECK(secp256k1_keypair_create(secp256k1_context_sign, &keypair, UCharCast(key.begin())));
374+
secp256k1_xonly_pubkey xonly_pubkey;
375+
BOOST_CHECK(secp256k1_keypair_xonly_pub(secp256k1_context_sign, &xonly_pubkey, nullptr, &keypair));
376+
unsigned char xonly_bytes[32];
377+
BOOST_CHECK(secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, xonly_bytes, &xonly_pubkey));
378+
uint256 tweak_old = XOnlyPubKey(xonly_bytes).ComputeTapTweakHash(&merkle_root);
379+
380+
// CPubKey
381+
CPubKey pubkey = key.GetPubKey();
382+
uint256 tweak_new = XOnlyPubKey(pubkey).ComputeTapTweakHash(&merkle_root);
383+
384+
BOOST_CHECK_EQUAL(tweak_old, tweak_new);
385+
386+
secp256k1_context_destroy(secp256k1_context_sign);
387+
}
388+
348389
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)