diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 0112a261ce747..e195117263d49 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -32,6 +32,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setmocktime", 0, "timestamp" }, { "mockscheduler", 0, "delta_time" }, { "utxoupdatepsbt", 1, "descriptors" }, + { "utxoupdatepsbt", 2, "prevtxs" }, { "generatetoaddress", 0, "nblocks" }, { "generatetoaddress", 2, "maxtries" }, { "generatetodescriptor", 0, "num_blocks" }, @@ -173,6 +174,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "descriptorprocesspsbt", 1, "descriptors"}, { "descriptorprocesspsbt", 3, "bip32derivs" }, { "descriptorprocesspsbt", 4, "finalize" }, + { "descriptorprocesspsbt", 5, "prevtxs" }, { "createpsbt", 0, "inputs" }, { "createpsbt", 1, "outputs" }, { "createpsbt", 2, "locktime" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 21bc0e52f130d..a6a4a35f7a2f3 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -174,7 +174,7 @@ static std::vector CreateTxDoc() // Update PSBT with information from the mempool, the UTXO set, the txindex, and the provided descriptors. // Optionally, sign the inputs that we can using information from the descriptors. -PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider, int sighash_type, bool finalize) +PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider, int sighash_type, const std::optional>& prev_txs, bool finalize) { // Unserialize the transactions PartiallySignedTransaction psbtx; @@ -191,8 +191,20 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std // the full transaction isn't found std::map coins; + // Filter prev_txs to unique txids and create lookup + std::map prev_tx_map; + if (prev_txs.has_value()) { + for (const auto& tx : prev_txs.value()) { + const auto txid = tx->GetHash(); + if (prev_tx_map.count(txid)) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Duplicate txids in prev_txs %s", txid.GetHex())); + } + prev_tx_map[txid] = tx; + } + } + // Fetch previous transactions: - // First, look in the txindex and the mempool + // First, look in prev_txs, the txindex, and the mempool for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) { PSBTInput& psbt_input = psbtx.inputs.at(i); const CTxIn& tx_in = psbtx.tx->vin.at(i); @@ -202,8 +214,17 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std CTransactionRef tx; - // Look in the txindex - if (g_txindex) { + // First look in provided dependant transactions + if (prev_tx_map.contains(tx_in.prevout.hash)) { + tx = prev_tx_map[tx_in.prevout.hash]; + // Sanity check it has an output + // at the right index + if (tx_in.prevout.n >= tx->vout.size()) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Previous tx has too few outputs for PSBT input %s", tx->GetHash().GetHex())); + } + } + // Then look in the txindex + if (!tx && g_txindex) { uint256 block_hash; g_txindex->FindTx(tx_in.prevout.hash, block_hash, tx); } @@ -1670,7 +1691,7 @@ static RPCHelpMan converttopsbt() static RPCHelpMan utxoupdatepsbt() { return RPCHelpMan{"utxoupdatepsbt", - "\nUpdates all segwit inputs and outputs in a PSBT with data from output descriptors, the UTXO set, txindex, or the mempool.\n", + "\nUpdates all segwit inputs and outputs in a PSBT with data from output descriptors, provided dependant transactions, the UTXO set, txindex, or the mempool.\n", { {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}, {"descriptors", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "An array of either strings or objects", { @@ -1680,6 +1701,9 @@ static RPCHelpMan utxoupdatepsbt() {"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "Up to what index HD chains should be explored (either end or [begin,end])"}, }}, }}, + {"prevtxs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "An array of dependant serialized transactions as hex", { + {"", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A serialized previous transaction in hex"}, + }}, }, RPCResult { RPCResult::Type::STR, "", "The base64-encoded partially signed transaction with inputs updated" @@ -1698,12 +1722,18 @@ static RPCHelpMan utxoupdatepsbt() } } + std::vector prev_txns; + if (!request.params[2].isNull()) { + prev_txns = ParseTransactionVector(request.params[2]); + } + // We don't actually need private keys further on; hide them as a precaution. const PartiallySignedTransaction& psbtx = ProcessPSBT( request.params[0].get_str(), request.context, HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false), /*sighash_type=*/SIGHASH_ALL, + /*prev_txs=*/prev_txns, /*finalize=*/false); DataStream ssTx{}; @@ -1947,6 +1977,9 @@ RPCHelpMan descriptorprocesspsbt() " \"SINGLE|ANYONECANPAY\""}, {"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"}, {"finalize", RPCArg::Type::BOOL, RPCArg::Default{true}, "Also finalize inputs if possible"}, + {"prevtxs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "An array of dependant serialized transactions as hex", { + {"", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A serialized previous transaction in hex"}, + }}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -1974,11 +2007,17 @@ RPCHelpMan descriptorprocesspsbt() bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool(); bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool(); + std::vector prev_txns; + if (!request.params[5].isNull()) { + prev_txns = ParseTransactionVector(request.params[5]); + } + const PartiallySignedTransaction& psbtx = ProcessPSBT( request.params[0].get_str(), request.context, HidingSigningProvider(&provider, /*hide_secret=*/false, !bip32derivs), sighash_type, + /*prev_txs=*/prev_txns, finalize); // Check whether or not all of the inputs are now signed diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 678bac7a185fa..c4e1d4f8e64b6 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -1370,6 +1370,23 @@ std::vector EvalDescriptorStringOrObject(const UniValue& scanobject, Fl return ret; } +std::vector ParseTransactionVector(const UniValue txns_param) +{ + std::vector txns; + const UniValue& raw_transactions = txns_param.get_array(); + txns.reserve(raw_transactions.size()); + + for (const auto& rawtx : raw_transactions.getValues()) { + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, rawtx.get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, + "TX decode failed: " + rawtx.get_str() + " Make sure the prev tx has at least one input."); + } + txns.emplace_back(MakeTransactionRef(std::move(mtx))); + } + return txns; +} + /** Convert a vector of bilingual strings to a UniValue::VARR containing their original untranslated values. */ [[nodiscard]] static UniValue BilingualStringsToUniValue(const std::vector& bilingual_strings) { diff --git a/src/rpc/util.h b/src/rpc/util.h index 23024376e096c..ea626a403120c 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -142,6 +142,9 @@ std::pair ParseDescriptorRange(const UniValue& value); /** Evaluate a descriptor given as a string, or as a {"desc":...,"range":...} object, with default range of 1000. */ std::vector EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv = false); +/** Parses a vector of transactions from a univalue array. */ +std::vector ParseTransactionVector(const UniValue txns_param); + /** * Serializing JSON objects depends on the outer type. Only arrays and * dictionaries can be nested in json. The top-level outer type is "NONE". diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 8042bdf0715ac..c2368d3995e78 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -210,6 +210,55 @@ def assert_change_type(self, psbtx, expected_type): assert_equal(decoded_psbt["tx"]["vout"][changepos]["scriptPubKey"]["type"], expected_type) def run_test(self): + + self.log.info("Test that PSBT can have user-provided UTXOs filled and signed") + + # Create 1 parent 1 child chain from same wallet + psbtx_parent = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[0].getnewaddress():10})['psbt'] + processed_parent = self.nodes[0].walletprocesspsbt(psbtx_parent) + parent_txinfo = self.nodes[0].decoderawtransaction(processed_parent["hex"]) + parent_txid = parent_txinfo["txid"] + parent_vout = 0 # just take the first output to spend + + psbtx_child = self.nodes[0].createpsbt([{"txid": parent_txid, "vout": parent_vout}], {self.nodes[0].getnewaddress(): parent_txinfo["vout"][0]["value"] - Decimal("0.01")}) + + # Can not sign due to lack of utxo + res = self.nodes[0].walletprocesspsbt(psbtx_child) + assert not res["complete"] + + prev_txs = [processed_parent["hex"]] + utxo_updated = self.nodes[0].utxoupdatepsbt(psbt=psbtx_child, prevtxs=prev_txs) + res = self.nodes[0].walletprocesspsbt(utxo_updated) + assert res["complete"] + + # And descriptorprocesspsbt does the same + utxo_updated = self.nodes[0].descriptorprocesspsbt(psbt=psbtx_child, descriptors=[], prevtxs=prev_txs) + res = self.nodes[0].walletprocesspsbt(utxo_updated["psbt"]) + assert res["complete"] + + # Multiple inputs are ok, even if unrelated transactions included + prev_txs = [processed_parent["hex"], self.nodes[0].createrawtransaction([], [])] + utxo_updated = self.nodes[0].utxoupdatepsbt(psbt=psbtx_child, prevtxs=prev_txs) + res = self.nodes[0].walletprocesspsbt(utxo_updated) + assert res["complete"] + + # If only irrelevant previous transactions are included, it's a no-op + prev_txs = [self.nodes[0].createrawtransaction([], [])] + utxo_updated = self.nodes[0].utxoupdatepsbt(psbt=psbtx_child, prevtxs=prev_txs) + assert_equal(utxo_updated, psbtx_child) + res = self.nodes[0].walletprocesspsbt(utxo_updated) + assert not res["complete"] + + # If there's a txid collision, it's rejected + prev_txs = [processed_parent["hex"], processed_parent["hex"]] + assert_raises_rpc_error(-22, f"Duplicate txids in prev_txs {parent_txid}", self.nodes[0].utxoupdatepsbt, psbt=psbtx_child, prevtxs=prev_txs) + + # Should abort safely if supplied transaction matches txid of prevout, but has insufficient outputs to match with prevout.n + psbtx_bad_child = self.nodes[0].createpsbt([{"txid": parent_txid, "vout": len(parent_txinfo["vout"])}], {self.nodes[0].getnewaddress(): parent_txinfo["vout"][0]["value"] - Decimal("0.01")}) + + prev_txs = [processed_parent["hex"]] + assert_raises_rpc_error(-22, f"Previous tx has too few outputs for PSBT input {parent_txid}", self.nodes[0].utxoupdatepsbt, psbt=psbtx_bad_child, prevtxs=prev_txs) + # Create and fund a raw tx for sending 10 BTC psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']