mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 03:52:49 +01:00
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:
parent
448ad8a198
commit
2a3a873524
15
doc/release-notes-5017.md
Normal file
15
doc/release-notes-5017.md
Normal 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.
|
20
src/psbt.cpp
20
src/psbt.cpp
@ -189,13 +189,12 @@ void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransactio
|
||||
// Put redeem_script, key paths, into PSBTOutput.
|
||||
psbt_out.FromSignatureData(sigdata);
|
||||
}
|
||||
|
||||
bool PSBTInputSigned(PSBTInput& input)
|
||||
{
|
||||
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);
|
||||
const CMutableTransaction& tx = *psbt.tx;
|
||||
@ -227,9 +226,22 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
|
||||
return false;
|
||||
}
|
||||
|
||||
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
|
||||
bool sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata);
|
||||
bool sig_complete;
|
||||
if (use_dummy) {
|
||||
sig_complete = ProduceSignature(provider, DUMMY_SIGNATURE_CREATOR, utxo.scriptPubKey, sigdata);
|
||||
} else {
|
||||
MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash);
|
||||
sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, 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;
|
||||
}
|
||||
|
||||
|
@ -488,7 +488,7 @@ struct PartiallySignedTransaction
|
||||
bool PSBTInputSigned(PSBTInput& input);
|
||||
|
||||
/** 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.
|
||||
*
|
||||
|
@ -1575,6 +1575,197 @@ UniValue joinpsbts(const JSONRPCRequest& request)
|
||||
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
|
||||
static const CRPCCommand commands[] =
|
||||
{ // category name actor (function) argNames
|
||||
@ -1594,6 +1785,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"} },
|
||||
|
@ -263,6 +263,28 @@ 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()
|
||||
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__':
|
||||
PSBTTest().main()
|
||||
|
Loading…
Reference in New Issue
Block a user