Merge #13932: Additional utility RPCs for PSBT

540729ef4b Implement analyzepsbt RPC and tests (Andrew Chow)
77542cf2a5 Move PSBT UTXO fetching to a separate method (Andrew Chow)
cb40b3abd4 Figure out what is missing during signing (Andrew Chow)
08f749c914 Implement joinpsbts RPC and tests (Andrew Chow)
7344a7b998 Implement utxoupdatepsbt RPC and tests (Andrew Chow)

Pull request description:

  This PR adds 3 new utility RPCs for interacting with PSBTs.

  `utxoupdatepsbt` updates a PSBT with UTXO information from the node. It only works with witness UTXOs because full transactions (as would be needed for non-witness UTXOs) are not available unless txindex is enabled.

  `joinpsbts` joins the inputs from multiple distinct PSBTs into one PSBT. e.g. if PSBT 1 has inputs 1 and 2, and PSBT 2 has inputs 3 and 4, `joinpsbts` would create a new PSBT with inputs 1, 2, 3, and 4.

  `analyzepsbt` analyzes a PSBT and determines the current state of it and all of its inputs, and the next step that needs to be done.

Tree-SHA512: 3c1fa302201abca76a8901d0c2be7b4ccbce334d989533c215f8b3e50e22f2f018ce6209544b26789f58f5980a253c0655111e1e20d47d5656e0414c64891a5c
This commit is contained in:
Wladimir J. van der Laan 2019-02-16 20:31:05 +01:00
commit d5b929c813
No known key found for this signature in database
GPG key ID: 1E4AED62986CD25D
7 changed files with 487 additions and 9 deletions

View file

@ -42,6 +42,40 @@ bool PartiallySignedTransaction::IsSane() const
return true;
}
bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin)
{
if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) {
return false;
}
tx->vin.push_back(txin);
psbtin.partial_sigs.clear();
psbtin.final_script_sig.clear();
psbtin.final_script_witness.SetNull();
inputs.push_back(psbtin);
return true;
}
bool PartiallySignedTransaction::AddOutput(const CTxOut& txout, const PSBTOutput& psbtout)
{
tx->vout.push_back(txout);
outputs.push_back(psbtout);
return true;
}
bool PartiallySignedTransaction::GetInputUTXO(CTxOut& utxo, int input_index) const
{
PSBTInput input = inputs[input_index];
int prevout_index = tx->vin[input_index].prevout.n;
if (input.non_witness_utxo) {
utxo = input.non_witness_utxo->vout[prevout_index];
} else if (!input.witness_utxo.IsNull()) {
utxo = input.witness_utxo;
} else {
return false;
}
return true;
}
bool PSBTInput::IsNull() const
{
return !non_witness_utxo && witness_utxo.IsNull() && partial_sigs.empty() && unknown.empty() && hd_keypaths.empty() && redeem_script.empty() && witness_script.empty();
@ -171,13 +205,12 @@ void PSBTOutput::Merge(const PSBTOutput& output)
if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script;
if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script;
}
bool PSBTInputSigned(PSBTInput& input)
{
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
}
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash)
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash, SignatureData* out_sigdata, bool use_dummy)
{
PSBTInput& input = psbt.inputs.at(index);
const CMutableTransaction& tx = *psbt.tx;
@ -217,9 +250,14 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
return false;
}
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
sigdata.witness = false;
bool sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata);
bool sig_complete;
if (use_dummy) {
sig_complete = ProduceSignature(provider, DUMMY_SIGNATURE_CREATOR, utxo.scriptPubKey, sigdata);
} else {
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata);
}
// Verify that a witness signature was produced in case one was required.
if (require_witness_sig && !sigdata.witness) return false;
input.FromSignatureData(sigdata);
@ -230,6 +268,14 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
input.non_witness_utxo = nullptr;
}
// Fill in the missing info
if (out_sigdata) {
out_sigdata->missing_pubkeys = sigdata.missing_pubkeys;
out_sigdata->missing_sigs = sigdata.missing_sigs;
out_sigdata->missing_redeem_script = sigdata.missing_redeem_script;
out_sigdata->missing_witness_script = sigdata.missing_witness_script;
}
return sig_complete;
}

View file

@ -389,9 +389,19 @@ struct PartiallySignedTransaction
* same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */
NODISCARD bool Merge(const PartiallySignedTransaction& psbt);
bool IsSane() const;
bool AddInput(const CTxIn& txin, PSBTInput& psbtin);
bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout);
PartiallySignedTransaction() {}
PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {}
explicit PartiallySignedTransaction(const CMutableTransaction& tx);
/**
* Finds the UTXO for a given input index
*
* @param[out] utxo The UTXO of the input if found
* @param[in] input_index Index of the input to retrieve the UTXO of
* @return Whether the UTXO for the specified input was found
*/
bool GetInputUTXO(CTxOut& utxo, int input_index) const;
template <typename Stream>
inline void Serialize(Stream& s) const {
@ -542,7 +552,7 @@ struct PartiallySignedTransaction
bool PSBTInputSigned(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);
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
/**
* Finalizes a PSBT if possible, combining partial signatures.

View file

@ -112,6 +112,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createpsbt", 2, "locktime" },
{ "createpsbt", 3, "replaceable" },
{ "combinepsbt", 0, "txs"},
{ "joinpsbts", 0, "txs"},
{ "finalizepsbt", 1, "extract"},
{ "converttopsbt", 1, "permitsigdata"},
{ "converttopsbt", 2, "iswitness"},

View file

@ -7,6 +7,7 @@
#include <coins.h>
#include <compat/byteswap.h>
#include <consensus/validation.h>
#include <consensus/tx_verify.h>
#include <core_io.h>
#include <index/txindex.h>
#include <init.h>
@ -31,6 +32,8 @@
#include <validation.h>
#include <validationinterface.h>
#include <numeric>
#include <stdint.h>
#include <univalue.h>
@ -1703,6 +1706,338 @@ UniValue converttopsbt(const JSONRPCRequest& request)
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
}
UniValue utxoupdatepsbt(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 1) {
throw std::runtime_error(
RPCHelpMan{"utxoupdatepsbt",
"\nUpdates a PSBT with witness UTXOs retrieved from the UTXO set or the mempool.\n",
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
},
RPCResult {
" \"psbt\" (string) The base64-encoded partially signed transaction with inputs updated\n"
},
RPCExamples {
HelpExampleCli("utxoupdatepsbt", "\"psbt\"")
}}.ToString());
}
RPCTypeCheck(request.params, {UniValue::VSTR}, true);
// Unserialize the transactions
PartiallySignedTransaction psbtx;
std::string error;
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
// Fetch previous transactions (inputs):
CCoinsView viewDummy;
CCoinsViewCache view(&viewDummy);
{
LOCK2(cs_main, mempool.cs);
CCoinsViewCache &viewChain = *pcoinsTip;
CCoinsViewMemPool viewMempool(&viewChain, mempool);
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
for (const CTxIn& txin : psbtx.tx->vin) {
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
}
view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long
}
// Fill the inputs
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs.at(i);
if (input.non_witness_utxo || !input.witness_utxo.IsNull()) {
continue;
}
const Coin& coin = view.AccessCoin(psbtx.tx->vin[i].prevout);
std::vector<std::vector<unsigned char>> solutions_data;
txnouttype which_type = Solver(coin.out.scriptPubKey, solutions_data);
if (which_type == TX_WITNESS_V0_SCRIPTHASH || which_type == TX_WITNESS_V0_KEYHASH || which_type == TX_WITNESS_UNKNOWN) {
input.witness_utxo = coin.out;
}
}
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
}
UniValue joinpsbts(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 1) {
throw std::runtime_error(
RPCHelpMan{"joinpsbts",
"\nJoins multiple distinct PSBTs with different inputs and outputs into one PSBT with inputs and outputs from all of the PSBTs\n"
"No input in any of the PSBTs can be in more than one of the PSBTs.\n",
{
{"txs", RPCArg::Type::ARR, RPCArg::Optional::NO, "A json array of base64 strings of partially signed transactions",
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
}}
},
RPCResult {
" \"psbt\" (string) The base64-encoded partially signed transaction\n"
},
RPCExamples {
HelpExampleCli("joinpsbts", "\"psbt\"")
}}.ToString());
}
RPCTypeCheck(request.params, {UniValue::VARR}, true);
// Unserialize the transactions
std::vector<PartiallySignedTransaction> psbtxs;
UniValue txs = request.params[0].get_array();
if (txs.size() <= 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "At least two PSBTs are required to join PSBTs.");
}
int32_t best_version = 1;
uint32_t best_locktime = 0xffffffff;
for (unsigned int i = 0; i < txs.size(); ++i) {
PartiallySignedTransaction psbtx;
std::string error;
if (!DecodeBase64PSBT(psbtx, txs[i].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
psbtxs.push_back(psbtx);
// Choose the highest version number
if (psbtx.tx->nVersion > best_version) {
best_version = psbtx.tx->nVersion;
}
// Choose the lowest lock time
if (psbtx.tx->nLockTime < best_locktime) {
best_locktime = psbtx.tx->nLockTime;
}
}
// Create a blank psbt where everything will be added
PartiallySignedTransaction merged_psbt;
merged_psbt.tx = CMutableTransaction();
merged_psbt.tx->nVersion = best_version;
merged_psbt.tx->nLockTime = best_locktime;
// Merge
for (auto& psbt : psbtxs) {
for (unsigned int i = 0; i < psbt.tx->vin.size(); ++i) {
if (!merged_psbt.AddInput(psbt.tx->vin[i], psbt.inputs[i])) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input %s:%d exists in multiple PSBTs", psbt.tx->vin[i].prevout.hash.ToString().c_str(), psbt.tx->vin[i].prevout.n));
}
}
for (unsigned int i = 0; i < psbt.tx->vout.size(); ++i) {
merged_psbt.AddOutput(psbt.tx->vout[i], psbt.outputs[i]);
}
merged_psbt.unknown.insert(psbt.unknown.begin(), psbt.unknown.end());
}
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << merged_psbt;
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
}
UniValue analyzepsbt(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 1) {
throw std::runtime_error(
RPCHelpMan{"analyzepsbt",
"\nAnalyzes and provides information about the current status of a PSBT and its inputs\n",
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
},
RPCResult {
"{\n"
" \"inputs\" : [ (array of json objects)\n"
" {\n"
" \"has_utxo\" : true|false (boolean) Whether a UTXO is provided\n"
" \"is_final\" : true|false (boolean) Whether the input is finalized\n"
" \"missing\" : { (json object, optional) Things that are missing that are required to complete this input\n"
" \"pubkeys\" : [ (array)\n"
" \"keyid\" (string) Public key ID, hash160 of the public key, of a public key whose BIP 32 derivation path is missing\n"
" ]\n"
" \"signatures\" : [ (array)\n"
" \"keyid\" (string) Public key ID, hash160 of the public key, of a public key whose signature is missing\n"
" ]\n"
" \"redeemscript\" : \"hash\" (string) Hash160 of the redeemScript that is missing\n"
" \"witnessscript\" : \"hash\" (string) SHA256 of the witnessScript that is missing\n"
" }\n"
" \"next\" : \"role\" (string) Role of the next person that this input needs to go to\n"
" }\n"
" ,...\n"
" ]\n"
" \"estimated_vsize\" : vsize (numeric) Estimated vsize of the final signed transaction\n"
" \"estimated_feerate\" : feerate (numeric, optional) Estimated feerate of the final signed transaction. Shown only if all UTXO slots in the PSBT have been filled.\n"
" \"fee\" : fee (numeric, optional) The transaction fee paid. Shown only if all UTXO slots in the PSBT have been filled.\n"
" \"next\" : \"role\" (string) Role of the next person that this psbt needs to go to\n"
"}\n"
},
RPCExamples {
HelpExampleCli("analyzepsbt", "\"psbt\"")
}}.ToString());
}
RPCTypeCheck(request.params, {UniValue::VSTR});
// Unserialize the transaction
PartiallySignedTransaction psbtx;
std::string error;
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
// Go through each input and build status
UniValue result(UniValue::VOBJ);
UniValue inputs_result(UniValue::VARR);
bool calc_fee = true;
bool all_final = true;
bool only_missing_sigs = true;
bool only_missing_final = false;
CAmount in_amt = 0;
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs[i];
UniValue input_univ(UniValue::VOBJ);
UniValue missing(UniValue::VOBJ);
// Check for a UTXO
CTxOut utxo;
if (psbtx.GetInputUTXO(utxo, i)) {
in_amt += utxo.nValue;
input_univ.pushKV("has_utxo", true);
} else {
input_univ.pushKV("has_utxo", false);
input_univ.pushKV("is_final", false);
input_univ.pushKV("next", "updater");
calc_fee = false;
}
// Check if it is final
if (!utxo.IsNull() && !PSBTInputSigned(input)) {
input_univ.pushKV("is_final", false);
all_final = false;
// Figure out what is missing
SignatureData outdata;
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, &outdata);
// Things are missing
if (!complete) {
if (!outdata.missing_pubkeys.empty()) {
// Missing pubkeys
UniValue missing_pubkeys_univ(UniValue::VARR);
for (const CKeyID& pubkey : outdata.missing_pubkeys) {
missing_pubkeys_univ.push_back(HexStr(pubkey));
}
missing.pushKV("pubkeys", missing_pubkeys_univ);
}
if (!outdata.missing_redeem_script.IsNull()) {
// Missing redeemScript
missing.pushKV("redeemscript", HexStr(outdata.missing_redeem_script));
}
if (!outdata.missing_witness_script.IsNull()) {
// Missing witnessScript
missing.pushKV("witnessscript", HexStr(outdata.missing_witness_script));
}
if (!outdata.missing_sigs.empty()) {
// Missing sigs
UniValue missing_sigs_univ(UniValue::VARR);
for (const CKeyID& pubkey : outdata.missing_sigs) {
missing_sigs_univ.push_back(HexStr(pubkey));
}
missing.pushKV("signatures", missing_sigs_univ);
}
input_univ.pushKV("missing", missing);
// If we are only missing signatures and nothing else, then next is signer
if (outdata.missing_pubkeys.empty() && outdata.missing_redeem_script.IsNull() && outdata.missing_witness_script.IsNull() && !outdata.missing_sigs.empty()) {
input_univ.pushKV("next", "signer");
} else {
only_missing_sigs = false;
input_univ.pushKV("next", "updater");
}
} else {
only_missing_final = true;
input_univ.pushKV("next", "finalizer");
}
} else if (!utxo.IsNull()){
input_univ.pushKV("is_final", true);
}
inputs_result.push_back(input_univ);
}
result.pushKV("inputs", inputs_result);
if (all_final) {
only_missing_sigs = false;
result.pushKV("next", "extractor");
}
if (calc_fee) {
// Get the output amount
CAmount out_amt = std::accumulate(psbtx.tx->vout.begin(), psbtx.tx->vout.end(), 0,
[](int a, const CTxOut& b) {
return a += b.nValue;
}
);
// Get the fee
CAmount fee = in_amt - out_amt;
// Estimate the size
CMutableTransaction mtx(*psbtx.tx);
CCoinsView view_dummy;
CCoinsViewCache view(&view_dummy);
bool success = true;
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs[i];
if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, nullptr, true)) {
mtx.vin[i].scriptSig = input.final_script_sig;
mtx.vin[i].scriptWitness = input.final_script_witness;
Coin newcoin;
if (!psbtx.GetInputUTXO(newcoin.out, i)) {
success = false;
break;
}
newcoin.nHeight = 1;
view.AddCoin(psbtx.tx->vin[i].prevout, std::move(newcoin), true);
} else {
success = false;
break;
}
}
if (success) {
CTransaction ctx = CTransaction(mtx);
size_t size = GetVirtualTransactionSize(ctx, GetTransactionSigOpCost(ctx, view, STANDARD_SCRIPT_VERIFY_FLAGS));
result.pushKV("estimated_vsize", (int)size);
// Estimate fee rate
CFeeRate feerate(fee, size);
result.pushKV("estimated_feerate", feerate.ToString());
}
result.pushKV("fee", ValueFromAmount(fee));
if (only_missing_sigs) {
result.pushKV("next", "signer");
} else if (only_missing_final) {
result.pushKV("next", "finalizer");
} else if (all_final) {
result.pushKV("next", "extractor");
} else {
result.pushKV("next", "updater");
}
} else {
result.pushKV("next", "updater");
}
return result;
}
// clang-format off
static const CRPCCommand commands[] =
{ // category name actor (function) argNames
@ -1721,6 +2056,9 @@ static const CRPCCommand commands[] =
{ "rawtransactions", "finalizepsbt", &finalizepsbt, {"psbt", "extract"} },
{ "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime","replaceable"} },
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
{ "rawtransactions", "analyzepsbt", &analyzepsbt, {"psbt"} },
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },

View file

@ -83,6 +83,8 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
assert(i.second);
return true;
}
// Could not make signature or signature not found, add keyid to missing
sigdata.missing_sigs.push_back(keyid);
return false;
}
@ -116,17 +118,24 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
case TX_PUBKEYHASH: {
CKeyID keyID = CKeyID(uint160(vSolutions[0]));
CPubKey pubkey;
if (!GetPubKey(provider, sigdata, keyID, pubkey)) return false;
if (!GetPubKey(provider, sigdata, keyID, pubkey)) {
// Pubkey could not be found, add to missing
sigdata.missing_pubkeys.push_back(keyID);
return false;
}
if (!CreateSig(creator, sigdata, provider, sig, pubkey, scriptPubKey, sigversion)) return false;
ret.push_back(std::move(sig));
ret.push_back(ToByteVector(pubkey));
return true;
}
case TX_SCRIPTHASH:
if (GetCScript(provider, sigdata, uint160(vSolutions[0]), scriptRet)) {
h160 = uint160(vSolutions[0]);
if (GetCScript(provider, sigdata, h160, scriptRet)) {
ret.push_back(std::vector<unsigned char>(scriptRet.begin(), scriptRet.end()));
return true;
}
// Could not find redeemScript, add to missing
sigdata.missing_redeem_script = h160;
return false;
case TX_MULTISIG: {
@ -154,6 +163,8 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
ret.push_back(std::vector<unsigned char>(scriptRet.begin(), scriptRet.end()));
return true;
}
// Could not find witnessScript, add to missing
sigdata.missing_witness_script = uint256(vSolutions[0]);
return false;
default:

View file

@ -131,6 +131,10 @@ struct SignatureData {
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.
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<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)
uint256 missing_witness_script; ///< SHA256 of the missing witnessScript (if any)
SignatureData() {}
explicit SignatureData(const CScript& script) : scriptSig(script) {}

View file

@ -5,8 +5,9 @@
"""Test the Partially Signed Transaction RPCs.
"""
from decimal import Decimal
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error, find_output, disconnect_nodes, connect_nodes_bi, sync_blocks
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, disconnect_nodes, find_output, sync_blocks
import json
import os
@ -20,7 +21,7 @@ class PSBTTest(BitcoinTestFramework):
self.setup_clean_chain = False
self.num_nodes = 3
# TODO: remove -txindex. Currently required for getrawtransaction call.
self.extra_args = [[], ["-txindex"], ["-txindex"]]
self.extra_args = [["-txindex"], ["-txindex"], ["-txindex"]]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
@ -296,5 +297,72 @@ class PSBTTest(BitcoinTestFramework):
# Test decoding error: invalid base64
assert_raises_rpc_error(-22, "TX decode failed invalid base64", self.nodes[0].decodepsbt, ";definitely not base64;")
# Send to all types of addresses
addr1 = self.nodes[1].getnewaddress("", "bech32")
txid1 = self.nodes[0].sendtoaddress(addr1, 11)
vout1 = find_output(self.nodes[0], txid1, 11)
addr2 = self.nodes[1].getnewaddress("", "legacy")
txid2 = self.nodes[0].sendtoaddress(addr2, 11)
vout2 = find_output(self.nodes[0], txid2, 11)
addr3 = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid3 = self.nodes[0].sendtoaddress(addr3, 11)
vout3 = find_output(self.nodes[0], txid3, 11)
self.sync_all()
# Update a PSBT with UTXOs from the node
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
psbt = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1},{"txid":txid2, "vout":vout2},{"txid":txid3, "vout":vout3}], {self.nodes[0].getnewaddress():32.999})
decoded = self.nodes[1].decodepsbt(psbt)
assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
updated = self.nodes[1].utxoupdatepsbt(psbt)
decoded = self.nodes[1].decodepsbt(updated)
assert "witness_utxo" in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
# Two PSBTs with a common input should not be joinable
psbt1 = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1}], {self.nodes[0].getnewaddress():Decimal('10.999')})
assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated])
# Join two distinct PSBTs
addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid4 = self.nodes[0].sendtoaddress(addr4, 5)
vout4 = find_output(self.nodes[0], txid4, 5)
self.nodes[0].generate(6)
self.sync_all()
psbt2 = self.nodes[1].createpsbt([{"txid":txid4, "vout":vout4}], {self.nodes[0].getnewaddress():Decimal('4.999')})
psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt']
psbt2_decoded = self.nodes[0].decodepsbt(psbt2)
assert "final_scriptwitness" in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0]
joined = self.nodes[0].joinpsbts([psbt, psbt2])
joined_decoded = self.nodes[0].decodepsbt(joined)
assert len(joined_decoded['inputs']) == 4 and len(joined_decoded['outputs']) == 2 and "final_scriptwitness" not in joined_decoded['inputs'][3] and "final_scriptSig" not in joined_decoded['inputs'][3]
# Newly created PSBT needs UTXOs and updating
addr = self.nodes[1].getnewaddress("", "p2sh-segwit")
txid = self.nodes[0].sendtoaddress(addr, 7)
addrinfo = self.nodes[1].getaddressinfo(addr)
self.nodes[0].generate(6)
self.sync_all()
vout = find_output(self.nodes[0], txid, 7)
psbt = self.nodes[1].createpsbt([{"txid":txid, "vout":vout}], {self.nodes[0].getnewaddress("", "p2sh-segwit"):Decimal('6.999')})
analyzed = self.nodes[0].analyzepsbt(psbt)
assert not analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0]['is_final'] and analyzed['inputs'][0]['next'] == 'updater' and analyzed['next'] == 'updater'
# After update with wallet, only needs signing
updated = self.nodes[1].walletprocesspsbt(psbt, False, 'ALL', True)['psbt']
analyzed = self.nodes[0].analyzepsbt(updated)
assert analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0]['is_final'] and analyzed['inputs'][0]['next'] == 'signer' and analyzed['next'] == 'signer' and analyzed['inputs'][0]['missing']['signatures'][0] == addrinfo['embedded']['witness_program']
# Check fee and size things
assert analyzed['fee'] == Decimal('0.001') and analyzed['estimated_vsize'] == 134 and analyzed['estimated_feerate'] == '0.00746268 BTC/kB'
# After signing and finalizing, needs extracting
signed = self.nodes[1].walletprocesspsbt(updated)['psbt']
analyzed = self.nodes[0].analyzepsbt(signed)
assert analyzed['inputs'][0]['has_utxo'] and analyzed['inputs'][0]['is_final'] and analyzed['next'] == 'extractor'
if __name__ == '__main__':
PSBTTest().main()