partial bitcoin#13932: Additional utility RPCs for PSBT

Contains cb40b3abd4514361a024a1e7a1a281da9261261b and 540729ef4bf1b6c6da1ec795e441d2ce56a9a58b

Verbatim for release notes borrowed from https://raw.githubusercontent.com/bitcoin/bitcoin/master/doc/release-notes/release-notes-0.18.0.md
This commit is contained in:
Kittywhiskers Van Gogh 2022-09-20 13:56:17 +05:30
parent 448ad8a198
commit 2a3a873524
5 changed files with 246 additions and 5 deletions

15
doc/release-notes-5017.md Normal file
View File

@ -0,0 +1,15 @@
New RPCs
--------
- `analyzepsbt` examines a PSBT and provides information about what
the PSBT contains and the next steps that need to be taken in order
to complete the transaction. For each input of a PSBT, `analyzepsbt`
provides information about what information is missing for that
input, including whether a UTXO needs to be provided, what pubkeys
still need to be provided, which scripts need to be provided, and
what signatures are still needed. Every input will also list which
role is needed to complete that input, and `analyzepsbt` will also
list the next role in general needed to complete the PSBT.
`analyzepsbt` will also provide the estimated fee rate and estimated
virtual size of the completed transaction if it has enough
information to do so.

View File

@ -189,13 +189,12 @@ void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransactio
// Put redeem_script, key paths, into PSBTOutput. // Put redeem_script, key paths, into PSBTOutput.
psbt_out.FromSignatureData(sigdata); psbt_out.FromSignatureData(sigdata);
} }
bool PSBTInputSigned(PSBTInput& input) bool PSBTInputSigned(PSBTInput& input)
{ {
return !input.final_script_sig.empty(); return !input.final_script_sig.empty();
} }
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash) bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash, SignatureData* out_sigdata, bool use_dummy)
{ {
PSBTInput& input = psbt.inputs.at(index); PSBTInput& input = psbt.inputs.at(index);
const CMutableTransaction& tx = *psbt.tx; const CMutableTransaction& tx = *psbt.tx;
@ -227,9 +226,22 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
return false; return false;
} }
bool sig_complete;
if (use_dummy) {
sig_complete = ProduceSignature(provider, DUMMY_SIGNATURE_CREATOR, utxo.scriptPubKey, sigdata);
} else {
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash); MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
bool sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata); sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata);
}
input.FromSignatureData(sigdata); input.FromSignatureData(sigdata);
// Fill in the missing info
if (out_sigdata) {
out_sigdata->missing_pubkeys = sigdata.missing_pubkeys;
out_sigdata->missing_sigs = sigdata.missing_sigs;
out_sigdata->missing_redeem_script = sigdata.missing_redeem_script;
}
return sig_complete; return sig_complete;
} }

View File

@ -488,7 +488,7 @@ struct PartiallySignedTransaction
bool PSBTInputSigned(PSBTInput& input); bool PSBTInputSigned(PSBTInput& input);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */ /** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL); bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
/** Updates a PSBTOutput with information from provider. /** Updates a PSBTOutput with information from provider.
* *

View File

@ -1575,6 +1575,197 @@ UniValue joinpsbts(const JSONRPCRequest& request)
return EncodeBase64(ssTx.str()); return EncodeBase64(ssTx.str());
} }
UniValue analyzepsbt(const JSONRPCRequest& request)
{
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 {
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::ARR, "inputs", "",
{
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::BOOL, "has_utxo", "Whether a UTXO is provided"},
{RPCResult::Type::BOOL, "is_final", "Whether the input is finalized"},
{RPCResult::Type::OBJ, "missing", /* optional */ true, "Things that are missing that are required to complete this input",
{
{RPCResult::Type::ARR, "pubkeys", /* optional */ true, "",
{
{RPCResult::Type::STR_HEX, "keyid", "Public key ID, hash160 of the public key, of a public key whose BIP 32 derivation path is missing"},
}},
{RPCResult::Type::ARR, "signatures", /* optional */ true, "",
{
{RPCResult::Type::STR_HEX, "keyid", "Public key ID, hash160 of the public key, of a public key whose signature is missing"},
}},
{RPCResult::Type::STR_HEX, "redeemscript", /* optional */ true, "Hash160 of the redeemScript that is missing"},
}},
{RPCResult::Type::STR, "next", /* optional */ true, "Role of the next person that this input needs to go to"},
}},
}},
{RPCResult::Type::NUM, "estimated_vsize", /* optional */ true, "Estimated vsize of the final signed transaction"},
{RPCResult::Type::STR_AMOUNT, "estimated_feerate", /* optional */ true, "Estimated feerate of the final signed transaction in " + CURRENCY_UNIT + "/kB. Shown only if all UTXO slots in the PSBT have been filled"},
{RPCResult::Type::STR_AMOUNT, "fee", /* optional */ true, "The transaction fee paid. Shown only if all UTXO slots in the PSBT have been filled"},
{RPCResult::Type::STR, "next", "Role of the next person that this psbt needs to go to"},
{RPCResult::Type::STR, "error", "Error message if there is one"},
}
},
RPCExamples {
HelpExampleCli("analyzepsbt", "\"psbt\"")
}}.Check(request);
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_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
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;
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
@ -1594,6 +1785,7 @@ static const CRPCCommand commands[] =
{ "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata"} }, { "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata"} },
{ "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"} },

View File

@ -263,6 +263,28 @@ 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()
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', 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'] == Decimal('0.00523560')
# 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()