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
7 changed files with 487 additions and 9 deletions

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"} },