diff options
author | Wladimir J. van der Laan <laanwj@gmail.com> | 2019-02-16 20:31:05 +0100 |
---|---|---|
committer | Wladimir J. van der Laan <laanwj@gmail.com> | 2019-02-16 20:45:03 +0100 |
commit | d5b929c813ff3d7d93bf4e3164b34e10eeb63801 (patch) | |
tree | 23e8fe95c2815c71054af988de16d749c1ca4085 /src/rpc | |
parent | f9d50e83e290efd1147aad576b82bd9599fc6467 (diff) | |
parent | 540729ef4bf1b6c6da1ec795e441d2ce56a9a58b (diff) |
Merge #13932: Additional utility RPCs for PSBT
540729ef4bf1b6c6da1ec795e441d2ce56a9a58b Implement analyzepsbt RPC and tests (Andrew Chow)
77542cf2a5f8abb97dd46f782c1b0199cc062033 Move PSBT UTXO fetching to a separate method (Andrew Chow)
cb40b3abd4514361a024a1e7a1a281da9261261b Figure out what is missing during signing (Andrew Chow)
08f749c9147a5f3fdbbd880e0974b97084429002 Implement joinpsbts RPC and tests (Andrew Chow)
7344a7b9984b99882e136efc8ad48fb31740df93 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
Diffstat (limited to 'src/rpc')
-rw-r--r-- | src/rpc/client.cpp | 1 | ||||
-rw-r--r-- | src/rpc/rawtransaction.cpp | 338 |
2 files changed, 339 insertions, 0 deletions
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index c5694e6d55..1cdc9f87a7 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -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"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 4d8a1b87fc..38e2dc237e 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -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"} }, |