Skip to content

Commit 2f717fb

Browse files
committed
Merge #15427: Add support for descriptors to utxoupdatepsbt
26fe9b9 Add support for descriptors to utxoupdatepsbt (Pieter Wuille) 3135c1a Abstract out UpdatePSBTOutput from FillPSBT (Pieter Wuille) fb90ec3 Abstract out EvalDescriptorStringOrObject from scantxoutset (Pieter Wuille) eaf4f88 Abstract out IsSegWitOutput from utxoupdatepsbt (Pieter Wuille) Pull request description: This adds a descriptors argument to the `utxoupdatepsbt` RPC. This means: * Input and output scripts and keys will be filled in when known. * P2SH-witness inputs will be filled in from the UTXO set when a descriptor is provided that shows they're spending segwit outputs. This also moves some (newly) shared code to separate functions: `UpdatePSBTOutput` (an analogue to `SignPSBTInput`), `IsSegWitOutput`, and `EvalDescriptorStringOrObject` (implementing the string or object notation parsing used in `scantxoutset`). ACKs for top commit: jnewbery: utACK 26fe9b9 laanwj: utACK 26fe9b9 (will hold merging until response to promag's comments) promag: ACK 26fe9b9, checked refactors and tests look comprehensive. Still missing a release note but can be added later. Tree-SHA512: 1d833b7351b59d6c5ded6da399ff371a8a2a6ad04c0a8f90e6e46105dc737fa6f2740b1e5340280d59e01f42896c40b720c042f44417e38dfbee6477b894b245
2 parents c6e42f1 + 26fe9b9 commit 2f717fb

File tree

10 files changed

+148
-59
lines changed

10 files changed

+148
-59
lines changed

src/psbt.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,25 @@ bool PSBTInputSigned(const PSBTInput& input)
212212
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
213213
}
214214

215+
void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index)
216+
{
217+
const CTxOut& out = psbt.tx->vout.at(index);
218+
PSBTOutput& psbt_out = psbt.outputs.at(index);
219+
220+
// Fill a SignatureData with output info
221+
SignatureData sigdata;
222+
psbt_out.FillSignatureData(sigdata);
223+
224+
// Construct a would-be spend of this output, to update sigdata with.
225+
// Note that ProduceSignature is used to fill in metadata (not actual signatures),
226+
// so provider does not need to provide any private keys (it can be a HidingSigningProvider).
227+
MutableTransactionSignatureCreator creator(psbt.tx.get_ptr(), /* index */ 0, out.nValue, SIGHASH_ALL);
228+
ProduceSignature(provider, creator, out.scriptPubKey, sigdata);
229+
230+
// Put redeem_script, witness_script, key paths, into PSBTOutput.
231+
psbt_out.FromSignatureData(sigdata);
232+
}
233+
215234
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash, SignatureData* out_sigdata, bool use_dummy)
216235
{
217236
PSBTInput& input = psbt.inputs.at(index);

src/psbt.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,12 @@ bool PSBTInputSigned(const PSBTInput& input);
565565
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
566566
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
567567

568+
/** Updates a PSBTOutput with information from provider.
569+
*
570+
* This fills in the redeem_script, witness_script, and hd_keypaths where possible.
571+
*/
572+
void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index);
573+
568574
/**
569575
* Finalizes a PSBT if possible, combining partial signatures.
570576
*

src/rpc/blockchain.cpp

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,41 +2247,12 @@ UniValue scantxoutset(const JSONRPCRequest& request)
22472247

22482248
// loop through the scan objects
22492249
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
2250-
std::string desc_str;
2251-
std::pair<int64_t, int64_t> range = {0, 1000};
2252-
if (scanobject.isStr()) {
2253-
desc_str = scanobject.get_str();
2254-
} else if (scanobject.isObject()) {
2255-
UniValue desc_uni = find_value(scanobject, "desc");
2256-
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
2257-
desc_str = desc_uni.get_str();
2258-
UniValue range_uni = find_value(scanobject, "range");
2259-
if (!range_uni.isNull()) {
2260-
range = ParseDescriptorRange(range_uni);
2261-
}
2262-
} else {
2263-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
2264-
}
2265-
22662250
FlatSigningProvider provider;
2267-
auto desc = Parse(desc_str, provider);
2268-
if (!desc) {
2269-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
2270-
}
2271-
if (!desc->IsRange()) {
2272-
range.first = 0;
2273-
range.second = 0;
2274-
}
2275-
for (int i = range.first; i <= range.second; ++i) {
2276-
std::vector<CScript> scripts;
2277-
if (!desc->Expand(i, provider, scripts, provider)) {
2278-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
2279-
}
2280-
for (const auto& script : scripts) {
2281-
std::string inferred = InferDescriptor(script, provider)->ToString();
2282-
needles.emplace(script);
2283-
descriptors.emplace(std::move(script), std::move(inferred));
2284-
}
2251+
auto scripts = EvalDescriptorStringOrObject(scanobject, provider);
2252+
for (const auto& script : scripts) {
2253+
std::string inferred = InferDescriptor(script, provider)->ToString();
2254+
needles.emplace(script);
2255+
descriptors.emplace(std::move(script), std::move(inferred));
22852256
}
22862257
}
22872258

src/rpc/rawtransaction.cpp

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,12 +1495,19 @@ UniValue converttopsbt(const JSONRPCRequest& request)
14951495

14961496
UniValue utxoupdatepsbt(const JSONRPCRequest& request)
14971497
{
1498-
if (request.fHelp || request.params.size() != 1) {
1498+
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) {
14991499
throw std::runtime_error(
15001500
RPCHelpMan{"utxoupdatepsbt",
1501-
"\nUpdates a PSBT with witness UTXOs retrieved from the UTXO set or the mempool.\n",
1501+
"\nUpdates all segwit inputs and outputs in a PSBT with data from output descriptors, the UTXO set or the mempool.\n",
15021502
{
1503-
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"}
1503+
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "A base64 string of a PSBT"},
1504+
{"descriptors", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "An array of either strings or objects", {
1505+
{"", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
1506+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with an output descriptor and extra information", {
1507+
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
1508+
{"range", RPCArg::Type::RANGE, "1000", "Up to what index HD chains should be explored (either end or [begin,end])"},
1509+
}},
1510+
}},
15041511
},
15051512
RPCResult {
15061513
" \"psbt\" (string) The base64-encoded partially signed transaction with inputs updated\n"
@@ -1510,7 +1517,7 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
15101517
}}.ToString());
15111518
}
15121519

1513-
RPCTypeCheck(request.params, {UniValue::VSTR}, true);
1520+
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR}, true);
15141521

15151522
// Unserialize the transactions
15161523
PartiallySignedTransaction psbtx;
@@ -1519,6 +1526,17 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
15191526
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
15201527
}
15211528

1529+
// Parse descriptors, if any.
1530+
FlatSigningProvider provider;
1531+
if (!request.params[1].isNull()) {
1532+
auto descs = request.params[1].get_array();
1533+
for (size_t i = 0; i < descs.size(); ++i) {
1534+
EvalDescriptorStringOrObject(descs[i], provider);
1535+
}
1536+
}
1537+
// We don't actually need private keys further on; hide them as a precaution.
1538+
HidingSigningProvider public_provider(&provider, /* nosign */ true, /* nobip32derivs */ false);
1539+
15221540
// Fetch previous transactions (inputs):
15231541
CCoinsView viewDummy;
15241542
CCoinsViewCache view(&viewDummy);
@@ -1545,11 +1563,19 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
15451563

15461564
const Coin& coin = view.AccessCoin(psbtx.tx->vin[i].prevout);
15471565

1548-
std::vector<std::vector<unsigned char>> solutions_data;
1549-
txnouttype which_type = Solver(coin.out.scriptPubKey, solutions_data);
1550-
if (which_type == TX_WITNESS_V0_SCRIPTHASH || which_type == TX_WITNESS_V0_KEYHASH || which_type == TX_WITNESS_UNKNOWN) {
1566+
if (IsSegWitOutput(provider, coin.out.scriptPubKey)) {
15511567
input.witness_utxo = coin.out;
15521568
}
1569+
1570+
// Update script/keypath information using descriptor data.
1571+
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures
1572+
// we don't actually care about those here, in fact.
1573+
SignPSBTInput(public_provider, psbtx, i, /* sighash_type */ 1);
1574+
}
1575+
1576+
// Update script/keypath information using descriptor data.
1577+
for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) {
1578+
UpdatePSBTOutput(public_provider, psbtx, i);
15531579
}
15541580

15551581
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);

src/rpc/util.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <keystore.h>
77
#include <outputtype.h>
88
#include <rpc/util.h>
9+
#include <script/descriptor.h>
910
#include <tinyformat.h>
1011
#include <util/strencodings.h>
1112

@@ -697,3 +698,40 @@ std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value)
697698
}
698699
return {low, high};
699700
}
701+
702+
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider)
703+
{
704+
std::string desc_str;
705+
std::pair<int64_t, int64_t> range = {0, 1000};
706+
if (scanobject.isStr()) {
707+
desc_str = scanobject.get_str();
708+
} else if (scanobject.isObject()) {
709+
UniValue desc_uni = find_value(scanobject, "desc");
710+
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
711+
desc_str = desc_uni.get_str();
712+
UniValue range_uni = find_value(scanobject, "range");
713+
if (!range_uni.isNull()) {
714+
range = ParseDescriptorRange(range_uni);
715+
}
716+
} else {
717+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
718+
}
719+
720+
auto desc = Parse(desc_str, provider);
721+
if (!desc) {
722+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
723+
}
724+
if (!desc->IsRange()) {
725+
range.first = 0;
726+
range.second = 0;
727+
}
728+
std::vector<CScript> ret;
729+
for (int i = range.first; i <= range.second; ++i) {
730+
std::vector<CScript> scripts;
731+
if (!desc->Expand(i, provider, scripts, provider)) {
732+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
733+
}
734+
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
735+
}
736+
return ret;
737+
}

src/rpc/util.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include <outputtype.h>
1010
#include <pubkey.h>
1111
#include <rpc/protocol.h>
12+
#include <script/script.h>
13+
#include <script/sign.h>
1214
#include <script/standard.h>
1315
#include <univalue.h>
1416

@@ -84,6 +86,9 @@ UniValue JSONRPCTransactionError(TransactionError terr, const std::string& err_s
8486
//! Parse a JSON range specified as int64, or [int64, int64]
8587
std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value);
8688

89+
/** Evaluate a descriptor given as a string, or as a {"desc":...,"range":...} object, with default range of 1000. */
90+
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider);
91+
8792
struct RPCArg {
8893
enum class Type {
8994
OBJ,

src/script/sign.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,19 @@ FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvide
505505
ret.origins.insert(b.origins.begin(), b.origins.end());
506506
return ret;
507507
}
508+
509+
bool IsSegWitOutput(const SigningProvider& provider, const CScript& script)
510+
{
511+
std::vector<valtype> solutions;
512+
auto whichtype = Solver(script, solutions);
513+
if (whichtype == TX_WITNESS_V0_SCRIPTHASH || whichtype == TX_WITNESS_V0_KEYHASH || whichtype == TX_WITNESS_UNKNOWN) return true;
514+
if (whichtype == TX_SCRIPTHASH) {
515+
auto h160 = uint160(solutions[0]);
516+
CScript subscript;
517+
if (provider.GetCScript(h160, subscript)) {
518+
whichtype = Solver(subscript, solutions);
519+
if (whichtype == TX_WITNESS_V0_SCRIPTHASH || whichtype == TX_WITNESS_V0_KEYHASH || whichtype == TX_WITNESS_UNKNOWN) return true;
520+
}
521+
}
522+
return false;
523+
}

src/script/sign.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,7 @@ void UpdateInput(CTxIn& input, const SignatureData& data);
232232
* Solvability is unrelated to whether we consider this output to be ours. */
233233
bool IsSolvable(const SigningProvider& provider, const CScript& script);
234234

235+
/** Check whether a scriptPubKey is known to be segwit. */
236+
bool IsSegWitOutput(const SigningProvider& provider, const CScript& script);
237+
235238
#endif // BITCOIN_SCRIPT_SIGN_H

src/wallet/psbtwallet.cpp

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,7 @@ TransactionError FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& ps
4444

4545
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
4646
for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) {
47-
const CTxOut& out = psbtx.tx->vout.at(i);
48-
PSBTOutput& psbt_out = psbtx.outputs.at(i);
49-
50-
// Fill a SignatureData with output info
51-
SignatureData sigdata;
52-
psbt_out.FillSignatureData(sigdata);
53-
54-
MutableTransactionSignatureCreator creator(psbtx.tx.get_ptr(), 0, out.nValue, 1);
55-
ProduceSignature(HidingSigningProvider(pwallet, true, !bip32derivs), creator, out.scriptPubKey, sigdata);
56-
psbt_out.FromSignatureData(sigdata);
47+
UpdatePSBTOutput(HidingSigningProvider(pwallet, true, !bip32derivs), psbtx, i);
5748
}
5849

5950
return TransactionError::OK;

test/functional/rpc_psbt.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -325,18 +325,32 @@ def run_test(self):
325325
vout3 = find_output(self.nodes[0], txid3, 11)
326326
self.sync_all()
327327

328-
# Update a PSBT with UTXOs from the node
329-
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
328+
def test_psbt_input_keys(psbt_input, keys):
329+
"""Check that the psbt input has only the expected keys."""
330+
assert_equal(set(keys), set(psbt_input.keys()))
331+
332+
# Create a PSBT. None of the inputs are filled initially
330333
psbt = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1},{"txid":txid2, "vout":vout2},{"txid":txid3, "vout":vout3}], {self.nodes[0].getnewaddress():32.999})
331334
decoded = self.nodes[1].decodepsbt(psbt)
332-
assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
333-
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
334-
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
335+
test_psbt_input_keys(decoded['inputs'][0], [])
336+
test_psbt_input_keys(decoded['inputs'][1], [])
337+
test_psbt_input_keys(decoded['inputs'][2], [])
338+
339+
# Update a PSBT with UTXOs from the node
340+
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
335341
updated = self.nodes[1].utxoupdatepsbt(psbt)
336342
decoded = self.nodes[1].decodepsbt(updated)
337-
assert "witness_utxo" in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
338-
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
339-
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
343+
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo'])
344+
test_psbt_input_keys(decoded['inputs'][1], [])
345+
test_psbt_input_keys(decoded['inputs'][2], [])
346+
347+
# Try again, now while providing descriptors, making P2SH-segwit work, and causing bip32_derivs and redeem_script to be filled in
348+
descs = [self.nodes[1].getaddressinfo(addr)['desc'] for addr in [addr1,addr2,addr3]]
349+
updated = self.nodes[1].utxoupdatepsbt(psbt, descs)
350+
decoded = self.nodes[1].decodepsbt(updated)
351+
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'bip32_derivs'])
352+
test_psbt_input_keys(decoded['inputs'][1], [])
353+
test_psbt_input_keys(decoded['inputs'][2], ['witness_utxo', 'bip32_derivs', 'redeem_script'])
340354

341355
# Two PSBTs with a common input should not be joinable
342356
psbt1 = self.nodes[1].createpsbt([{"txid":txid1, "vout":vout1}], {self.nodes[0].getnewaddress():Decimal('10.999')})

0 commit comments

Comments
 (0)