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..4035f6b908a3aa 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -213,6 +213,34 @@ def run_test(self): # Create and fund a raw tx for sending 10 BTC psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt'] + processed_psbt1x1 = self.nodes[0].walletprocesspsbt(psbtx1) + + # Create a chained PSBT from psbtx1 and make sure utxoupdatepsbt can help fill UTXO information + prev_txid = self.nodes[0].decodepsbt(processed_psbt1x1["psbt"])["tx"]["txid"] + txid2 = self.nodes[0].decoderawtransaction(processed_psbt1x1["hex"])["txid"] + # prev_txid should match... + #assert_equal(prev_txid, txid2) + + psbt_child = self.nodes[0].createpsbt([{"txid": txid2, "vout": 0}], {self.nodes[2].getnewaddress():10}) + + # Can not sign due to lack of utxo + res = self.nodes[0].walletprocesspsbt(psbt_child) + assert not res["complete"] + + prev_txs = [processed_psbt1x1["hex"]] + utxo_updated = self.nodes[0].utxoupdatepsbt(psbt=psbt_child, prevtxs=prev_txs) + res = self.nodes[0].walletprocesspsbt(utxo_updated) + # sometimes fails... why? + assert res["complete"] + + # And descriptorprocesspsbt does the same + utxo_updated = self.nodes[0].descriptorprocesspsbt(psbt=psbt_child, descriptors=[], prevtxs=prev_txs) + # FIXME why is this needed? + res = self.nodes[0].walletprocesspsbt(utxo_updated["psbt"]) + assert res["complete"] + + return + self.log.info("Test for invalid maximum transaction weights") dest_arg = [{self.nodes[0].getnewaddress(): 1}] min_tx_weight = MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR