Skip to content

Commit

Permalink
Merge bitcoin#25796: rpc: add descriptorprocesspsbt rpc
Browse files Browse the repository at this point in the history
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#25796 (comment)

ACKs for top commit:
  achow101:
    re-ACK 1bce12a
  instagibbs:
    reACK bitcoin@1bce12a

Tree-SHA512: e1d0334739943e71f2ee68b4db7637ebe725da62e7aa4be071f71c7196d2a5970a31ece96d91e372d34454cde8509e95ab0eebd2c8edb94f7d5a781a84f8fc5d
  • Loading branch information
achow101 authored and sidhujag committed May 23, 2023
1 parent c434df6 commit a17baa1
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 9 deletions.
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Expand Up @@ -134,6 +134,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" },
{ "walletprocesspsbt", 4, "finalize" },
{ "descriptorprocesspsbt", 1, "descriptors"},
{ "descriptorprocesspsbt", 3, "bip32derivs" },
{ "descriptorprocesspsbt", 4, "finalize" },
{ "createpsbt", 0, "inputs" },
{ "createpsbt", 1, "outputs" },
{ "createpsbt", 2, "locktime" },
Expand Down
93 changes: 87 additions & 6 deletions src/rpc/rawtransaction.cpp
Expand Up @@ -295,8 +295,9 @@ static std::vector<RPCArg> CreateTxDoc()
};
}

// Update PSBT with information from the mempool, the UTXO set, the txindex, and the provided descriptors
PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider)
// 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)
{
// Unserialize the transactions
PartiallySignedTransaction psbtx;
Expand Down Expand Up @@ -365,9 +366,10 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std
}

// 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(provider, psbtx, /*index=*/i, &txdata, /*sighash=*/1);
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures.
// We only actually care about those if our signing provider doesn't hide private
// information, as is the case with `descriptorprocesspsbt`
SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, sighash_type, /*out_sigdata=*/nullptr, finalize);
}

// Update script/keypath information using descriptor data.
Expand Down Expand Up @@ -1829,7 +1831,9 @@ static RPCHelpMan utxoupdatepsbt()
const PartiallySignedTransaction& psbtx = ProcessPSBT(
request.params[0].get_str(),
request.context,
HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false));
HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false),
/*sighash_type=*/SIGHASH_ALL,
/*finalize=*/false);

CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
Expand Down Expand Up @@ -2048,6 +2052,82 @@ static RPCHelpMan analyzepsbt()
};
}

RPCHelpMan descriptorprocesspsbt()
{
return RPCHelpMan{"descriptorprocesspsbt",
"\nUpdate all segwit inputs in a PSBT with information from output descriptors, the UTXO set or the mempool. \n"
"Then, sign the inputs we are able to with information from the output descriptors. ",
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction base64 string"},
{"descriptors", RPCArg::Type::ARR, RPCArg::Optional::NO, "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, RPCArg::Default{1000}, "Up to what index HD chains should be explored (either end or [begin,end])"},
}},
}},
{"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"
" \"DEFAULT\"\n"
" \"ALL\"\n"
" \"NONE\"\n"
" \"SINGLE\"\n"
" \"ALL|ANYONECANPAY\"\n"
" \"NONE|ANYONECANPAY\"\n"
" \"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"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"},
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
}
},
RPCExamples{
HelpExampleCli("descriptorprocesspsbt", "\"psbt\" \"[\\\"descriptor1\\\", \\\"descriptor2\\\"]\"") +
HelpExampleCli("descriptorprocesspsbt", "\"psbt\" \"[{\\\"desc\\\":\\\"mydescriptor\\\", \\\"range\\\":21}]\"")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
// Add descriptor information to a signing provider
FlatSigningProvider provider;

auto descs = request.params[1].get_array();
for (size_t i = 0; i < descs.size(); ++i) {
EvalDescriptorStringOrObject(descs[i], provider, /*expand_priv=*/true);
}

int sighash_type = ParseSighashString(request.params[2]);
bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();

const PartiallySignedTransaction& psbtx = ProcessPSBT(
request.params[0].get_str(),
request.context,
HidingSigningProvider(&provider, /*hide_secret=*/false, !bip32derivs),
sighash_type,
finalize);

// Check whether or not all of the inputs are now signed
bool complete = true;
for (const auto& input : psbtx.inputs) {
complete &= PSBTInputSigned(input);
}

CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;

UniValue result(UniValue::VOBJ);

result.pushKV("psbt", EncodeBase64(ssTx));
result.pushKV("complete", complete);

return result;
},
};
}

void RegisterRawTransactionRPCCommands(CRPCTable& t)
{
static const CRPCCommand commands[]{
Expand All @@ -2063,6 +2143,7 @@ void RegisterRawTransactionRPCCommands(CRPCTable& t)
{"rawtransactions", &createpsbt},
{"rawtransactions", &converttopsbt},
{"rawtransactions", &utxoupdatepsbt},
{"rawtransactions", &descriptorprocesspsbt},
{"rawtransactions", &joinpsbts},
{"rawtransactions", &analyzepsbt},
};
Expand Down
5 changes: 4 additions & 1 deletion src/rpc/util.cpp
Expand Up @@ -1163,7 +1163,7 @@ std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value)
return {low, high};
}

std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider)
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv)
{
std::string desc_str;
std::pair<int64_t, int64_t> range = {0, 1000};
Expand Down Expand Up @@ -1196,6 +1196,9 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
if (expand_priv) {
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
}
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
}
return ret;
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/util.h
Expand Up @@ -113,7 +113,7 @@ UniValue JSONRPCTransactionError(TransactionError terr, const std::string& err_s
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);
std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider, const bool expand_priv = false);

/** Returns, given services flags, a list of humanly readable (known) network services */
UniValue GetServicesNames(ServiceFlags services);
Expand Down
1 change: 1 addition & 0 deletions src/test/fuzz/rpc.cpp
Expand Up @@ -157,6 +157,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"decoderawtransaction",
"decodescript",
"deriveaddresses",
"descriptorprocesspsbt",
"disconnectnode",
"echo",
"echojson",
Expand Down
47 changes: 46 additions & 1 deletion test/functional/rpc_psbt.py
Expand Up @@ -42,7 +42,10 @@
find_vout_for_address,
random_bytes,
)
from test_framework.wallet_util import bytes_to_wif
from test_framework.wallet_util import (
bytes_to_wif,
get_generate_key
)

import json
import os
Expand Down Expand Up @@ -943,6 +946,48 @@ def test_psbt_input_keys(psbt_input, keys):
self.log.info("Test we don't crash when making a 0-value funded transaction at 0 fee without forcing an input selection")
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"})

self.log.info("Test descriptorprocesspsbt updates and signs a psbt with descriptors")

self.generate(self.nodes[2], 1)

# Disable the wallet for node 2 since `descriptorprocesspsbt` does not use the wallet
self.restart_node(2, extra_args=["-disablewallet"])
self.connect_nodes(0, 2)
self.connect_nodes(1, 2)

key_info = get_generate_key()
key = key_info.privkey
address = key_info.p2wpkh_addr

descriptor = descsum_create(f"wpkh({key})")

txid = self.nodes[0].sendtoaddress(address, 1)
self.sync_all()
vout = find_output(self.nodes[0], txid, 1)

psbt = self.nodes[2].createpsbt([{"txid": txid, "vout": vout}], {self.nodes[0].getnewaddress(): 0.99999})
decoded = self.nodes[2].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], [])

# Test that even if the wrong descriptor is given, `witness_utxo` and `non_witness_utxo`
# are still added to the psbt
alt_descriptor = descsum_create(f"wpkh({get_generate_key().privkey})")
alt_psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[alt_descriptor], sighashtype="ALL")["psbt"]
decoded = self.nodes[2].decodepsbt(alt_psbt)
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo'])

# Test that the psbt is not finalized and does not have bip32_derivs unless specified
psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[descriptor], sighashtype="ALL", bip32derivs=True, finalize=False)["psbt"]
decoded = self.nodes[2].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo', 'partial_signatures', 'bip32_derivs'])

psbt = self.nodes[2].descriptorprocesspsbt(psbt=psbt, descriptors=[descriptor], sighashtype="ALL", bip32derivs=False, finalize=True)["psbt"]
decoded = self.nodes[2].decodepsbt(psbt)
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo', 'non_witness_utxo', 'final_scriptwitness'])

# Broadcast transaction
rawtx = self.nodes[2].finalizepsbt(psbt)["hex"]
self.nodes[2].sendrawtransaction(rawtx)

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

0 comments on commit a17baa1

Please sign in to comment.