Skip to content

Commit

Permalink
[backport#13932] Implement analyzepsbt RPC and tests
Browse files Browse the repository at this point in the history
Summary:
bitcoin/bitcoin@540729e

---

This completes the backport of Core [[bitcoin/bitcoin#13932 | PR13932]]

Test Plan:
  cmake .. -GNinja -DENABLE_WERROR=ON
  ninja check-all

Reviewers: #bitcoin_abc, nakihito, deadalnix

Reviewed By: #bitcoin_abc, nakihito, deadalnix

Subscribers: nakihito

Differential Revision: https://reviews.bitcoinabc.org/D6058
  • Loading branch information
achow101 authored and majcosta committed May 16, 2020
1 parent 638072b commit 397f2c9
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 1 deletion.
210 changes: 210 additions & 0 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <coins.h>
#include <compat/byteswap.h>
#include <config.h>
#include <consensus/tx_verify.h>
#include <consensus/validation.h>
#include <core_io.h>
#include <index/txindex.h>
Expand Down Expand Up @@ -35,6 +36,7 @@
#include <validationinterface.h>

#include <cstdint>
#include <numeric>

#include <univalue.h>

Expand Down Expand Up @@ -1835,6 +1837,213 @@ UniValue joinpsbts(const Config &config, const JSONRPCRequest &request) {
return EncodeBase64((uint8_t *)ssTx.data(), ssTx.size());
}

UniValue analyzepsbt(const Config &config, 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, /* opt */ false, /* default_var */ "",
"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"
" }\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;
Amount in_amt{Amount::zero()};
for (size_t 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,
SigHashType().withForkId(), &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_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_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
Amount out_amt = std::accumulate(
psbtx.tx->vout.begin(), psbtx.tx->vout.end(), Amount::zero(),
[](Amount a, const CTxOut &b) { return a += b.nValue; });

// Get the fee
Amount fee = in_amt - out_amt;

// Estimate the size
CMutableTransaction mtx(*psbtx.tx);
CCoinsView view_dummy;
CCoinsViewCache view(&view_dummy);
bool success = true;

for (size_t i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput &input = psbtx.inputs[i];
if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i,
SigHashType().withForkId(), nullptr, true)) {
mtx.vin[i].scriptSig = input.final_script_sig;

CTxOut newUtxo;
if (!psbtx.GetInputUTXO(newUtxo, i)) {
success = false;
break;
}
view.AddCoin(psbtx.tx->vin[i].prevout, Coin(newUtxo, 1, false),
true);
} else {
success = false;
break;
}
}

if (success) {
CTransaction ctx = CTransaction(mtx);
size_t size = ctx.GetTotalSize();
result.pushKV("estimated_vsize", uint64_t(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
Expand All @@ -1854,6 +2063,7 @@ static const CRPCCommand commands[] = {
{ "rawtransactions", "converttopsbt", converttopsbt, {"hexstring","permitsigdata"} },
{ "rawtransactions", "utxoupdatepsbt", utxoupdatepsbt, {"psbt"} },
{ "rawtransactions", "joinpsbts", joinpsbts, {"txs"} },
{ "rawtransactions", "analyzepsbt", analyzepsbt, {"psbt"} },
{ "blockchain", "gettxoutproof", gettxoutproof, {"txids", "blockhash"} },
{ "blockchain", "verifytxoutproof", verifytxoutproof, {"proof"} },
};
Expand Down
2 changes: 1 addition & 1 deletion src/script/sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ static bool SignStep(const SigningProvider &provider,
case TX_PUBKEYHASH: {
CKeyID keyID = CKeyID(uint160(vSolutions[0]));
CPubKey pubkey;
if (!provider.GetPubKey(keyID, pubkey)) {
if (!GetPubKey(provider, sigdata, keyID, pubkey)) {
// Pubkey could not be found, add to missing
sigdata.missing_pubkeys.push_back(keyID);
return false;
Expand Down
28 changes: 28 additions & 0 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,34 @@ def run_test(self):
assert_raises_rpc_error(-8,
"At least two PSBTs are required to join PSBTs.", self.nodes[1].joinpsbts, [psbt2])

# Newly created PSBT needs UTXOs and updating
addr = self.nodes[1].getnewaddress("")
txid = self.nodes[0].sendtoaddress(addr, 7)
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(""): 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|FORKID', 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'

# Check fee and size things
assert analyzed['fee'] == Decimal(
'0.00100000') and analyzed['estimated_vsize'] == 191 and analyzed['estimated_feerate'] == '0.00523560 BCH/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()

0 comments on commit 397f2c9

Please sign in to comment.