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
This commit is contained in:
2026-02-08 00:55:02 +01:00
parent aee82dfad1
commit ac6ae69329
18 changed files with 319 additions and 19 deletions

View File

@@ -7,7 +7,10 @@
#include <crypto/common.h>
#include <crypto/hmac_sha512.h>
#include <crypto/sha256.h>
#include <random.h>
#include <util/strencodings.h>
#include <logging.h>
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
@@ -255,6 +258,43 @@ bool CKey::SignSchnorr(const uint256& hash, std::vector<unsigned char>& vchSig,
return true;
}
bool CKey::SignSchnorrTaproot(const uint256& hash, std::vector<unsigned char>& 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;

View File

@@ -130,6 +130,13 @@ public:
*/
bool SignSchnorr(const uint256& hash, std::vector<unsigned char>& 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<unsigned char>& 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.

View File

@@ -25,14 +25,21 @@ const std::array<OutputType, 4> 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;
}

View File

@@ -5,6 +5,7 @@
#include <pubkey.h>
#include <crypto/sha256.h>
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_recovery.h>
@@ -299,6 +300,63 @@ bool XOnlyPubKey::VerifySchnorr(const uint256& hash, const std::vector<unsigned
return secp256k1_schnorrsig_verify(secp256k1_context_verify, sig.data(), hash.begin(), 32, &pubkey);
}
uint256 XOnlyPubKey::ComputeTapTweak(const uint256* merkle_root) const
{
// BIP341: The tweak is H_TapTweak(internal_key || merkle_root)
// For key-path only, merkle_root is empty, so: H_TapTweak(internal_key)
// H_TapTweak is SHA256 with "TapTweak" tag
// Compute tagged hash: SHA256(SHA256("TapTweak") || SHA256("TapTweak") || data)
CSHA256 hasher;
unsigned char tag_hash[CSHA256::OUTPUT_SIZE];
const char* tag = "TapTweak";
CSHA256().Write((const unsigned char*)tag, 8).Finalize(tag_hash);
hasher.Write(tag_hash, sizeof(tag_hash));
hasher.Write(tag_hash, sizeof(tag_hash));
hasher.Write(m_keydata, SIZE);
if (merkle_root) {
hasher.Write(merkle_root->begin(), 32);
}
uint256 result;
hasher.Finalize(result.begin());
return result;
}
std::pair<XOnlyPubKey, bool> 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);

View File

@@ -87,6 +87,20 @@ public:
bool IsFullyValid() const;
bool VerifySchnorr(const uint256& hash, const std::vector<unsigned char>& 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<XOnlyPubKey, bool> CreatePayToTaprootPubKey(const uint256* merkle_root = nullptr) const;
};
/** An encapsulated public key. */

View File

@@ -11,6 +11,8 @@
#include <script/signingprovider.h>
#include <script/standard.h>
#include <uint256.h>
#include <logging.h>
#include <util/strencodings.h>
#include <algorithm>
#include <array>
@@ -26,11 +28,28 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid
return false;
if (sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT) {
if (!txdata || !txdata->m_bip341_taproot_ready || !key.IsCompressed()) return false;
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion, txdata);
if (!key.SignSchnorr(hash, vchSig)) {
if (!txdata) {
return false;
}
if (!txdata->m_bip341_taproot_ready) {
return false;
}
if (!key.IsCompressed()) {
return false;
}
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion, txdata);
// For Taproot key-path spending, we need to apply the taproot tweak before signing
// For TAPSCRIPT, we sign without the tweak (the script takes care of the commitment)
if (sigversion == SigVersion::TAPROOT) {
if (!key.SignSchnorrTaproot(hash, vchSig, nullptr)) {
return false;
}
} else {
// For script-path (TAPSCRIPT), use regular Schnorr signing
if (!key.SignSchnorr(hash, vchSig)) {
return false;
}
}
if (nHashType != SIGHASH_DEFAULT) {
vchSig.push_back((unsigned char)nHashType);
}
@@ -84,18 +103,43 @@ static bool GetPubKey(const SigningProvider& provider, const SignatureData& sigd
static bool FindTaprootPubKey(const SigningProvider& provider, const std::vector<unsigned char>& xonly_bytes, CPubKey& pubkey)
{
if (xonly_bytes.size() != WITNESS_V1_TAPROOT_SIZE) return false;
if (xonly_bytes.size() != WITNESS_V1_TAPROOT_SIZE) {
return false;
}
const XOnlyPubKey output_key(xonly_bytes.begin(), xonly_bytes.end());
if (!output_key.IsFullyValid()) return false;
if (!output_key.IsFullyValid()) {
return false;
}
// First, try to look up the internal key directly from the provider's mapping.
// This is the correct and efficient way for wallets that store the mapping.
if (provider.GetTaprootInternalKey(output_key, pubkey)) {
return true;
}
// Fallback: for backwards compatibility or providers without the mapping,
// enumerate keys and check if their tweaked version matches the output key.
// This is less efficient but ensures compatibility with older code.
std::array<unsigned char, CPubKey::COMPRESSED_SIZE> candidate;
std::copy(xonly_bytes.begin(), xonly_bytes.end(), candidate.begin() + 1);
for (unsigned char prefix : {0x02, 0x03}) {
candidate[0] = prefix;
const CPubKey candidate_pubkey(candidate.begin(), candidate.end());
if (!candidate_pubkey.IsFullyValid()) continue;
// Try to get a key from the provider
CPubKey found_pubkey;
if (provider.GetPubKey(candidate_pubkey.GetID(), found_pubkey) && found_pubkey.GetXOnlyPubKey() == output_key) {
if (!provider.GetPubKey(candidate_pubkey.GetID(), found_pubkey)) continue;
// Compute the tweaked output key for this internal key
XOnlyPubKey internal_key = found_pubkey.GetXOnlyPubKey();
if (!internal_key.IsFullyValid()) continue;
auto [tweaked_key, parity] = internal_key.CreatePayToTaprootPubKey(nullptr);
// Check if tweaked key matches the output key
if (tweaked_key == output_key) {
pubkey = found_pubkey;
return true;
}
@@ -213,7 +257,9 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
if (!FindTaprootPubKey(provider, vSolutions[0], pubkey)) {
return false;
}
if (!CreateSig(creator, sigdata, provider, sig, pubkey, scriptPubKey, SigVersion::TAPROOT)) return false;
// For Taproot key-path spending, scriptCode should be empty per BIP341
CScript empty_script;
if (!CreateSig(creator, sigdata, provider, sig, pubkey, empty_script, SigVersion::TAPROOT)) return false;
ret.push_back(std::move(sig));
return true;
}

View File

@@ -25,6 +25,7 @@ public:
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
virtual bool HaveKey(const CKeyID &address) const { return false; }
virtual bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return false; }
virtual bool GetTaprootInternalKey(const XOnlyPubKey& output_key, CPubKey& internal_key) const { return false; }
};
extern const SigningProvider& DUMMY_SIGNING_PROVIDER;

View File

@@ -1026,7 +1026,12 @@ bool MemPoolAccept::AcceptSingleTransaction(const CTransactionRef& ptx, ATMPArgs
// scripts (ie, other policy checks pass). We perform the inexpensive
// checks first and avoid hashing and signature verification unless those
// checks pass, to mitigate CPU exhaustion denial-of-service attacks.
PrecomputedTransactionData txdata(*ptx);
std::vector<CTxOut> spent_outputs;
spent_outputs.reserve(ptx->vin.size());
for (const auto& txin : ptx->vin) {
spent_outputs.emplace_back(m_view.AccessCoin(txin.prevout).out);
}
PrecomputedTransactionData txdata(*ptx, spent_outputs);
if (!PolicyScriptChecks(args, workspace, txdata)) return false;

View File

@@ -79,14 +79,35 @@ bool HaveKeys(const std::vector<valtype>& pubkeys, const LegacyScriptPubKeyMan&
bool HaveTaprootKey(const valtype& xonly, const LegacyScriptPubKeyMan& keystore)
{
if (xonly.size() != WITNESS_V1_TAPROOT_SIZE) return false;
const XOnlyPubKey output_key(xonly.begin(), xonly.end());
if (!output_key.IsFullyValid()) return false;
// First, try the new method: look up the internal key directly from our mapping
CPubKey internal_pubkey;
if (keystore.GetTaprootInternalKey(output_key, internal_pubkey)) {
// Verify we have the private key for this internal pubkey
return keystore.HaveKey(internal_pubkey.GetID());
}
// Fallback: for backwards compatibility, also check if we have a key
// whose tweak matches the output key. This handles cases where the
// mapping wasn't stored (e.g., older wallet versions).
std::array<unsigned char, CPubKey::COMPRESSED_SIZE> candidate;
std::copy(xonly.begin(), xonly.end(), candidate.begin() + 1);
candidate[0] = 0x02;
const CKeyID even_key(Hash160(candidate.begin(), candidate.end()));
if (keystore.HaveKey(even_key)) return true;
candidate[0] = 0x03;
const CKeyID odd_key(Hash160(candidate.begin(), candidate.end()));
return keystore.HaveKey(odd_key);
for (unsigned char prefix : {0x02, 0x03}) {
candidate[0] = prefix;
const CKeyID key_id(Hash160(candidate.begin(), candidate.end()));
CKey key;
if (keystore.GetKey(key_id, key)) {
// Found a key, check if its tweaked version matches output_key
XOnlyPubKey internal_key = key.GetPubKey().GetXOnlyPubKey();
auto [tweaked, parity] = internal_key.CreatePayToTaprootPubKey(nullptr);
if (tweaked == output_key) {
return true;
}
}
}
return false;
}
//! Recursively solve script and return spendable/watchonly/invalid status.
@@ -1333,6 +1354,17 @@ void LegacyScriptPubKeyMan::LearnRelatedScripts(const CPubKey& key, OutputType t
CScript taproot_prog = GetScriptForDestination(taproot_dest);
if (!taproot_prog.empty()) {
AddCScript(taproot_prog);
// Store the mapping from tweaked output key to internal key
// This allows us to find the internal key when signing
XOnlyPubKey internal_key = key.GetXOnlyPubKey();
auto [output_key, parity] = internal_key.CreatePayToTaprootPubKey(nullptr);
if (output_key.IsFullyValid()) {
// Convert XOnlyPubKey to uint256 for storage
uint256 output_key_hash;
memcpy(output_key_hash.begin(), output_key.data(), XOnlyPubKey::SIZE);
m_taproot_internal_keys[output_key_hash] = key;
}
}
}
}
@@ -1528,3 +1560,19 @@ std::set<CKeyID> LegacyScriptPubKeyMan::GetKeys() const
}
return set_address;
}
bool LegacyScriptPubKeyMan::GetTaprootInternalKey(const XOnlyPubKey& output_key, CPubKey& internal_key) const
{
LOCK(cs_KeyStore);
// Convert XOnlyPubKey to uint256 for lookup
uint256 output_key_hash;
memcpy(output_key_hash.begin(), output_key.data(), XOnlyPubKey::SIZE);
auto it = m_taproot_internal_keys.find(output_key_hash);
if (it != m_taproot_internal_keys.end()) {
internal_key = it->second;
return true;
}
return false;
}

View File

@@ -299,6 +299,10 @@ private:
// Tracks keypool indexes to CKeyIDs of keys that have been taken out of the keypool but may be returned to it
std::map<int64_t, CKeyID> m_index_to_reserved_key;
//! Map from tweaked Taproot output key (as XOnlyPubKey bytes stored in uint256) to internal CPubKey
//! This allows looking up the internal key needed for signing when given the output key from scriptPubKey
std::map<uint256, CPubKey> m_taproot_internal_keys GUARDED_BY(cs_KeyStore);
//! Fetches a key from the keypool
bool GetKeyFromPool(CPubKey &key, const OutputType type, bool internal = false);
@@ -415,6 +419,9 @@ public:
bool AddCScript(const CScript& redeemScript) override;
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
//! Get the internal key for a Taproot output key (returns false if not found)
bool GetTaprootInternalKey(const XOnlyPubKey& output_key, CPubKey& internal_key) const;
//! Load a keypool entry
void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool);
bool NewKeyPool();
@@ -477,6 +484,7 @@ public:
bool GetKey(const CKeyID &address, CKey& key) const override { return false; }
bool HaveKey(const CKeyID &address) const override { return false; }
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override { return m_spk_man.GetKeyOrigin(keyid, info); }
bool GetTaprootInternalKey(const XOnlyPubKey& output_key, CPubKey& internal_key) const override { return m_spk_man.GetTaprootInternalKey(output_key, internal_key); }
};
#endif // PALLADIUM_WALLET_SCRIPTPUBKEYMAN_H

View File

@@ -18,7 +18,7 @@ BOOST_AUTO_TEST_CASE(getwalletenv_file)
std::string test_name = "test_name.dat";
const fs::path datadir = GetDataDir();
fs::path file_path = datadir / test_name;
std::ofstream f(file_path.BOOST_FILESYSTEM_C_STR);
std::ofstream f(file_path.string().c_str());
f.close();
std::string filename;

View File

@@ -30,7 +30,7 @@ InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainNam
fs::create_directories(m_walletdir_path_cases["default"]);
fs::create_directories(m_walletdir_path_cases["custom"]);
fs::create_directories(m_walletdir_path_cases["relative"]);
std::ofstream f(m_walletdir_path_cases["file"].BOOST_FILESYSTEM_C_STR);
std::ofstream f(m_walletdir_path_cases["file"].string().c_str());
f.close();
}

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
from test_framework.test_framework import PalladiumTestFramework
from test_framework.util import assert_equal
class TaprootReproTest(PalladiumTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
self.extra_args = [[]] # standard args
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
node = self.nodes[0]
self.log.info("Create wallet")
node.createwallet("tr_test")
wallet = node.get_wallet_rpc("tr_test")
self.log.info("Generate blocks to get coins")
mining_addr = wallet.getnewaddress()
node.generatetoaddress(125, mining_addr)
self.log.info(f"Wallet Info: {wallet.getwalletinfo()}")
self.log.info(f"Unspent: {wallet.listunspent()}")
self.log.info(f"Address Info: {wallet.getaddressinfo(mining_addr)}")
balance_start = wallet.getbalance()
self.log.info(f"Balance: {balance_start}")
self.log.info("Get new P2TR address (bech32m)")
tr_addr = wallet.getnewaddress("", "bech32m")
self.log.info(f"P2TR Address: {tr_addr}")
self.log.info("Send funds TO P2TR address")
txid_to = wallet.sendtoaddress(tr_addr, 1.0)
self.log.info(f"Sent to P2TR, txid: {txid_to}")
node.generatetoaddress(1, mining_addr)
# Check that the wallet sees the funds
unspent = wallet.listunspent(0, 999999, [tr_addr])
assert_equal(len(unspent), 1)
assert_equal(unspent[0]['amount'], 1.0)
self.log.info("Funds confirmed in P2TR address")
self.log.info("Attempt to spend FROM P2TR address")
dest_addr = wallet.getnewaddress("", "bech32")
try:
txid_from = wallet.sendtoaddress(dest_addr, 0.5)
self.log.info(f"Spent from P2TR, txid: {txid_from}")
node.generatetoaddress(1, mining_addr)
# Verify transaction is confirmed
tx = wallet.gettransaction(txid_from)
assert_equal(tx['confirmations'], 1)
self.log.info("P2TR spend confirmed success!")
except Exception as e:
self.log.error(f"Failed to spend from P2TR: {e}")
raise
if __name__ == '__main__':
TaprootReproTest().main()

0
test/functional/rpc_createmultisig.py Normal file → Executable file
View File

0
test/functional/rpc_psbt.py Normal file → Executable file
View File

0
test/functional/test_runner.py Normal file → Executable file
View File

0
test/functional/wallet_address_types.py Normal file → Executable file
View File

0
test/functional/wallet_basic.py Normal file → Executable file
View File