Skip to content

Commit 22139f6

Browse files
committed
Merge bitcoin/bitcoin#25796: rpc: add descriptorprocesspsbt rpc
1bce12a test: add test for `descriptorprocesspsbt` RPC (ishaanam) fb2a3a7 rpc: add descriptorprocesspsbt rpc (ishaanam) Pull request description: This PR implements an RPC called `descriptorprocesspsbt`. This RPC is based off of `walletprocesspsbt`, but instead of interacting with the wallet to update, sign and finalize a psbt, it instead accepts an array of output descriptors and uses that information along with information from the mempool, txindex, and the utxo set to do so. `utxoupdatepsbt` also updates a psbt in this manner, but doesn't sign or finalize it. Because of this overlap, a helper function that is added in this PR is called by both `utxoupdatepsbt` and `descriptorprocesspsbt`. Whether or not the helper function signs a psbt is dictated by if the HidingSigningProvider passed to it contains any private information. There is also a test added in this PR for this new RPC that uses p2wsh, p2wpkh, and legacy outputs. Edit: see bitcoin/bitcoin#25796 (comment) ACKs for top commit: achow101: re-ACK 1bce12a instagibbs: reACK bitcoin/bitcoin@1bce12a Tree-SHA512: e1d0334739943e71f2ee68b4db7637ebe725da62e7aa4be071f71c7196d2a5970a31ece96d91e372d34454cde8509e95ab0eebd2c8edb94f7d5a781a84f8fc5d
2 parents 4567014 + 1bce12a commit 22139f6

File tree

6 files changed

+142
-9
lines changed

6 files changed

+142
-9
lines changed

src/rpc/client.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
133133
{ "walletprocesspsbt", 1, "sign" },
134134
{ "walletprocesspsbt", 3, "bip32derivs" },
135135
{ "walletprocesspsbt", 4, "finalize" },
136+
{ "descriptorprocesspsbt", 1, "descriptors"},
137+
{ "descriptorprocesspsbt", 3, "bip32derivs" },
138+
{ "descriptorprocesspsbt", 4, "finalize" },
136139
{ "createpsbt", 0, "inputs" },
137140
{ "createpsbt", 1, "outputs" },
138141
{ "createpsbt", 2, "locktime" },

src/rpc/rawtransaction.cpp

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,9 @@ static std::vector<RPCArg> CreateTxDoc()
170170
};
171171
}
172172

173-
// Update PSBT with information from the mempool, the UTXO set, the txindex, and the provided descriptors
174-
PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider)
173+
// Update PSBT with information from the mempool, the UTXO set, the txindex, and the provided descriptors.
174+
// Optionally, sign the inputs that we can using information from the descriptors.
175+
PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider, int sighash_type, bool finalize)
175176
{
176177
// Unserialize the transactions
177178
PartiallySignedTransaction psbtx;
@@ -240,9 +241,10 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std
240241
}
241242

242243
// Update script/keypath information using descriptor data.
243-
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures
244-
// we don't actually care about those here, in fact.
245-
SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, /*sighash=*/1);
244+
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures.
245+
// We only actually care about those if our signing provider doesn't hide private
246+
// information, as is the case with `descriptorprocesspsbt`
247+
SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, sighash_type, /*out_sigdata=*/nullptr, finalize);
246248
}
247249

248250
// Update script/keypath information using descriptor data.
@@ -1695,7 +1697,9 @@ static RPCHelpMan utxoupdatepsbt()
16951697
const PartiallySignedTransaction& psbtx = ProcessPSBT(
16961698
request.params[0].get_str(),
16971699
request.context,
1698-
HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false));
1700+
HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false),
1701+
/*sighash_type=*/SIGHASH_ALL,
1702+
/*finalize=*/false);
16991703

17001704
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
17011705
ssTx << psbtx;
@@ -1914,6 +1918,82 @@ static RPCHelpMan analyzepsbt()
19141918
};
19151919
}
19161920

1921+
RPCHelpMan descriptorprocesspsbt()
1922+
{
1923+
return RPCHelpMan{"descriptorprocesspsbt",
1924+
"\nUpdate all segwit inputs in a PSBT with information from output descriptors, the UTXO set or the mempool. \n"
1925+
"Then, sign the inputs we are able to with information from the output descriptors. ",
1926+
{
1927+
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction base64 string"},
1928+
{"descriptors", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of either strings or objects", {
1929+
{"", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "An output descriptor"},
1930+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "An object with an output descriptor and extra information", {
1931+
{"desc", RPCArg::Type::STR, RPCArg::Optional::NO, "An output descriptor"},
1932+
{"range", RPCArg::Type::RANGE, RPCArg::Default{1000}, "Up to what index HD chains should be explored (either end or [begin,end])"},
1933+
}},
1934+
}},
1935+
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"DEFAULT for Taproot, ALL otherwise"}, "The signature hash type to sign with if not specified by the PSBT. Must be one of\n"
1936+
" \"DEFAULT\"\n"
1937+
" \"ALL\"\n"
1938+
" \"NONE\"\n"
1939+
" \"SINGLE\"\n"
1940+
" \"ALL|ANYONECANPAY\"\n"
1941+
" \"NONE|ANYONECANPAY\"\n"
1942+
" \"SINGLE|ANYONECANPAY\""},
1943+
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true}, "Include BIP 32 derivation paths for public keys if we know them"},
1944+
{"finalize", RPCArg::Type::BOOL, RPCArg::Default{true}, "Also finalize inputs if possible"},
1945+
},
1946+
RPCResult{
1947+
RPCResult::Type::OBJ, "", "",
1948+
{
1949+
{RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"},
1950+
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
1951+
}
1952+
},
1953+
RPCExamples{
1954+
HelpExampleCli("descriptorprocesspsbt", "\"psbt\" \"[\\\"descriptor1\\\", \\\"descriptor2\\\"]\"") +
1955+
HelpExampleCli("descriptorprocesspsbt", "\"psbt\" \"[{\\\"desc\\\":\\\"mydescriptor\\\", \\\"range\\\":21}]\"")
1956+
},
1957+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1958+
{
1959+
// Add descriptor information to a signing provider
1960+
FlatSigningProvider provider;
1961+
1962+
auto descs = request.params[1].get_array();
1963+
for (size_t i = 0; i < descs.size(); ++i) {
1964+
EvalDescriptorStringOrObject(descs[i], provider, /*expand_priv=*/true);
1965+
}
1966+
1967+
int sighash_type = ParseSighashString(request.params[2]);
1968+
bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
1969+
bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();
1970+
1971+
const PartiallySignedTransaction& psbtx = ProcessPSBT(
1972+
request.params[0].get_str(),
1973+
request.context,
1974+
HidingSigningProvider(&provider, /*hide_secret=*/false, !bip32derivs),
1975+
sighash_type,
1976+
finalize);
1977+
1978+
// Check whether or not all of the inputs are now signed
1979+
bool complete = true;
1980+
for (const auto& input : psbtx.inputs) {
1981+
complete &= PSBTInputSigned(input);
1982+
}
1983+
1984+
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
1985+
ssTx << psbtx;
1986+
1987+
UniValue result(UniValue::VOBJ);
1988+
1989+
result.pushKV("psbt", EncodeBase64(ssTx));
1990+
result.pushKV("complete", complete);
1991+
1992+
return result;
1993+
},
1994+
};
1995+
}
1996+
19171997
void RegisterRawTransactionRPCCommands(CRPCTable& t)
19181998
{
19191999
static const CRPCCommand commands[]{
@@ -1929,6 +2009,7 @@ void RegisterRawTransactionRPCCommands(CRPCTable& t)
19292009
{"rawtransactions", &createpsbt},
19302010
{"rawtransactions", &converttopsbt},
19312011
{"rawtransactions", &utxoupdatepsbt},
2012+
{"rawtransactions", &descriptorprocesspsbt},
19322013
{"rawtransactions", &joinpsbts},
19332014
{"rawtransactions", &analyzepsbt},
19342015
};

src/rpc/util.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@ std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value)
11261126
return {low, high};
11271127
}
11281128

1129-
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider)
1129+
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv)
11301130
{
11311131
std::string desc_str;
11321132
std::pair<int64_t, int64_t> range = {0, 1000};
@@ -1159,6 +1159,9 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
11591159
if (!desc->Expand(i, provider, scripts, provider)) {
11601160
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
11611161
}
1162+
if (expand_priv) {
1163+
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
1164+
}
11621165
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
11631166
}
11641167
return ret;

src/rpc/util.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ UniValue JSONRPCTransactionError(TransactionError terr, const std::string& err_s
110110
std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value);
111111

112112
/** Evaluate a descriptor given as a string, or as a {"desc":...,"range":...} object, with default range of 1000. */
113-
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider);
113+
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv = false);
114114

115115
/** Returns, given services flags, a list of humanly readable (known) network services */
116116
UniValue GetServicesNames(ServiceFlags services);

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
9898
"decoderawtransaction",
9999
"decodescript",
100100
"deriveaddresses",
101+
"descriptorprocesspsbt",
101102
"disconnectnode",
102103
"echo",
103104
"echojson",

test/functional/rpc_psbt.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
find_vout_for_address,
4343
random_bytes,
4444
)
45-
from test_framework.wallet_util import bytes_to_wif
45+
from test_framework.wallet_util import (
46+
bytes_to_wif,
47+
get_generate_key
48+
)
4649

4750
import json
4851
import os
@@ -942,6 +945,48 @@ def test_psbt_input_keys(psbt_input, keys):
942945
self.log.info("Test we don't crash when making a 0-value funded transaction at 0 fee without forcing an input selection")
943946
assert_raises_rpc_error(-4, "Transaction requires one destination of non-0 value, a non-0 feerate, or a pre-selected input", self.nodes[0].walletcreatefundedpsbt, [], [{"data": "deadbeef"}], 0, {"fee_rate": "0"})
944947

948+
self.log.info("Test descriptorprocesspsbt updates and signs a psbt with descriptors")
949+
950+
self.generate(self.nodes[2], 1)
951+
952+
# Disable the wallet for node 2 since `descriptorprocesspsbt` does not use the wallet
953+
self.restart_node(2, extra_args=["-disablewallet"])
954+
self.connect_nodes(0, 2)
955+
self.connect_nodes(1, 2)
956+
957+
key_info = get_generate_key()
958+
key = key_info.privkey
959+
address = key_info.p2wpkh_addr
960+
961+
descriptor = descsum_create(f"wpkh({key})")
962+
963+
txid = self.nodes[0].sendtoaddress(address, 1)
964+
self.sync_all()
965+
vout = find_output(self.nodes[0], txid, 1)
966+
967+
psbt = self.nodes[2].createpsbt([{"txid": txid, "vout": vout}], {self.nodes[0].getnewaddress(): 0.99999})
968+
decoded = self.nodes[2].decodepsbt(psbt)
969+
test_psbt_input_keys(decoded['inputs'][0], [])
970+
971+
# Test that even if the wrong descriptor is given, `witness_utxo` and `non_witness_utxo`
972+
# are still added to the psbt
973+
alt_descriptor = descsum_create(f"wpkh({get_generate_key().privkey})")
974+
alt_psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[alt_descriptor], sighashtype="ALL")["psbt"]
975+
decoded = self.nodes[2].decodepsbt(alt_psbt)
976+
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo'])
977+
978+
# Test that the psbt is not finalized and does not have bip32_derivs unless specified
979+
psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[descriptor], sighashtype="ALL", bip32derivs=True, finalize=False)["psbt"]
980+
decoded = self.nodes[2].decodepsbt(psbt)
981+
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo', 'partial_signatures', 'bip32_derivs'])
982+
983+
psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[descriptor], sighashtype="ALL", bip32derivs=False, finalize=True)["psbt"]
984+
decoded = self.nodes[2].decodepsbt(psbt)
985+
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo', 'final_scriptwitness'])
986+
987+
# Broadcast transaction
988+
rawtx = self.nodes[2].finalizepsbt(psbt)["hex"]
989+
self.nodes[2].sendrawtransaction(rawtx)
945990

946991
if __name__ == '__main__':
947992
PSBTTest().main()

0 commit comments

Comments
 (0)