Skip to content

Commit

Permalink
Merge bitcoin#15427: Add support for descriptors to utxoupdatepsbt
Browse files Browse the repository at this point in the history
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
  • Loading branch information
laanwj committed Jul 2, 2019
2 parents c6e42f1 + 26fe9b9 commit 2f717fb
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 59 deletions.
19 changes: 19 additions & 0 deletions src/psbt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,25 @@ bool PSBTInputSigned(const PSBTInput& input)
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
}

void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index)
{
const CTxOut& out = psbt.tx->vout.at(index);
PSBTOutput& psbt_out = psbt.outputs.at(index);

// Fill a SignatureData with output info
SignatureData sigdata;
psbt_out.FillSignatureData(sigdata);

// Construct a would-be spend of this output, to update sigdata with.
// Note that ProduceSignature is used to fill in metadata (not actual signatures),
// so provider does not need to provide any private keys (it can be a HidingSigningProvider).
MutableTransactionSignatureCreator creator(psbt.tx.get_ptr(), /* index */ 0, out.nValue, SIGHASH_ALL);
ProduceSignature(provider, creator, out.scriptPubKey, sigdata);

// Put redeem_script, witness_script, key paths, into PSBTOutput.
psbt_out.FromSignatureData(sigdata);
}

bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash, SignatureData* out_sigdata, bool use_dummy)
{
PSBTInput& input = psbt.inputs.at(index);
Expand Down
6 changes: 6 additions & 0 deletions src/psbt.h
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,12 @@ bool PSBTInputSigned(const PSBTInput& input);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);

/** Updates a PSBTOutput with information from provider.
*
* This fills in the redeem_script, witness_script, and hd_keypaths where possible.
*/
void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index);

/**
* Finalizes a PSBT if possible, combining partial signatures.
*
Expand Down
39 changes: 5 additions & 34 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2247,41 +2247,12 @@ UniValue scantxoutset(const JSONRPCRequest& request)

// loop through the scan objects
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
std::string desc_str;
std::pair<int64_t, int64_t> range = {0, 1000};
if (scanobject.isStr()) {
desc_str = scanobject.get_str();
} else if (scanobject.isObject()) {
UniValue desc_uni = find_value(scanobject, "desc");
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
desc_str = desc_uni.get_str();
UniValue range_uni = find_value(scanobject, "range");
if (!range_uni.isNull()) {
range = ParseDescriptorRange(range_uni);
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
}

FlatSigningProvider provider;
auto desc = Parse(desc_str, provider);
if (!desc) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
}
if (!desc->IsRange()) {
range.first = 0;
range.second = 0;
}
for (int i = range.first; i <= range.second; ++i) {
std::vector<CScript> scripts;
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
for (const auto& script : scripts) {
std::string inferred = InferDescriptor(script, provider)->ToString();
needles.emplace(script);
descriptors.emplace(std::move(script), std::move(inferred));
}
auto scripts = EvalDescriptorStringOrObject(scanobject, provider);
for (const auto& script : scripts) {
std::string inferred = InferDescriptor(script, provider)->ToString();
needles.emplace(script);
descriptors.emplace(std::move(script), std::move(inferred));
}
}

Expand Down
40 changes: 33 additions & 7 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1495,12 +1495,19 @@ UniValue converttopsbt(const JSONRPCRequest& request)

UniValue utxoupdatepsbt(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 1) {
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) {
throw std::runtime_error(
RPCHelpMan{"utxoupdatepsbt",
"\nUpdates a PSBT with witness UTXOs retrieved from the UTXO set or the mempool.\n",
"\nUpdates all segwit 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"}
{"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"
Expand All @@ -1510,7 +1517,7 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
}}.ToString());
}

RPCTypeCheck(request.params, {UniValue::VSTR}, true);
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR}, true);

// Unserialize the transactions
PartiallySignedTransaction psbtx;
Expand All @@ -1519,6 +1526,17 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, 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);
Expand All @@ -1545,11 +1563,19 @@ UniValue utxoupdatepsbt(const JSONRPCRequest& request)

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

std::vector<std::vector<unsigned char>> solutions_data;
txnouttype which_type = Solver(coin.out.scriptPubKey, solutions_data);
if (which_type == TX_WITNESS_V0_SCRIPTHASH || which_type == TX_WITNESS_V0_KEYHASH || which_type == TX_WITNESS_UNKNOWN) {
if (IsSegWitOutput(provider, coin.out.scriptPubKey)) {
input.witness_utxo = coin.out;
}

// 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 */ 1);
}

// 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);
Expand Down
38 changes: 38 additions & 0 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <keystore.h>
#include <outputtype.h>
#include <rpc/util.h>
#include <script/descriptor.h>
#include <tinyformat.h>
#include <util/strencodings.h>

Expand Down Expand Up @@ -697,3 +698,40 @@ std::pair<int64_t, int64_t> ParseDescriptorRange(const UniValue& value)
}
return {low, high};
}

std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider)
{
std::string desc_str;
std::pair<int64_t, int64_t> range = {0, 1000};
if (scanobject.isStr()) {
desc_str = scanobject.get_str();
} else if (scanobject.isObject()) {
UniValue desc_uni = find_value(scanobject, "desc");
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
desc_str = desc_uni.get_str();
UniValue range_uni = find_value(scanobject, "range");
if (!range_uni.isNull()) {
range = ParseDescriptorRange(range_uni);
}
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
}

auto desc = Parse(desc_str, provider);
if (!desc) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
}
if (!desc->IsRange()) {
range.first = 0;
range.second = 0;
}
std::vector<CScript> ret;
for (int i = range.first; i <= range.second; ++i) {
std::vector<CScript> scripts;
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
}
return ret;
}
5 changes: 5 additions & 0 deletions src/rpc/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <outputtype.h>
#include <pubkey.h>
#include <rpc/protocol.h>
#include <script/script.h>
#include <script/sign.h>
#include <script/standard.h>
#include <univalue.h>

Expand Down Expand Up @@ -84,6 +86,9 @@ UniValue JSONRPCTransactionError(TransactionError terr, const std::string& err_s
//! Parse a JSON range specified as int64, or [int64, int64]
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);

struct RPCArg {
enum class Type {
OBJ,
Expand Down
16 changes: 16 additions & 0 deletions src/script/sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,19 @@ FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvide
ret.origins.insert(b.origins.begin(), b.origins.end());
return ret;
}

bool IsSegWitOutput(const SigningProvider& provider, const CScript& script)
{
std::vector<valtype> solutions;
auto whichtype = Solver(script, solutions);
if (whichtype == TX_WITNESS_V0_SCRIPTHASH || whichtype == TX_WITNESS_V0_KEYHASH || whichtype == TX_WITNESS_UNKNOWN) return true;
if (whichtype == TX_SCRIPTHASH) {
auto h160 = uint160(solutions[0]);
CScript subscript;
if (provider.GetCScript(h160, subscript)) {
whichtype = Solver(subscript, solutions);
if (whichtype == TX_WITNESS_V0_SCRIPTHASH || whichtype == TX_WITNESS_V0_KEYHASH || whichtype == TX_WITNESS_UNKNOWN) return true;
}
}
return false;
}
3 changes: 3 additions & 0 deletions src/script/sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,7 @@ void UpdateInput(CTxIn& input, const SignatureData& data);
* Solvability is unrelated to whether we consider this output to be ours. */
bool IsSolvable(const SigningProvider& provider, const CScript& script);

/** Check whether a scriptPubKey is known to be segwit. */
bool IsSegWitOutput(const SigningProvider& provider, const CScript& script);

#endif // BITCOIN_SCRIPT_SIGN_H
11 changes: 1 addition & 10 deletions src/wallet/psbtwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,7 @@ TransactionError FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& ps

// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) {
const CTxOut& out = psbtx.tx->vout.at(i);
PSBTOutput& psbt_out = psbtx.outputs.at(i);

// Fill a SignatureData with output info
SignatureData sigdata;
psbt_out.FillSignatureData(sigdata);

MutableTransactionSignatureCreator creator(psbtx.tx.get_ptr(), 0, out.nValue, 1);
ProduceSignature(HidingSigningProvider(pwallet, true, !bip32derivs), creator, out.scriptPubKey, sigdata);
psbt_out.FromSignatureData(sigdata);
UpdatePSBTOutput(HidingSigningProvider(pwallet, true, !bip32derivs), psbtx, i);
}

return TransactionError::OK;
Expand Down
30 changes: 22 additions & 8 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,18 +325,32 @@ def run_test(self):
vout3 = find_output(self.nodes[0], txid3, 11)
self.sync_all()

# Update a PSBT with UTXOs from the node
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
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)
assert "witness_utxo" not in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
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
# Bech32 inputs should be filled with witness UTXO. Other inputs should not be filled because they are non-witness
updated = self.nodes[1].utxoupdatepsbt(psbt)
decoded = self.nodes[1].decodepsbt(updated)
assert "witness_utxo" in decoded['inputs'][0] and "non_witness_utxo" not in decoded['inputs'][0]
assert "witness_utxo" not in decoded['inputs'][1] and "non_witness_utxo" not in decoded['inputs'][1]
assert "witness_utxo" not in decoded['inputs'][2] and "non_witness_utxo" not in decoded['inputs'][2]
test_psbt_input_keys(decoded['inputs'][0], ['witness_utxo'])
test_psbt_input_keys(decoded['inputs'][1], [])
test_psbt_input_keys(decoded['inputs'][2], [])

# Try again, now while providing descriptors, making P2SH-segwit work, and causing bip32_derivs and redeem_script to be filled in
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'][0], ['witness_utxo', 'bip32_derivs'])
test_psbt_input_keys(decoded['inputs'][1], [])
test_psbt_input_keys(decoded['inputs'][2], ['witness_utxo', 'bip32_derivs', 'redeem_script'])

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

0 comments on commit 2f717fb

Please sign in to comment.