Skip to content

Commit

Permalink
rpc: Add suppoer to populate PSBT input utxos from supplied txns
Browse files Browse the repository at this point in the history
  • Loading branch information
instagibbs committed Sep 11, 2024
1 parent 349632e commit 4e5d0a4
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 5 deletions.
71 changes: 66 additions & 5 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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", {
Expand All @@ -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"
Expand All @@ -1698,12 +1722,29 @@ static RPCHelpMan utxoupdatepsbt()
}
}

std::vector<CTransactionRef> 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{};
Expand Down Expand Up @@ -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, "", "",
Expand Down Expand Up @@ -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<CTransactionRef> 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
Expand Down
28 changes: 28 additions & 0 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4e5d0a4

Please sign in to comment.