Skip to content

Commit 540729e

Browse files
committed
Implement analyzepsbt RPC and tests
1 parent 77542cf commit 540729e

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

src/rpc/rawtransaction.cpp

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <coins.h>
88
#include <compat/byteswap.h>
99
#include <consensus/validation.h>
10+
#include <consensus/tx_verify.h>
1011
#include <core_io.h>
1112
#include <index/txindex.h>
1213
#include <init.h>
@@ -30,6 +31,8 @@
3031
#include <validation.h>
3132
#include <validationinterface.h>
3233

34+
35+
#include <numeric>
3336
#include <stdint.h>
3437

3538
#include <univalue.h>
@@ -1829,6 +1832,200 @@ UniValue joinpsbts(const JSONRPCRequest& request)
18291832
return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size());
18301833
}
18311834

1835+
UniValue analyzepsbt(const JSONRPCRequest& request)
1836+
{
1837+
if (request.fHelp || request.params.size() != 1) {
1838+
throw std::runtime_error(
1839+
RPCHelpMan{"analyzepsbt",
1840+
"\nAnalyzes and provides information about the current status of a PSBT and its inputs\n",
1841+
{
1842+
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
1843+
},
1844+
RPCResult {
1845+
"{\n"
1846+
" \"inputs\" : [ (array of json objects)\n"
1847+
" {\n"
1848+
" \"has_utxo\" : true|false (boolean) Whether a UTXO is provided\n"
1849+
" \"is_final\" : true|false (boolean) Whether the input is finalized\n"
1850+
" \"missing\" : { (json object, optional) Things that are missing that are required to complete this input\n"
1851+
" \"pubkeys\" : [ (array)\n"
1852+
" \"keyid\" (string) Public key ID, hash160 of the public key, of a public key whose BIP 32 derivation path is missing\n"
1853+
" ]\n"
1854+
" \"signatures\" : [ (array)\n"
1855+
" \"keyid\" (string) Public key ID, hash160 of the public key, of a public key whose signature is missing\n"
1856+
" ]\n"
1857+
" \"redeemscript\" : \"hash\" (string) Hash160 of the redeemScript that is missing\n"
1858+
" \"witnessscript\" : \"hash\" (string) SHA256 of the witnessScript that is missing\n"
1859+
" }\n"
1860+
" \"next\" : \"role\" (string) Role of the next person that this input needs to go to\n"
1861+
" }\n"
1862+
" ,...\n"
1863+
" ]\n"
1864+
" \"estimated_vsize\" : vsize (numeric) Estimated vsize of the final signed transaction\n"
1865+
" \"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"
1866+
" \"fee\" : fee (numeric, optional) The transaction fee paid. Shown only if all UTXO slots in the PSBT have been filled.\n"
1867+
" \"next\" : \"role\" (string) Role of the next person that this psbt needs to go to\n"
1868+
"}\n"
1869+
},
1870+
RPCExamples {
1871+
HelpExampleCli("analyzepsbt", "\"psbt\"")
1872+
}}.ToString());
1873+
}
1874+
1875+
RPCTypeCheck(request.params, {UniValue::VSTR});
1876+
1877+
// Unserialize the transaction
1878+
PartiallySignedTransaction psbtx;
1879+
std::string error;
1880+
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
1881+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
1882+
}
1883+
1884+
// Go through each input and build status
1885+
UniValue result(UniValue::VOBJ);
1886+
UniValue inputs_result(UniValue::VARR);
1887+
bool calc_fee = true;
1888+
bool all_final = true;
1889+
bool only_missing_sigs = true;
1890+
bool only_missing_final = false;
1891+
CAmount in_amt = 0;
1892+
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
1893+
PSBTInput& input = psbtx.inputs[i];
1894+
UniValue input_univ(UniValue::VOBJ);
1895+
UniValue missing(UniValue::VOBJ);
1896+
1897+
// Check for a UTXO
1898+
CTxOut utxo;
1899+
if (psbtx.GetInputUTXO(utxo, i)) {
1900+
in_amt += utxo.nValue;
1901+
input_univ.pushKV("has_utxo", true);
1902+
} else {
1903+
input_univ.pushKV("has_utxo", false);
1904+
input_univ.pushKV("is_final", false);
1905+
input_univ.pushKV("next", "updater");
1906+
calc_fee = false;
1907+
}
1908+
1909+
// Check if it is final
1910+
if (!utxo.IsNull() && !PSBTInputSigned(input)) {
1911+
input_univ.pushKV("is_final", false);
1912+
all_final = false;
1913+
1914+
// Figure out what is missing
1915+
SignatureData outdata;
1916+
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, &outdata);
1917+
1918+
// Things are missing
1919+
if (!complete) {
1920+
if (!outdata.missing_pubkeys.empty()) {
1921+
// Missing pubkeys
1922+
UniValue missing_pubkeys_univ(UniValue::VARR);
1923+
for (const CKeyID& pubkey : outdata.missing_pubkeys) {
1924+
missing_pubkeys_univ.push_back(HexStr(pubkey));
1925+
}
1926+
missing.pushKV("pubkeys", missing_pubkeys_univ);
1927+
}
1928+
if (!outdata.missing_redeem_script.IsNull()) {
1929+
// Missing redeemScript
1930+
missing.pushKV("redeemscript", HexStr(outdata.missing_redeem_script));
1931+
}
1932+
if (!outdata.missing_witness_script.IsNull()) {
1933+
// Missing witnessScript
1934+
missing.pushKV("witnessscript", HexStr(outdata.missing_witness_script));
1935+
}
1936+
if (!outdata.missing_sigs.empty()) {
1937+
// Missing sigs
1938+
UniValue missing_sigs_univ(UniValue::VARR);
1939+
for (const CKeyID& pubkey : outdata.missing_sigs) {
1940+
missing_sigs_univ.push_back(HexStr(pubkey));
1941+
}
1942+
missing.pushKV("signatures", missing_sigs_univ);
1943+
}
1944+
input_univ.pushKV("missing", missing);
1945+
1946+
// If we are only missing signatures and nothing else, then next is signer
1947+
if (outdata.missing_pubkeys.empty() && outdata.missing_redeem_script.IsNull() && outdata.missing_witness_script.IsNull() && !outdata.missing_sigs.empty()) {
1948+
input_univ.pushKV("next", "signer");
1949+
} else {
1950+
only_missing_sigs = false;
1951+
input_univ.pushKV("next", "updater");
1952+
}
1953+
} else {
1954+
only_missing_final = true;
1955+
input_univ.pushKV("next", "finalizer");
1956+
}
1957+
} else if (!utxo.IsNull()){
1958+
input_univ.pushKV("is_final", true);
1959+
}
1960+
inputs_result.push_back(input_univ);
1961+
}
1962+
result.pushKV("inputs", inputs_result);
1963+
1964+
if (all_final) {
1965+
only_missing_sigs = false;
1966+
result.pushKV("next", "extractor");
1967+
}
1968+
if (calc_fee) {
1969+
// Get the output amount
1970+
CAmount out_amt = std::accumulate(psbtx.tx->vout.begin(), psbtx.tx->vout.end(), 0,
1971+
[](int a, const CTxOut& b) {
1972+
return a += b.nValue;
1973+
}
1974+
);
1975+
1976+
// Get the fee
1977+
CAmount fee = in_amt - out_amt;
1978+
1979+
// Estimate the size
1980+
CMutableTransaction mtx(*psbtx.tx);
1981+
CCoinsView view_dummy;
1982+
CCoinsViewCache view(&view_dummy);
1983+
bool success = true;
1984+
1985+
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
1986+
PSBTInput& input = psbtx.inputs[i];
1987+
if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, nullptr, true)) {
1988+
mtx.vin[i].scriptSig = input.final_script_sig;
1989+
mtx.vin[i].scriptWitness = input.final_script_witness;
1990+
1991+
Coin newcoin;
1992+
if (!psbtx.GetInputUTXO(newcoin.out, i)) {
1993+
success = false;
1994+
break;
1995+
}
1996+
newcoin.nHeight = 1;
1997+
view.AddCoin(psbtx.tx->vin[i].prevout, std::move(newcoin), true);
1998+
} else {
1999+
success = false;
2000+
break;
2001+
}
2002+
}
2003+
2004+
if (success) {
2005+
CTransaction ctx = CTransaction(mtx);
2006+
size_t size = GetVirtualTransactionSize(ctx, GetTransactionSigOpCost(ctx, view, STANDARD_SCRIPT_VERIFY_FLAGS));
2007+
result.pushKV("estimated_vsize", (int)size);
2008+
// Estimate fee rate
2009+
CFeeRate feerate(fee, size);
2010+
result.pushKV("estimated_feerate", feerate.ToString());
2011+
}
2012+
result.pushKV("fee", ValueFromAmount(fee));
2013+
2014+
if (only_missing_sigs) {
2015+
result.pushKV("next", "signer");
2016+
} else if (only_missing_final) {
2017+
result.pushKV("next", "finalizer");
2018+
} else if (all_final) {
2019+
result.pushKV("next", "extractor");
2020+
} else {
2021+
result.pushKV("next", "updater");
2022+
}
2023+
} else {
2024+
result.pushKV("next", "updater");
2025+
}
2026+
return result;
2027+
}
2028+
18322029
// clang-format off
18332030
static const CRPCCommand commands[] =
18342031
{ // category name actor (function) argNames
@@ -1849,6 +2046,7 @@ static const CRPCCommand commands[] =
18492046
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} },
18502047
{ "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} },
18512048
{ "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} },
2049+
{ "rawtransactions", "analyzepsbt", &analyzepsbt, {"psbt"} },
18522050

18532051
{ "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} },
18542052
{ "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} },

test/functional/rpc_psbt.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"""Test the Partially Signed Transaction RPCs.
66
"""
77

8+
from decimal import Decimal
89
from test_framework.test_framework import BitcoinTestFramework
9-
from test_framework.util import assert_equal, assert_raises_rpc_error, find_output, disconnect_nodes, connect_nodes_bi, sync_blocks
10+
from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, disconnect_nodes, find_output, sync_blocks
1011

1112
import json
1213
import os
@@ -339,6 +340,29 @@ def run_test(self):
339340
joined_decoded = self.nodes[0].decodepsbt(joined)
340341
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]
341342

343+
# Newly created PSBT needs UTXOs and updating
344+
addr = self.nodes[1].getnewaddress("", "p2sh-segwit")
345+
txid = self.nodes[0].sendtoaddress(addr, 7)
346+
addrinfo = self.nodes[1].getaddressinfo(addr)
347+
self.nodes[0].generate(6)
348+
self.sync_all()
349+
vout = find_output(self.nodes[0], txid, 7)
350+
psbt = self.nodes[1].createpsbt([{"txid":txid, "vout":vout}], {self.nodes[0].getnewaddress("", "p2sh-segwit"):Decimal('6.999')})
351+
analyzed = self.nodes[0].analyzepsbt(psbt)
352+
assert not analyzed['inputs'][0]['has_utxo'] and not analyzed['inputs'][0]['is_final'] and analyzed['inputs'][0]['next'] == 'updater' and analyzed['next'] == 'updater'
353+
354+
# After update with wallet, only needs signing
355+
updated = self.nodes[1].walletprocesspsbt(psbt, False, 'ALL', True)['psbt']
356+
analyzed = self.nodes[0].analyzepsbt(updated)
357+
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']
358+
359+
# Check fee and size things
360+
assert analyzed['fee'] == Decimal('0.001') and analyzed['estimated_vsize'] == 134 and analyzed['estimated_feerate'] == '0.00746268 BTC/kB'
361+
362+
# After signing and finalizing, needs extracting
363+
signed = self.nodes[1].walletprocesspsbt(updated)['psbt']
364+
analyzed = self.nodes[0].analyzepsbt(signed)
365+
assert analyzed['inputs'][0]['has_utxo'] and analyzed['inputs'][0]['is_final'] and analyzed['next'] == 'extractor'
342366

343367
if __name__ == '__main__':
344368
PSBTTest().main()

0 commit comments

Comments
 (0)