mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-11-12 06:58:57 +01:00
Merge #13932: Additional utility RPCs for PSBT
540729ef4bImplement analyzepsbt RPC and tests (Andrew Chow)77542cf2a5Move PSBT UTXO fetching to a separate method (Andrew Chow)cb40b3abd4Figure out what is missing during signing (Andrew Chow)08f749c914Implement joinpsbts RPC and tests (Andrew Chow)7344a7b998Implement 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:
@@ -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"} },
|
||||
|
||||
Reference in New Issue
Block a user