mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-07-04 04:32:20 +02:00
Implement analyzepsbt RPC and tests
This commit is contained in:
@ -7,6 +7,7 @@
|
|||||||
#include <coins.h>
|
#include <coins.h>
|
||||||
#include <compat/byteswap.h>
|
#include <compat/byteswap.h>
|
||||||
#include <consensus/validation.h>
|
#include <consensus/validation.h>
|
||||||
|
#include <consensus/tx_verify.h>
|
||||||
#include <core_io.h>
|
#include <core_io.h>
|
||||||
#include <index/txindex.h>
|
#include <index/txindex.h>
|
||||||
#include <init.h>
|
#include <init.h>
|
||||||
@ -30,6 +31,8 @@
|
|||||||
#include <validation.h>
|
#include <validation.h>
|
||||||
#include <validationinterface.h>
|
#include <validationinterface.h>
|
||||||
|
|
||||||
|
|
||||||
|
#include <numeric>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <univalue.h>
|
#include <univalue.h>
|
||||||
@ -1829,6 +1832,200 @@ UniValue joinpsbts(const JSONRPCRequest& request)
|
|||||||
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
|
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
|
// clang-format off
|
||||||
static const CRPCCommand commands[] =
|
static const CRPCCommand commands[] =
|
||||||
{ // category name actor (function) argNames
|
{ // category name actor (function) argNames
|
||||||
@ -1849,6 +2046,7 @@ static const CRPCCommand commands[] =
|
|||||||
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
|
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
|
||||||
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
|
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
|
||||||
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
|
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
|
||||||
|
{ "rawtransactions", "analyzepsbt", &analyzepsbt, {"psbt"} },
|
||||||
|
|
||||||
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
|
||||||
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
"""Test the Partially Signed Transaction RPCs.
|
"""Test the Partially Signed Transaction RPCs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
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 json
|
||||||
import os
|
import os
|
||||||
@ -339,6 +340,29 @@ class PSBTTest(BitcoinTestFramework):
|
|||||||
joined_decoded = self.nodes[0].decodepsbt(joined)
|
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]
|
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__':
|
if __name__ == '__main__':
|
||||||
PSBTTest().main()
|
PSBTTest().main()
|
||||||
|
Reference in New Issue
Block a user