Merge bitcoin/bitcoin#21365: Basic Taproot signing support for descriptor wallets

458a345b05 Add support for SIGHASH_DEFAULT in RPCs, and make it default (Pieter Wuille)
c0f0c8eccb tests: check spending of P2TR (Pieter Wuille)
a2380127e9 Basic Taproot signing logic in script/sign.cpp (Pieter Wuille)
49487bc3b6 Make GetInputUTXO safer: verify non-witness UTXO match (Pieter Wuille)
fd3f6890f3 Construct and use PrecomputedTransactionData in PSBT signing (Pieter Wuille)
5cb6502ac5 Construct and use PrecomputedTransactionData in SignTransaction (Pieter Wuille)
5d2e22437b Don't nuke witness data when signing fails (Pieter Wuille)
ce9353164b Permit full precomputation in PrecomputedTransactionData (Pieter Wuille)
e841fb503d Add precomputed txdata support to MutableTransactionSignatureCreator (Pieter Wuille)
a91d532338 Add CKey::SignSchnorr function for BIP 340/341 signing (Pieter Wuille)
e77a2839b5 Use HandleMissingData also in CheckSchnorrSignature (Pieter Wuille)
dbb0ce9fbf Add TaprootSpendData data structure, equivalent to script map for P2[W]SH (Pieter Wuille)

Pull request description:

  Builds on top of #22051, adding signing support after derivation support.

  Nothing is changed in descriptor features. Signing works for key path and script path spending, through the normal sending functions, and PSBT-based RPCs. However, PSBT usability is rather low as no extensions have been defined to convey Taproot-specific information, so all script information must be known to the signing wallet.

ACKs for top commit:
  achow101:
    re-ACK 458a345b05
  fjahr:
    Code review ACK 458a345b05
  Sjors:
    ACK 458a345b05

Tree-SHA512: 30ed212cf7754763a4a81624ebc084c51727b8322711ac0b390369213c1a891d367ed8b123882ac08c99595320c11ec57ee42304ff22a69afdc3d1a0d55cc711
This commit is contained in:
Samuel Dobson 2021-06-18 08:38:04 +12:00
commit 5c2e2afe99
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
28 changed files with 594 additions and 65 deletions

View file

@ -506,11 +506,12 @@ static void MutateTxDelOutput(CMutableTransaction& tx, const std::string& strOut
tx.vout.erase(tx.vout.begin() + outIdx);
}
static const unsigned int N_SIGHASH_OPTS = 6;
static const unsigned int N_SIGHASH_OPTS = 7;
static const struct {
const char *flagStr;
int flags;
} sighashOptions[N_SIGHASH_OPTS] = {
{"DEFAULT", SIGHASH_DEFAULT},
{"ALL", SIGHASH_ALL},
{"NONE", SIGHASH_NONE},
{"SINGLE", SIGHASH_SINGLE},

View file

@ -260,6 +260,7 @@ int ParseSighashString(const UniValue& sighash)
int hash_type = SIGHASH_ALL;
if (!sighash.isNull()) {
static std::map<std::string, int> map_sighash_values = {
{std::string("DEFAULT"), int(SIGHASH_DEFAULT)},
{std::string("ALL"), int(SIGHASH_ALL)},
{std::string("ALL|ANYONECANPAY"), int(SIGHASH_ALL|SIGHASH_ANYONECANPAY)},
{std::string("NONE"), int(SIGHASH_NONE)},

View file

@ -7,10 +7,13 @@
#include <crypto/common.h>
#include <crypto/hmac_sha512.h>
#include <hash.h>
#include <random.h>
#include <secp256k1.h>
#include <secp256k1_extrakeys.h>
#include <secp256k1_recovery.h>
#include <secp256k1_schnorrsig.h>
static secp256k1_context* secp256k1_context_sign = nullptr;
@ -258,6 +261,24 @@ bool CKey::SignCompact(const uint256 &hash, std::vector<unsigned char>& vchSig)
return true;
}
bool CKey::SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256* merkle_root, const uint256* aux) const
{
assert(sig.size() == 64);
secp256k1_keypair keypair;
if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, begin())) return false;
if (merkle_root) {
secp256k1_xonly_pubkey pubkey;
if (!secp256k1_keypair_xonly_pub(secp256k1_context_sign, &pubkey, nullptr, &keypair)) return false;
unsigned char pubkey_bytes[32];
if (!secp256k1_xonly_pubkey_serialize(secp256k1_context_sign, pubkey_bytes, &pubkey)) return false;
uint256 tweak = XOnlyPubKey(pubkey_bytes).ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root);
if (!secp256k1_keypair_xonly_tweak_add(GetVerifyContext(), &keypair, tweak.data())) return false;
}
bool ret = secp256k1_schnorrsig_sign(secp256k1_context_sign, sig.data(), hash.data(), &keypair, secp256k1_nonce_function_bip340, aux ? (void*)aux->data() : nullptr);
memory_cleanse(&keypair, sizeof(keypair));
return ret;
}
bool CKey::Load(const CPrivKey &seckey, const CPubKey &vchPubKey, bool fSkipCheck=false) {
if (!ec_seckey_import_der(secp256k1_context_sign, (unsigned char*)begin(), seckey.data(), seckey.size()))
return false;

View file

@ -128,6 +128,18 @@ public:
*/
bool SignCompact(const uint256& hash, std::vector<unsigned char>& vchSig) const;
/**
* Create a BIP-340 Schnorr signature, for the xonly-pubkey corresponding to *this,
* optionally tweaked by *merkle_root. Additional nonce entropy can be provided through
* aux.
*
* When merkle_root is not nullptr, this results in a signature with a modified key as
* specified in BIP341:
* - If merkle_root->IsNull(): key + H_TapTweak(pubkey)*G
* - Otherwise: key + H_TapTweak(pubkey || *merkle_root)
*/
bool SignSchnorr(const uint256& hash, Span<unsigned char> sig, const uint256* merkle_root = nullptr, const uint256* aux = nullptr) const;
//! Derive BIP32 child key.
bool Derive(CKey& keyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const;

View file

@ -23,6 +23,8 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
result.inputs.resize(psbtx.tx->vin.size());
const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs[i];
PSBTInputAnalysis& input_analysis = result.inputs[i];
@ -61,7 +63,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
// Figure out what is missing
SignatureData outdata;
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, &outdata);
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, 1, &outdata);
// Things are missing
if (!complete) {
@ -121,7 +123,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
PSBTInput& input = psbtx.inputs[i];
Coin newcoin;
if (!SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, nullptr, true) || !psbtx.GetInputUTXO(newcoin.out, i)) {
if (!SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, nullptr, 1) || !psbtx.GetInputUTXO(newcoin.out, i)) {
success = false;
break;
} else {

View file

@ -59,12 +59,15 @@ bool PartiallySignedTransaction::AddOutput(const CTxOut& txout, const PSBTOutput
bool PartiallySignedTransaction::GetInputUTXO(CTxOut& utxo, int input_index) const
{
PSBTInput input = inputs[input_index];
const PSBTInput& input = inputs[input_index];
uint32_t prevout_index = tx->vin[input_index].prevout.n;
if (input.non_witness_utxo) {
if (prevout_index >= input.non_witness_utxo->vout.size()) {
return false;
}
if (input.non_witness_utxo->GetHash() != tx->vin[input_index].prevout.hash) {
return false;
}
utxo = input.non_witness_utxo->vout[prevout_index];
} else if (!input.witness_utxo.IsNull()) {
utxo = input.witness_utxo;
@ -227,7 +230,24 @@ void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransactio
psbt_out.FromSignatureData(sigdata);
}
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash, SignatureData* out_sigdata, bool use_dummy)
PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& psbt)
{
const CMutableTransaction& tx = *psbt.tx;
bool have_all_spent_outputs = true;
std::vector<CTxOut> utxos(tx.vin.size());
for (size_t idx = 0; idx < tx.vin.size(); ++idx) {
if (!psbt.GetInputUTXO(utxos[idx], idx)) have_all_spent_outputs = false;
}
PrecomputedTransactionData txdata;
if (have_all_spent_outputs) {
txdata.Init(tx, std::move(utxos), true);
} else {
txdata.Init(tx, {}, true);
}
return txdata;
}
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, int sighash, SignatureData* out_sigdata)
{
PSBTInput& input = psbt.inputs.at(index);
const CMutableTransaction& tx = *psbt.tx;
@ -267,10 +287,10 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
sigdata.witness = false;
bool sig_complete;
if (use_dummy) {
if (txdata == nullptr) {
sig_complete = ProduceSignature(provider, DUMMY_SIGNATURE_CREATOR, utxo.scriptPubKey, sigdata);
} else {
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, txdata, sighash);
sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata);
}
// Verify that a witness signature was produced in case one was required.
@ -302,8 +322,9 @@ bool FinalizePSBT(PartiallySignedTransaction& psbtx)
// PartiallySignedTransaction did not understand them), this will combine them into a final
// script.
bool complete = true;
const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
complete &= SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, SIGHASH_ALL);
complete &= SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, SIGHASH_ALL);
}
return complete;

View file

@ -567,11 +567,18 @@ enum class PSBTRole {
std::string PSBTRoleName(PSBTRole role);
/** Compute a PrecomputedTransactionData object from a psbt. */
PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& psbt);
/** Checks whether a PSBTInput is already signed. */
bool PSBTInputSigned(const PSBTInput& input);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed.
*
* txdata should be the output of PrecomputePSBTData (which can be shared across
* multiple SignPSBTInput calls). If it is nullptr, a dummy signature will be created.
**/
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr);
/** Counts the unsigned inputs of a PSBT. */
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt);

View file

@ -373,3 +373,7 @@ ECCVerifyHandle::~ECCVerifyHandle()
secp256k1_context_verify = nullptr;
}
}
const secp256k1_context* GetVerifyContext() {
return secp256k1_context_verify;
}

View file

@ -234,6 +234,10 @@ public:
* fail. */
bool IsFullyValid() const;
/** Test whether this is the 0 key (the result of default construction). This implies
* !IsFullyValid(). */
bool IsNull() const { return m_keydata.IsNull(); }
/** Construct an x-only pubkey from exactly 32 bytes. */
explicit XOnlyPubKey(Span<const unsigned char> bytes);
@ -312,4 +316,10 @@ public:
~ECCVerifyHandle();
};
typedef struct secp256k1_context_struct secp256k1_context;
/** Access to the internal secp256k1 context used for verification. Only intended to be used
* by key.cpp. */
const secp256k1_context* GetVerifyContext();
#endif // BITCOIN_PUBKEY_H

View file

@ -753,7 +753,8 @@ static RPCHelpMan signrawtransactionwithkey()
},
},
},
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL"}, "The signature hash type. Must be one of:\n"
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"DEFAULT"}, "The signature hash type. Must be one of:\n"
" \"DEFAULT\"\n"
" \"ALL\"\n"
" \"NONE\"\n"
" \"SINGLE\"\n"
@ -1655,6 +1656,7 @@ static RPCHelpMan utxoupdatepsbt()
}
// Fill the inputs
const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs.at(i);
@ -1671,7 +1673,7 @@ static RPCHelpMan utxoupdatepsbt()
// Update script/keypath information using descriptor data.
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures
// we don't actually care about those here, in fact.
SignPSBTInput(public_provider, psbtx, i, /* sighash_type */ 1);
SignPSBTInput(public_provider, psbtx, i, &txdata, /* sighash_type */ 1);
}
// Update script/keypath information using descriptor data.

View file

@ -843,7 +843,9 @@ protected:
XOnlyPubKey xpk(keys[0]);
if (!xpk.IsFullyValid()) return {};
builder.Finalize(xpk);
return Vector(GetScriptForDestination(builder.GetOutput()));
WitnessV1Taproot output = builder.GetOutput();
out.tr_spenddata[output].Merge(builder.GetSpendData());
return Vector(GetScriptForDestination(output));
}
bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, bool priv, bool normalized) const override
{

View file

@ -1420,7 +1420,7 @@ uint256 GetSpentScriptsSHA256(const std::vector<CTxOut>& outputs_spent)
} // namespace
template <class T>
void PrecomputedTransactionData::Init(const T& txTo, std::vector<CTxOut>&& spent_outputs)
void PrecomputedTransactionData::Init(const T& txTo, std::vector<CTxOut>&& spent_outputs, bool force)
{
assert(!m_spent_outputs_ready);
@ -1431,9 +1431,9 @@ void PrecomputedTransactionData::Init(const T& txTo, std::vector<CTxOut>&& spent
}
// Determine which precomputation-impacting features this transaction uses.
bool uses_bip143_segwit = false;
bool uses_bip341_taproot = false;
for (size_t inpos = 0; inpos < txTo.vin.size(); ++inpos) {
bool uses_bip143_segwit = force;
bool uses_bip341_taproot = force;
for (size_t inpos = 0; inpos < txTo.vin.size() && !(uses_bip143_segwit && uses_bip341_taproot); ++inpos) {
if (!txTo.vin[inpos].scriptWitness.IsNull()) {
if (m_spent_outputs_ready && m_spent_outputs[inpos].scriptPubKey.size() == 2 + WITNESS_V1_TAPROOT_SIZE &&
m_spent_outputs[inpos].scriptPubKey[0] == OP_1) {
@ -1478,8 +1478,8 @@ PrecomputedTransactionData::PrecomputedTransactionData(const T& txTo)
}
// explicit instantiation
template void PrecomputedTransactionData::Init(const CTransaction& txTo, std::vector<CTxOut>&& spent_outputs);
template void PrecomputedTransactionData::Init(const CMutableTransaction& txTo, std::vector<CTxOut>&& spent_outputs);
template void PrecomputedTransactionData::Init(const CTransaction& txTo, std::vector<CTxOut>&& spent_outputs, bool force);
template void PrecomputedTransactionData::Init(const CMutableTransaction& txTo, std::vector<CTxOut>&& spent_outputs, bool force);
template PrecomputedTransactionData::PrecomputedTransactionData(const CTransaction& txTo);
template PrecomputedTransactionData::PrecomputedTransactionData(const CMutableTransaction& txTo);
@ -1711,7 +1711,7 @@ bool GenericTransactionSignatureChecker<T>::CheckSchnorrSignature(Span<const uns
if (hashtype == SIGHASH_DEFAULT) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_HASHTYPE);
}
uint256 sighash;
assert(this->txdata);
if (!this->txdata) return HandleMissingData(m_mdb);
if (!SignatureHashSchnorr(sighash, execdata, *txTo, nIn, hashtype, sigversion, *this->txdata, m_mdb)) {
return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_HASHTYPE);
}

View file

@ -168,7 +168,7 @@ struct PrecomputedTransactionData
PrecomputedTransactionData() = default;
template <class T>
void Init(const T& tx, std::vector<CTxOut>&& spent_outputs);
void Init(const T& tx, std::vector<CTxOut>&& spent_outputs, bool force = false);
template <class T>
explicit PrecomputedTransactionData(const T& tx);
@ -260,6 +260,9 @@ enum class MissingDataBehavior
FAIL, //!< Just act as if the signature was invalid
};
template<typename T>
bool SignatureHashSchnorr(uint256& hash_out, const ScriptExecutionData& execdata, const T& tx_to, uint32_t in_pos, uint8_t hash_type, SigVersion sigversion, const PrecomputedTransactionData& cache, MissingDataBehavior mdb);
template <class T>
class GenericTransactionSignatureChecker : public BaseSignatureChecker
{

View file

@ -11,13 +11,28 @@
#include <script/signingprovider.h>
#include <script/standard.h>
#include <uint256.h>
#include <util/vector.h>
typedef std::vector<unsigned char> valtype;
MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn) : txTo(txToIn), nIn(nInIn), nHashType(nHashTypeIn), amount(amountIn), checker(txTo, nIn, amountIn, MissingDataBehavior::FAIL) {}
MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn)
: txTo(txToIn), nIn(nInIn), nHashType(nHashTypeIn), amount(amountIn), checker(txTo, nIn, amountIn, MissingDataBehavior::FAIL),
m_txdata(nullptr)
{
}
MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, const PrecomputedTransactionData* txdata, int nHashTypeIn)
: txTo(txToIn), nIn(nInIn), nHashType(nHashTypeIn), amount(amountIn),
checker(txdata ? MutableTransactionSignatureChecker(txTo, nIn, amount, *txdata, MissingDataBehavior::FAIL) :
MutableTransactionSignatureChecker(txTo, nIn, amount, MissingDataBehavior::FAIL)),
m_txdata(txdata)
{
}
bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& address, const CScript& scriptCode, SigVersion sigversion) const
{
assert(sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0);
CKey key;
if (!provider.GetKey(address, key))
return false;
@ -26,13 +41,61 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid
if (sigversion == SigVersion::WITNESS_V0 && !key.IsCompressed())
return false;
// Signing for witness scripts needs the amount.
if (sigversion == SigVersion::WITNESS_V0 && amount < 0) return false;
// Signing without known amount does not work in witness scripts.
if (sigversion == SigVersion::WITNESS_V0 && !MoneyRange(amount)) return false;
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, nHashType, amount, sigversion);
// BASE/WITNESS_V0 signatures don't support explicit SIGHASH_DEFAULT, use SIGHASH_ALL instead.
const int hashtype = nHashType == SIGHASH_DEFAULT ? SIGHASH_ALL : nHashType;
uint256 hash = SignatureHash(scriptCode, *txTo, nIn, hashtype, amount, sigversion, m_txdata);
if (!key.Sign(hash, vchSig))
return false;
vchSig.push_back((unsigned char)nHashType);
vchSig.push_back((unsigned char)hashtype);
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
{
assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
CKey key;
{
// For now, use the old full pubkey-based key derivation logic. As it indexed by
// Hash160(full pubkey), we need to try both a version prefixed with 0x02, and one
// with 0x03.
unsigned char b[33] = {0x02};
std::copy(pubkey.begin(), pubkey.end(), b + 1);
CPubKey fullpubkey;
fullpubkey.Set(b, b + 33);
CKeyID keyid = fullpubkey.GetID();
if (!provider.GetKey(keyid, key)) {
b[0] = 0x03;
fullpubkey.Set(b, b + 33);
CKeyID keyid = fullpubkey.GetID();
if (!provider.GetKey(keyid, 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;
ScriptExecutionData execdata;
execdata.m_annex_init = true;
execdata.m_annex_present = false; // Only support annex-less signing for now.
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.
execdata.m_tapleaf_hash_init = true;
execdata.m_tapleaf_hash = *leaf_hash;
}
uint256 hash;
if (!SignatureHashSchnorr(hash, execdata, *txTo, nIn, nHashType, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false;
sig.resize(64);
if (!key.SignSchnorr(hash, sig, merkle_root, nullptr)) return false;
if (nHashType) sig.push_back(nHashType);
return true;
}
@ -92,6 +155,86 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
return false;
}
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)
{
auto lookup_key = std::make_pair(pubkey, leaf_hash);
auto it = sigdata.taproot_script_sigs.find(lookup_key);
if (it != sigdata.taproot_script_sigs.end()) {
sig_out = it->second;
}
if (creator.CreateSchnorrSig(provider, sig_out, pubkey, &leaf_hash, nullptr, sigversion)) {
sigdata.taproot_script_sigs[lookup_key] = sig_out;
return true;
}
return false;
}
static bool SignTaprootScript(const SigningProvider& provider, const BaseSignatureCreator& creator, SignatureData& sigdata, int leaf_version, const CScript& script, std::vector<valtype>& result)
{
// Only BIP342 tapscript signing is supported for now.
if (leaf_version != TAPROOT_LEAF_TAPSCRIPT) return false;
SigVersion sigversion = SigVersion::TAPSCRIPT;
uint256 leaf_hash = (CHashWriter(HASHER_TAPLEAF) << uint8_t(leaf_version) << script).GetSHA256();
// <xonly pubkey> OP_CHECKSIG
if (script.size() == 34 && script[33] == OP_CHECKSIG && script[0] == 0x20) {
XOnlyPubKey pubkey(MakeSpan(script).subspan(1, 32));
std::vector<unsigned char> sig;
if (CreateTaprootScriptSig(creator, sigdata, provider, sig, pubkey, leaf_hash, sigversion)) {
result = Vector(std::move(sig));
return true;
}
}
return false;
}
static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCreator& creator, const WitnessV1Taproot& output, SignatureData& sigdata, std::vector<valtype>& result)
{
TaprootSpendData spenddata;
// Gather information about this output.
if (provider.GetTaprootSpendData(output, spenddata)) {
sigdata.tr_spenddata.Merge(spenddata);
}
// Try key path spending.
{
std::vector<unsigned char> sig;
if (sigdata.taproot_key_path_sig.size() == 0) {
if (creator.CreateSchnorrSig(provider, sig, spenddata.internal_key, nullptr, &spenddata.merkle_root, SigVersion::TAPROOT)) {
sigdata.taproot_key_path_sig = sig;
}
}
if (sigdata.taproot_key_path_sig.size()) {
result = Vector(sigdata.taproot_key_path_sig);
return true;
}
}
// Try script path spending.
std::vector<std::vector<unsigned char>> smallest_result_stack;
for (const auto& [key, control_blocks] : sigdata.tr_spenddata.scripts) {
const auto& [script, leaf_ver] = key;
std::vector<std::vector<unsigned char>> result_stack;
if (SignTaprootScript(provider, creator, sigdata, leaf_ver, script, result_stack)) {
result_stack.emplace_back(std::begin(script), std::end(script)); // Push the script
result_stack.push_back(*control_blocks.begin()); // Push the smallest control block
if (smallest_result_stack.size() == 0 ||
GetSerializeSize(result_stack, PROTOCOL_VERSION) < GetSerializeSize(smallest_result_stack, PROTOCOL_VERSION)) {
smallest_result_stack = std::move(result_stack);
}
}
}
if (smallest_result_stack.size() != 0) {
result = std::move(smallest_result_stack);
return true;
}
return false;
}
/**
* Sign scriptPubKey using signature made with creator.
* Signatures are returned in scriptSigRet (or returns false if scriptPubKey can't be signed),
@ -113,7 +256,6 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
case TxoutType::NONSTANDARD:
case TxoutType::NULL_DATA:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::WITNESS_V1_TAPROOT:
return false;
case TxoutType::PUBKEY:
if (!CreateSig(creator, sigdata, provider, sig, CPubKey(vSolutions[0]), scriptPubKey, sigversion)) return false;
@ -175,6 +317,9 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
// Could not find witnessScript, add to missing
sigdata.missing_witness_script = uint256(vSolutions[0]);
return false;
case TxoutType::WITNESS_V1_TAPROOT:
return SignTaproot(provider, creator, WitnessV1Taproot(XOnlyPubKey{vSolutions[0]}), sigdata, ret);
} // no default case, so the compiler can warn about missing cases
assert(false);
}
@ -205,7 +350,6 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato
bool solved = SignStep(provider, creator, fromPubKey, result, whichType, SigVersion::BASE, sigdata);
bool P2SH = false;
CScript subscript;
sigdata.scriptWitness.stack.clear();
if (solved && whichType == TxoutType::SCRIPTHASH)
{
@ -238,10 +382,17 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato
sigdata.scriptWitness.stack = result;
sigdata.witness = true;
result.clear();
} else if (whichType == TxoutType::WITNESS_V1_TAPROOT && !P2SH) {
sigdata.witness = true;
if (solved) {
sigdata.scriptWitness.stack = std::move(result);
}
result.clear();
} else if (solved && whichType == TxoutType::WITNESS_UNKNOWN) {
sigdata.witness = true;
}
if (!sigdata.witness) sigdata.scriptWitness.stack.clear();
if (P2SH) {
result.push_back(std::vector<unsigned char>(subscript.begin(), subscript.end()));
}
@ -402,6 +553,7 @@ class DummySignatureChecker final : public BaseSignatureChecker
public:
DummySignatureChecker() {}
bool CheckECDSASignature(const std::vector<unsigned char>& scriptSig, const std::vector<unsigned char>& vchPubKey, const CScript& scriptCode, SigVersion sigversion) const override { return true; }
bool CheckSchnorrSignature(Span<const unsigned char> sig, Span<const unsigned char> pubkey, SigVersion sigversion, const ScriptExecutionData& execdata, ScriptError* serror) const override { return true; }
};
const DummySignatureChecker DUMMY_CHECKER;
@ -427,6 +579,11 @@ public:
vchSig[6 + m_r_len + m_s_len] = SIGHASH_ALL;
return true;
}
bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* tweak, SigVersion sigversion) const override
{
sig.assign(64, '\000');
return true;
}
};
}
@ -476,6 +633,26 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore,
// Use CTransaction for the constant parts of the
// transaction to avoid rehashing.
const CTransaction txConst(mtx);
PrecomputedTransactionData txdata;
std::vector<CTxOut> spent_outputs;
spent_outputs.resize(mtx.vin.size());
bool have_all_spent_outputs = true;
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
CTxIn& txin = mtx.vin[i];
auto coin = coins.find(txin.prevout);
if (coin == coins.end() || coin->second.IsSpent()) {
have_all_spent_outputs = false;
} else {
spent_outputs[i] = CTxOut(coin->second.out.nValue, coin->second.out.scriptPubKey);
}
}
if (have_all_spent_outputs) {
txdata.Init(txConst, std::move(spent_outputs), true);
} else {
txdata.Init(txConst, {}, true);
}
// Sign what we can:
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
CTxIn& txin = mtx.vin[i];
@ -490,7 +667,7 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore,
SignatureData sigdata = DataFromTransaction(mtx, i, coin->second.out);
// Only sign SIGHASH_SINGLE if there's a corresponding output:
if (!fHashSingle || (i < mtx.vout.size())) {
ProduceSignature(*keystore, MutableTransactionSignatureCreator(&mtx, i, amount, nHashType), prevPubKey, sigdata);
ProduceSignature(*keystore, MutableTransactionSignatureCreator(&mtx, i, amount, &txdata, nHashType), prevPubKey, sigdata);
}
UpdateInput(txin, sigdata);
@ -502,7 +679,7 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore,
}
ScriptError serror = SCRIPT_ERR_OK;
if (!VerifyScript(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount, MissingDataBehavior::FAIL), &serror)) {
if (!VerifyScript(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount, txdata, MissingDataBehavior::FAIL), &serror)) {
if (serror == SCRIPT_ERR_INVALID_STACK_OPERATION) {
// Unable to sign input and verification failed (possible attempt to partially sign).
input_errors[i] = "Unable to sign input, invalid stack size (possibly missing key)";

View file

@ -11,13 +11,13 @@
#include <pubkey.h>
#include <script/interpreter.h>
#include <script/keyorigin.h>
#include <script/standard.h>
#include <span.h>
#include <streams.h>
class CKey;
class CKeyID;
class CScript;
class CScriptID;
class CTransaction;
class SigningProvider;
@ -31,6 +31,7 @@ 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;
};
/** A signature creator for transactions. */
@ -40,11 +41,14 @@ class MutableTransactionSignatureCreator : public BaseSignatureCreator {
int nHashType;
CAmount amount;
const MutableTransactionSignatureChecker checker;
const PrecomputedTransactionData* m_txdata;
public:
MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn = SIGHASH_ALL);
MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, const PrecomputedTransactionData* txdata, int nHashTypeIn = SIGHASH_ALL);
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;
};
/** A signature creator that just produces 71-byte empty signatures. */
@ -64,8 +68,11 @@ struct SignatureData {
CScript redeem_script; ///< The redeemScript (if any) for the input
CScript witness_script; ///< The witnessScript (if any) for the input. witnessScripts are used in P2WSH outputs.
CScriptWitness scriptWitness; ///< The scriptWitness of an input. Contains complete signatures or the traditional partial signatures format. scriptWitness is part of a transaction input per BIP 144.
TaprootSpendData tr_spenddata; ///< Taproot spending data.
std::map<CKeyID, SigPair> signatures; ///< BIP 174 style partial signatures for the input. May contain all signatures necessary for producing a final scriptSig or scriptWitness.
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::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
uint160 missing_redeem_script; ///< ScriptID of the missing redeemScript (if any)

View file

@ -44,6 +44,11 @@ bool HidingSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& inf
return m_provider->GetKeyOrigin(keyid, info);
}
bool HidingSigningProvider::GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const
{
return m_provider->GetTaprootSpendData(output_key, spenddata);
}
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
@ -54,6 +59,10 @@ bool FlatSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info)
return ret;
}
bool FlatSigningProvider::GetKey(const CKeyID& keyid, CKey& key) const { return LookupHelper(keys, keyid, key); }
bool FlatSigningProvider::GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const
{
return LookupHelper(tr_spenddata, output_key, spenddata);
}
FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b)
{
@ -66,6 +75,10 @@ FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvide
ret.keys.insert(b.keys.begin(), b.keys.end());
ret.origins = a.origins;
ret.origins.insert(b.origins.begin(), b.origins.end());
ret.tr_spenddata = a.tr_spenddata;
for (const auto& [output_key, spenddata] : b.tr_spenddata) {
ret.tr_spenddata[output_key].Merge(spenddata);
}
return ret;
}

View file

@ -25,6 +25,7 @@ public:
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
virtual bool HaveKey(const CKeyID &address) const { return false; }
virtual bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return false; }
virtual bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const { return false; }
};
extern const SigningProvider& DUMMY_SIGNING_PROVIDER;
@ -42,6 +43,7 @@ public:
bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
bool GetKey(const CKeyID& keyid, CKey& key) const override;
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
};
struct FlatSigningProvider final : public SigningProvider
@ -50,11 +52,13 @@ struct FlatSigningProvider final : public SigningProvider
std::map<CKeyID, CPubKey> pubkeys;
std::map<CKeyID, std::pair<CPubKey, KeyOriginInfo>> origins;
std::map<CKeyID, CKey> keys;
std::map<XOnlyPubKey, TaprootSpendData> tr_spenddata; /** Map from output key to spend data. */
bool GetCScript(const CScriptID& scriptid, CScript& script) const override;
bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
bool GetKey(const CKeyID& keyid, CKey& key) const override;
bool GetTaprootSpendData(const XOnlyPubKey& output_key, TaprootSpendData& spenddata) const override;
};
FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b);

View file

@ -377,6 +377,16 @@ bool IsValidDestination(const CTxDestination& dest) {
/*static*/ TaprootBuilder::NodeInfo TaprootBuilder::Combine(NodeInfo&& a, NodeInfo&& b)
{
NodeInfo ret;
/* Iterate over all tracked leaves in a, add b's hash to their Merkle branch, and move them to ret. */
for (auto& leaf : a.leaves) {
leaf.merkle_branch.push_back(b.hash);
ret.leaves.emplace_back(std::move(leaf));
}
/* Iterate over all tracked leaves in b, add a's hash to their Merkle branch, and move them to ret. */
for (auto& leaf : b.leaves) {
leaf.merkle_branch.push_back(a.hash);
ret.leaves.emplace_back(std::move(leaf));
}
/* Lexicographically sort a and b's hash, and compute parent hash. */
if (a.hash < b.hash) {
ret.hash = (CHashWriter(HASHER_TAPBRANCH) << a.hash << b.hash).GetSHA256();
@ -386,6 +396,21 @@ bool IsValidDestination(const CTxDestination& dest) {
return ret;
}
void TaprootSpendData::Merge(TaprootSpendData other)
{
// TODO: figure out how to better deal with conflicting information
// being merged.
if (internal_key.IsNull() && !other.internal_key.IsNull()) {
internal_key = other.internal_key;
}
if (merkle_root.IsNull() && !other.merkle_root.IsNull()) {
merkle_root = other.merkle_root;
}
for (auto& [key, control_blocks] : other.scripts) {
scripts[key].merge(std::move(control_blocks));
}
}
void TaprootBuilder::Insert(TaprootBuilder::NodeInfo&& node, int depth)
{
assert(depth >= 0 && (size_t)depth <= TAPROOT_CONTROL_MAX_NODE_COUNT);
@ -435,13 +460,14 @@ void TaprootBuilder::Insert(TaprootBuilder::NodeInfo&& node, int depth)
return branch.size() == 0 || (branch.size() == 1 && branch[0]);
}
TaprootBuilder& TaprootBuilder::Add(int depth, const CScript& script, int leaf_version)
TaprootBuilder& TaprootBuilder::Add(int depth, const CScript& script, int leaf_version, bool track)
{
assert((leaf_version & ~TAPROOT_LEAF_MASK) == 0);
if (!IsValid()) return *this;
/* Construct NodeInfo object with leaf hash. */
/* Construct NodeInfo object with leaf hash and (if track is true) also leaf information. */
NodeInfo node;
node.hash = (CHashWriter{HASHER_TAPLEAF} << uint8_t(leaf_version) << script).GetSHA256();
if (track) node.leaves.emplace_back(LeafInfo{script, leaf_version, {}});
/* Insert into the branch. */
Insert(std::move(node), depth);
return *this;
@ -464,8 +490,33 @@ TaprootBuilder& TaprootBuilder::Finalize(const XOnlyPubKey& internal_key)
m_internal_key = internal_key;
auto ret = m_internal_key.CreateTapTweak(m_branch.size() == 0 ? nullptr : &m_branch[0]->hash);
assert(ret.has_value());
std::tie(m_output_key, std::ignore) = *ret;
std::tie(m_output_key, m_parity) = *ret;
return *this;
}
WitnessV1Taproot TaprootBuilder::GetOutput() { return WitnessV1Taproot{m_output_key}; }
TaprootSpendData TaprootBuilder::GetSpendData() const
{
TaprootSpendData spd;
spd.merkle_root = m_branch.size() == 0 ? uint256() : m_branch[0]->hash;
spd.internal_key = m_internal_key;
if (m_branch.size()) {
// If any script paths exist, they have been combined into the root m_branch[0]
// by now. Compute the control block for each of its tracked leaves, and put them in
// spd.scripts.
for (const auto& leaf : m_branch[0]->leaves) {
std::vector<unsigned char> control_block;
control_block.resize(TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * leaf.merkle_branch.size());
control_block[0] = leaf.leaf_version | (m_parity ? 1 : 0);
std::copy(m_internal_key.begin(), m_internal_key.end(), control_block.begin() + 1);
if (leaf.merkle_branch.size()) {
std::copy(leaf.merkle_branch[0].begin(),
leaf.merkle_branch[0].begin() + TAPROOT_CONTROL_NODE_SIZE * leaf.merkle_branch.size(),
control_block.begin() + TAPROOT_CONTROL_BASE_SIZE);
}
spd.scripts[{leaf.script, leaf.leaf_version}].insert(std::move(control_block));
}
}
return spd;
}

View file

@ -11,6 +11,7 @@
#include <uint256.h>
#include <util/hash_type.h>
#include <map>
#include <string>
#include <variant>
@ -209,15 +210,50 @@ CScript GetScriptForRawPubKey(const CPubKey& pubkey);
/** Generate a multisig script. */
CScript GetScriptForMultisig(int nRequired, const std::vector<CPubKey>& keys);
struct ShortestVectorFirstComparator
{
bool operator()(const std::vector<unsigned char>& a, const std::vector<unsigned char>& b) const
{
if (a.size() < b.size()) return true;
if (a.size() > b.size()) return false;
return a < b;
}
};
struct TaprootSpendData
{
/** The BIP341 internal key. */
XOnlyPubKey internal_key;
/** The Merkle root of the script tree (0 if no scripts). */
uint256 merkle_root;
/** Map from (script, leaf_version) to (sets of) control blocks.
* The control blocks are sorted by size, so that the signing logic can
* easily prefer the cheapest one. */
std::map<std::pair<CScript, int>, std::set<std::vector<unsigned char>, ShortestVectorFirstComparator>> scripts;
/** Merge other TaprootSpendData (for the same scriptPubKey) into this. */
void Merge(TaprootSpendData other);
};
/** Utility class to construct Taproot outputs from internal key and script tree. */
class TaprootBuilder
{
private:
/** Information about a tracked leaf in the Merkle tree. */
struct LeafInfo
{
CScript script; //!< The script.
int leaf_version; //!< The leaf version for that script.
std::vector<uint256> merkle_branch; //!< The hashing partners above this leaf.
};
/** Information associated with a node in the Merkle tree. */
struct NodeInfo
{
/** Merkle hash of this node. */
uint256 hash;
/** Tracked leaves underneath this node (either from the node itself, or its children).
* The merkle_branch field for each is the partners to get to *this* node. */
std::vector<LeafInfo> leaves;
};
/** Whether the builder is in a valid state so far. */
bool m_valid = true;
@ -260,7 +296,8 @@ private:
std::vector<std::optional<NodeInfo>> m_branch;
XOnlyPubKey m_internal_key; //!< The internal key, set when finalizing.
XOnlyPubKey m_output_key; //!< The output key, computed when finalizing. */
XOnlyPubKey m_output_key; //!< The output key, computed when finalizing.
bool m_parity; //!< The tweak parity, computed when finalizing.
/** Combine information about a parent Merkle tree node from its child nodes. */
static NodeInfo Combine(NodeInfo&& a, NodeInfo&& b);
@ -269,8 +306,9 @@ private:
public:
/** Add a new script at a certain depth in the tree. Add() operations must be called
* in depth-first traversal order of binary tree. */
TaprootBuilder& Add(int depth, const CScript& script, int leaf_version);
* in depth-first traversal order of binary tree. If track is true, it will be included in
* the GetSpendData() output. */
TaprootBuilder& Add(int depth, const CScript& script, int leaf_version, bool track = true);
/** Like Add(), but for a Merkle node with a given hash to the tree. */
TaprootBuilder& AddOmitted(int depth, const uint256& hash);
/** Finalize the construction. Can only be called when IsComplete() is true.
@ -285,6 +323,8 @@ public:
WitnessV1Taproot GetOutput();
/** Check if a list of depths is legal (will lead to IsComplete()). */
static bool ValidDepths(const std::vector<int>& depths);
/** Compute spending data (after Finalize()). */
TaprootSpendData GetSpendData() const;
};
#endif // BITCOIN_SCRIPT_STANDARD_H

View file

@ -300,6 +300,48 @@ BOOST_AUTO_TEST_CASE(bip340_test_vectors)
auto sig = ParseHex(test.first[2]);
BOOST_CHECK_EQUAL(XOnlyPubKey(pubkey).VerifySchnorr(uint256(msg), sig), test.second);
}
static const std::vector<std::array<std::string, 5>> SIGN_VECTORS = {
{{"0000000000000000000000000000000000000000000000000000000000000003", "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", "0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000000", "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"}},
{{"B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF", "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", "0000000000000000000000000000000000000000000000000000000000000001", "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89", "6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A"}},
{{"C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9", "DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", "C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906", "7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C", "5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7"}},
{{"0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710", "25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3"}},
};
for (const auto& [sec_hex, pub_hex, aux_hex, msg_hex, sig_hex] : SIGN_VECTORS) {
auto sec = ParseHex(sec_hex);
auto pub = ParseHex(pub_hex);
uint256 aux256(ParseHex(aux_hex));
uint256 msg256(ParseHex(msg_hex));
auto sig = ParseHex(sig_hex);
unsigned char sig64[64];
// Run the untweaked test vectors above, comparing with exact expected signature.
CKey key;
key.Set(sec.begin(), sec.end(), true);
XOnlyPubKey pubkey(key.GetPubKey());
BOOST_CHECK(std::equal(pubkey.begin(), pubkey.end(), pub.begin(), pub.end()));
bool ok = key.SignSchnorr(msg256, sig64, nullptr, &aux256);
BOOST_CHECK(ok);
BOOST_CHECK(std::vector<unsigned char>(sig64, sig64 + 64) == sig);
// Verify those signatures for good measure.
BOOST_CHECK(pubkey.VerifySchnorr(msg256, sig64));
// Do 10 iterations where we sign with a random Merkle root to tweak,
// and compare against the resulting tweaked keys, with random aux.
// In iteration i=0 we tweak with empty Merkle tree.
for (int i = 0; i < 10; ++i) {
uint256 merkle_root;
if (i) merkle_root = InsecureRand256();
auto tweaked = pubkey.CreateTapTweak(i ? &merkle_root : nullptr);
BOOST_CHECK(tweaked);
XOnlyPubKey tweaked_key = tweaked->first;
aux256 = InsecureRand256();
bool ok = key.SignSchnorr(msg256, sig64, &merkle_root, &aux256);
BOOST_CHECK(ok);
BOOST_CHECK(tweaked_key.VerifySchnorr(msg256, sig64));
}
}
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -60,10 +60,10 @@ bool ExternalSignerScriptPubKeyMan::DisplayAddress(const CScript scriptPubKey, c
}
// If sign is true, transaction must previously have been filled
TransactionError ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
TransactionError ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (!sign) {
return DescriptorScriptPubKeyMan::FillPSBT(psbt, sighash_type, false, bip32derivs, n_signed);
return DescriptorScriptPubKeyMan::FillPSBT(psbt, txdata, sighash_type, false, bip32derivs, n_signed);
}
// Already complete if every input is now signed

View file

@ -28,6 +28,6 @@ class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
bool DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
};
#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H

View file

@ -3320,7 +3320,8 @@ RPCHelpMan signrawtransactionwithwallet()
},
},
},
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL"}, "The signature hash type. Must be one of\n"
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"DEFAULT"}, "The signature hash type. Must be one of\n"
" \"DEFAULT\"\n"
" \"ALL\"\n"
" \"NONE\"\n"
" \"SINGLE\"\n"
@ -3542,7 +3543,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
} else {
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, false /* sign */, true /* bip32derivs */);
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, false /* sign */, true /* bip32derivs */);
CHECK_NONFATAL(err == TransactionError::OK);
CHECK_NONFATAL(!complete);
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
@ -4175,8 +4176,8 @@ static RPCHelpMan send()
// First fill transaction with our data without signing,
// so external signers are not asked sign more than once.
bool complete;
pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, false, true);
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, false, true);
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, true, false);
if (err != TransactionError::OK) {
throw JSONRPCTransactionError(err);
}
@ -4291,7 +4292,8 @@ static RPCHelpMan walletprocesspsbt()
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction base64 string"},
{"sign", RPCArg::Type::BOOL, RPCArg::Default{true}, "Also sign the transaction when updating"},
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL"}, "The signature hash type to sign with if not specified by the PSBT. Must be one of\n"
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"DEFAULT"}, "The signature hash type to sign with if not specified by the PSBT. Must be one of\n"
" \"DEFAULT\"\n"
" \"ALL\"\n"
" \"NONE\"\n"
" \"SINGLE\"\n"

View file

@ -596,7 +596,7 @@ SigningResult LegacyScriptPubKeyMan::SignMessage(const std::string& message, con
return SigningResult::SIGNING_FAILED;
}
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
@ -625,7 +625,7 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb
}
SignatureData sigdata;
input.FillSignatureData(sigdata);
SignPSBTInput(HidingSigningProvider(this, !sign, !bip32derivs), psbtx, i, sighash_type);
SignPSBTInput(HidingSigningProvider(this, !sign, !bip32derivs), psbtx, i, &txdata, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
@ -2082,7 +2082,7 @@ SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message,
return SigningResult::OK;
}
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
@ -2132,7 +2132,7 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction&
}
}
SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, sighash_type);
SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, &txdata, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {

View file

@ -235,7 +235,7 @@ public:
/** Sign a message with the given script */
virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; };
/** Adds script and derivation path information to a PSBT, and optionally signs it. */
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const { return TransactionError::INVALID_PSBT; }
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const { return TransactionError::INVALID_PSBT; }
virtual uint256 GetID() const { return uint256(); }
@ -394,7 +394,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;
@ -605,7 +605,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;

View file

@ -71,7 +71,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test)
// Try to sign the mutated input
SignatureData sigdata;
BOOST_CHECK(spk_man->FillPSBT(psbtx, SIGHASH_ALL, true, true) != TransactionError::OK);
BOOST_CHECK(spk_man->FillPSBT(psbtx, PrecomputePSBTData(psbtx), SIGHASH_ALL, true, true) != TransactionError::OK);
}
BOOST_AUTO_TEST_CASE(parse_hd_keypath)

View file

@ -1808,7 +1808,7 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const
coins[input.prevout] = Coin(wtx.tx->vout[input.prevout.n], wtx.m_confirm.block_height, wtx.IsCoinBase());
}
std::map<int, std::string> input_errors;
return SignTransaction(tx, coins, SIGHASH_ALL, input_errors);
return SignTransaction(tx, coins, SIGHASH_DEFAULT, input_errors);
}
bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const
@ -1831,6 +1831,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp
if (n_signed) {
*n_signed = 0;
}
const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
LOCK(cs_wallet);
// Get all of the previous transactions
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
@ -1857,7 +1858,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp
// Fill in information from ScriptPubKeyMans
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
int n_signed_this_spkm = 0;
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
TransactionError res = spk_man->FillPSBT(psbtx, txdata, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
if (res != TransactionError::OK) {
return res;
}

View file

@ -6,6 +6,7 @@
import random
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.descriptors import descsum_create
@ -233,20 +234,85 @@ class WalletTaprootTest(BitcoinTestFramework):
# tr descriptors cannot be imported when Taproot is not active
result = self.privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
assert(result[0]["success"])
result = self.privs_tr_disabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
assert(not result[0]["success"])
assert_equal(result[0]["error"]["code"], -4)
assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active")
result = self.pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
assert(result[0]["success"])
result = self.pubs_tr_disabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
assert(not result[0]["success"])
assert_equal(result[0]["error"]["code"], -4)
assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active")
if desc.startswith("tr"):
result = self.privs_tr_disabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
assert(not result[0]["success"])
assert_equal(result[0]["error"]["code"], -4)
assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active")
result = self.pubs_tr_disabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
assert(not result[0]["success"])
assert_equal(result[0]["error"]["code"], -4)
assert_equal(result[0]["error"]["message"], "Cannot import tr() descriptor when Taproot is not active")
def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
self.log.info("Testing %s through sendtoaddress" % comment)
desc_pay = self.make_desc(pattern, privmap, keys_pay)
desc_change = self.make_desc(pattern, privmap, keys_change)
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
result = self.rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
assert(result[0]['success'])
result = self.rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
assert(result[0]['success'])
for i in range(4):
addr_g = self.rpc_online.getnewaddress(address_type='bech32')
if treefn is not None:
addr_r = self.make_addr(treefn, keys_pay, i)
assert_equal(addr_g, addr_r)
boring_balance = int(self.boring.getbalance() * 100000000)
to_amnt = random.randrange(1000000, boring_balance)
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
test_balance = int(self.rpc_online.getbalance() * 100000000)
ret_amnt = random.randrange(100000, test_balance)
res = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
assert(self.rpc_online.gettransaction(res)["confirmations"] > 0)
def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
self.log.info("Testing %s through PSBT" % comment)
desc_pay = self.make_desc(pattern, privmap, keys_pay, False)
desc_change = self.make_desc(pattern, privmap, keys_change, False)
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
result = self.psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])
assert(result[0]['success'])
result = self.psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])
assert(result[0]['success'])
result = self.psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
assert(result[0]['success'])
result = self.psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
assert(result[0]['success'])
for i in range(4):
addr_g = self.psbt_online.getnewaddress(address_type='bech32')
if treefn is not None:
addr_r = self.make_addr(treefn, keys_pay, i)
assert_equal(addr_g, addr_r)
boring_balance = int(self.boring.getbalance() * 100000000)
to_amnt = random.randrange(1000000, boring_balance)
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
test_balance = int(self.psbt_online.getbalance() * 100000000)
ret_amnt = random.randrange(100000, test_balance)
psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0]})['psbt']
res = self.psbt_offline.walletprocesspsbt(psbt)
assert(res['complete'])
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
txid = self.nodes[0].sendrawtransaction(rawtx)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0)
def do_test(self, comment, pattern, privmap, treefn, nkeys):
keys = self.rand_keys(nkeys)
self.do_test_addr(comment, pattern, privmap, treefn, keys)
keys = self.rand_keys(nkeys * 4)
self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys])
self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys])
self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
def run_test(self):
self.log.info("Creating wallets...")
@ -258,8 +324,20 @@ class WalletTaprootTest(BitcoinTestFramework):
self.pubs_tr_enabled = self.nodes[0].get_wallet_rpc("pubs_tr_enabled")
self.nodes[2].createwallet(wallet_name="pubs_tr_disabled", descriptors=True, blank=True, disable_private_keys=True)
self.pubs_tr_disabled=self.nodes[2].get_wallet_rpc("pubs_tr_disabled")
self.nodes[0].createwallet(wallet_name="boring")
self.nodes[0].createwallet(wallet_name="addr_gen", descriptors=True, disable_private_keys=True, blank=True)
self.nodes[0].createwallet(wallet_name="rpc_online", descriptors=True, blank=True)
self.nodes[0].createwallet(wallet_name="psbt_online", descriptors=True, disable_private_keys=True, blank=True)
self.nodes[1].createwallet(wallet_name="psbt_offline", descriptors=True, blank=True)
self.boring = self.nodes[0].get_wallet_rpc("boring")
self.addr_gen = self.nodes[0].get_wallet_rpc("addr_gen")
self.rpc_online = self.nodes[0].get_wallet_rpc("rpc_online")
self.psbt_online = self.nodes[0].get_wallet_rpc("psbt_online")
self.psbt_offline = self.nodes[1].get_wallet_rpc("psbt_offline")
self.log.info("Mining blocks...")
gen_addr = self.boring.getnewaddress()
self.nodes[0].generatetoaddress(101, gen_addr)
self.do_test(
"tr(XPRV)",
@ -275,6 +353,13 @@ class WalletTaprootTest(BitcoinTestFramework):
lambda k1: (key(H_POINT), [pk(k1)]),
1
)
self.do_test(
"wpkh(XPRV)",
"wpkh($1/*)",
[True],
None,
1
)
self.do_test(
"tr(XPRV,{H,{H,XPUB}})",
"tr($1/*,{pk($H),{pk($H),pk($2/*)}})",
@ -282,6 +367,13 @@ class WalletTaprootTest(BitcoinTestFramework):
lambda k1, k2: (key(k1), [pk(H_POINT), [pk(H_POINT), pk(k2)]]),
2
)
self.do_test(
"wsh(multi(1,XPRV,XPUB))",
"wsh(multi(1,$1/*,$2/*))",
[True, False],
None,
2
)
self.do_test(
"tr(XPUB,{{H,{H,XPUB}},{H,{H,{H,XPRV}}}})",
"tr($1/*,{{pk($H),{pk($H),pk($2/*)}},{pk($H),{pk($H),{pk($H),pk($3/*)}}}})",
@ -290,5 +382,19 @@ class WalletTaprootTest(BitcoinTestFramework):
3
)
self.log.info("Sending everything back...")
txid = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=self.rpc_online.getbalance(), subtractfeefromamount=True)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
assert(self.rpc_online.gettransaction(txid)["confirmations"] > 0)
psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): self.psbt_online.getbalance()}], None, {"subtractFeeFromOutputs": [0]})['psbt']
res = self.psbt_offline.walletprocesspsbt(psbt)
assert(res['complete'])
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
txid = self.nodes[0].sendrawtransaction(rawtx)
self.nodes[0].generatetoaddress(1, self.boring.getnewaddress())
assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0)
if __name__ == '__main__':
WalletTaprootTest().main()