From ac6ae69329d75e73646b0dfcecc996539432d006 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 8 Feb 2026 00:55:02 +0100 Subject: [PATCH] fix: implement BIP341 key tweaking for P2TR transaction signing This commit fixes P2TR (Pay-to-Taproot) transaction signing by properly implementing BIP341 key path spending. Key changes: - Add SignSchnorrTaproot() method to CKey for BIP341 tweaked signing - Implement ComputeTapTweak() and CreatePayToTaprootPubKey() in XOnlyPubKey - Add GetTaprootInternalKey() to SigningProvider interface for internal key lookup - Store taproot internal key mappings in LegacyScriptPubKeyMan - Fix FindTaprootPubKey() to use internal key mapping with fallback - Use empty scriptCode for Taproot key-path spending (per BIP341 spec) - Update HaveTaprootKey() to verify tweaked keys correctly Technical details: - Internal keys are tweaked using secp256k1_keypair_xonly_tweak_add - Parity handling is automatic via secp256k1 library - Empty scriptCode ensures correct sighash for key-path spending - Internal key to output key mapping stored for efficient lookup Testing: - P2TR address creation, funding, and spending work end-to-end - Multi-hop P2TR transactions tested successfully - All functional tests pass (feature_taproot.py, wallet_*, rpc_*) Fixes: non-mandatory-script-verify-flag error on P2TR spending --- src/key.cpp | 40 +++++++++++++++ src/key.h | 7 +++ src/outputtype.cpp | 13 +++-- src/pubkey.cpp | 58 ++++++++++++++++++++++ src/pubkey.h | 14 ++++++ src/script/sign.cpp | 60 +++++++++++++++++++--- src/script/signingprovider.h | 1 + src/validation.cpp | 7 ++- src/wallet/scriptpubkeyman.cpp | 60 +++++++++++++++++++--- src/wallet/scriptpubkeyman.h | 8 +++ src/wallet/test/db_tests.cpp | 2 +- src/wallet/test/init_test_fixture.cpp | 2 +- test/functional/feature_taproot.py | 66 +++++++++++++++++++++++++ test/functional/rpc_createmultisig.py | 0 test/functional/rpc_psbt.py | 0 test/functional/test_runner.py | 0 test/functional/wallet_address_types.py | 0 test/functional/wallet_basic.py | 0 18 files changed, 319 insertions(+), 19 deletions(-) create mode 100755 test/functional/feature_taproot.py mode change 100644 => 100755 test/functional/rpc_createmultisig.py mode change 100644 => 100755 test/functional/rpc_psbt.py mode change 100644 => 100755 test/functional/test_runner.py mode change 100644 => 100755 test/functional/wallet_address_types.py mode change 100644 => 100755 test/functional/wallet_basic.py diff --git a/src/key.cpp b/src/key.cpp index ddf92d0..70ada8e 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -7,7 +7,10 @@ #include #include +#include #include +#include +#include #include #include @@ -255,6 +258,43 @@ bool CKey::SignSchnorr(const uint256& hash, std::vector& vchSig, return true; } +bool CKey::SignSchnorrTaproot(const uint256& hash, std::vector& vchSig, const uint256* merkle_root, const unsigned char* aux) const +{ + if (!fValid) { + return false; + } + + // BIP341: Create keypair from internal private key + secp256k1_keypair keypair; + if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, begin())) { + return false; + } + + // Get the internal x-only public key to compute the tweak + CPubKey internal_pubkey = GetPubKey(); + XOnlyPubKey internal_xonly = internal_pubkey.GetXOnlyPubKey(); + + // Compute BIP341 TapTweak: tagged_hash("TapTweak", internal_xonly || merkle_root) + uint256 tweak = internal_xonly.ComputeTapTweak(merkle_root); + + // Apply BIP341 tweak to the keypair + // This handles the parity negation automatically: + // - If internal pubkey has odd y, it negates the privkey before adding tweak + // - Then adds tweak: tweaked_privkey = (possibly_negated_privkey + tweak) mod n + if (!secp256k1_keypair_xonly_tweak_add(secp256k1_context_sign, &keypair, tweak.begin())) { + return false; + } + + // Sign with the tweaked keypair using BIP340 Schnorr + vchSig.resize(XOnlyPubKey::SCHNORR_SIGNATURE_SIZE); + if (!secp256k1_schnorrsig_sign32(secp256k1_context_sign, vchSig.data(), hash.begin(), &keypair, aux)) { + vchSig.clear(); + return false; + } + + return true; +} + bool CKey::VerifyPubKey(const CPubKey& pubkey) const { if (pubkey.IsCompressed() != fCompressed) { return false; diff --git a/src/key.h b/src/key.h index c330525..3b41d34 100644 --- a/src/key.h +++ b/src/key.h @@ -130,6 +130,13 @@ public: */ bool SignSchnorr(const uint256& hash, std::vector& vchSig, const unsigned char* aux = nullptr) const; + /** + * Create a 64-byte BIP340 Schnorr signature for Taproot key-path spending. + * This applies the Taproot tweak before signing. + * merkle_root: nullptr for key-path only spend (no script tree) + */ + bool SignSchnorrTaproot(const uint256& hash, std::vector& vchSig, const uint256* merkle_root = nullptr, const unsigned char* aux = nullptr) const; + /** * Create a compact signature (65 bytes), which allows reconstructing the used public key. * The format is one header byte, followed by two times 32 bytes for the serialized r and s values. diff --git a/src/outputtype.cpp b/src/outputtype.cpp index a6ea3a6..1d0df42 100644 --- a/src/outputtype.cpp +++ b/src/outputtype.cpp @@ -25,14 +25,21 @@ const std::array OUTPUT_TYPES = {OutputType::LEGACY, OutputType:: static CTxDestination GetDestinationForTaprootKey(const CPubKey& key) { - XOnlyPubKey xonly = key.GetXOnlyPubKey(); - if (!xonly.IsFullyValid()) { + XOnlyPubKey internal_key = key.GetXOnlyPubKey(); + if (!internal_key.IsFullyValid()) { return CNoDestination(); } + + // Compute the tweaked output key: output_key = internal_key + H(internal_key) * G + auto [output_key, parity] = internal_key.CreatePayToTaprootPubKey(nullptr); + if (!output_key.IsFullyValid()) { + return CNoDestination(); + } + WitnessUnknown dest; dest.version = 1; dest.length = WITNESS_V1_TAPROOT_SIZE; - std::copy(xonly.begin(), xonly.end(), dest.program); + std::copy(output_key.begin(), output_key.end(), dest.program); return dest; } diff --git a/src/pubkey.cpp b/src/pubkey.cpp index fbfdac1..f6a2794 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -5,6 +5,7 @@ #include +#include #include #include #include @@ -299,6 +300,63 @@ bool XOnlyPubKey::VerifySchnorr(const uint256& hash, const std::vectorbegin(), 32); + } + + uint256 result; + hasher.Finalize(result.begin()); + return result; +} + +std::pair XOnlyPubKey::CreatePayToTaprootPubKey(const uint256* merkle_root) const +{ + assert(secp256k1_context_verify && "secp256k1_context_verify must be initialized to use XOnlyPubKey."); + + // Parse internal pubkey + secp256k1_xonly_pubkey internal_pubkey; + if (!secp256k1_xonly_pubkey_parse(secp256k1_context_verify, &internal_pubkey, m_keydata)) { + return {XOnlyPubKey(), false}; + } + + // Compute tweak + uint256 tweak = ComputeTapTweak(merkle_root); + + // Apply tweak to get output pubkey + secp256k1_pubkey output_pubkey; + if (!secp256k1_xonly_pubkey_tweak_add(secp256k1_context_verify, &output_pubkey, &internal_pubkey, tweak.begin())) { + return {XOnlyPubKey(), false}; + } + + // Convert to xonly and get parity + secp256k1_xonly_pubkey output_xonly; + int parity; + if (!secp256k1_xonly_pubkey_from_pubkey(secp256k1_context_verify, &output_xonly, &parity, &output_pubkey)) { + return {XOnlyPubKey(), false}; + } + + // Serialize the output xonly pubkey + unsigned char output_keydata[SIZE]; + secp256k1_xonly_pubkey_serialize(secp256k1_context_verify, output_keydata, &output_xonly); + + return {XOnlyPubKey(output_keydata, output_keydata + SIZE), parity != 0}; +} + void CExtPubKey::Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const { code[0] = nDepth; memcpy(code+1, vchFingerprint, 4); diff --git a/src/pubkey.h b/src/pubkey.h index da9efa8..7b00ae9 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -87,6 +87,20 @@ public: bool IsFullyValid() const; bool VerifySchnorr(const uint256& hash, const std::vector& sig) const; + + /** + * Compute the BIP340/BIP341 tagged hash for TapTweak. + * tweak = H_TapTweak(xonly_pubkey || merkle_root) if merkle_root is provided + * tweak = H_TapTweak(xonly_pubkey) otherwise (key-path only spend) + */ + uint256 ComputeTapTweak(const uint256* merkle_root = nullptr) const; + + /** + * Compute the Taproot output key from this internal key. + * output_key = internal_key + H_TapTweak(internal_key || merkle_root) * G + * Returns the tweaked output key and optionally the parity. + */ + std::pair CreatePayToTaprootPubKey(const uint256* merkle_root = nullptr) const; }; /** An encapsulated public key. */ diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 809c515..01a7129 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -11,6 +11,8 @@ #include