diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include +#include #include @@ -1849,6 +1851,213 @@ 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 @@ -1868,6 +2077,7 @@ { "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"} }, }; diff --git a/src/script/sign.cpp b/src/script/sign.cpp --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -134,7 +134,7 @@ 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; diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -278,6 +278,34 @@ 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()