diff options
-rw-r--r-- | src/rpc/rawtransaction.cpp | 198 | ||||
-rwxr-xr-x | test/functional/rpc_psbt.py | 26 |
2 files changed, 223 insertions, 1 deletions
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 866158e003..ac41207404 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> @@ -30,6 +31,8 @@ #include <validation.h> #include <validationinterface.h> + +#include <numeric> #include <stdint.h> #include <univalue.h> @@ -1829,6 +1832,200 @@ UniValue joinpsbts(const JSONRPCRequest& request) 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 @@ -1849,6 +2046,7 @@ static const CRPCCommand commands[] = { "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"} }, diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index f1538b34a8..9a79a7ee43 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -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 @@ -339,6 +340,29 @@ class PSBTTest(BitcoinTestFramework): 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() |