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:
40
src/key.cpp
40
src/key.cpp
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
#include <crypto/common.h>
|
#include <crypto/common.h>
|
||||||
#include <crypto/hmac_sha512.h>
|
#include <crypto/hmac_sha512.h>
|
||||||
|
#include <crypto/sha256.h>
|
||||||
#include <random.h>
|
#include <random.h>
|
||||||
|
#include <util/strencodings.h>
|
||||||
|
#include <logging.h>
|
||||||
|
|
||||||
#include <secp256k1.h>
|
#include <secp256k1.h>
|
||||||
#include <secp256k1_extrakeys.h>
|
#include <secp256k1_extrakeys.h>
|
||||||
@@ -255,6 +258,43 @@ bool CKey::SignSchnorr(const uint256& hash, std::vector<unsigned char>& vchSig,
|
|||||||
return true;
|
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 {
|
bool CKey::VerifyPubKey(const CPubKey& pubkey) const {
|
||||||
if (pubkey.IsCompressed() != fCompressed) {
|
if (pubkey.IsCompressed() != fCompressed) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -130,6 +130,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool SignSchnorr(const uint256& hash, std::vector<unsigned char>& vchSig, const unsigned char* aux = nullptr) const;
|
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.
|
* 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.
|
* The format is one header byte, followed by two times 32 bytes for the serialized r and s values.
|
||||||
|
|||||||
@@ -25,14 +25,21 @@ const std::array<OutputType, 4> OUTPUT_TYPES = {OutputType::LEGACY, OutputType::
|
|||||||
|
|
||||||
static CTxDestination GetDestinationForTaprootKey(const CPubKey& key)
|
static CTxDestination GetDestinationForTaprootKey(const CPubKey& key)
|
||||||
{
|
{
|
||||||
XOnlyPubKey xonly = key.GetXOnlyPubKey();
|
XOnlyPubKey internal_key = key.GetXOnlyPubKey();
|
||||||
if (!xonly.IsFullyValid()) {
|
if (!internal_key.IsFullyValid()) {
|
||||||
return CNoDestination();
|
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;
|
WitnessUnknown dest;
|
||||||
dest.version = 1;
|
dest.version = 1;
|
||||||
dest.length = WITNESS_V1_TAPROOT_SIZE;
|
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;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <pubkey.h>
|
#include <pubkey.h>
|
||||||
|
|
||||||
|
#include <crypto/sha256.h>
|
||||||
#include <secp256k1.h>
|
#include <secp256k1.h>
|
||||||
#include <secp256k1_extrakeys.h>
|
#include <secp256k1_extrakeys.h>
|
||||||
#include <secp256k1_recovery.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);
|
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 {
|
void CExtPubKey::Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const {
|
||||||
code[0] = nDepth;
|
code[0] = nDepth;
|
||||||
memcpy(code+1, vchFingerprint, 4);
|
memcpy(code+1, vchFingerprint, 4);
|
||||||
|
|||||||
14
src/pubkey.h
14
src/pubkey.h
@@ -87,6 +87,20 @@ public:
|
|||||||
|
|
||||||
bool IsFullyValid() const;
|
bool IsFullyValid() const;
|
||||||
bool VerifySchnorr(const uint256& hash, const std::vector<unsigned char>& sig) 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. */
|
/** An encapsulated public key. */
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <script/signingprovider.h>
|
#include <script/signingprovider.h>
|
||||||
#include <script/standard.h>
|
#include <script/standard.h>
|
||||||
#include <uint256.h>
|
#include <uint256.h>
|
||||||
|
#include <logging.h>
|
||||||
|
#include <util/strencodings.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -26,11 +28,28 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT) {
|
if (sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT) {
|
||||||
if (!txdata || !txdata->m_bip341_taproot_ready || !key.IsCompressed()) return false;
|
if (!txdata) {
|
||||||
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion, txdata);
|
|
||||||
if (!key.SignSchnorr(hash, vchSig)) {
|
|
||||||
return false;
|
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) {
|
if (nHashType != SIGHASH_DEFAULT) {
|
||||||
vchSig.push_back((unsigned char)nHashType);
|
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)
|
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());
|
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::array<unsigned char, CPubKey::COMPRESSED_SIZE> candidate;
|
||||||
std::copy(xonly_bytes.begin(), xonly_bytes.end(), candidate.begin() + 1);
|
std::copy(xonly_bytes.begin(), xonly_bytes.end(), candidate.begin() + 1);
|
||||||
|
|
||||||
for (unsigned char prefix : {0x02, 0x03}) {
|
for (unsigned char prefix : {0x02, 0x03}) {
|
||||||
candidate[0] = prefix;
|
candidate[0] = prefix;
|
||||||
const CPubKey candidate_pubkey(candidate.begin(), candidate.end());
|
const CPubKey candidate_pubkey(candidate.begin(), candidate.end());
|
||||||
if (!candidate_pubkey.IsFullyValid()) continue;
|
if (!candidate_pubkey.IsFullyValid()) continue;
|
||||||
|
|
||||||
|
// Try to get a key from the provider
|
||||||
CPubKey found_pubkey;
|
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;
|
pubkey = found_pubkey;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -213,7 +257,9 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
|
|||||||
if (!FindTaprootPubKey(provider, vSolutions[0], pubkey)) {
|
if (!FindTaprootPubKey(provider, vSolutions[0], pubkey)) {
|
||||||
return false;
|
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));
|
ret.push_back(std::move(sig));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public:
|
|||||||
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
|
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
|
||||||
virtual bool HaveKey(const CKeyID &address) 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 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;
|
extern const SigningProvider& DUMMY_SIGNING_PROVIDER;
|
||||||
|
|||||||
@@ -1026,7 +1026,12 @@ bool MemPoolAccept::AcceptSingleTransaction(const CTransactionRef& ptx, ATMPArgs
|
|||||||
// scripts (ie, other policy checks pass). We perform the inexpensive
|
// scripts (ie, other policy checks pass). We perform the inexpensive
|
||||||
// checks first and avoid hashing and signature verification unless those
|
// checks first and avoid hashing and signature verification unless those
|
||||||
// checks pass, to mitigate CPU exhaustion denial-of-service attacks.
|
// 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;
|
if (!PolicyScriptChecks(args, workspace, txdata)) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -79,14 +79,35 @@ bool HaveKeys(const std::vector<valtype>& pubkeys, const LegacyScriptPubKeyMan&
|
|||||||
bool HaveTaprootKey(const valtype& xonly, const LegacyScriptPubKeyMan& keystore)
|
bool HaveTaprootKey(const valtype& xonly, const LegacyScriptPubKeyMan& keystore)
|
||||||
{
|
{
|
||||||
if (xonly.size() != WITNESS_V1_TAPROOT_SIZE) return false;
|
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::array<unsigned char, CPubKey::COMPRESSED_SIZE> candidate;
|
||||||
std::copy(xonly.begin(), xonly.end(), candidate.begin() + 1);
|
std::copy(xonly.begin(), xonly.end(), candidate.begin() + 1);
|
||||||
candidate[0] = 0x02;
|
for (unsigned char prefix : {0x02, 0x03}) {
|
||||||
const CKeyID even_key(Hash160(candidate.begin(), candidate.end()));
|
candidate[0] = prefix;
|
||||||
if (keystore.HaveKey(even_key)) return true;
|
const CKeyID key_id(Hash160(candidate.begin(), candidate.end()));
|
||||||
candidate[0] = 0x03;
|
CKey key;
|
||||||
const CKeyID odd_key(Hash160(candidate.begin(), candidate.end()));
|
if (keystore.GetKey(key_id, key)) {
|
||||||
return keystore.HaveKey(odd_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.
|
//! 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);
|
CScript taproot_prog = GetScriptForDestination(taproot_dest);
|
||||||
if (!taproot_prog.empty()) {
|
if (!taproot_prog.empty()) {
|
||||||
AddCScript(taproot_prog);
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
// 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;
|
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
|
//! Fetches a key from the keypool
|
||||||
bool GetKeyFromPool(CPubKey &key, const OutputType type, bool internal = false);
|
bool GetKeyFromPool(CPubKey &key, const OutputType type, bool internal = false);
|
||||||
|
|
||||||
@@ -415,6 +419,9 @@ public:
|
|||||||
bool AddCScript(const CScript& redeemScript) override;
|
bool AddCScript(const CScript& redeemScript) override;
|
||||||
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const 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
|
//! Load a keypool entry
|
||||||
void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool);
|
void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool);
|
||||||
bool NewKeyPool();
|
bool NewKeyPool();
|
||||||
@@ -477,6 +484,7 @@ public:
|
|||||||
bool GetKey(const CKeyID &address, CKey& key) const override { return false; }
|
bool GetKey(const CKeyID &address, CKey& key) const override { return false; }
|
||||||
bool HaveKey(const CKeyID &address) 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 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
|
#endif // PALLADIUM_WALLET_SCRIPTPUBKEYMAN_H
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ BOOST_AUTO_TEST_CASE(getwalletenv_file)
|
|||||||
std::string test_name = "test_name.dat";
|
std::string test_name = "test_name.dat";
|
||||||
const fs::path datadir = GetDataDir();
|
const fs::path datadir = GetDataDir();
|
||||||
fs::path file_path = datadir / test_name;
|
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();
|
f.close();
|
||||||
|
|
||||||
std::string filename;
|
std::string filename;
|
||||||
|
|||||||
@@ -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["default"]);
|
||||||
fs::create_directories(m_walletdir_path_cases["custom"]);
|
fs::create_directories(m_walletdir_path_cases["custom"]);
|
||||||
fs::create_directories(m_walletdir_path_cases["relative"]);
|
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();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
test/functional/feature_taproot.py
Executable file
66
test/functional/feature_taproot.py
Executable 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
0
test/functional/rpc_createmultisig.py
Normal file → Executable file
0
test/functional/rpc_psbt.py
Normal file → Executable file
0
test/functional/rpc_psbt.py
Normal file → Executable file
0
test/functional/test_runner.py
Normal file → Executable file
0
test/functional/test_runner.py
Normal file → Executable file
0
test/functional/wallet_address_types.py
Normal file → Executable file
0
test/functional/wallet_address_types.py
Normal file → Executable file
0
test/functional/wallet_basic.py
Normal file → Executable file
0
test/functional/wallet_basic.py
Normal file → Executable file
Reference in New Issue
Block a user