0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-01 09:35:52 -05:00
This commit is contained in:
Gregory Sanders 2025-02-01 00:05:35 +01:00 committed by GitHub
commit 077e1f97c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 5 deletions

View file

@ -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" },
@ -176,6 +177,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" },

View file

@ -163,7 +163,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;
@ -180,8 +180,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);
@ -191,8 +203,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);
}
@ -1656,7 +1677,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", {
@ -1666,6 +1687,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"
@ -1684,12 +1708,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{};
@ -1933,6 +1963,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, "", "",
@ -1960,11 +1993,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

View file

@ -1387,6 +1387,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)
{

View file

@ -153,6 +153,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".

View file

@ -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']