mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-31 10:41:08 +00:00
Merge bitcoin/bitcoin#29675: wallet: Be able to receive and spend inputs involving MuSig2 aggregate keys
ac599c4a9cb3b2d424932d3fd91f9eed17426827 test: Test MuSig2 in the wallet (Ava Chow)
68ef954c4c59802a6810a462eaa8dd61728ba820 wallet: Keep secnonces in DescriptorScriptPubKeyMan (Ava Chow)
4a273edda0ec10f0c5ae5d94b9925fa334d1c6e6 sign: Create MuSig2 signatures for known MuSig2 aggregate keys (Ava Chow)
258db938899409c8ee1cef04e16ba1795ea0038d sign: Add CreateMuSig2AggregateSig (Ava Chow)
bf69442b3f5004dc3df5a1b1d752114ba68fa5f4 sign: Add CreateMuSig2PartialSig (Ava Chow)
512b17fc56eac3a2e2b9ba489b5423d098cce0db sign: Add CreateMuSig2Nonce (Ava Chow)
82ea67c607cde6187d7082429d27b927dc21c0c6 musig: Add MuSig2AggregatePubkeys variant that validates the aggregate (Ava Chow)
d99a081679e16668458512aba2fd13a3e1bdb09f psbt: MuSig2 data in Fill/FromSignatureData (Ava Chow)
4d8b4f53363f013ed3972997f0b05b9c19e9db9d signingprovider: Add musig2 secnonces (Ava Chow)
c06a1dc86ff2347538e95041ab7b97af25342958 Add MuSig2SecNonce class for secure allocation of musig nonces (Ava Chow)
9baff05e494443cd82708490f384aa3034ad43bd sign: Include taproot output key's KeyOriginInfo in sigdata (Ava Chow)
4b24bfeab9d6732aae3e69efd33105792ef1198f pubkey: Return tweaks from BIP32 derivation (Ava Chow)
f14876213aad0e67088b75cae24323db9f2576d8 musig: Move synthetic xpub construction to its own function (Ava Chow)
fb8720f1e09f4e41802f07be53fb220d6f6c127f sign: Refactor Schnorr sighash computation out of CreateSchnorrSig (Ava Chow)
a4cfddda644f1fc9a815b2d16c997716cd63554a tests: Clarify why musig derivation adds a pubkey and xpub (Ava Chow)
39a63bf2e7e38dd3f30b5d1a8f6b2fff0e380d12 descriptors: Add a doxygen comment for has_hardened output_parameter (Ava Chow)
2320184d0ea87279558a8e6cbb3bccf5ba1bb781 descriptors: Fix meaning of any_key_parsed (Ava Chow)
Pull request description:
This PR implements MuSig2 signing so that the wallet can receive and spend from imported `musig(0` descriptors.
The libsecp musig module is enabled so that it can be used for all of the MuSig2 cryptography.
Secnonces are handled in a separate class which holds the libsecp secnonce object in a `secure_unique_ptr`. Since secnonces must not be used, this class has no serialization and will only live in memory. A restart of the software will require a restart of the MuSig2 signing process.
ACKs for top commit:
fjahr:
tACK ac599c4a9cb3b2d424932d3fd91f9eed17426827
rkrux:
lgtm tACK ac599c4a9cb3b2d424932d3fd91f9eed17426827
theStack:
Code-review ACK ac599c4a9cb3b2d424932d3fd91f9eed17426827 🗝️
Tree-SHA512: 626b9adc42ed2403e2f4405321eb9ce009a829c07d968e95ab288fe4940b195b0af35ca279a4a7fa51af76e55382bad6f63a23bca14a84140559b3c667e7041e
This commit is contained in:
commit
48aa0e98d0
123
src/key.cpp
123
src/key.cpp
@ -13,6 +13,7 @@
|
||||
#include <secp256k1.h>
|
||||
#include <secp256k1_ellswift.h>
|
||||
#include <secp256k1_extrakeys.h>
|
||||
#include <secp256k1_musig.h>
|
||||
#include <secp256k1_recovery.h>
|
||||
#include <secp256k1_schnorrsig.h>
|
||||
|
||||
@ -349,6 +350,128 @@ KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const
|
||||
return KeyPair(*this, merkle_root);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys)
|
||||
{
|
||||
// Get the keyagg cache and aggregate pubkey
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return {};
|
||||
|
||||
// Parse participant pubkey
|
||||
CPubKey our_pubkey = GetPubKey();
|
||||
secp256k1_pubkey pubkey;
|
||||
if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, our_pubkey.data(), our_pubkey.size())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Generate randomness for nonce
|
||||
uint256 rand;
|
||||
GetStrongRandBytes(rand);
|
||||
|
||||
// Generate nonce
|
||||
secp256k1_musig_pubnonce pubnonce;
|
||||
if (!secp256k1_musig_nonce_gen(secp256k1_context_sign, secnonce.Get(), &pubnonce, rand.data(), UCharCast(begin()), &pubkey, sighash.data(), &keyagg_cache, nullptr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Serialize pubnonce
|
||||
std::vector<uint8_t> out;
|
||||
out.resize(MUSIG2_PUBNONCE_SIZE);
|
||||
if (!secp256k1_musig_pubnonce_serialize(secp256k1_context_static, out.data(), &pubnonce)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<uint256> CKey::CreateMuSig2PartialSig(const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks)
|
||||
{
|
||||
secp256k1_keypair keypair;
|
||||
if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, UCharCast(begin()))) return std::nullopt;
|
||||
|
||||
// Get the keyagg cache and aggregate pubkey
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
|
||||
|
||||
// Check that there are enough pubnonces
|
||||
if (pubnonces.size() != pubkeys.size()) return std::nullopt;
|
||||
|
||||
// Parse the pubnonces
|
||||
std::vector<std::pair<secp256k1_pubkey, secp256k1_musig_pubnonce>> signers_data;
|
||||
std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
|
||||
std::optional<size_t> our_pubkey_idx;
|
||||
CPubKey our_pubkey = GetPubKey();
|
||||
for (const CPubKey& part_pk : pubkeys) {
|
||||
const auto& pn_it = pubnonces.find(part_pk);
|
||||
if (pn_it == pubnonces.end()) return std::nullopt;
|
||||
const std::vector<uint8_t> pubnonce = pn_it->second;
|
||||
if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) return std::nullopt;
|
||||
if (part_pk == our_pubkey) {
|
||||
our_pubkey_idx = signers_data.size();
|
||||
}
|
||||
|
||||
auto& [secp_pk, secp_pn] = signers_data.emplace_back();
|
||||
|
||||
if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &secp_pk, part_pk.data(), part_pk.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!secp256k1_musig_pubnonce_parse(secp256k1_context_static, &secp_pn, pubnonce.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
if (our_pubkey_idx == std::nullopt) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pubnonce_ptrs.reserve(signers_data.size());
|
||||
for (auto& [_, pn] : signers_data) {
|
||||
pubnonce_ptrs.push_back(&pn);
|
||||
}
|
||||
|
||||
// Aggregate nonces
|
||||
secp256k1_musig_aggnonce aggnonce;
|
||||
if (!secp256k1_musig_nonce_agg(secp256k1_context_static, &aggnonce, pubnonce_ptrs.data(), pubnonce_ptrs.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Apply tweaks
|
||||
for (const auto& [tweak, xonly] : tweaks) {
|
||||
if (xonly) {
|
||||
if (!secp256k1_musig_pubkey_xonly_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
} else if (!secp256k1_musig_pubkey_ec_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Create musig_session
|
||||
secp256k1_musig_session session;
|
||||
if (!secp256k1_musig_nonce_process(secp256k1_context_static, &session, &aggnonce, sighash.data(), &keyagg_cache)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Create partial signature
|
||||
secp256k1_musig_partial_sig psig;
|
||||
if (!secp256k1_musig_partial_sign(secp256k1_context_static, &psig, secnonce.Get(), &keypair, &keyagg_cache, &session)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// The secnonce must be deleted after signing to prevent nonce reuse.
|
||||
secnonce.Invalidate();
|
||||
|
||||
// Verify partial signature
|
||||
if (!secp256k1_musig_partial_sig_verify(secp256k1_context_static, &psig, &(signers_data.at(*our_pubkey_idx).second), &(signers_data.at(*our_pubkey_idx).first), &keyagg_cache, &session)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Serialize
|
||||
uint256 sig;
|
||||
if (!secp256k1_musig_partial_sig_serialize(secp256k1_context_static, sig.data(), &psig)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
CKey GenerateRandomKey(bool compressed) noexcept
|
||||
{
|
||||
CKey key;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#ifndef BITCOIN_KEY_H
|
||||
#define BITCOIN_KEY_H
|
||||
|
||||
#include <musig.h>
|
||||
#include <pubkey.h>
|
||||
#include <serialize.h>
|
||||
#include <support/allocators/secure.h>
|
||||
@ -220,6 +221,9 @@ public:
|
||||
* Merkle root of the script tree).
|
||||
*/
|
||||
KeyPair ComputeKeyPair(const uint256* merkle_root) const;
|
||||
|
||||
std::vector<uint8_t> CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys);
|
||||
std::optional<uint256> CreateMuSig2PartialSig(const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks);
|
||||
};
|
||||
|
||||
CKey GenerateRandomKey(bool compressed = true) noexcept;
|
||||
|
||||
163
src/musig.cpp
163
src/musig.cpp
@ -3,10 +3,11 @@
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <musig.h>
|
||||
#include <support/allocators/secure.h>
|
||||
|
||||
#include <secp256k1_musig.h>
|
||||
|
||||
bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache)
|
||||
static bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache)
|
||||
{
|
||||
// Parse the pubkeys
|
||||
std::vector<secp256k1_pubkey> secp_pubkeys;
|
||||
@ -28,7 +29,7 @@ bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_k
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_cache& keyagg_cache)
|
||||
static std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_cache& keyagg_cache)
|
||||
{
|
||||
// Get the plain aggregated pubkey
|
||||
secp256k1_pubkey agg_pubkey;
|
||||
@ -43,11 +44,163 @@ std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_ca
|
||||
return CPubKey(ser_agg_pubkey, ser_agg_pubkey + ser_agg_pubkey_len);
|
||||
}
|
||||
|
||||
std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys)
|
||||
std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache, const std::optional<CPubKey>& expected_aggregate)
|
||||
{
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
if (!GetMuSig2KeyAggCache(pubkeys, keyagg_cache)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return GetCPubKeyFromMuSig2KeyAggCache(keyagg_cache);
|
||||
std::optional<CPubKey> agg_key = GetCPubKeyFromMuSig2KeyAggCache(keyagg_cache);
|
||||
if (!agg_key.has_value()) return std::nullopt;
|
||||
if (expected_aggregate.has_value() && expected_aggregate != agg_key) return std::nullopt;
|
||||
return agg_key;
|
||||
}
|
||||
|
||||
std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys)
|
||||
{
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
return MuSig2AggregatePubkeys(pubkeys, keyagg_cache, std::nullopt);
|
||||
}
|
||||
|
||||
CExtPubKey CreateMuSig2SyntheticXpub(const CPubKey& pubkey)
|
||||
{
|
||||
CExtPubKey extpub;
|
||||
extpub.nDepth = 0;
|
||||
std::memset(extpub.vchFingerprint, 0, 4);
|
||||
extpub.nChild = 0;
|
||||
extpub.chaincode = MUSIG_CHAINCODE;
|
||||
extpub.pubkey = pubkey;
|
||||
return extpub;
|
||||
}
|
||||
|
||||
class MuSig2SecNonceImpl
|
||||
{
|
||||
private:
|
||||
//! The actual secnonce itself
|
||||
secure_unique_ptr<secp256k1_musig_secnonce> m_nonce;
|
||||
|
||||
public:
|
||||
MuSig2SecNonceImpl() : m_nonce{make_secure_unique<secp256k1_musig_secnonce>()} {}
|
||||
|
||||
// Delete copy constructors
|
||||
MuSig2SecNonceImpl(const MuSig2SecNonceImpl&) = delete;
|
||||
MuSig2SecNonceImpl& operator=(const MuSig2SecNonceImpl&) = delete;
|
||||
|
||||
secp256k1_musig_secnonce* Get() const { return m_nonce.get(); }
|
||||
void Invalidate() { m_nonce.reset(); }
|
||||
bool IsValid() { return m_nonce != nullptr; }
|
||||
};
|
||||
|
||||
MuSig2SecNonce::MuSig2SecNonce() : m_impl{std::make_unique<MuSig2SecNonceImpl>()} {}
|
||||
|
||||
MuSig2SecNonce::MuSig2SecNonce(MuSig2SecNonce&&) noexcept = default;
|
||||
MuSig2SecNonce& MuSig2SecNonce::operator=(MuSig2SecNonce&&) noexcept = default;
|
||||
|
||||
MuSig2SecNonce::~MuSig2SecNonce() = default;
|
||||
|
||||
secp256k1_musig_secnonce* MuSig2SecNonce::Get() const
|
||||
{
|
||||
return m_impl->Get();
|
||||
}
|
||||
|
||||
void MuSig2SecNonce::Invalidate()
|
||||
{
|
||||
return m_impl->Invalidate();
|
||||
}
|
||||
|
||||
bool MuSig2SecNonce::IsValid()
|
||||
{
|
||||
return m_impl->IsValid();
|
||||
}
|
||||
|
||||
uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash)
|
||||
{
|
||||
HashWriter hasher;
|
||||
hasher << script_pubkey << part_pubkey << sighash;
|
||||
return hasher.GetSHA256();
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& part_pubkeys, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs)
|
||||
{
|
||||
if (!part_pubkeys.size()) return std::nullopt;
|
||||
|
||||
// Get the keyagg cache and aggregate pubkey
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
if (!MuSig2AggregatePubkeys(part_pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
|
||||
|
||||
// Check if enough pubnonces and partial sigs
|
||||
if (pubnonces.size() != part_pubkeys.size()) return std::nullopt;
|
||||
if (partial_sigs.size() != part_pubkeys.size()) return std::nullopt;
|
||||
|
||||
// Parse the pubnonces and partial sigs
|
||||
std::vector<std::tuple<secp256k1_pubkey, secp256k1_musig_pubnonce, secp256k1_musig_partial_sig>> signers_data;
|
||||
std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
|
||||
std::vector<const secp256k1_musig_partial_sig*> partial_sig_ptrs;
|
||||
for (const CPubKey& part_pk : part_pubkeys) {
|
||||
const auto& pn_it = pubnonces.find(part_pk);
|
||||
if (pn_it == pubnonces.end()) return std::nullopt;
|
||||
const std::vector<uint8_t> pubnonce = pn_it->second;
|
||||
if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) return std::nullopt;
|
||||
const auto& it = partial_sigs.find(part_pk);
|
||||
if (it == partial_sigs.end()) return std::nullopt;
|
||||
const uint256& partial_sig = it->second;
|
||||
|
||||
auto& [secp_pk, secp_pn, secp_ps] = signers_data.emplace_back();
|
||||
|
||||
if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &secp_pk, part_pk.data(), part_pk.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!secp256k1_musig_pubnonce_parse(secp256k1_context_static, &secp_pn, pubnonce.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!secp256k1_musig_partial_sig_parse(secp256k1_context_static, &secp_ps, partial_sig.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
pubnonce_ptrs.reserve(signers_data.size());
|
||||
partial_sig_ptrs.reserve(signers_data.size());
|
||||
for (auto& [_, pn, ps] : signers_data) {
|
||||
pubnonce_ptrs.push_back(&pn);
|
||||
partial_sig_ptrs.push_back(&ps);
|
||||
}
|
||||
|
||||
// Aggregate nonces
|
||||
secp256k1_musig_aggnonce aggnonce;
|
||||
if (!secp256k1_musig_nonce_agg(secp256k1_context_static, &aggnonce, pubnonce_ptrs.data(), pubnonce_ptrs.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Apply tweaks
|
||||
for (const auto& [tweak, xonly] : tweaks) {
|
||||
if (xonly) {
|
||||
if (!secp256k1_musig_pubkey_xonly_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
} else if (!secp256k1_musig_pubkey_ec_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Create musig_session
|
||||
secp256k1_musig_session session;
|
||||
if (!secp256k1_musig_nonce_process(secp256k1_context_static, &session, &aggnonce, sighash.data(), &keyagg_cache)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Verify partial sigs
|
||||
for (const auto& [pk, pb, ps] : signers_data) {
|
||||
if (!secp256k1_musig_partial_sig_verify(secp256k1_context_static, &ps, &pb, &pk, &keyagg_cache, &session)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate partial sigs
|
||||
std::vector<uint8_t> sig;
|
||||
sig.resize(64);
|
||||
if (!secp256k1_musig_partial_sig_agg(secp256k1_context_static, sig.data(), &session, partial_sig_ptrs.data(), partial_sig_ptrs.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
51
src/musig.h
51
src/musig.h
@ -11,6 +11,8 @@
|
||||
#include <vector>
|
||||
|
||||
struct secp256k1_musig_keyagg_cache;
|
||||
class MuSig2SecNonceImpl;
|
||||
struct secp256k1_musig_secnonce;
|
||||
|
||||
//! MuSig2 chaincode as defined by BIP 328
|
||||
using namespace util::hex_literals;
|
||||
@ -21,11 +23,50 @@ constexpr uint256 MUSIG_CHAINCODE{
|
||||
|
||||
|
||||
|
||||
//! Create a secp256k1_musig_keyagg_cache from the pubkeys in their current order. This is necessary for most MuSig2 operations
|
||||
bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache);
|
||||
//! Retrieve the full aggregate pubkey from the secp256k1_musig_keyagg_cache
|
||||
std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_cache& cache);
|
||||
//! Compute the full aggregate pubkey from the given participant pubkeys in their current order
|
||||
constexpr size_t MUSIG2_PUBNONCE_SIZE{66};
|
||||
|
||||
//! Compute the full aggregate pubkey from the given participant pubkeys in their current order.
|
||||
//! Outputs the secp256k1_musig_keyagg_cache and validates that the computed aggregate pubkey matches an expected aggregate pubkey.
|
||||
//! This is necessary for most MuSig2 operations.
|
||||
std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache, const std::optional<CPubKey>& expected_aggregate);
|
||||
std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys);
|
||||
|
||||
//! Construct the BIP 328 synthetic xpub for a pubkey
|
||||
CExtPubKey CreateMuSig2SyntheticXpub(const CPubKey& pubkey);
|
||||
|
||||
/**
|
||||
* MuSig2SecNonce encapsulates a secret nonce in use in a MuSig2 signing session.
|
||||
* Since this nonce persists outside of libsecp256k1 signing code, we must handle
|
||||
* its construction and destruction ourselves.
|
||||
* The secret nonce must be kept a secret, otherwise the private key may be leaked.
|
||||
* As such, it needs to be treated in the same way that CKeys are treated.
|
||||
* So this class handles the secure allocation of the secp256k1_musig_secnonce object
|
||||
* that libsecp256k1 uses, and only gives out references to this object to avoid
|
||||
* any possibility of copies being made. Furthermore, objects of this class are not
|
||||
* copyable to avoid nonce reuse.
|
||||
*/
|
||||
class MuSig2SecNonce
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<MuSig2SecNonceImpl> m_impl;
|
||||
|
||||
public:
|
||||
MuSig2SecNonce();
|
||||
MuSig2SecNonce(MuSig2SecNonce&&) noexcept;
|
||||
MuSig2SecNonce& operator=(MuSig2SecNonce&&) noexcept;
|
||||
~MuSig2SecNonce();
|
||||
|
||||
// Delete copy constructors
|
||||
MuSig2SecNonce(const MuSig2SecNonce&) = delete;
|
||||
MuSig2SecNonce& operator=(const MuSig2SecNonce&) = delete;
|
||||
|
||||
secp256k1_musig_secnonce* Get() const;
|
||||
void Invalidate();
|
||||
bool IsValid();
|
||||
};
|
||||
|
||||
uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash);
|
||||
|
||||
std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs);
|
||||
|
||||
#endif // BITCOIN_MUSIG_H
|
||||
|
||||
24
src/psbt.cpp
24
src/psbt.cpp
@ -149,6 +149,13 @@ void PSBTInput::FillSignatureData(SignatureData& sigdata) const
|
||||
for (const auto& [hash, preimage] : hash256_preimages) {
|
||||
sigdata.hash256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
|
||||
}
|
||||
sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
|
||||
for (const auto& [agg_key_lh, pubnonces] : m_musig2_pubnonces) {
|
||||
sigdata.musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
|
||||
}
|
||||
for (const auto& [agg_key_lh, psigs] : m_musig2_partial_sigs) {
|
||||
sigdata.musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
|
||||
}
|
||||
}
|
||||
|
||||
void PSBTInput::FromSignatureData(const SignatureData& sigdata)
|
||||
@ -196,6 +203,13 @@ void PSBTInput::FromSignatureData(const SignatureData& sigdata)
|
||||
for (const auto& [pubkey, leaf_origin] : sigdata.taproot_misc_pubkeys) {
|
||||
m_tap_bip32_paths.emplace(pubkey, leaf_origin);
|
||||
}
|
||||
m_musig2_participants.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());
|
||||
for (const auto& [agg_key_lh, pubnonces] : sigdata.musig2_pubnonces) {
|
||||
m_musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
|
||||
}
|
||||
for (const auto& [agg_key_lh, psigs] : sigdata.musig2_partial_sigs) {
|
||||
m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
|
||||
}
|
||||
}
|
||||
|
||||
void PSBTInput::Merge(const PSBTInput& input)
|
||||
@ -223,6 +237,13 @@ void PSBTInput::Merge(const PSBTInput& input)
|
||||
if (m_tap_key_sig.empty() && !input.m_tap_key_sig.empty()) m_tap_key_sig = input.m_tap_key_sig;
|
||||
if (m_tap_internal_key.IsNull() && !input.m_tap_internal_key.IsNull()) m_tap_internal_key = input.m_tap_internal_key;
|
||||
if (m_tap_merkle_root.IsNull() && !input.m_tap_merkle_root.IsNull()) m_tap_merkle_root = input.m_tap_merkle_root;
|
||||
m_musig2_participants.insert(input.m_musig2_participants.begin(), input.m_musig2_participants.end());
|
||||
for (const auto& [agg_key_lh, pubnonces] : input.m_musig2_pubnonces) {
|
||||
m_musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
|
||||
}
|
||||
for (const auto& [agg_key_lh, psigs] : input.m_musig2_partial_sigs) {
|
||||
m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
|
||||
}
|
||||
}
|
||||
|
||||
void PSBTOutput::FillSignatureData(SignatureData& sigdata) const
|
||||
@ -252,6 +273,7 @@ void PSBTOutput::FillSignatureData(SignatureData& sigdata) const
|
||||
sigdata.taproot_misc_pubkeys.emplace(pubkey, leaf_origin);
|
||||
sigdata.tap_pubkeys.emplace(Hash160(pubkey), pubkey);
|
||||
}
|
||||
sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
|
||||
}
|
||||
|
||||
void PSBTOutput::FromSignatureData(const SignatureData& sigdata)
|
||||
@ -274,6 +296,7 @@ void PSBTOutput::FromSignatureData(const SignatureData& sigdata)
|
||||
for (const auto& [pubkey, leaf_origin] : sigdata.taproot_misc_pubkeys) {
|
||||
m_tap_bip32_paths.emplace(pubkey, leaf_origin);
|
||||
}
|
||||
m_musig2_participants.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());
|
||||
}
|
||||
|
||||
bool PSBTOutput::IsNull() const
|
||||
@ -291,6 +314,7 @@ void PSBTOutput::Merge(const PSBTOutput& output)
|
||||
if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script;
|
||||
if (m_tap_internal_key.IsNull() && !output.m_tap_internal_key.IsNull()) m_tap_internal_key = output.m_tap_internal_key;
|
||||
if (m_tap_tree.empty() && !output.m_tap_tree.empty()) m_tap_tree = output.m_tap_tree;
|
||||
m_musig2_participants.insert(output.m_musig2_participants.begin(), output.m_musig2_participants.end());
|
||||
}
|
||||
|
||||
bool PSBTInputSigned(const PSBTInput& input)
|
||||
|
||||
@ -794,7 +794,7 @@ struct PSBTInput
|
||||
|
||||
std::vector<uint8_t> pubnonce;
|
||||
s >> pubnonce;
|
||||
if (pubnonce.size() != 66) {
|
||||
if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) {
|
||||
throw std::ios_base::failure("Input musig2 pubnonce value is not 66 bytes");
|
||||
}
|
||||
|
||||
|
||||
@ -338,13 +338,16 @@ bool CPubKey::Decompress() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const {
|
||||
bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc, uint256* bip32_tweak_out) const {
|
||||
assert(IsValid());
|
||||
assert((nChild >> 31) == 0);
|
||||
assert(size() == COMPRESSED_SIZE);
|
||||
unsigned char out[64];
|
||||
BIP32Hash(cc, nChild, *begin(), begin()+1, out);
|
||||
memcpy(ccChild.begin(), out+32, 32);
|
||||
if (bip32_tweak_out) {
|
||||
memcpy(bip32_tweak_out->begin(), out, 32);
|
||||
}
|
||||
secp256k1_pubkey pubkey;
|
||||
if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &pubkey, vch, size())) {
|
||||
return false;
|
||||
@ -409,13 +412,13 @@ void CExtPubKey::DecodeWithVersion(const unsigned char code[BIP32_EXTKEY_WITH_VE
|
||||
Decode(&code[4]);
|
||||
}
|
||||
|
||||
bool CExtPubKey::Derive(CExtPubKey &out, unsigned int _nChild) const {
|
||||
bool CExtPubKey::Derive(CExtPubKey &out, unsigned int _nChild, uint256* bip32_tweak_out) const {
|
||||
if (nDepth == std::numeric_limits<unsigned char>::max()) return false;
|
||||
out.nDepth = nDepth + 1;
|
||||
CKeyID id = pubkey.GetID();
|
||||
memcpy(out.vchFingerprint, &id, 4);
|
||||
out.nChild = _nChild;
|
||||
return pubkey.Derive(out.pubkey, out.chaincode, _nChild, chaincode);
|
||||
return pubkey.Derive(out.pubkey, out.chaincode, _nChild, chaincode, bip32_tweak_out);
|
||||
}
|
||||
|
||||
/* static */ bool CPubKey::CheckLowS(const std::vector<unsigned char>& vchSig) {
|
||||
|
||||
@ -224,7 +224,7 @@ public:
|
||||
bool Decompress();
|
||||
|
||||
//! Derive BIP32 child pubkey.
|
||||
[[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const;
|
||||
[[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc, uint256* bip32_tweak_out = nullptr) const;
|
||||
};
|
||||
|
||||
class XOnlyPubKey
|
||||
@ -379,7 +379,7 @@ struct CExtPubKey {
|
||||
void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]);
|
||||
void EncodeWithVersion(unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]) const;
|
||||
void DecodeWithVersion(const unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]);
|
||||
[[nodiscard]] bool Derive(CExtPubKey& out, unsigned int nChild) const;
|
||||
[[nodiscard]] bool Derive(CExtPubKey& out, unsigned int nChild, uint256* bip32_tweak_out = nullptr) const;
|
||||
};
|
||||
|
||||
#endif // BITCOIN_PUBKEY_H
|
||||
|
||||
@ -641,13 +641,7 @@ public:
|
||||
// Make our pubkey provider
|
||||
if (IsRangedDerivation() || !m_path.empty()) {
|
||||
// Make the synthetic xpub and construct the BIP32PubkeyProvider
|
||||
CExtPubKey extpub;
|
||||
extpub.nDepth = 0;
|
||||
std::memset(extpub.vchFingerprint, 0, 4);
|
||||
extpub.nChild = 0;
|
||||
extpub.chaincode = MUSIG_CHAINCODE;
|
||||
extpub.pubkey = m_aggregate_pubkey.value();
|
||||
|
||||
CExtPubKey extpub = CreateMuSig2SyntheticXpub(m_aggregate_pubkey.value());
|
||||
m_aggregate_provider = std::make_unique<BIP32PubkeyProvider>(m_expr_index, extpub, m_path, m_derive, /*apostrophe=*/false);
|
||||
} else {
|
||||
m_aggregate_provider = std::make_unique<ConstPubkeyProvider>(m_expr_index, m_aggregate_pubkey.value(), /*xonly=*/false);
|
||||
@ -1647,6 +1641,7 @@ std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostr
|
||||
* @param[out] apostrophe only updated if hardened derivation is found
|
||||
* @param[out] error parsing error message
|
||||
* @param[in] allow_multipath Allows the parsed path to use the multipath specifier
|
||||
* @param[out] has_hardened Records whether the path contains any hardened derivation
|
||||
* @returns false if parsing failed
|
||||
**/
|
||||
[[nodiscard]] bool ParseKeyPath(const std::vector<std::span<const char>>& split, std::vector<KeyPath>& out, bool& apostrophe, std::string& error, bool allow_multipath, bool& has_hardened)
|
||||
@ -1843,20 +1838,20 @@ std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkey(uint32_t& key_exp_index
|
||||
bool any_ranged = false;
|
||||
bool all_bip32 = true;
|
||||
std::vector<std::vector<std::unique_ptr<PubkeyProvider>>> providers;
|
||||
bool any_key_parsed = true;
|
||||
bool any_key_parsed = false;
|
||||
size_t max_multipath_len = 0;
|
||||
while (expr.size()) {
|
||||
if (!any_key_parsed && !Const(",", expr)) {
|
||||
if (any_key_parsed && !Const(",", expr)) {
|
||||
error = strprintf("musig(): expected ',', got '%c'", expr[0]);
|
||||
return {};
|
||||
}
|
||||
any_key_parsed = false;
|
||||
auto arg = Expr(expr);
|
||||
auto pk = ParsePubkey(key_exp_index, arg, ParseScriptContext::MUSIG, out, error);
|
||||
if (pk.empty()) {
|
||||
error = strprintf("musig(): %s", error);
|
||||
return {};
|
||||
}
|
||||
any_key_parsed = true;
|
||||
|
||||
any_ranged = any_ranged || pk.at(0)->IsRange();
|
||||
all_bip32 = all_bip32 && pk.at(0)->IsBIP32();
|
||||
@ -1866,7 +1861,7 @@ std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkey(uint32_t& key_exp_index
|
||||
providers.emplace_back(std::move(pk));
|
||||
key_exp_index++;
|
||||
}
|
||||
if (any_key_parsed) {
|
||||
if (!any_key_parsed) {
|
||||
error = "musig(): Must contain key expressions";
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
|
||||
#include <consensus/amount.h>
|
||||
#include <key.h>
|
||||
#include <musig.h>
|
||||
#include <policy/policy.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
#include <script/keyorigin.h>
|
||||
#include <script/miniscript.h>
|
||||
#include <script/script.h>
|
||||
@ -59,17 +61,14 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const
|
||||
std::optional<uint256> MutableTransactionSignatureCreator::ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const
|
||||
{
|
||||
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
|
||||
CKey key;
|
||||
if (!provider.GetKeyByXOnly(pubkey, key)) return false;
|
||||
|
||||
// BIP341/BIP342 signing needs lots of precomputed transaction data. While some
|
||||
// (non-SIGHASH_DEFAULT) sighash modes exist that can work with just some subset
|
||||
// of data present, for now, only support signing when everything is provided.
|
||||
if (!m_txdata || !m_txdata->m_bip341_taproot_ready || !m_txdata->m_spent_outputs_ready) return false;
|
||||
if (!m_txdata || !m_txdata->m_bip341_taproot_ready || !m_txdata->m_spent_outputs_ready) return std::nullopt;
|
||||
|
||||
ScriptExecutionData execdata;
|
||||
execdata.m_annex_init = true;
|
||||
@ -77,19 +76,134 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider&
|
||||
if (sigversion == SigVersion::TAPSCRIPT) {
|
||||
execdata.m_codeseparator_pos_init = true;
|
||||
execdata.m_codeseparator_pos = 0xFFFFFFFF; // Only support non-OP_CODESEPARATOR BIP342 signing for now.
|
||||
if (!leaf_hash) return false; // BIP342 signing needs leaf hash.
|
||||
if (!leaf_hash) return std::nullopt; // BIP342 signing needs leaf hash.
|
||||
execdata.m_tapleaf_hash_init = true;
|
||||
execdata.m_tapleaf_hash = *leaf_hash;
|
||||
}
|
||||
uint256 hash;
|
||||
if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, nHashType, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false;
|
||||
if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, nHashType, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return std::nullopt;
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const
|
||||
{
|
||||
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
|
||||
CKey key;
|
||||
if (!provider.GetKeyByXOnly(pubkey, key)) return false;
|
||||
|
||||
std::optional<uint256> hash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
|
||||
if (!hash.has_value()) return false;
|
||||
|
||||
sig.resize(64);
|
||||
// Use uint256{} as aux_rnd for now.
|
||||
if (!key.SignSchnorr(hash, sig, merkle_root, {})) return false;
|
||||
if (!key.SignSchnorr(*hash, sig, merkle_root, {})) return false;
|
||||
if (nHashType) sig.push_back(nHashType);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const
|
||||
{
|
||||
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
|
||||
// Retrieve the private key
|
||||
CKey key;
|
||||
if (!provider.GetKey(part_pubkey.GetID(), key)) return {};
|
||||
|
||||
// Retrieve participant pubkeys
|
||||
auto it = sigdata.musig2_pubkeys.find(aggregate_pubkey);
|
||||
if (it == sigdata.musig2_pubkeys.end()) return {};
|
||||
const std::vector<CPubKey>& pubkeys = it->second;
|
||||
if (std::find(pubkeys.begin(), pubkeys.end(), part_pubkey) == pubkeys.end()) return {};
|
||||
|
||||
// Compute sighash
|
||||
std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
|
||||
if (!sighash.has_value()) return {};
|
||||
|
||||
MuSig2SecNonce secnonce;
|
||||
std::vector<uint8_t> out = key.CreateMuSig2Nonce(secnonce, *sighash, aggregate_pubkey, pubkeys);
|
||||
if (out.empty()) return {};
|
||||
|
||||
// Store the secnonce in the SigningProvider
|
||||
provider.SetMuSig2SecNonce(MuSig2SessionID(script_pubkey, part_pubkey, *sighash), std::move(secnonce));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const
|
||||
{
|
||||
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
|
||||
// Retrieve private key
|
||||
CKey key;
|
||||
if (!provider.GetKey(part_pubkey.GetID(), key)) return false;
|
||||
|
||||
// Retrieve participant pubkeys
|
||||
auto it = sigdata.musig2_pubkeys.find(aggregate_pubkey);
|
||||
if (it == sigdata.musig2_pubkeys.end()) return false;
|
||||
const std::vector<CPubKey>& pubkeys = it->second;
|
||||
if (std::find(pubkeys.begin(), pubkeys.end(), part_pubkey) == pubkeys.end()) return {};
|
||||
|
||||
// Retrieve pubnonces
|
||||
auto this_leaf_aggkey = std::make_pair(script_pubkey, leaf_hash ? *leaf_hash : uint256());
|
||||
auto pubnonce_it = sigdata.musig2_pubnonces.find(this_leaf_aggkey);
|
||||
if (pubnonce_it == sigdata.musig2_pubnonces.end()) return false;
|
||||
const std::map<CPubKey, std::vector<uint8_t>>& pubnonces = pubnonce_it->second;
|
||||
|
||||
// Check if enough pubnonces
|
||||
if (pubnonces.size() != pubkeys.size()) return false;
|
||||
|
||||
// Compute sighash
|
||||
std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
|
||||
if (!sighash.has_value()) return false;
|
||||
|
||||
// Retrieve the secnonce
|
||||
uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, *sighash);
|
||||
std::optional<std::reference_wrapper<MuSig2SecNonce>> secnonce = provider.GetMuSig2SecNonce(session_id);
|
||||
if (!secnonce || !secnonce->get().IsValid()) return false;
|
||||
|
||||
// Compute the sig
|
||||
std::optional<uint256> sig = key.CreateMuSig2PartialSig(*sighash, aggregate_pubkey, pubkeys, pubnonces, *secnonce, tweaks);
|
||||
if (!sig) return false;
|
||||
partial_sig = std::move(*sig);
|
||||
|
||||
// Delete the secnonce now that we're done with it
|
||||
assert(!secnonce->get().IsValid());
|
||||
provider.DeleteMuSig2Session(session_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MutableTransactionSignatureCreator::CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const
|
||||
{
|
||||
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
if (!participants.size()) return false;
|
||||
|
||||
// Retrieve pubnonces and partial sigs
|
||||
auto this_leaf_aggkey = std::make_pair(script_pubkey, leaf_hash ? *leaf_hash : uint256());
|
||||
auto pubnonce_it = sigdata.musig2_pubnonces.find(this_leaf_aggkey);
|
||||
if (pubnonce_it == sigdata.musig2_pubnonces.end()) return false;
|
||||
const std::map<CPubKey, std::vector<uint8_t>>& pubnonces = pubnonce_it->second;
|
||||
auto partial_sigs_it = sigdata.musig2_partial_sigs.find(this_leaf_aggkey);
|
||||
if (partial_sigs_it == sigdata.musig2_partial_sigs.end()) return false;
|
||||
const std::map<CPubKey, uint256>& partial_sigs = partial_sigs_it->second;
|
||||
|
||||
// Check if enough pubnonces and partial sigs
|
||||
if (pubnonces.size() != participants.size()) return false;
|
||||
if (partial_sigs.size() != participants.size()) return false;
|
||||
|
||||
// Compute sighash
|
||||
std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
|
||||
if (!sighash.has_value()) return false;
|
||||
|
||||
std::optional<std::vector<uint8_t>> res = ::CreateMuSig2AggregateSig(participants, aggregate_pubkey, tweaks, *sighash, pubnonces, partial_sigs);
|
||||
if (!res) return false;
|
||||
sig = res.value();
|
||||
if (nHashType) sig.push_back(nHashType);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool GetCScript(const SigningProvider& provider, const SignatureData& sigdata, const CScriptID& scriptid, CScript& script)
|
||||
{
|
||||
if (provider.GetCScript(scriptid, script)) {
|
||||
@ -151,6 +265,100 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector<unsigned char>& sig_out, const XOnlyPubKey& script_pubkey, const uint256* merkle_root, const uint256* leaf_hash, SigVersion sigversion)
|
||||
{
|
||||
Assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
|
||||
|
||||
// Lookup derivation paths for the script pubkey
|
||||
KeyOriginInfo agg_info;
|
||||
auto misc_pk_it = sigdata.taproot_misc_pubkeys.find(script_pubkey);
|
||||
if (misc_pk_it != sigdata.taproot_misc_pubkeys.end()) {
|
||||
agg_info = misc_pk_it->second.second;
|
||||
}
|
||||
|
||||
for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
|
||||
if (part_pks.empty()) continue;
|
||||
|
||||
// Fill participant derivation path info
|
||||
for (const auto& part_pk : part_pks) {
|
||||
KeyOriginInfo part_info;
|
||||
if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
|
||||
XOnlyPubKey xonly_part(part_pk);
|
||||
auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
|
||||
if (it == sigdata.taproot_misc_pubkeys.end()) {
|
||||
it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), part_info)).first;
|
||||
}
|
||||
if (leaf_hash) it->second.first.insert(*leaf_hash);
|
||||
}
|
||||
}
|
||||
|
||||
// The pubkey in the script may not be the actual aggregate of the participants, but derived from it.
|
||||
// Check the derivation, and compute the BIP 32 derivation tweaks
|
||||
std::vector<std::pair<uint256, bool>> tweaks;
|
||||
CPubKey plain_pub = agg_pub;
|
||||
if (XOnlyPubKey(agg_pub) != script_pubkey) {
|
||||
if (agg_info.path.empty()) continue;
|
||||
// Compute and compare fingerprint
|
||||
CKeyID keyid = agg_pub.GetID();
|
||||
if (!std::equal(agg_info.fingerprint, agg_info.fingerprint + sizeof(agg_info.fingerprint), keyid.data())) {
|
||||
continue;
|
||||
}
|
||||
// Get the BIP32 derivation tweaks
|
||||
CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
|
||||
for (const int i : agg_info.path) {
|
||||
auto& [t, xonly] = tweaks.emplace_back();
|
||||
xonly = false;
|
||||
if (!extpub.Derive(extpub, i, &t)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey);
|
||||
plain_pub = extpub.pubkey;
|
||||
}
|
||||
|
||||
// Add the merkle root tweak
|
||||
if (sigversion == SigVersion::TAPROOT && merkle_root) {
|
||||
tweaks.emplace_back(script_pubkey.ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root), true);
|
||||
std::optional<std::pair<XOnlyPubKey, bool>> tweaked = script_pubkey.CreateTapTweak(merkle_root->IsNull() ? nullptr : merkle_root);
|
||||
if (!Assume(tweaked)) return false;
|
||||
plain_pub = tweaked->first.GetCPubKeys().at(tweaked->second ? 1 : 0);
|
||||
}
|
||||
|
||||
// First try to aggregate
|
||||
if (creator.CreateMuSig2AggregateSig(part_pks, sig_out, agg_pub, plain_pub, leaf_hash, tweaks, sigversion, sigdata)) {
|
||||
if (sigversion == SigVersion::TAPROOT) {
|
||||
sigdata.taproot_key_path_sig = sig_out;
|
||||
} else {
|
||||
auto lookup_key = std::make_pair(script_pubkey, leaf_hash ? *leaf_hash : uint256());
|
||||
sigdata.taproot_script_sigs[lookup_key] = sig_out;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Cannot aggregate, try making partial sigs for every participant
|
||||
auto pub_key_leaf_hash = std::make_pair(plain_pub, leaf_hash ? *leaf_hash : uint256());
|
||||
for (const CPubKey& part_pk : part_pks) {
|
||||
uint256 partial_sig;
|
||||
if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, leaf_hash, tweaks, sigversion, sigdata) && Assume(!partial_sig.IsNull())) {
|
||||
sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
|
||||
}
|
||||
}
|
||||
// If there are any partial signatures, exit early
|
||||
auto partial_sigs_it = sigdata.musig2_partial_sigs.find(pub_key_leaf_hash);
|
||||
if (partial_sigs_it != sigdata.musig2_partial_sigs.end() && !partial_sigs_it->second.empty()) {
|
||||
continue;
|
||||
}
|
||||
// No partial sigs, try to make pubnonces
|
||||
std::map<CPubKey, std::vector<uint8_t>>& pubnonces = sigdata.musig2_pubnonces[pub_key_leaf_hash];
|
||||
for (const CPubKey& part_pk : part_pks) {
|
||||
if (pubnonces.contains(part_pk)) continue;
|
||||
std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, leaf_hash, merkle_root, sigversion, sigdata);
|
||||
if (pubnonce.empty()) continue;
|
||||
pubnonces[part_pk] = std::move(pubnonce);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector<unsigned char>& sig_out, const XOnlyPubKey& pubkey, const uint256& leaf_hash, SigVersion sigversion)
|
||||
{
|
||||
KeyOriginInfo info;
|
||||
@ -169,11 +377,14 @@ static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, Signatur
|
||||
sig_out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (creator.CreateSchnorrSig(provider, sig_out, pubkey, &leaf_hash, nullptr, sigversion)) {
|
||||
sigdata.taproot_script_sigs[lookup_key] = sig_out;
|
||||
return true;
|
||||
} else if (!SignMuSig2(creator, sigdata, provider, sig_out, pubkey, /*merkle_root=*/nullptr, &leaf_hash, sigversion)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
return sigdata.taproot_script_sigs.contains(lookup_key);
|
||||
}
|
||||
|
||||
template<typename M, typename K, typename V>
|
||||
@ -342,27 +553,45 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
|
||||
if (provider.GetTaprootBuilder(output, builder)) {
|
||||
sigdata.tr_builder = builder;
|
||||
}
|
||||
if (auto agg_keys = provider.GetAllMuSig2ParticipantPubkeys(); !agg_keys.empty()) {
|
||||
sigdata.musig2_pubkeys.insert(agg_keys.begin(), agg_keys.end());
|
||||
}
|
||||
|
||||
|
||||
// Try key path spending.
|
||||
{
|
||||
KeyOriginInfo info;
|
||||
if (provider.GetKeyOriginByXOnly(sigdata.tr_spenddata.internal_key, info)) {
|
||||
KeyOriginInfo internal_key_info;
|
||||
if (provider.GetKeyOriginByXOnly(sigdata.tr_spenddata.internal_key, internal_key_info)) {
|
||||
auto it = sigdata.taproot_misc_pubkeys.find(sigdata.tr_spenddata.internal_key);
|
||||
if (it == sigdata.taproot_misc_pubkeys.end()) {
|
||||
sigdata.taproot_misc_pubkeys.emplace(sigdata.tr_spenddata.internal_key, std::make_pair(std::set<uint256>(), info));
|
||||
sigdata.taproot_misc_pubkeys.emplace(sigdata.tr_spenddata.internal_key, std::make_pair(std::set<uint256>(), internal_key_info));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned char> sig;
|
||||
if (sigdata.taproot_key_path_sig.size() == 0) {
|
||||
if (creator.CreateSchnorrSig(provider, sig, sigdata.tr_spenddata.internal_key, nullptr, &sigdata.tr_spenddata.merkle_root, SigVersion::TAPROOT)) {
|
||||
sigdata.taproot_key_path_sig = sig;
|
||||
KeyOriginInfo output_key_info;
|
||||
if (provider.GetKeyOriginByXOnly(output, output_key_info)) {
|
||||
auto it = sigdata.taproot_misc_pubkeys.find(output);
|
||||
if (it == sigdata.taproot_misc_pubkeys.end()) {
|
||||
sigdata.taproot_misc_pubkeys.emplace(output, std::make_pair(std::set<uint256>(), output_key_info));
|
||||
}
|
||||
}
|
||||
if (sigdata.taproot_key_path_sig.size() == 0) {
|
||||
if (creator.CreateSchnorrSig(provider, sig, output, nullptr, nullptr, SigVersion::TAPROOT)) {
|
||||
|
||||
auto make_keypath_sig = [&](const XOnlyPubKey& pk, const uint256* merkle_root) {
|
||||
std::vector<unsigned char> sig;
|
||||
if (creator.CreateSchnorrSig(provider, sig, pk, nullptr, merkle_root, SigVersion::TAPROOT)) {
|
||||
sigdata.taproot_key_path_sig = sig;
|
||||
} else {
|
||||
SignMuSig2(creator, sigdata, provider, sig, pk, merkle_root, /*leaf_hash=*/nullptr, SigVersion::TAPROOT);
|
||||
}
|
||||
};
|
||||
|
||||
// First try signing with internal key
|
||||
if (sigdata.taproot_key_path_sig.size() == 0) {
|
||||
make_keypath_sig(sigdata.tr_spenddata.internal_key, &sigdata.tr_spenddata.merkle_root);
|
||||
}
|
||||
// Try signing with output key if still no signature
|
||||
if (sigdata.taproot_key_path_sig.size() == 0) {
|
||||
make_keypath_sig(output, nullptr);
|
||||
}
|
||||
if (sigdata.taproot_key_path_sig.size()) {
|
||||
result = Vector(sigdata.taproot_key_path_sig);
|
||||
@ -737,6 +966,22 @@ public:
|
||||
sig.assign(64, '\000');
|
||||
return true;
|
||||
}
|
||||
std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const override
|
||||
{
|
||||
std::vector<uint8_t> out;
|
||||
out.assign(MUSIG2_PUBNONCE_SIZE, '\000');
|
||||
return out;
|
||||
}
|
||||
bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override
|
||||
{
|
||||
partial_sig = uint256::ONE;
|
||||
return true;
|
||||
}
|
||||
bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override
|
||||
{
|
||||
sig.assign(64, '\000');
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ class SigningProvider;
|
||||
|
||||
struct bilingual_str;
|
||||
struct CMutableTransaction;
|
||||
struct SignatureData;
|
||||
|
||||
/** Interface for signature creators. */
|
||||
class BaseSignatureCreator {
|
||||
@ -33,6 +34,9 @@ public:
|
||||
/** Create a singular (non-script) signature. */
|
||||
virtual bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const =0;
|
||||
virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const =0;
|
||||
virtual std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const =0;
|
||||
virtual bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
|
||||
virtual bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
|
||||
};
|
||||
|
||||
/** A signature creator for transactions. */
|
||||
@ -45,12 +49,17 @@ class MutableTransactionSignatureCreator : public BaseSignatureCreator
|
||||
const MutableTransactionSignatureChecker checker;
|
||||
const PrecomputedTransactionData* m_txdata;
|
||||
|
||||
std::optional<uint256> ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const;
|
||||
|
||||
public:
|
||||
MutableTransactionSignatureCreator(const CMutableTransaction& tx LIFETIMEBOUND, unsigned int input_idx, const CAmount& amount, int hash_type);
|
||||
MutableTransactionSignatureCreator(const CMutableTransaction& tx LIFETIMEBOUND, unsigned int input_idx, const CAmount& amount, const PrecomputedTransactionData* txdata, int hash_type);
|
||||
const BaseSignatureChecker& Checker() const override { return checker; }
|
||||
bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const override;
|
||||
bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const override;
|
||||
std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const override;
|
||||
bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
|
||||
bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
|
||||
};
|
||||
|
||||
/** A signature checker that accepts all signatures */
|
||||
@ -78,7 +87,7 @@ struct SignatureData {
|
||||
std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>> misc_pubkeys;
|
||||
std::vector<unsigned char> taproot_key_path_sig; /// Schnorr signature for key path spending
|
||||
std::map<std::pair<XOnlyPubKey, uint256>, std::vector<unsigned char>> taproot_script_sigs; ///< (Partial) schnorr signatures, indexed by XOnlyPubKey and leaf_hash.
|
||||
std::map<XOnlyPubKey, std::pair<std::set<uint256>, KeyOriginInfo>> taproot_misc_pubkeys; ///< Miscellaneous Taproot pubkeys involved in this input along with their leaf script hashes and key origin data. Also includes the Taproot internal key (may have no leaf script hashes).
|
||||
std::map<XOnlyPubKey, std::pair<std::set<uint256>, KeyOriginInfo>> taproot_misc_pubkeys; ///< Miscellaneous Taproot pubkeys involved in this input along with their leaf script hashes and key origin data. Also includes the Taproot internal and output keys (may have no leaf script hashes).
|
||||
std::map<CKeyID, XOnlyPubKey> tap_pubkeys; ///< Misc Taproot pubkeys involved in this input, by hash. (Equivalent of misc_pubkeys but for Taproot.)
|
||||
std::vector<CKeyID> missing_pubkeys; ///< KeyIDs of pubkeys which could not be found
|
||||
std::vector<CKeyID> missing_sigs; ///< KeyIDs of pubkeys for signatures which could not be found
|
||||
@ -88,6 +97,12 @@ struct SignatureData {
|
||||
std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash256_preimages; ///< Mapping from a HASH256 hash to its preimage provided to solve a Script
|
||||
std::map<std::vector<uint8_t>, std::vector<uint8_t>> ripemd160_preimages; ///< Mapping from a RIPEMD160 hash to its preimage provided to solve a Script
|
||||
std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash160_preimages; ///< Mapping from a HASH160 hash to its preimage provided to solve a Script
|
||||
//! Map MuSig2 aggregate pubkeys to its participants
|
||||
std::map<CPubKey, std::vector<CPubKey>> musig2_pubkeys;
|
||||
//! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 public nonce
|
||||
std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>> musig2_pubnonces;
|
||||
//! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 partial signature
|
||||
std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>> musig2_partial_sigs;
|
||||
|
||||
SignatureData() = default;
|
||||
explicit SignatureData(const CScript& script) : scriptSig(script) {}
|
||||
|
||||
@ -58,6 +58,26 @@ std::vector<CPubKey> HidingSigningProvider::GetMuSig2ParticipantPubkeys(const CP
|
||||
return m_provider->GetMuSig2ParticipantPubkeys(pubkey);
|
||||
}
|
||||
|
||||
std::map<CPubKey, std::vector<CPubKey>> HidingSigningProvider::GetAllMuSig2ParticipantPubkeys() const
|
||||
{
|
||||
return m_provider->GetAllMuSig2ParticipantPubkeys();
|
||||
}
|
||||
|
||||
void HidingSigningProvider::SetMuSig2SecNonce(const uint256& id, MuSig2SecNonce&& nonce) const
|
||||
{
|
||||
m_provider->SetMuSig2SecNonce(id, std::move(nonce));
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<MuSig2SecNonce>> HidingSigningProvider::GetMuSig2SecNonce(const uint256& session_id) const
|
||||
{
|
||||
return m_provider->GetMuSig2SecNonce(session_id);
|
||||
}
|
||||
|
||||
void HidingSigningProvider::DeleteMuSig2Session(const uint256& session_id) const
|
||||
{
|
||||
m_provider->DeleteMuSig2Session(session_id);
|
||||
}
|
||||
|
||||
bool FlatSigningProvider::GetCScript(const CScriptID& scriptid, CScript& script) const { return LookupHelper(scripts, scriptid, script); }
|
||||
bool FlatSigningProvider::GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const { return LookupHelper(pubkeys, keyid, pubkey); }
|
||||
bool FlatSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const
|
||||
@ -94,6 +114,31 @@ std::vector<CPubKey> FlatSigningProvider::GetMuSig2ParticipantPubkeys(const CPub
|
||||
return participant_pubkeys;
|
||||
}
|
||||
|
||||
std::map<CPubKey, std::vector<CPubKey>> FlatSigningProvider::GetAllMuSig2ParticipantPubkeys() const
|
||||
{
|
||||
return aggregate_pubkeys;
|
||||
}
|
||||
|
||||
void FlatSigningProvider::SetMuSig2SecNonce(const uint256& session_id, MuSig2SecNonce&& nonce) const
|
||||
{
|
||||
if (!Assume(musig2_secnonces)) return;
|
||||
musig2_secnonces->emplace(session_id, std::move(nonce));
|
||||
}
|
||||
|
||||
std::optional<std::reference_wrapper<MuSig2SecNonce>> FlatSigningProvider::GetMuSig2SecNonce(const uint256& session_id) const
|
||||
{
|
||||
if (!Assume(musig2_secnonces)) return std::nullopt;
|
||||
const auto& it = musig2_secnonces->find(session_id);
|
||||
if (it == musig2_secnonces->end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void FlatSigningProvider::DeleteMuSig2Session(const uint256& session_id) const
|
||||
{
|
||||
if (!Assume(musig2_secnonces)) return;
|
||||
musig2_secnonces->erase(session_id);
|
||||
}
|
||||
|
||||
FlatSigningProvider& FlatSigningProvider::Merge(FlatSigningProvider&& b)
|
||||
{
|
||||
scripts.merge(b.scripts);
|
||||
@ -102,6 +147,8 @@ FlatSigningProvider& FlatSigningProvider::Merge(FlatSigningProvider&& b)
|
||||
origins.merge(b.origins);
|
||||
tr_trees.merge(b.tr_trees);
|
||||
aggregate_pubkeys.merge(b.aggregate_pubkeys);
|
||||
// We shouldn't be merging 2 different sessions, just overwrite with b's sessions.
|
||||
if (!musig2_secnonces) musig2_secnonces = b.musig2_secnonces;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@ -9,11 +9,15 @@
|
||||
#include <addresstype.h>
|
||||
#include <attributes.h>
|
||||
#include <key.h>
|
||||
#include <musig.h>
|
||||
#include <pubkey.h>
|
||||
#include <script/keyorigin.h>
|
||||
#include <script/script.h>
|
||||
#include <sync.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
struct ShortestVectorFirstComparator
|
||||
{
|
||||
bool operator()(const std::vector<unsigned char>& a, const std::vector<unsigned char>& b) const
|
||||
@ -162,6 +166,10 @@ public:
|
||||
virtual bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const { return false; }
|
||||
virtual bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const { return false; }
|
||||
virtual std::vector<CPubKey> GetMuSig2ParticipantPubkeys(const CPubKey& pubkey) const { return {}; }
|
||||
virtual std::map<CPubKey, std::vector<CPubKey>> GetAllMuSig2ParticipantPubkeys() const {return {}; }
|
||||
virtual void SetMuSig2SecNonce(const uint256& id, MuSig2SecNonce&& nonce) const {}
|
||||
virtual std::optional<std::reference_wrapper<MuSig2SecNonce>> GetMuSig2SecNonce(const uint256& session_id) const { return std::nullopt; }
|
||||
virtual void DeleteMuSig2Session(const uint256& session_id) const {}
|
||||
|
||||
bool GetKeyByXOnly(const XOnlyPubKey& pubkey, CKey& key) const
|
||||
{
|
||||
@ -206,6 +214,10 @@ public:
|
||||
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
|
||||
bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const override;
|
||||
std::vector<CPubKey> GetMuSig2ParticipantPubkeys(const CPubKey& pubkey) const override;
|
||||
std::map<CPubKey, std::vector<CPubKey>> GetAllMuSig2ParticipantPubkeys() const override;
|
||||
void SetMuSig2SecNonce(const uint256& id, MuSig2SecNonce&& nonce) const override;
|
||||
std::optional<std::reference_wrapper<MuSig2SecNonce>> GetMuSig2SecNonce(const uint256& session_id) const override;
|
||||
void DeleteMuSig2Session(const uint256& session_id) const override;
|
||||
};
|
||||
|
||||
struct FlatSigningProvider final : public SigningProvider
|
||||
@ -216,6 +228,7 @@ struct FlatSigningProvider final : public SigningProvider
|
||||
std::map<CKeyID, CKey> keys;
|
||||
std::map<XOnlyPubKey, TaprootBuilder> tr_trees; /** Map from output key to Taproot tree (which can then make the TaprootSpendData */
|
||||
std::map<CPubKey, std::vector<CPubKey>> aggregate_pubkeys; /** MuSig2 aggregate pubkeys */
|
||||
std::map<uint256, MuSig2SecNonce>* musig2_secnonces{nullptr};
|
||||
|
||||
bool GetCScript(const CScriptID& scriptid, CScript& script) const override;
|
||||
bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
|
||||
@ -225,6 +238,10 @@ struct FlatSigningProvider final : public SigningProvider
|
||||
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
|
||||
bool GetTaprootBuilder(const XOnlyPubKey& output_key, TaprootBuilder& builder) const override;
|
||||
std::vector<CPubKey> GetMuSig2ParticipantPubkeys(const CPubKey& pubkey) const override;
|
||||
std::map<CPubKey, std::vector<CPubKey>> GetAllMuSig2ParticipantPubkeys() const override;
|
||||
void SetMuSig2SecNonce(const uint256& id, MuSig2SecNonce&& nonce) const override;
|
||||
std::optional<std::reference_wrapper<MuSig2SecNonce>> GetMuSig2SecNonce(const uint256& session_id) const override;
|
||||
void DeleteMuSig2Session(const uint256& session_id) const override;
|
||||
|
||||
FlatSigningProvider& Merge(FlatSigningProvider&& b) LIFETIMEBOUND;
|
||||
};
|
||||
|
||||
@ -52,7 +52,7 @@ constexpr int XONLY_KEYS = 1 << 6; // X-only pubkeys are in use (and thus inferr
|
||||
constexpr int MISSING_PRIVKEYS = 1 << 7; // Not all private keys are available, so ToPrivateString will fail.
|
||||
constexpr int SIGNABLE_FAILS = 1 << 8; // We can sign with this descriptor, but actually trying to sign will fail
|
||||
constexpr int MUSIG = 1 << 9; // This is a MuSig so key counts will have an extra key
|
||||
constexpr int MUSIG_DERIVATION = 1 << 10; // MuSig with derivation from the aggregate key
|
||||
constexpr int MUSIG_DERIVATION = 1 << 10; // MuSig with BIP 328 derivation from the aggregate key
|
||||
constexpr int MIXED_MUSIG = 1 << 11; // Both MuSig and normal key expressions are present
|
||||
constexpr int UNIQUE_XPUBS = 1 << 12; // Whether the xpub count should be of unique xpubs
|
||||
|
||||
@ -315,6 +315,7 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int
|
||||
size_t num_xpubs = CountXpubs(pub1);
|
||||
size_t num_unique_xpubs = CountUniqueXpubs(pub1);
|
||||
if (flags & MUSIG_DERIVATION) {
|
||||
// Deriving from the aggregate will include the synthetic xpub of the aggregate in the caches and SigningProviders.
|
||||
num_xpubs++;
|
||||
num_unique_xpubs++;
|
||||
}
|
||||
|
||||
@ -1256,6 +1256,10 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
|
||||
FlatSigningProvider master_provider;
|
||||
master_provider.keys = GetKeys();
|
||||
m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
|
||||
|
||||
// Always include musig_secnonces as this descriptor may have a participant private key
|
||||
// but not a musig() descriptor
|
||||
out_keys->musig2_secnonces = &m_musig2_secnonces;
|
||||
}
|
||||
|
||||
return out_keys;
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include <common/signmessage.h>
|
||||
#include <common/types.h>
|
||||
#include <logging.h>
|
||||
#include <musig.h>
|
||||
#include <node/types.h>
|
||||
#include <psbt.h>
|
||||
#include <script/descriptor.h>
|
||||
@ -293,6 +294,19 @@ private:
|
||||
//! Number of pre-generated keys/scripts (part of the look-ahead process, used to detect payments)
|
||||
int64_t m_keypool_size GUARDED_BY(cs_desc_man){DEFAULT_KEYPOOL_SIZE};
|
||||
|
||||
/** Map of a session id to MuSig2 secnonce
|
||||
*
|
||||
* Stores MuSig2 secnonces while the MuSig2 signing session is still ongoing.
|
||||
* Note that these secnonces must not be reused. In order to avoid being tricked into
|
||||
* reusing a nonce, this map is held only in memory and must not be written to disk.
|
||||
* The side effect is that signing sessions cannot persist across restarts, but this
|
||||
* must be done in order to prevent nonce reuse.
|
||||
*
|
||||
* The session id is an arbitrary value set by the signer in order for the signing logic
|
||||
* to find ongoing signing sessions. It is the SHA256 of aggregate xonly key, + participant pubkey + sighash.
|
||||
*/
|
||||
mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
|
||||
|
||||
bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
|
||||
|
||||
@ -348,6 +348,7 @@ BASE_SCRIPTS = [
|
||||
'feature_coinstatsindex.py',
|
||||
'feature_coinstatsindex_compatibility.py',
|
||||
'wallet_orphanedreward.py',
|
||||
'wallet_musig.py',
|
||||
'wallet_timelock.py',
|
||||
'p2p_permissions.py',
|
||||
'feature_blocksdir.py',
|
||||
|
||||
242
test/functional/wallet_musig.py
Executable file
242
test/functional/wallet_musig.py
Executable file
@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2024 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import re
|
||||
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.key import H_POINT
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
)
|
||||
|
||||
PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
|
||||
PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
|
||||
ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")
|
||||
MULTIPATH_TWO_RE = re.compile(r"<(\d+);(\d+)>")
|
||||
MUSIG_RE = re.compile(r"musig\((.*?)\)")
|
||||
PLACEHOLDER_RE = re.compile(r"\$\d")
|
||||
|
||||
class WalletMuSigTest(BitcoinTestFramework):
|
||||
WALLET_NUM = 0
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):
|
||||
self.log.info(f"Testing {comment}")
|
||||
has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
|
||||
|
||||
wallets = []
|
||||
keys = []
|
||||
|
||||
pat = pattern.replace("$H", H_POINT)
|
||||
|
||||
# Figure out how many wallets are needed and create them
|
||||
expected_pubnonces = 0
|
||||
expected_partial_sigs = 0
|
||||
for musig in MUSIG_RE.findall(pat):
|
||||
musig_partial_sigs = 0
|
||||
for placeholder in PLACEHOLDER_RE.findall(musig):
|
||||
wallet_index = int(placeholder[1:])
|
||||
if nosign_wallets is None or wallet_index not in nosign_wallets:
|
||||
expected_pubnonces += 1
|
||||
else:
|
||||
musig_partial_sigs = None
|
||||
if musig_partial_sigs is not None:
|
||||
musig_partial_sigs += 1
|
||||
if wallet_index < len(wallets):
|
||||
continue
|
||||
wallet_name = f"musig_{self.WALLET_NUM}"
|
||||
self.WALLET_NUM += 1
|
||||
self.nodes[0].createwallet(wallet_name)
|
||||
wallet = self.nodes[0].get_wallet_rpc(wallet_name)
|
||||
wallets.append(wallet)
|
||||
|
||||
for priv_desc in wallet.listdescriptors(True)["descriptors"]:
|
||||
desc = priv_desc["desc"]
|
||||
if not desc.startswith("tr("):
|
||||
continue
|
||||
privkey = PRIVKEY_RE.search(desc).group(1)
|
||||
break
|
||||
for pub_desc in wallet.listdescriptors()["descriptors"]:
|
||||
desc = pub_desc["desc"]
|
||||
if not desc.startswith("tr("):
|
||||
continue
|
||||
pubkey = PUBKEY_RE.search(desc).group(1)
|
||||
# Since the pubkey is derived from the private key that we have, we need
|
||||
# to extract and insert the origin path from the pubkey as well.
|
||||
privkey += ORIGIN_PATH_RE.search(pubkey).group(1)
|
||||
break
|
||||
keys.append((privkey, pubkey))
|
||||
if musig_partial_sigs is not None:
|
||||
expected_partial_sigs += musig_partial_sigs
|
||||
|
||||
# Construct and import each wallet's musig descriptor that
|
||||
# contains the private key from that wallet and pubkeys of the others
|
||||
for i, wallet in enumerate(wallets):
|
||||
if only_one_musig_wallet and i > 0:
|
||||
continue
|
||||
desc = pat
|
||||
import_descs = []
|
||||
for j, (priv, pub) in enumerate(keys):
|
||||
if j == i:
|
||||
desc = desc.replace(f"${i}", priv)
|
||||
else:
|
||||
desc = desc.replace(f"${j}", pub)
|
||||
|
||||
import_descs.append({
|
||||
"desc": descsum_create(desc),
|
||||
"active": True,
|
||||
"timestamp": "now",
|
||||
})
|
||||
|
||||
res = wallet.importdescriptors(import_descs)
|
||||
for r in res:
|
||||
assert_equal(r["success"], True)
|
||||
|
||||
# Check that the wallets agree on the same musig address
|
||||
addr = None
|
||||
change_addr = None
|
||||
for i, wallet in enumerate(wallets):
|
||||
if only_one_musig_wallet and i > 0:
|
||||
continue
|
||||
if addr is None:
|
||||
addr = wallet.getnewaddress(address_type="bech32m")
|
||||
else:
|
||||
assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
|
||||
if has_internal:
|
||||
if change_addr is None:
|
||||
change_addr = wallet.getrawchangeaddress(address_type="bech32m")
|
||||
else:
|
||||
assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
|
||||
|
||||
# Fund that address
|
||||
self.def_wallet.sendtoaddress(addr, 10)
|
||||
self.generate(self.nodes[0], 1)
|
||||
|
||||
# Spend that UTXO
|
||||
utxo = None
|
||||
for i, wallet in enumerate(wallets):
|
||||
if only_one_musig_wallet and i > 0:
|
||||
continue
|
||||
if utxo is None:
|
||||
utxo = wallet.listunspent()[0]
|
||||
else:
|
||||
assert_equal(utxo, wallet.listunspent()[0])
|
||||
psbt = wallets[0].walletcreatefundedpsbt(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m", changePosition=1)["psbt"]
|
||||
|
||||
dec_psbt = self.nodes[0].decodepsbt(psbt)
|
||||
assert_equal(len(dec_psbt["inputs"]), 1)
|
||||
assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
|
||||
if has_internal:
|
||||
assert_equal(len(dec_psbt["outputs"][1]["musig2_participant_pubkeys"]), pattern.count("musig("))
|
||||
|
||||
# Check all participant pubkeys in the input and change output
|
||||
psbt_maps = [dec_psbt["inputs"][0]]
|
||||
if has_internal:
|
||||
psbt_maps.append(dec_psbt["outputs"][1])
|
||||
for psbt_map in psbt_maps:
|
||||
part_pks = set()
|
||||
for agg in psbt_map["musig2_participant_pubkeys"]:
|
||||
for part_pub in agg["participant_pubkeys"]:
|
||||
part_pks.add(part_pub[2:])
|
||||
# Check that there are as many participants as we expected
|
||||
assert_equal(len(part_pks), len(keys))
|
||||
# Check that each participant has a derivation path
|
||||
for deriv_path in psbt_map["taproot_bip32_derivs"]:
|
||||
if deriv_path["pubkey"] in part_pks:
|
||||
part_pks.remove(deriv_path["pubkey"])
|
||||
assert_equal(len(part_pks), 0)
|
||||
|
||||
# Add pubnonces
|
||||
nonce_psbts = []
|
||||
for i, wallet in enumerate(wallets):
|
||||
if nosign_wallets and i in nosign_wallets:
|
||||
continue
|
||||
proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
|
||||
assert_equal(proc["complete"], False)
|
||||
nonce_psbts.append(proc["psbt"])
|
||||
|
||||
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
|
||||
|
||||
dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
|
||||
assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
|
||||
for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
|
||||
pubkey = pn["aggregate_pubkey"][2:]
|
||||
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
|
||||
continue
|
||||
elif "taproot_scripts" in dec_psbt["inputs"][0]:
|
||||
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
|
||||
if pubkey in leaf_scripts["script"]:
|
||||
break
|
||||
else:
|
||||
assert False, "Aggregate pubkey for pubnonce not seen as output key, or in any scripts"
|
||||
else:
|
||||
assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
|
||||
|
||||
# Add partial sigs
|
||||
psig_psbts = []
|
||||
for i, wallet in enumerate(wallets):
|
||||
if nosign_wallets and i in nosign_wallets:
|
||||
continue
|
||||
proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
|
||||
assert_equal(proc["complete"], False)
|
||||
psig_psbts.append(proc["psbt"])
|
||||
|
||||
comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
|
||||
|
||||
dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
|
||||
assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), expected_partial_sigs)
|
||||
for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
|
||||
pubkey = ps["aggregate_pubkey"][2:]
|
||||
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
|
||||
continue
|
||||
elif "taproot_scripts" in dec_psbt["inputs"][0]:
|
||||
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
|
||||
if pubkey in leaf_scripts["script"]:
|
||||
break
|
||||
else:
|
||||
assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
|
||||
else:
|
||||
assert False, "Aggregate pubkey for partial sig not seen as output key"
|
||||
|
||||
# Non-participant aggregates partial sigs and send
|
||||
finalized = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt, extract=False)
|
||||
assert_equal(finalized["complete"], True)
|
||||
witness = self.nodes[0].decodepsbt(finalized["psbt"])["inputs"][0]["final_scriptwitness"]
|
||||
if scriptpath:
|
||||
assert_greater_than(len(witness), 1)
|
||||
else:
|
||||
assert_equal(len(witness), 1)
|
||||
finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
|
||||
assert "hex" in finalized
|
||||
self.nodes[0].sendrawtransaction(finalized["hex"])
|
||||
|
||||
def run_test(self):
|
||||
self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||
|
||||
self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
|
||||
self.do_test("rawtr(musig(keys/*)) with ALL|ANYONECANPAY", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", "ALL|ANYONECANPAY")
|
||||
self.do_test("tr(musig(keys/*)) no multipath", "tr(musig($0/0/*,$1/1/*,$2/2/*))")
|
||||
self.do_test("tr(musig(keys/*)) 2 index multipath", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
|
||||
self.do_test("tr(musig(keys/*)) 3 index multipath", "tr(musig($0/<0;1;2>/*,$1/<1;2;3>/*,$2/<2;3;4>/*))")
|
||||
self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
|
||||
self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
|
||||
self.do_test("rawtr(musig(keys/*)) without all wallets importing", "rawtr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
|
||||
self.do_test("tr(musig(keys/*)) without all wallets importing", "tr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
|
||||
self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", scriptpath=True)
|
||||
self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True)
|
||||
self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
|
||||
self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
|
||||
self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
|
||||
self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletMuSigTest(__file__).main()
|
||||
Loading…
x
Reference in New Issue
Block a user