From 7344a7b9984b99882e136efc8ad48fb31740df93 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Jul 2018 17:08:25 -0700 Subject: [PATCH 1/5] Implement utxoupdatepsbt RPC and tests --- src/rpc/rawtransaction.cpp | 65 +++++++++++++++++++++++++++++++++++++ test/functional/rpc_psbt.py | 29 ++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 7fe73e56da160..2adfd0c0fc5fc 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1691,6 +1691,70 @@ UniValue converttopsbt(const JSONRPCRequest& request) return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); } +UniValue utxoupdatepsbt(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + RPCHelpMan{"utxoupdatepsbt", + "\nUpdates a PSBT with witness UTXOs retrieved from the UTXO set or the mempool.\n", + { + {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"} + }, + RPCResult { + " \"psbt\" (string) The base64-encoded partially signed transaction with inputs updated\n" + }, + RPCExamples { + HelpExampleCli("utxoupdatepsbt", "\"psbt\"") + }}.ToString()); + } + + RPCTypeCheck(request.params, {UniValue::VSTR}, true); + + // Unserialize the transactions + 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)); + } + + // Fetch previous transactions (inputs): + CCoinsView viewDummy; + CCoinsViewCache view(&viewDummy); + { + LOCK2(cs_main, mempool.cs); + CCoinsViewCache &viewChain = *pcoinsTip; + CCoinsViewMemPool viewMempool(&viewChain, mempool); + view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view + + for (const CTxIn& txin : psbtx.tx->vin) { + view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail. + } + + view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long + } + + // Fill the inputs + for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) { + PSBTInput& input = psbtx.inputs.at(i); + + if (input.non_witness_utxo || !input.witness_utxo.IsNull()) { + continue; + } + + const Coin& coin = view.AccessCoin(psbtx.tx->vin[i].prevout); + + std::vector> solutions_data; + txnouttype which_type = Solver(coin.out.scriptPubKey, solutions_data); + if (which_type == TX_WITNESS_V0_SCRIPTHASH || which_type == TX_WITNESS_V0_KEYHASH || which_type == TX_WITNESS_UNKNOWN) { + input.witness_utxo = coin.out; + } + } + + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << psbtx; + return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); +} + // clang-format off static const CRPCCommand commands[] = { // category name actor (function) argNames @@ -1709,6 +1773,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "finalizepsbt", &finalizepsbt, {"psbt", "extract"} }, { "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime","replaceable"} }, { "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} }, + { "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} }, { "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} }, diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index c98f1058282c9..28507267ce570 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -20,7 +20,7 @@ def set_test_params(self): self.setup_clean_chain = False self.num_nodes = 3 # TODO: remove -txindex. Currently required for getrawtransaction call. - self.extra_args = [[], ["-txindex"], ["-txindex"]] + self.extra_args = [["-txindex"], ["-txindex"], ["-txindex"]] def skip_test_if_missing_module(self): self.skip_if_no_wallet() @@ -296,5 +296,32 @@ def run_test(self): # Test decoding error: invalid base64 assert_raises_rpc_error(-22, "TX decode failed invalid base64", self.nodes[0].decodepsbt, ";definitely not base64;") + # Send to all types of addresses + addr1 = self.nodes[1].getnewaddress("", "bech32") + txid1 = self.nodes[0].sendtoaddress(addr1, 11) + vout1 = find_output(self.nodes[0], txid1, 11) + addr2 = self.nodes[1].getnewaddress("", "legacy") + txid2 = self.nodes[0].sendtoaddress(addr2, 11) + vout2 = find_output(self.nodes[0], txid2, 11) + addr3 = self.nodes[1].getnewaddress("", "p2sh-segwit") + txid3 = self.nodes[0].sendtoaddress(addr3, 11) + vout3 = find_output(self.nodes[0], txid3, 11) + self.sync_all() + + # Update a PSBT with UTXOs from the node + # Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness + psbt = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1},{"txid":txid2, "vout":vout2},{"txid":txid3, "vout":vout3}], {self.nodes[0].getnewaddress():32.999}) + decoded = self.nodes[1].decodepsbt(psbt) + assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0] + assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1] + assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2] + updated = self.nodes[1].utxoupdatepsbt(psbt) + decoded = self.nodes[1].decodepsbt(updated) + assert "witness_utxo" in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0] + assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1] + assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2] + + + if __name__ == '__main__': PSBTTest().main() From 08f749c9147a5f3fdbbd880e0974b97084429002 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 20 Jul 2018 18:24:16 -0700 Subject: [PATCH 2/5] Implement joinpsbts RPC and tests Adds a joinpsbts RPC which combines multiple distinct PSBTs into one PSBT. --- src/psbt.cpp | 20 ++++++++++ src/psbt.h | 2 + src/rpc/client.cpp | 1 + src/rpc/rawtransaction.cpp | 75 +++++++++++++++++++++++++++++++++++++ test/functional/rpc_psbt.py | 17 +++++++++ 5 files changed, 115 insertions(+) diff --git a/src/psbt.cpp b/src/psbt.cpp index 81633c0cc7ffa..424b92cfb5c2e 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -42,6 +42,26 @@ bool PartiallySignedTransaction::IsSane() const return true; } +bool PartiallySignedTransaction::AddInput(const CTxIn& txin, PSBTInput& psbtin) +{ + if (std::find(tx->vin.begin(), tx->vin.end(), txin) != tx->vin.end()) { + return false; + } + tx->vin.push_back(txin); + psbtin.partial_sigs.clear(); + psbtin.final_script_sig.clear(); + psbtin.final_script_witness.SetNull(); + inputs.push_back(psbtin); + return true; +} + +bool PartiallySignedTransaction::AddOutput(const CTxOut& txout, const PSBTOutput& psbtout) +{ + tx->vout.push_back(txout); + outputs.push_back(psbtout); + return true; +} + bool PSBTInput::IsNull() const { return !non_witness_utxo && witness_utxo.IsNull() && partial_sigs.empty() && unknown.empty() && hd_keypaths.empty() && redeem_script.empty() && witness_script.empty(); diff --git a/src/psbt.h b/src/psbt.h index e18790322bf99..982fb803d59f8 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -389,6 +389,8 @@ struct PartiallySignedTransaction * same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */ NODISCARD bool Merge(const PartiallySignedTransaction& psbt); bool IsSane() const; + bool AddInput(const CTxIn& txin, PSBTInput& psbtin); + bool AddOutput(const CTxOut& txout, const PSBTOutput& psbtout); PartiallySignedTransaction() {} PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {} explicit PartiallySignedTransaction(const CMutableTransaction& tx); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index c5694e6d558d4..1cdc9f87a769d 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -112,6 +112,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createpsbt", 2, "locktime" }, { "createpsbt", 3, "replaceable" }, { "combinepsbt", 0, "txs"}, + { "joinpsbts", 0, "txs"}, { "finalizepsbt", 1, "extract"}, { "converttopsbt", 1, "permitsigdata"}, { "converttopsbt", 2, "iswitness"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 2adfd0c0fc5fc..866158e003d46 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1755,6 +1755,80 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request) return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); } +UniValue joinpsbts(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + RPCHelpMan{"joinpsbts", + "\nJoins multiple distinct PSBTs with different inputs and outputs into one PSBT with inputs and outputs from all of the PSBTs\n" + "No input in any of the PSBTs can be in more than one of the PSBTs.\n", + { + {"txs", RPCArg::Type::ARR, RPCArg::Optional::NO, "A json array of base64 strings of partially signed transactions", + { + {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"} + }} + }, + RPCResult { + " \"psbt\" (string) The base64-encoded partially signed transaction\n" + }, + RPCExamples { + HelpExampleCli("joinpsbts", "\"psbt\"") + }}.ToString()); + } + + RPCTypeCheck(request.params, {UniValue::VARR}, true); + + // Unserialize the transactions + std::vector psbtxs; + UniValue txs = request.params[0].get_array(); + + if (txs.size() <= 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "At least two PSBTs are required to join PSBTs."); + } + + int32_t best_version = 1; + uint32_t best_locktime = 0xffffffff; + for (unsigned int i = 0; i < txs.size(); ++i) { + PartiallySignedTransaction psbtx; + std::string error; + if (!DecodeBase64PSBT(psbtx, txs[i].get_str(), error)) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error)); + } + psbtxs.push_back(psbtx); + // Choose the highest version number + if (psbtx.tx->nVersion > best_version) { + best_version = psbtx.tx->nVersion; + } + // Choose the lowest lock time + if (psbtx.tx->nLockTime < best_locktime) { + best_locktime = psbtx.tx->nLockTime; + } + } + + // Create a blank psbt where everything will be added + PartiallySignedTransaction merged_psbt; + merged_psbt.tx = CMutableTransaction(); + merged_psbt.tx->nVersion = best_version; + merged_psbt.tx->nLockTime = best_locktime; + + // Merge + for (auto& psbt : psbtxs) { + for (unsigned int i = 0; i < psbt.tx->vin.size(); ++i) { + if (!merged_psbt.AddInput(psbt.tx->vin[i], psbt.inputs[i])) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Input %s:%d exists in multiple PSBTs", psbt.tx->vin[i].prevout.hash.ToString().c_str(), psbt.tx->vin[i].prevout.n)); + } + } + for (unsigned int i = 0; i < psbt.tx->vout.size(); ++i) { + merged_psbt.AddOutput(psbt.tx->vout[i], psbt.outputs[i]); + } + merged_psbt.unknown.insert(psbt.unknown.begin(), psbt.unknown.end()); + } + + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << merged_psbt; + return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); +} + // clang-format off static const CRPCCommand commands[] = { // category name actor (function) argNames @@ -1774,6 +1848,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "createpsbt", &createpsbt, {"inputs","outputs","locktime","replaceable"} }, { "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} }, { "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} }, + { "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} }, { "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} }, diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 28507267ce570..f1538b34a8aff 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -321,6 +321,23 @@ def run_test(self): assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1] assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2] + # Two PSBTs with a common input should not be joinable + psbt1 = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1}], {self.nodes[0].getnewaddress():Decimal('10.999')}) + assert_raises_rpc_error(-8, "exists in multiple PSBTs", self.nodes[1].joinpsbts, [psbt1, updated]) + + # Join two distinct PSBTs + addr4 = self.nodes[1].getnewaddress("", "p2sh-segwit") + txid4 = self.nodes[0].sendtoaddress(addr4, 5) + vout4 = find_output(self.nodes[0], txid4, 5) + self.nodes[0].generate(6) + self.sync_all() + psbt2 = self.nodes[1].createpsbt([{"txid":txid4, "vout":vout4}], {self.nodes[0].getnewaddress():Decimal('4.999')}) + psbt2 = self.nodes[1].walletprocesspsbt(psbt2)['psbt'] + psbt2_decoded = self.nodes[0].decodepsbt(psbt2) + assert "final_scriptwitness" in psbt2_decoded['inputs'][0] and "final_scriptSig" in psbt2_decoded['inputs'][0] + joined = self.nodes[0].joinpsbts([psbt, psbt2]) + 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] if __name__ == '__main__': From cb40b3abd4514361a024a1e7a1a281da9261261b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 31 Jul 2018 17:57:15 -0700 Subject: [PATCH 3/5] Figure out what is missing during signing When signing an input, figure out what was requested for but was unable to be found and store it in a SignatureData. Return this information in SignPSBTInput. --- src/psbt.cpp | 20 ++++++++++++++++---- src/psbt.h | 2 +- src/script/sign.cpp | 15 +++++++++++++-- src/script/sign.h | 4 ++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/psbt.cpp b/src/psbt.cpp index 424b92cfb5c2e..0734edcf8e966 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -191,13 +191,12 @@ 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) { return !input.final_script_sig.empty() || !input.final_script_witness.IsNull(); } -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; @@ -237,9 +236,14 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& return false; } - MutableTransactionSignatureCreator creator(&tx, index, utxo.nValue, sighash); sigdata.witness = false; - 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); + } // Verify that a witness signature was produced in case one was required. if (require_witness_sig && !sigdata.witness) return false; input.FromSignatureData(sigdata); @@ -250,6 +254,14 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& input.non_witness_utxo = nullptr; } + // 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; + out_sigdata->missing_witness_script = sigdata.missing_witness_script; + } + return sig_complete; } diff --git a/src/psbt.h b/src/psbt.h index 982fb803d59f8..cc4882c580022 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -544,7 +544,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); /** * Finalizes a PSBT if possible, combining partial signatures. diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 9d6a390ea2119..320956d0c4c6b 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -83,6 +83,8 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat assert(i.second); return true; } + // Could not make signature or signature not found, add keyid to missing + sigdata.missing_sigs.push_back(keyid); return false; } @@ -116,17 +118,24 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator case TX_PUBKEYHASH: { CKeyID keyID = CKeyID(uint160(vSolutions[0])); CPubKey pubkey; - if (!GetPubKey(provider, sigdata, keyID, pubkey)) return false; + if (!GetPubKey(provider, sigdata, keyID, pubkey)) { + // Pubkey could not be found, add to missing + sigdata.missing_pubkeys.push_back(keyID); + return false; + } if (!CreateSig(creator, sigdata, provider, sig, pubkey, scriptPubKey, sigversion)) return false; ret.push_back(std::move(sig)); ret.push_back(ToByteVector(pubkey)); return true; } case TX_SCRIPTHASH: - if (GetCScript(provider, sigdata, uint160(vSolutions[0]), scriptRet)) { + h160 = uint160(vSolutions[0]); + if (GetCScript(provider, sigdata, h160, scriptRet)) { ret.push_back(std::vector(scriptRet.begin(), scriptRet.end())); return true; } + // Could not find redeemScript, add to missing + sigdata.missing_redeem_script = h160; return false; case TX_MULTISIG: { @@ -154,6 +163,8 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator ret.push_back(std::vector(scriptRet.begin(), scriptRet.end())); return true; } + // Could not find witnessScript, add to missing + sigdata.missing_witness_script = uint256(vSolutions[0]); return false; default: diff --git a/src/script/sign.h b/src/script/sign.h index 64eb3eb8e5328..e229c375c762f 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -117,6 +117,10 @@ struct SignatureData { CScriptWitness scriptWitness; ///< The scriptWitness of an input. Contains complete signatures or the traditional partial signatures format. scriptWitness is part of a transaction input per BIP 144. std::map signatures; ///< BIP 174 style partial signatures for the input. May contain all signatures necessary for producing a final scriptSig or scriptWitness. std::map> misc_pubkeys; + std::vector missing_pubkeys; ///< KeyIDs of pubkeys which could not be found + std::vector missing_sigs; ///< KeyIDs of pubkeys for signatures which could not be found + uint160 missing_redeem_script; ///< ScriptID of the missing redeemScript (if any) + uint256 missing_witness_script; ///< SHA256 of the missing witnessScript (if any) SignatureData() {} explicit SignatureData(const CScript& script) : scriptSig(script) {} From 77542cf2a5f8abb97dd46f782c1b0199cc062033 Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 31 Jul 2018 17:56:47 -0700 Subject: [PATCH 4/5] Move PSBT UTXO fetching to a separate method --- src/psbt.cpp | 14 ++++++++++++++ src/psbt.h | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/psbt.cpp b/src/psbt.cpp index 0734edcf8e966..32fb459decfc6 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -62,6 +62,20 @@ bool PartiallySignedTransaction::AddOutput(const CTxOut& txout, const PSBTOutput return true; } +bool PartiallySignedTransaction::GetInputUTXO(CTxOut& utxo, int input_index) const +{ + PSBTInput input = inputs[input_index]; + int prevout_index = tx->vin[input_index].prevout.n; + if (input.non_witness_utxo) { + utxo = input.non_witness_utxo->vout[prevout_index]; + } else if (!input.witness_utxo.IsNull()) { + utxo = input.witness_utxo; + } else { + return false; + } + return true; +} + bool PSBTInput::IsNull() const { return !non_witness_utxo && witness_utxo.IsNull() && partial_sigs.empty() && unknown.empty() && hd_keypaths.empty() && redeem_script.empty() && witness_script.empty(); diff --git a/src/psbt.h b/src/psbt.h index cc4882c580022..27b0aedd055e1 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -394,6 +394,14 @@ struct PartiallySignedTransaction PartiallySignedTransaction() {} PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {} explicit PartiallySignedTransaction(const CMutableTransaction& tx); + /** + * Finds the UTXO for a given input index + * + * @param[out] utxo The UTXO of the input if found + * @param[in] input_index Index of the input to retrieve the UTXO of + * @return Whether the UTXO for the specified input was found + */ + bool GetInputUTXO(CTxOut& utxo, int input_index) const; template inline void Serialize(Stream& s) const { From 540729ef4bf1b6c6da1ec795e441d2ce56a9a58b Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Tue, 31 Jul 2018 17:58:01 -0700 Subject: [PATCH 5/5] Implement analyzepsbt RPC and tests --- src/rpc/rawtransaction.cpp | 198 ++++++++++++++++++++++++++++++++++++ test/functional/rpc_psbt.py | 26 ++++- 2 files changed, 223 insertions(+), 1 deletion(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 866158e003d46..ac412074045b8 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,8 @@ #include #include + +#include #include #include @@ -1829,6 +1832,200 @@ UniValue joinpsbts(const JSONRPCRequest& request) return EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()); } +UniValue analyzepsbt(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, RPCArg::Optional::NO, "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" + " \"witnessscript\" : \"hash\" (string) SHA256 of the witnessScript 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; + 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_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"); + } + } 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; + 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", 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 @@ -1849,6 +2046,7 @@ static const CRPCCommand commands[] = { "rawtransactions", "converttopsbt", &converttopsbt, {"hexstring","permitsigdata","iswitness"} }, { "rawtransactions", "utxoupdatepsbt", &utxoupdatepsbt, {"psbt"} }, { "rawtransactions", "joinpsbts", &joinpsbts, {"txs"} }, + { "rawtransactions", "analyzepsbt", &analyzepsbt, {"psbt"} }, { "blockchain", "gettxoutproof", &gettxoutproof, {"txids", "blockhash"} }, { "blockchain", "verifytxoutproof", &verifytxoutproof, {"proof"} }, diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index f1538b34a8aff..9a79a7ee4373c 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -5,8 +5,9 @@ """Test the Partially Signed Transaction RPCs. """ +from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error, find_output, disconnect_nodes, connect_nodes_bi, sync_blocks +from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, disconnect_nodes, find_output, sync_blocks import json import os @@ -339,6 +340,29 @@ def run_test(self): 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("", "p2sh-segwit") + txid = self.nodes[0].sendtoaddress(addr, 7) + addrinfo = self.nodes[1].getaddressinfo(addr) + 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("", "p2sh-segwit"):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' and analyzed['inputs'][0]['missing']['signatures'][0] == addrinfo['embedded']['witness_program'] + + # Check fee and size things + assert analyzed['fee'] == Decimal('0.001') and analyzed['estimated_vsize'] == 134 and analyzed['estimated_feerate'] == '0.00746268 BTC/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()