Refactor analyzepsbt for use outside RPC code

Refactor the analyzepsbt RPC into (1) an AnalyzePSBT function, which returns
its output as a new strongly-typed PSBTAnalysis struct, and (2) a thin wrapper
which converts the struct into a UniValue for RPC use.
This commit is contained in:
Glenn Willen 2019-03-01 00:25:10 -08:00
parent afd20a25f2
commit ef22fe8c1f
3 changed files with 213 additions and 130 deletions

View file

@ -2,9 +2,14 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <coins.h>
#include <consensus/tx_verify.h>
#include <policy/policy.h>
#include <psbt.h>
#include <util/strencodings.h>
#include <numeric>
PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction& tx) : tx(tx)
{
inputs.resize(tx.vin.size());
@ -205,7 +210,7 @@ void PSBTOutput::Merge(const PSBTOutput& output)
if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script;
if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script;
}
bool PSBTInputSigned(PSBTInput& input)
bool PSBTInputSigned(const PSBTInput& input)
{
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
}
@ -326,6 +331,138 @@ TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector
return TransactionError::OK;
}
std::string PSBTRoleName(PSBTRole role) {
switch (role) {
case PSBTRole::UPDATER: return "updater";
case PSBTRole::SIGNER: return "signer";
case PSBTRole::FINALIZER: return "finalizer";
case PSBTRole::EXTRACTOR: return "extractor";
}
}
PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
{
// Go through each input and build status
PSBTAnalysis result;
bool calc_fee = true;
bool all_final = true;
bool only_missing_sigs = true;
bool only_missing_final = false;
CAmount in_amt = 0;
result.inputs.resize(psbtx.tx->vin.size());
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
PSBTInput& input = psbtx.inputs[i];
PSBTInputAnalysis& input_analysis = result.inputs[i];
// Check for a UTXO
CTxOut utxo;
if (psbtx.GetInputUTXO(utxo, i)) {
in_amt += utxo.nValue;
input_analysis.has_utxo = true;
} else {
input_analysis.has_utxo = false;
input_analysis.is_final = false;
input_analysis.next = PSBTRole::UPDATER;
calc_fee = false;
}
// Check if it is final
if (!utxo.IsNull() && !PSBTInputSigned(input)) {
input_analysis.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) {
input_analysis.missing_pubkeys = outdata.missing_pubkeys;
input_analysis.missing_redeem_script = outdata.missing_redeem_script;
input_analysis.missing_witness_script = outdata.missing_witness_script;
input_analysis.missing_sigs = outdata.missing_sigs;
// 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_analysis.next = PSBTRole::SIGNER;
} else {
only_missing_sigs = false;
input_analysis.next = PSBTRole::UPDATER;
}
} else {
only_missing_final = true;
input_analysis.next = PSBTRole::FINALIZER;
}
} else if (!utxo.IsNull()){
input_analysis.is_final = true;
}
}
if (all_final) {
only_missing_sigs = false;
result.next = PSBTRole::EXTRACTOR;
}
if (calc_fee) {
// Get the output amount
CAmount out_amt = std::accumulate(psbtx.tx->vout.begin(), psbtx.tx->vout.end(), CAmount(0),
[](CAmount a, const CTxOut& b) {
return a += b.nValue;
}
);
// Get the fee
CAmount fee = in_amt - out_amt;
result.fee = fee;
// 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];
Coin newcoin;
if (!SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, 1, nullptr, true) || !psbtx.GetInputUTXO(newcoin.out, i)) {
success = false;
break;
} else {
mtx.vin[i].scriptSig = input.final_script_sig;
mtx.vin[i].scriptWitness = input.final_script_witness;
newcoin.nHeight = 1;
view.AddCoin(psbtx.tx->vin[i].prevout, std::move(newcoin), true);
}
}
if (success) {
CTransaction ctx = CTransaction(mtx);
size_t size = GetVirtualTransactionSize(ctx, GetTransactionSigOpCost(ctx, view, STANDARD_SCRIPT_VERIFY_FLAGS));
result.estimated_vsize = size;
// Estimate fee rate
CFeeRate feerate(fee, size);
result.estimated_feerate = feerate;
}
if (only_missing_sigs) {
result.next = PSBTRole::SIGNER;
} else if (only_missing_final) {
result.next = PSBTRole::FINALIZER;
} else if (all_final) {
result.next = PSBTRole::EXTRACTOR;
} else {
result.next = PSBTRole::UPDATER;
}
} else {
result.next = PSBTRole::UPDATER;
}
return result;
}
bool DecodeBase64PSBT(PartiallySignedTransaction& psbt, const std::string& base64_tx, std::string& error)
{
bool invalid;

View file

@ -7,6 +7,8 @@
#include <attributes.h>
#include <node/transaction.h>
#include <optional.h>
#include <policy/feerate.h>
#include <primitives/transaction.h>
#include <pubkey.h>
#include <script/sign.h>
@ -548,8 +550,36 @@ struct PartiallySignedTransaction
}
};
enum class PSBTRole {
UPDATER,
SIGNER,
FINALIZER,
EXTRACTOR
};
struct PSBTInputAnalysis {
bool has_utxo;
bool is_final;
PSBTRole next;
std::vector<CKeyID> missing_pubkeys;
std::vector<CKeyID> missing_sigs;
uint160 missing_redeem_script;
uint256 missing_witness_script;
};
struct PSBTAnalysis {
Optional<size_t> estimated_vsize;
Optional<CFeeRate> estimated_feerate;
Optional<CAmount> fee;
std::vector<PSBTInputAnalysis> inputs;
PSBTRole next;
};
std::string PSBTRoleName(PSBTRole role);
/** Checks whether a PSBTInput is already signed. */
bool PSBTInputSigned(PSBTInput& input);
bool PSBTInputSigned(const 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, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
@ -580,6 +610,14 @@ bool FinalizeAndExtractPSBT(PartiallySignedTransaction& psbtx, CMutableTransacti
*/
NODISCARD TransactionError CombinePSBTs(PartiallySignedTransaction& out, const std::vector<PartiallySignedTransaction>& psbtxs);
/**
* Provides helpful miscellaneous information about where a PSBT is in the signing workflow.
*
* @param[in] psbtx the PSBT to analyze
* @return A PSBTAnalysis with information about the provided PSBT.
*/
PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx);
//! Decode a base64ed PSBT into a PartiallySignedTransaction
NODISCARD bool DecodeBase64PSBT(PartiallySignedTransaction& decoded_psbt, const std::string& base64_psbt, std::string& error);
//! Decode a raw (binary blob) PSBT into a PartiallySignedTransaction

View file

@ -1895,148 +1895,56 @@ UniValue analyzepsbt(const JSONRPCRequest& request)
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
}
// Go through each input and build status
PSBTAnalysis psbta = AnalyzePSBT(psbtx);
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];
for (const auto& input : psbta.inputs) {
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;
}
input_univ.pushKV("has_utxo", input.has_utxo);
input_univ.pushKV("is_final", input.is_final);
input_univ.pushKV("next", PSBTRoleName(input.next));
// 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");
if (!input.missing_pubkeys.empty()) {
UniValue missing_pubkeys_univ(UniValue::VARR);
for (const CKeyID& pubkey : input.missing_pubkeys) {
missing_pubkeys_univ.push_back(HexStr(pubkey));
}
} else if (!utxo.IsNull()){
input_univ.pushKV("is_final", true);
missing.pushKV("pubkeys", missing_pubkeys_univ);
}
if (!input.missing_redeem_script.IsNull()) {
missing.pushKV("redeemscript", HexStr(input.missing_redeem_script));
}
if (!input.missing_witness_script.IsNull()) {
missing.pushKV("witnessscript", HexStr(input.missing_witness_script));
}
if (!input.missing_sigs.empty()) {
UniValue missing_sigs_univ(UniValue::VARR);
for (const CKeyID& pubkey : input.missing_sigs) {
missing_sigs_univ.push_back(HexStr(pubkey));
}
missing.pushKV("signatures", missing_sigs_univ);
}
if (!missing.getKeys().empty()) {
input_univ.pushKV("missing", missing);
}
inputs_result.push_back(input_univ);
}
result.pushKV("inputs", inputs_result);
if (all_final) {
only_missing_sigs = false;
result.pushKV("next", "extractor");
if (psbta.estimated_vsize != nullopt) {
result.pushKV("estimated_vsize", (int)*psbta.estimated_vsize);
}
if (calc_fee) {
// Get the output amount
CAmount out_amt = std::accumulate(psbtx.tx->vout.begin(), psbtx.tx->vout.end(), CAmount(0),
[](CAmount 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", ValueFromAmount(feerate.GetFeePerK()));
}
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");
if (psbta.estimated_feerate != nullopt) {
result.pushKV("estimated_feerate", ValueFromAmount(psbta.estimated_feerate->GetFeePerK()));
}
if (psbta.fee != nullopt) {
result.pushKV("fee", ValueFromAmount(*psbta.fee));
}
result.pushKV("next", PSBTRoleName(psbta.next));
return result;
}