diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 21bc0e52f130d2..edcfe27f972af4 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 previout transaction in hex"}, + }}, }, RPCResult { RPCResult::Type::STR, "", "The base64-encoded partially signed transaction with inputs updated" @@ -1698,12 +1722,29 @@ static RPCHelpMan utxoupdatepsbt() } } + std::vector prev_txns; + // Parse dependant transactions to populate input UTXOs + if (!request.params[2].isNull()) { + const UniValue raw_transactions = request.params[2].get_array(); + prev_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."); + } + prev_txns.emplace_back(MakeTransactionRef(std::move(mtx))); + } + } + // 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 +1988,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 previout transaction in hex"}, + }}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -1974,11 +2018,28 @@ 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; + // Parse dependant transactions to populate input UTXOs + if (!request.params[5].isNull()) { + const UniValue raw_transactions = request.params[5].get_array(); + prev_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."); + } + prev_txns.emplace_back(MakeTransactionRef(std::move(mtx))); + } + } + 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/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 8042bdf0715ac1..c2368d3995e78e 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']