Skip to content

Commit

Permalink
rpc: Add support to populate PSBT input utxos via rpc
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
instagibbs committed Sep 12, 2024
1 parent 349632e commit abc8352
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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" },
Expand Down
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
49 changes: 49 additions & 0 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down

0 comments on commit abc8352

Please sign in to comment.