From 87ceb610a72fda00e02f98426c6ee66b34fd6f54 Mon Sep 17 00:00:00 2001
From: Greg Sanders <gsanders87@gmail.com>
Date: Wed, 11 Sep 2024 16:22:07 -0400
Subject: [PATCH] rpc: Add support to populate PSBT input utxos via rpc

This feature is useful when construction a series of
presigned transactions that can not be entered into the
mempool or UTXO set before signing everything.
---
 src/rpc/client.cpp          |  2 ++
 src/rpc/rawtransaction.cpp  | 49 +++++++++++++++++++++++++++++++++----
 src/rpc/util.cpp            | 17 +++++++++++++
 src/rpc/util.h              |  3 +++
 test/functional/rpc_psbt.py | 49 +++++++++++++++++++++++++++++++++++++
 5 files changed, 115 insertions(+), 5 deletions(-)

diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 0112a261ce7..e195117263d 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 21bc0e52f13..a6a4a35f7a2 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -174,7 +174,7 @@ static std::vector<RPCArg> 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<std::vector<CTransactionRef>>& 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<COutPoint, Coin> coins;
 
+    // Filter prev_txs to unique txids and create lookup
+    std::map<Txid, CTransactionRef> 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<CTransactionRef> 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<CTransactionRef> 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 678bac7a185..c4e1d4f8e64 100644
--- a/src/rpc/util.cpp
+++ b/src/rpc/util.cpp
@@ -1370,6 +1370,23 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
     return ret;
 }
 
+std::vector<CTransactionRef> ParseTransactionVector(const UniValue txns_param)
+{
+    std::vector<CTransactionRef> 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_str>& bilingual_strings)
 {
diff --git a/src/rpc/util.h b/src/rpc/util.h
index 23024376e09..ea626a40312 100644
--- a/src/rpc/util.h
+++ b/src/rpc/util.h
@@ -142,6 +142,9 @@ std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value);
 /** Evaluate a descriptor given as a string, or as a {"desc":...,"range":...} object, with default range of 1000. */
 std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv = false);
 
+/** Parses a vector of transactions from a univalue array. */
+std::vector<CTransactionRef> 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 8042bdf0715..c2368d3995e 100755
--- a/test/functional/rpc_psbt.py
+++ b/test/functional/rpc_psbt.py
@@ -210,6 +210,55 @@ class PSBTTest(BitcoinTestFramework):
         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']