diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1655,17 +1655,39 @@ } UniValue utxoupdatepsbt(const Config &config, const JSONRPCRequest &request) { - RPCHelpMan{"utxoupdatepsbt", - "\nUpdates a PSBT with witness UTXOs retrieved from the UTXO " - "set or the mempool.\n", - {{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, - "A base64 string of a PSBT"}}, - RPCResult{" \"psbt\" (string) The base64-encoded " - "partially signed transaction with inputs updated\n"}, - RPCExamples{HelpExampleCli("utxoupdatepsbt", "\"psbt\"")}} + RPCHelpMan{ + "utxoupdatepsbt", + "\nUpdates all inputs and outputs in a PSBT with data from output " + "descriptors, the UTXO set or the mempool.\n", + { + {"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, + "A base64 string of a PSBT"}, + {"descriptors", + RPCArg::Type::ARR, + RPCArg::Optional::OMITTED_NAMED_ARG, + "An array of either strings or objects", + { + {"", RPCArg::Type::STR, RPCArg::Optional::OMITTED, + "An output descriptor"}, + {"", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "An object with an output descriptor and extra information", + { + {"desc", RPCArg::Type::STR, RPCArg::Optional::NO, + "An output descriptor"}, + {"range", RPCArg::Type::RANGE, "1000", + "Up to what index HD chains should be explored (either " + "end or [begin,end])"}, + }}, + }}, + }, + RPCResult{" \"psbt\" (string) The base64-encoded " + "partially signed transaction with inputs updated\n"}, + RPCExamples{HelpExampleCli("utxoupdatepsbt", "\"psbt\"")}} .Check(request); - RPCTypeCheck(request.params, {UniValue::VSTR}, true); + RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR}, true); // Unserialize the transactions PartiallySignedTransaction psbtx; @@ -1675,6 +1697,19 @@ strprintf("TX decode failed %s", error)); } + // Parse descriptors, if any. + FlatSigningProvider provider; + if (!request.params[1].isNull()) { + auto descs = request.params[1].get_array(); + for (size_t i = 0; i < descs.size(); ++i) { + EvalDescriptorStringOrObject(descs[i], provider); + } + } + // We don't actually need private keys further on; hide them as a + // precaution. + HidingSigningProvider public_provider(&provider, /* nosign */ true, + /* nobip32derivs */ false); + // Fetch previous transactions (inputs): CCoinsView viewDummy; CCoinsViewCache view(&viewDummy); @@ -1702,7 +1737,16 @@ continue; } - input.utxo = view.AccessCoin(psbtx.tx->vin[i].prevout).GetTxOut(); + // Update script/keypath information using descriptor data. + // Note that SignPSBTInput does a lot more than just constructing ECDSA + // signatures we don't actually care about those here, in fact. + SignPSBTInput(public_provider, psbtx, i, + /* sighash_type */ SigHashType().withForkId()); + } + + // Update script/keypath information using descriptor data. + for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) { + UpdatePSBTOutput(public_provider, psbtx, i); } CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -280,12 +280,30 @@ vout3 = find_output(self.nodes[0], txid3, 11) self.sync_all() - # Update a PSBT with UTXOs from the node + def test_psbt_input_keys(psbt_input, keys): + """Check that the psbt input has only the expected keys.""" + assert_equal(set(keys), set(psbt_input.keys())) + + # Create a PSBT. None of the inputs are filled initially psbt = self.nodes[1].createpsbt([{"txid": txid1, "vout": vout1}, {"txid": txid2, "vout": vout2}, { "txid": txid3, "vout": vout3}], {self.nodes[0].getnewaddress(): 32.999}) decoded = self.nodes[1].decodepsbt(psbt) + test_psbt_input_keys(decoded['inputs'][0], []) + test_psbt_input_keys(decoded['inputs'][1], []) + test_psbt_input_keys(decoded['inputs'][2], []) + + # Update a PSBT with UTXOs from the node updated = self.nodes[1].utxoupdatepsbt(psbt) decoded = self.nodes[1].decodepsbt(updated) + test_psbt_input_keys(decoded['inputs'][1], []) + test_psbt_input_keys(decoded['inputs'][2], []) + + # Try again, now while providing descriptors + descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [ + addr1, addr2, addr3]] + updated = self.nodes[1].utxoupdatepsbt(psbt, descs) + decoded = self.nodes[1].decodepsbt(updated) + test_psbt_input_keys(decoded['inputs'][1], []) # Two PSBTs with a common input should not be joinable psbt1 = self.nodes[1].createpsbt([{"txid": txid1, "vout": vout1}], {