Merge #16546: External signer support - Wallet Box edition

f75e0c1edd doc: add external-signer.md (Sjors Provoost)
d4b0107d68 rpc: send: support external signer (Sjors Provoost)
245b4457cf rpc: signerdisplayaddress (Sjors Provoost)
7ebc7c0215 wallet: ExternalSigner: add GetDescriptors method (Sjors Provoost)
fc5da520f5 wallet: add GetExternalSigner() (Sjors Provoost)
259f52cc33 test: external_signer wallet flag is immutable (Sjors Provoost)
2655197e1c rpc: add external_signer option to createwallet (Sjors Provoost)
2700f09c41 rpc: signer: add enumeratesigners to list external signers (Sjors Provoost)
07b7c940a7 rpc: add external signer RPC files (Sjors Provoost)
8ce7767071 wallet: add ExternalSignerScriptPubKeyMan (Sjors Provoost)
157ea7c614 wallet: add external_signer flag (Sjors Provoost)
f3e6ce78fb test: add external signer test (Sjors Provoost)
8cf543f96d wallet: add -signer argument for external signer command (Sjors Provoost)
f7eb7ecc67 test: framework: add skip_if_no_external_signer (Sjors Provoost)
87a97941f6 configure: add --enable-external-signer (Sjors Provoost)

Pull request description:

  Big picture overview in [this gist](https://gist.github.com/Sjors/29d06728c685e6182828c1ce9b74483d).

  This PR lets `bitcoind` call an arbitrary command `-signer=<cmd>`, e.g. a hardware wallet driver,  where it can fetch public keys, ask to display an address, and sign a transaction (using PSBT under the hood).

  It's design to work with https://github.com/bitcoin-core/HWI, which supports multiple hardware wallets. Any command with the same arguments and return values will work. It simplifies the manual procedure described [here](https://github.com/bitcoin-core/HWI/blob/master/docs/bitcoin-core-usage.md).

  Usage is documented in [doc/external-signer.md](
  https://github.com/Sjors/bitcoin/blob/2019/08/hww-box2/doc/external-signer.md), which also describes what protocol a different signer binary should conform to.

  Use `--enable-external-signer` to opt in, requires Boost::Process:

  ```
  Options used to compile and link:
    with wallet     = yes
    with gui / qt   = no
    external signer = yes
  ```

  It adds the following RPC methods:
  * `enumeratesigners`: asks <cmd> for a list of signers (e.g. devices) and their master key fingerprint
  * `signerdisplayaddress <address>`:  asks <cmd> to display an address

  It enhances the following RPC methods:
  * `createwallet`: takes an additional `external_signer` argument and fetches keys from device
  * `send`: automatically sends transaction to device and waits

  Usage TL&DR:
  * clone HWI repo somewhere and launch `bitcoind -signer=../HWI/hwi.py`
  * check if you can see your hardware device: `bitcoin-cli enumeratesigners`
  * create wallet and auto import keys `bitcoin-cli createwallet "hww" true true "" true true true`
  * display address on device: `bitcoin-cli signerdisplayaddress ...`
  * to spend, use `send` RPC and approve transaction on device

  Prerequisites:
  - [x] #21127 load wallet flags before everything else
  - [x] #21182 remove mostly pointless BOOST_PROCESS macro

  Potentially useful followups:
  - GUI support: bitcoin-core/gui#4
  - bumpfee support
  - (automatically) verify (a subset of) keys on the device after import, through message signing

ACKs for top commit:
  laanwj:
    re-ACK f75e0c1edd

Tree-SHA512: 7db8afd54762295c1424c3f01d8c587ec256a72f34bd5256e04b21832dabd5dc212be8ab975ae3b67de75259fd569a561491945750492f417111dc7b6641e77f
This commit is contained in:
Wladimir J. van der Laan 2021-02-23 17:56:24 +01:00
commit a9335e4f12
No known key found for this signature in database
GPG key ID: 1E4AED62986CD25D
33 changed files with 1184 additions and 85 deletions

View file

@ -50,8 +50,8 @@
/* define if the Boost::Filesystem library is available */
#define HAVE_BOOST_FILESYSTEM /**/
/* define if the Boost::Process library is available */
#define HAVE_BOOST_PROCESS /**/
/* define if external signer support is enabled (requires Boost::Process) */
#define ENABLE_EXTERNAL_SIGNER /**/
/* define if the Boost::System library is available */
#define HAVE_BOOST_SYSTEM /**/

View file

@ -338,10 +338,10 @@ AC_ARG_ENABLE([werror],
[enable_werror=$enableval],
[enable_werror=no])
AC_ARG_WITH([boost-process],
[AS_HELP_STRING([--with-boost-process],[Opt in to using Boost Process (default is no)])],
[boost_process=$withval],
[boost_process=no])
AC_ARG_ENABLE([external-signer],
[AS_HELP_STRING([--enable-external-signer],[compile external signer support (default is no, requires Boost::Process)])],
[use_external_signer=$enableval],
[use_external_signer=no])
AC_LANG_PUSH([C++])
@ -1253,6 +1253,7 @@ if test "x$enable_fuzz" = "xyes"; then
bitcoin_enable_qt_dbus=no
enable_wallet=no
use_bench=no
use_external_signer=no
use_upnp=no
use_natpmp=no
use_zmq=no
@ -1390,16 +1391,20 @@ fi
AX_BOOST_SYSTEM
AX_BOOST_FILESYSTEM
dnl Opt-in to Boost Process
if test "x$boost_process" != xno; then
dnl Opt-in to Boost Process if external signer support is requested
if test "x$use_external_signer" != xno; then
AC_MSG_CHECKING(for Boost Process)
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <boost/process.hpp>]],
[[ boost::process::child* child = new boost::process::child; delete child; ]])],
[ AC_MSG_RESULT(yes); AC_DEFINE([HAVE_BOOST_PROCESS],,[define if Boost::Process is available])],
[ AC_MSG_ERROR([Boost::Process is not available!])]
[ AC_MSG_RESULT(yes)
AC_DEFINE([ENABLE_EXTERNAL_SIGNER],,[define if external signer support is enabled])
],
[ AC_MSG_ERROR([Boost::Process is required for external signer support, but not available!])]
)
fi
AM_CONDITIONAL([ENABLE_EXTERNAL_SIGNER], [test "x$use_external_signer" = "xyes"])
if test x$suppress_external_warnings != xno; then
BOOST_CPPFLAGS=SUPPRESS_WARNINGS($BOOST_CPPFLAGS)
fi
@ -1810,6 +1815,7 @@ AC_SUBST(ARM_CRC_CXXFLAGS)
AC_SUBST(LIBTOOL_APP_LDFLAGS)
AC_SUBST(USE_SQLITE)
AC_SUBST(USE_BDB)
AC_SUBST(ENABLE_EXTERNAL_SIGNER)
AC_SUBST(USE_UPNP)
AC_SUBST(USE_QRCODE)
AC_SUBST(BOOST_LIBS)
@ -1885,43 +1891,43 @@ esac
echo
echo "Options used to compile and link:"
echo " boost process = $with_boost_process"
echo " multiprocess = $build_multiprocess"
echo " with libs = $build_bitcoin_libs"
echo " with wallet = $enable_wallet"
echo " external signer = $use_external_signer"
echo " multiprocess = $build_multiprocess"
echo " with libs = $build_bitcoin_libs"
echo " with wallet = $enable_wallet"
if test "x$enable_wallet" != "xno"; then
echo " with sqlite = $use_sqlite"
echo " with bdb = $use_bdb"
echo " with sqlite = $use_sqlite"
echo " with bdb = $use_bdb"
fi
echo " with gui / qt = $bitcoin_enable_qt"
echo " with gui / qt = $bitcoin_enable_qt"
if test x$bitcoin_enable_qt != xno; then
echo " with qr = $use_qr"
echo " with qr = $use_qr"
fi
echo " with zmq = $use_zmq"
echo " with zmq = $use_zmq"
if test x$enable_fuzz == xno; then
echo " with test = $use_tests"
echo " with test = $use_tests"
else
echo " with test = not building test_bitcoin because fuzzing is enabled"
echo " with fuzz = $enable_fuzz"
echo " with test = not building test_bitcoin because fuzzing is enabled"
echo " with fuzz = $enable_fuzz"
fi
echo " with bench = $use_bench"
echo " with upnp = $use_upnp"
echo " with natpmp = $use_natpmp"
echo " use asm = $use_asm"
echo " ebpf tracing = $have_sdt"
echo " sanitizers = $use_sanitizers"
echo " debug enabled = $enable_debug"
echo " gprof enabled = $enable_gprof"
echo " werror = $enable_werror"
echo " with bench = $use_bench"
echo " with upnp = $use_upnp"
echo " with natpmp = $use_natpmp"
echo " use asm = $use_asm"
echo " ebpf tracing = $have_sdt"
echo " sanitizers = $use_sanitizers"
echo " debug enabled = $enable_debug"
echo " gprof enabled = $enable_gprof"
echo " werror = $enable_werror"
echo
echo " target os = $TARGET_OS"
echo " build os = $build_os"
echo " target os = $TARGET_OS"
echo " build os = $build_os"
echo
echo " CC = $CC"
echo " CFLAGS = $PTHREAD_CFLAGS $CFLAGS"
echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CPPFLAGS"
echo " CXX = $CXX"
echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CXXFLAGS"
echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $LDFLAGS"
echo " ARFLAGS = $ARFLAGS"
echo " CC = $CC"
echo " CFLAGS = $PTHREAD_CFLAGS $CFLAGS"
echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CPPFLAGS"
echo " CXX = $CXX"
echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CXXFLAGS"
echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $LDFLAGS"
echo " ARFLAGS = $ARFLAGS"
echo

View file

@ -2073,7 +2073,7 @@ INCLUDE_FILE_PATTERNS =
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
PREDEFINED = HAVE_BOOST_PROCESS
PREDEFINED = ENABLE_EXTERNAL_SIGNER
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The

171
doc/external-signer.md Normal file
View file

@ -0,0 +1,171 @@
# Support for signing transactions outside of Bitcoin Core
Bitcoin Core can be launched with `-signer=<cmd>` where `<cmd>` is an external tool which can sign transactions and perform other functions. For example, it can be used to communicate with a hardware wallet.
## Example usage
The following example is based on the [HWI](https://github.com/bitcoin-core/HWI) tool. Although this tool is hosted under the Bitcoin Core GitHub organization and maintained by Bitcoin Core developers, it should be used with caution. It is considered experimental and has far less review than Bitcoin Core itself. Be particularly careful when running tools such as these on a computer with private keys on it.
When using a hardware wallet, consult the manufacturer website for (alternative) software they recommend. As long as their software conforms to the standard below, it should be able to work with Bitcoin Core.
Start Bitcoin Core:
```sh
$ bitcoind -signer=../HWI/hwi.py
```
### Device setup
Follow the hardware manufacturers instructions for the initial device setup, as well as their instructions for creating a backup. Alternatively, for some devices, you can use the `setup`, `restore` and `backup` commands provided by [HWI](https://github.com/bitcoin-core/HWI).
### Create wallet and import keys
Get a list of signing devices / services:
```
$ bitcoin-cli enumeratesigners
{
"signers": [
{
"fingerprint": "c8df832a"
}
]
```
The master key fingerprint is used to identify a device.
Create a wallet, this automatically imports the public keys:
```sh
$ bitcoin-cli createwallet "hww" true true "" true true true
```
### Verify an address
Display an address on the device:
```sh
$ bitcoin-cli -rpcwallet=<wallet> getnewaddress
$ bitcoin-cli -rpcwallet=<wallet> signerdisplayaddress <address>
```
Replace `<address>` with the result of `getnewaddress`.
### Spending
Under the hood this uses a [Partially Signed Bitcoin Transaction](psbt.md).
```sh
$ bitcoin-cli -rpcwallet=<wallet> sendtoaddress <address> <amount>
```
This prompts your hardware wallet to sign, and fail if it's not connected. If successful
it automatically broadcasts the transaction.
```sh
{"complete": true, "txid": <txid>}
```
## Signer API
In order to be compatible with Bitcoin Core any signer command should conform to the specification below. This specification is subject to change. Ideally a BIP should propose a standard so that other wallets can also make use of it.
Prerequisite knowledge:
* [Output Descriptors](descriptors.md)
* Partially Signed Bitcoin Transaction ([PSBT](psbt.md))
### `enumerate` (required)
Usage:
```
$ <cmd> enumerate
[
{
"fingerprint": "00000000"
}
]
```
The command MUST return an (empty) array with at least a `fingerprint` field.
A future extension could add an optional return field with device capabilities. Perhaps a descriptor with wildcards. For example: `["pkh("44'/0'/$'/{0,1}/*"), sh(wpkh("49'/0'/$'/{0,1}/*")), wpkh("84'/0'/$'/{0,1}/*")]`. This would indicate the device supports legacy, wrapped SegWit and native SegWit. In addition it restricts the derivation paths that can used for those, to maintain compatibility with other wallet software. It also indicates the device, or the driver, doesn't support multisig.
A future extension could add an optional return field `reachable`, in case `<cmd>` knows a signer exists but can't currently reach it.
### `signtransaction` (required)
Usage:
```
$ <cmd> --fingerprint=<fingerprint> (--testnet) signtransaction <psbt>
base64_encode_signed_psbt
```
The command returns a psbt with any signatures.
The `psbt` SHOULD include bip32 derivations. The command SHOULD fail if none of the bip32 derivations match a key owned by the device.
The command SHOULD fail if the user cancels.
The command MAY complain if `--testnet` is set, but any of the BIP32 derivation paths contain a coin type other than `1h` (and vice versa).
### `getdescriptors` (optional)
Usage:
```
$ <cmd> --fingerprint=<fingerprint> (--testnet) getdescriptors <account>
<xpub>
```
Returns descriptors supported by the device. Example:
```
$ <cmd> --fingerprint=00000000 --testnet getdescriptors
{
"receive": [
"pkh([00000000/44h/0h/0h]xpub6C.../0/*)#fn95jwmg",
"sh(wpkh([00000000/49h/0h/0h]xpub6B..../0/*))#j4r9hntt",
"wpkh([00000000/84h/0h/0h]xpub6C.../0/*)#qw72dxa9"
],
"internal": [
"pkh([00000000/44h/0h/0h]xpub6C.../1/*)#c8q40mts",
"sh(wpkh([00000000/49h/0h/0h]xpub6B..../1/*))#85dn0v75",
"wpkh([00000000/84h/0h/0h]xpub6C..../1/*)#36mtsnda"
]
}
```
### `displayaddress` (optional)
Usage:
```
<cmd> --fingerprint=<fingerprint> (--testnet) displayaddress --desc descriptor
```
Example, display the first native SegWit receive address on Testnet:
```
<cmd> --fingerprint=00000000 --testnet displayaddress --desc "wpkh([00000000/84h/1h/0h]tpubDDUZ..../0/0)"
```
The command MUST be able to figure out the address type from the descriptor.
If <descriptor> contains a master key fingerprint, the command MUST fail if it does not match the fingerprint known by the device.
If <descriptor> contains an xpub, the command MUST fail if it does not match the xpub known by the device.
The command MAY complain if `--testnet` is set, but the BIP32 coin type is not `1h` (and vice versa).
## How Bitcoin Core uses the Signer API
The `enumeratesigners` RPC simply calls `<cmd> enumerate`.
The `createwallet` RPC calls:
* `<cmd> --fingerprint=00000000 getdescriptors 0`
It then imports descriptors for all support address types, in a BIP44/49/84 compatible manner.
The `displayaddress` RPC reuses some code from `getaddressinfo` on the provided address and obtains the inferred descriptor. It then calls `<cmd> --fingerprint=00000000 displayaddress --desc=<descriptor>`.
`sendtoaddress` and `sendmany` check `inputs->bip32_derivs` to see if any inputs have the same `master_fingerprint` as the signer. If so, it calls `<cmd> --fingerprint=00000000 signtransaction <psbt>`. It waits for the device to return a (partially) signed psbt, tries to finalize it and broadcasts the transation.

View file

@ -265,10 +265,13 @@ BITCOIN_CORE_H = \
wallet/crypter.h \
wallet/db.h \
wallet/dump.h \
wallet/external_signer.h \
wallet/external_signer_scriptpubkeyman.h \
wallet/feebumper.h \
wallet/fees.h \
wallet/ismine.h \
wallet/load.h \
wallet/rpcsigner.h \
wallet/rpcwallet.h \
wallet/salvage.h \
wallet/scriptpubkeyman.h \
@ -379,11 +382,14 @@ libbitcoin_wallet_a_SOURCES = \
wallet/crypter.cpp \
wallet/db.cpp \
wallet/dump.cpp \
wallet/external_signer_scriptpubkeyman.cpp \
wallet/external_signer.cpp \
wallet/feebumper.cpp \
wallet/fees.cpp \
wallet/interfaces.cpp \
wallet/load.cpp \
wallet/rpcdump.cpp \
wallet/rpcsigner.cpp \
wallet/rpcwallet.cpp \
wallet/scriptpubkeyman.cpp \
wallet/wallet.cpp \

View file

@ -38,6 +38,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
"-paytxfee=<amt>",
"-rescan",
"-salvagewallet",
"-signer=<cmd>",
"-spendzeroconfchange",
"-txconfirmtarget=<n>",
"-wallet=<path>",

View file

@ -183,6 +183,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "createwallet", 4, "avoid_reuse"},
{ "createwallet", 5, "descriptors"},
{ "createwallet", 6, "load_on_startup"},
{ "createwallet", 7, "external_signer"},
{ "loadwallet", 1, "load_on_startup"},
{ "unloadwallet", 1, "load_on_startup"},
{ "getnodeaddresses", 0, "count"},

View file

@ -6,22 +6,22 @@
#include <util/system.h>
#include <univalue.h>
#ifdef HAVE_BOOST_PROCESS
#ifdef ENABLE_EXTERNAL_SIGNER
#include <boost/process.hpp>
#endif // HAVE_BOOST_PROCESS
#endif // ENABLE_EXTERNAL_SIGNER
#include <boost/test/unit_test.hpp>
BOOST_FIXTURE_TEST_SUITE(system_tests, BasicTestingSetup)
// At least one test is required (in case HAVE_BOOST_PROCESS is not defined).
// At least one test is required (in case ENABLE_EXTERNAL_SIGNER is not defined).
// Workaround for https://github.com/bitcoin/bitcoin/issues/19128
BOOST_AUTO_TEST_CASE(dummy)
{
BOOST_CHECK(true);
}
#ifdef HAVE_BOOST_PROCESS
#ifdef ENABLE_EXTERNAL_SIGNER
bool checkMessage(const std::runtime_error& ex)
{
@ -90,6 +90,6 @@ BOOST_AUTO_TEST_CASE(run_command)
}
#endif
}
#endif // HAVE_BOOST_PROCESS
#endif // ENABLE_EXTERNAL_SIGNER
BOOST_AUTO_TEST_SUITE_END()

View file

@ -31,6 +31,10 @@ bilingual_str TransactionErrorString(const TransactionError err)
return Untranslated("Specified sighash value does not match value stored in PSBT");
case TransactionError::MAX_FEE_EXCEEDED:
return Untranslated("Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)");
case TransactionError::EXTERNAL_SIGNER_NOT_FOUND:
return Untranslated("External signer not found");
case TransactionError::EXTERNAL_SIGNER_FAILED:
return Untranslated("External signer failed to sign");
// no default case, so the compiler can warn about missing cases
}
assert(false);

View file

@ -30,6 +30,8 @@ enum class TransactionError {
PSBT_MISMATCH,
SIGHASH_MISMATCH,
MAX_FEE_EXCEEDED,
EXTERNAL_SIGNER_NOT_FOUND,
EXTERNAL_SIGNER_FAILED,
};
bilingual_str TransactionErrorString(const TransactionError error);

View file

@ -5,9 +5,9 @@
#include <util/system.h>
#ifdef HAVE_BOOST_PROCESS
#ifdef ENABLE_EXTERNAL_SIGNER
#include <boost/process.hpp>
#endif // HAVE_BOOST_PROCESS
#endif // ENABLE_EXTERNAL_SIGNER
#include <chainparamsbase.h>
#include <sync.h>
@ -1247,7 +1247,7 @@ void runCommand(const std::string& strCommand)
}
#endif
#ifdef HAVE_BOOST_PROCESS
#ifdef ENABLE_EXTERNAL_SIGNER
UniValue RunCommandParseJSON(const std::string& str_command, const std::string& str_std_in)
{
namespace bp = boost::process;
@ -1282,7 +1282,7 @@ UniValue RunCommandParseJSON(const std::string& str_command, const std::string&
return result_json;
}
#endif // HAVE_BOOST_PROCESS
#endif // ENABLE_EXTERNAL_SIGNER
void SetupEnvironment()
{

View file

@ -108,7 +108,7 @@ std::string ShellEscape(const std::string& arg);
#if HAVE_SYSTEM
void runCommand(const std::string& strCommand);
#endif
#ifdef HAVE_BOOST_PROCESS
#ifdef ENABLE_EXTERNAL_SIGNER
/**
* Execute a command which returns JSON, and parse the result.
*
@ -117,7 +117,7 @@ void runCommand(const std::string& strCommand);
* @return parsed JSON
*/
UniValue RunCommandParseJSON(const std::string& str_command, const std::string& str_std_in="");
#endif // HAVE_BOOST_PROCESS
#endif // ENABLE_EXTERNAL_SIGNER
/**
* Most paths passed as configuration arguments are treated as relative to

View file

@ -0,0 +1,119 @@
// Copyright (c) 2018-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h>
#include <core_io.h>
#include <psbt.h>
#include <util/strencodings.h>
#include <util/system.h>
#include <wallet/external_signer.h>
ExternalSigner::ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name): m_command(command), m_fingerprint(fingerprint), m_chain(chain), m_name(name) {}
const std::string ExternalSigner::NetworkArg() const
{
return " --chain " + m_chain;
}
#ifdef ENABLE_EXTERNAL_SIGNER
bool ExternalSigner::Enumerate(const std::string& command, std::vector<ExternalSigner>& signers, std::string chain, bool ignore_errors)
{
// Call <command> enumerate
const UniValue result = RunCommandParseJSON(command + " enumerate");
if (!result.isArray()) {
if (ignore_errors) return false;
throw ExternalSignerException(strprintf("'%s' received invalid response, expected array of signers", command));
}
for (UniValue signer : result.getValues()) {
// Check for error
const UniValue& error = find_value(signer, "error");
if (!error.isNull()) {
if (ignore_errors) return false;
if (!error.isStr()) {
throw ExternalSignerException(strprintf("'%s' error", command));
}
throw ExternalSignerException(strprintf("'%s' error: %s", command, error.getValStr()));
}
// Check if fingerprint is present
const UniValue& fingerprint = find_value(signer, "fingerprint");
if (fingerprint.isNull()) {
if (ignore_errors) return false;
throw ExternalSignerException(strprintf("'%s' received invalid response, missing signer fingerprint", command));
}
std::string fingerprintStr = fingerprint.get_str();
// Skip duplicate signer
bool duplicate = false;
for (ExternalSigner signer : signers) {
if (signer.m_fingerprint.compare(fingerprintStr) == 0) duplicate = true;
}
if (duplicate) break;
std::string name = "";
const UniValue& model_field = find_value(signer, "model");
if (model_field.isStr() && model_field.getValStr() != "") {
name += model_field.getValStr();
}
signers.push_back(ExternalSigner(command, fingerprintStr, chain, name));
}
return true;
}
UniValue ExternalSigner::DisplayAddress(const std::string& descriptor) const
{
return RunCommandParseJSON(m_command + " --fingerprint \"" + m_fingerprint + "\"" + NetworkArg() + " displayaddress --desc \"" + descriptor + "\"");
}
UniValue ExternalSigner::GetDescriptors(int account)
{
return RunCommandParseJSON(m_command + " --fingerprint \"" + m_fingerprint + "\"" + NetworkArg() + " getdescriptors --account " + strprintf("%d", account));
}
bool ExternalSigner::SignTransaction(PartiallySignedTransaction& psbtx, std::string& error)
{
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
// Check if signer fingerprint matches any input master key fingerprint
bool match = false;
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
const PSBTInput& input = psbtx.inputs[i];
for (auto entry : input.hd_keypaths) {
if (m_fingerprint == strprintf("%08x", ReadBE32(entry.second.fingerprint))) match = true;
}
}
if (!match) {
error = "Signer fingerprint " + m_fingerprint + " does not match any of the inputs:\n" + EncodeBase64(ssTx.str());
return false;
}
std::string command = m_command + " --stdin --fingerprint \"" + m_fingerprint + "\"" + NetworkArg();
std::string stdinStr = "signtx \"" + EncodeBase64(ssTx.str()) + "\"";
const UniValue signer_result = RunCommandParseJSON(command, stdinStr);
if (find_value(signer_result, "error").isStr()) {
error = find_value(signer_result, "error").get_str();
return false;
}
if (!find_value(signer_result, "psbt").isStr()) {
error = "Unexpected result from signer";
return false;
}
PartiallySignedTransaction signer_psbtx;
std::string signer_psbt_error;
if (!DecodeBase64PSBT(signer_psbtx, find_value(signer_result, "psbt").get_str(), signer_psbt_error)) {
error = strprintf("TX decode failed %s", signer_psbt_error);
return false;
}
psbtx = signer_psbtx;
return true;
}
#endif

View file

@ -0,0 +1,73 @@
// Copyright (c) 2018-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_WALLET_EXTERNAL_SIGNER_H
#define BITCOIN_WALLET_EXTERNAL_SIGNER_H
#include <stdexcept>
#include <string>
#include <univalue.h>
#include <util/system.h>
struct PartiallySignedTransaction;
class ExternalSignerException : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
//! Enables interaction with an external signing device or service, such as
//! a hardware wallet. See doc/external-signer.md
class ExternalSigner
{
private:
//! The command which handles interaction with the external signer.
std::string m_command;
public:
//! @param[in] command the command which handles interaction with the external signer
//! @param[in] fingerprint master key fingerprint of the signer
//! @param[in] chain "main", "test", "regtest" or "signet"
//! @param[in] name device name
ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name);
//! Master key fingerprint of the signer
std::string m_fingerprint;
//! Bitcoin mainnet, testnet, etc
std::string m_chain;
//! Name of signer
std::string m_name;
const std::string NetworkArg() const;
#ifdef ENABLE_EXTERNAL_SIGNER
//! Obtain a list of signers. Calls `<command> enumerate`.
//! @param[in] command the command which handles interaction with the external signer
//! @param[in,out] signers vector to which new signers (with a unique master key fingerprint) are added
//! @param chain "main", "test", "regtest" or "signet"
//! @param[out] success Boolean
static bool Enumerate(const std::string& command, std::vector<ExternalSigner>& signers, std::string chain, bool ignore_errors = false);
//! Display address on the device. Calls `<command> displayaddress --desc <descriptor>`.
//! @param[in] descriptor Descriptor specifying which address to display.
//! Must include a public key or xpub, as well as key origin.
UniValue DisplayAddress(const std::string& descriptor) const;
//! Get receive and change Descriptor(s) from device for a given account.
//! Calls `<command> getdescriptors --account <account>`
//! @param[in] account which BIP32 account to use (e.g. `m/44'/0'/account'`)
//! @param[out] UniValue see doc/external-signer.md
UniValue GetDescriptors(int account);
//! Sign PartiallySignedTransaction on the device.
//! Calls `<command> signtransaction` and passes the PSBT via stdin.
//! @param[in,out] psbt PartiallySignedTransaction to be signed
bool SignTransaction(PartiallySignedTransaction& psbt, std::string& error);
#endif
};
#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_H

View file

@ -0,0 +1,81 @@
// Copyright (c) 2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h>
#include <wallet/external_signer.h>
#include <wallet/external_signer_scriptpubkeyman.h>
#ifdef ENABLE_EXTERNAL_SIGNER
bool ExternalSignerScriptPubKeyMan::SetupDescriptor(std::unique_ptr<Descriptor> desc)
{
LOCK(cs_desc_man);
assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
assert(m_storage.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
int64_t creation_time = GetTime();
// Make the descriptor
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
m_wallet_descriptor = w_desc;
// Store the descriptor
WalletBatch batch(m_storage.GetDatabase());
if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
}
// TopUp
TopUp();
m_storage.UnsetBlankWalletFlag(batch);
return true;
}
ExternalSigner ExternalSignerScriptPubKeyMan::GetExternalSigner() {
const std::string command = gArgs.GetArg("-signer", "");
if (command == "") throw std::runtime_error(std::string(__func__) + ": restart bitcoind with -signer=<cmd>");
std::vector<ExternalSigner> signers;
ExternalSigner::Enumerate(command, signers, Params().NetworkIDString());
if (signers.empty()) throw std::runtime_error(std::string(__func__) + ": No external signers found");
// TODO: add fingerprint argument in case of multiple signers
return signers[0];
}
bool ExternalSignerScriptPubKeyMan::DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const
{
// TODO: avoid the need to infer a descriptor from inside a descriptor wallet
auto provider = GetSolvingProvider(scriptPubKey);
auto descriptor = InferDescriptor(scriptPubKey, *provider);
signer.DisplayAddress(descriptor->ToString());
// TODO inspect result
return true;
}
// If sign is true, transaction must previously have been filled
TransactionError ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (!sign) {
return DescriptorScriptPubKeyMan::FillPSBT(psbt, sighash_type, false, bip32derivs, n_signed);
}
// Already complete if every input is now signed
bool complete = true;
for (const auto& input : psbt.inputs) {
// TODO: for multisig wallets, we should only care if all _our_ inputs are signed
complete &= PSBTInputSigned(input);
}
if (complete) return TransactionError::OK;
std::string strFailReason;
if(!GetExternalSigner().SignTransaction(psbt, strFailReason)) {
tfm::format(std::cerr, "Failed to sign: %s\n", strFailReason);
return TransactionError::EXTERNAL_SIGNER_FAILED;
}
FinalizePSBT(psbt); // This won't work in a multisig setup
return TransactionError::OK;
}
#endif

View file

@ -0,0 +1,34 @@
// Copyright (c) 2019-2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
#define BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
#ifdef ENABLE_EXTERNAL_SIGNER
#include <wallet/scriptpubkeyman.h>
class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
{
public:
ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor)
: DescriptorScriptPubKeyMan(storage, descriptor)
{}
ExternalSignerScriptPubKeyMan(WalletStorage& storage, bool internal)
: DescriptorScriptPubKeyMan(storage, internal)
{}
/** Provide a descriptor at setup time
* Returns false if already setup or setup fails, true if setup is successful
*/
bool SetupDescriptor(std::unique_ptr<Descriptor>desc);
static ExternalSigner GetExternalSigner();
bool DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
};
#endif
#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H

View file

@ -61,6 +61,9 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
argsman.AddArg("-paytxfee=<amt>", strprintf("Fee (in %s/kB) to add to transactions you send (default: %s)",
CURRENCY_UNIT, FormatMoney(CFeeRate{DEFAULT_PAY_TX_FEE}.GetFeePerK())), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.AddArg("-rescan", "Rescan the block chain for missing wallet transactions on startup", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
#ifdef ENABLE_EXTERNAL_SIGNER
argsman.AddArg("-signer=<cmd>", "External signing tool, see docs/external-signer.md", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
#endif
argsman.AddArg("-spendzeroconfchange", strprintf("Spend unconfirmed change when sending transactions (default: %u)", DEFAULT_SPEND_ZEROCONF_CHANGE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.AddArg("-txconfirmtarget=<n>", strprintf("If paytxfee is not set, include enough fee so transactions begin confirmation on average within n blocks (default: %u)", DEFAULT_TX_CONFIRM_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
argsman.AddArg("-wallet=<path>", "Specify wallet path to load at startup. Can be used multiple times to load multiple wallets. Path is to a directory containing wallet data and log files. If the path is not absolute, it is interpreted relative to <walletdir>. This only loads existing wallets and does not create new ones. For backwards compatibility this also accepts names of existing top-level data files in <walletdir>.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET);

View file

@ -23,6 +23,7 @@
#include <wallet/fees.h>
#include <wallet/ismine.h>
#include <wallet/load.h>
#include <wallet/rpcsigner.h>
#include <wallet/rpcwallet.h>
#include <wallet/wallet.h>
@ -518,6 +519,15 @@ public:
}, command.argNames, command.unique_id);
m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back()));
}
#ifdef ENABLE_EXTERNAL_SIGNER
for (const CRPCCommand& command : GetSignerRPCCommands()) {
m_rpc_commands.emplace_back(command.category, command.name, [this, &command](const JSONRPCRequest& request, UniValue& result, bool last_handler) {
return command.actor({request, m_context}, result, last_handler);
}, command.argNames, command.unique_id);
m_rpc_handlers.emplace_back(m_context.chain->handleRpc(m_rpc_commands.back()));
}
#endif
}
bool verify() override { return VerifyWallets(*m_context.chain); }
bool load() override { return LoadWallets(*m_context.chain); }

111
src/wallet/rpcsigner.cpp Normal file
View file

@ -0,0 +1,111 @@
// Copyright (c) 2018-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparamsbase.h>
#include <key_io.h>
#include <rpc/server.h>
#include <rpc/util.h>
#include <util/strencodings.h>
#include <wallet/rpcsigner.h>
#include <wallet/rpcwallet.h>
#include <wallet/wallet.h>
#ifdef ENABLE_EXTERNAL_SIGNER
static RPCHelpMan enumeratesigners()
{
return RPCHelpMan{
"enumeratesigners",
"Returns a list of external signers from -signer.",
{},
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::ARR, "signers", /* optional */ false, "",
{
{RPCResult::Type::STR_HEX, "masterkeyfingerprint", "Master key fingerprint"},
{RPCResult::Type::STR, "name", "Device name"},
},
}
}
},
RPCExamples{""},
[](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
const std::string command = gArgs.GetArg("-signer", "");
if (command == "") throw JSONRPCError(RPC_WALLET_ERROR, "Error: restart bitcoind with -signer=<cmd>");
std::string chain = gArgs.GetChainName();
UniValue signers_res = UniValue::VARR;
try {
std::vector<ExternalSigner> signers;
ExternalSigner::Enumerate(command, signers, chain);
for (ExternalSigner signer : signers) {
UniValue signer_res = UniValue::VOBJ;
signer_res.pushKV("fingerprint", signer.m_fingerprint);
signer_res.pushKV("name", signer.m_name);
signers_res.push_back(signer_res);
}
} catch (const ExternalSignerException& e) {
throw JSONRPCError(RPC_WALLET_ERROR, e.what());
}
UniValue result(UniValue::VOBJ);
result.pushKV("signers", signers_res);
return result;
}
};
}
static RPCHelpMan signerdisplayaddress()
{
return RPCHelpMan{
"signerdisplayaddress",
"Display address on an external signer for verification.\n",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO, /* default_val */ "", "bitcoin address to display"},
},
RPCResult{RPCResult::Type::NONE,"",""},
RPCExamples{""},
[](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
CWallet* const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
CTxDestination dest = DecodeDestination(request.params[0].get_str());
// Make sure the destination is valid
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address");
}
if (!pwallet->DisplayAddress(dest)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to display address");
}
UniValue result(UniValue::VOBJ);
result.pushKV("address", request.params[0].get_str());
return result;
}
};
}
Span<const CRPCCommand> GetSignerRPCCommands()
{
// clang-format off
static const CRPCCommand commands[] =
{ // category actor (function)
// --------------------- ------------------------
{ "signer", &enumeratesigners, },
{ "signer", &signerdisplayaddress, },
};
// clang-format on
return MakeSpan(commands);
}
#endif // ENABLE_EXTERNAL_SIGNER

25
src/wallet/rpcsigner.h Normal file
View file

@ -0,0 +1,25 @@
// Copyright (c) 2018-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_WALLET_RPCSIGNER_H
#define BITCOIN_WALLET_RPCSIGNER_H
#include <span.h>
#include <util/system.h>
#include <vector>
#ifdef ENABLE_EXTERNAL_SIGNER
class CRPCCommand;
namespace interfaces {
class Chain;
class Handler;
}
Span<const CRPCCommand> GetSignerRPCCommands();
#endif // ENABLE_EXTERNAL_SIGNER
#endif //BITCOIN_WALLET_RPCSIGNER_H

View file

@ -2726,6 +2726,7 @@ static RPCHelpMan createwallet()
{"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
{"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "null", "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
{"external_signer", RPCArg::Type::BOOL, /* default */ "false", "Use an external signer such as a hardware wallet. Requires -signer to be configured. Wallet creation will fail if keys cannot be fetched. Requires disable_private_keys and descriptors set to true."},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@ -2770,6 +2771,13 @@ static RPCHelpMan createwallet()
flags |= WALLET_FLAG_DESCRIPTORS;
warnings.emplace_back(Untranslated("Wallet is an experimental descriptor wallet"));
}
if (!request.params[7].isNull() && request.params[7].get_bool()) {
#ifdef ENABLE_EXTERNAL_SIGNER
flags |= WALLET_FLAG_EXTERNAL_SIGNER;
#else
throw JSONRPCError(RPC_WALLET_ERROR, "Configure with --enable-external-signer to use this");
#endif
}
#ifndef USE_BDB
if (!(flags & WALLET_FLAG_DESCRIPTORS)) {
@ -4187,8 +4195,10 @@ static RPCHelpMan send()
// Make a blank psbt
PartiallySignedTransaction psbtx(rawTx);
// Fill transaction with our data and sign
bool complete = true;
// First fill transaction with our data without signing,
// so external signers are not asked sign more than once.
bool complete;
pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, false, true);
const TransactionError err = pwallet->FillPSBT(psbtx, complete, SIGHASH_ALL, true, false);
if (err != TransactionError::OK) {
throw JSONRPCTransactionError(err);

View file

@ -13,6 +13,7 @@
#include <util/system.h>
#include <util/time.h>
#include <util/translation.h>
#include <wallet/external_signer.h>
#include <wallet/scriptpubkeyman.h>
//! Value for the first BIP 32 hardened derivation. Can be used as a bit mask and as a value. See BIP 32 for more details.

View file

@ -517,8 +517,6 @@ public:
class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
{
private:
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
@ -547,6 +545,9 @@ private:
// Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
protected:
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
public:
DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor)
: ScriptPubKeyMan(storage),
@ -581,6 +582,11 @@ public:
//! Setup descriptors based on the given CExtkey
bool SetupDescriptorGeneration(const CExtKey& master_key, OutputType addr_type);
/** Provide a descriptor at setup time
* Returns false if already setup or setup fails, true if setup is successful
*/
bool SetupDescriptor(std::unique_ptr<Descriptor>desc);
bool HavePrivateKeys() const override;
int64_t GetOldestKeyPoolTime() const override;

View file

@ -14,10 +14,12 @@
#include <key.h>
#include <key_io.h>
#include <optional.h>
#include <outputtype.h>
#include <policy/fees.h>
#include <policy/policy.h>
#include <primitives/block.h>
#include <primitives/transaction.h>
#include <psbt.h>
#include <script/descriptor.h>
#include <script/script.h>
#include <script/signingprovider.h>
@ -32,6 +34,7 @@
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/fees.h>
#include <wallet/external_signer_scriptpubkeyman.h>
#include <univalue.h>
@ -259,6 +262,20 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, const std::strin
wallet_creation_flags |= WALLET_FLAG_BLANK_WALLET;
}
// Private keys must be disabled for an external signer wallet
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
error = Untranslated("Private keys must be disabled when using an external signer");
status = DatabaseStatus::FAILED_CREATE;
return nullptr;
}
// Descriptor support must be enabled for an external signer wallet
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) && !(wallet_creation_flags & WALLET_FLAG_DESCRIPTORS)) {
error = Untranslated("Descriptor support must be enabled when using an external signer");
status = DatabaseStatus::FAILED_CREATE;
return nullptr;
}
// Wallet::Verify will check if we're trying to create a wallet with a duplicate name.
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(name, options, status, error);
if (!database) {
@ -3558,6 +3575,38 @@ void ReserveDestination::ReturnDestination()
address = CNoDestination();
}
#ifdef ENABLE_EXTERNAL_SIGNER
ExternalSigner CWallet::GetExternalSigner()
{
const std::string command = gArgs.GetArg("-signer", "");
if (command == "") throw std::runtime_error(std::string(__func__) + ": restart bitcoind with -signer=<cmd>");
std::vector<ExternalSigner> signers;
ExternalSigner::Enumerate(command, signers, Params().NetworkIDString());
if (signers.empty()) throw std::runtime_error(std::string(__func__) + ": No external signers found");
// TODO: add fingerprint argument in case of multiple signers
return signers[0];
}
#endif
bool CWallet::DisplayAddress(const CTxDestination& dest)
{
#ifdef ENABLE_EXTERNAL_SIGNER
CScript scriptPubKey = GetScriptForDestination(dest);
const auto spk_man = GetScriptPubKeyMan(scriptPubKey);
if (spk_man == nullptr) {
return false;
}
auto signer_spk_man = dynamic_cast<ExternalSignerScriptPubKeyMan*>(spk_man);
if (signer_spk_man == nullptr) {
return false;
}
ExternalSigner signer = GetExternalSigner(); // TODO: move signer in spk_man
return signer_spk_man->DisplayAddress(scriptPubKey, signer);
#else
return false;
#endif
}
void CWallet::LockCoin(const COutPoint& output)
{
AssertLockHeld(cs_wallet);
@ -3836,7 +3885,7 @@ std::shared_ptr<CWallet> CWallet::Create(interfaces::Chain& chain, const std::st
walletInstance->SetupLegacyScriptPubKeyMan();
}
if (!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) || !(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
LOCK(walletInstance->cs_wallet);
if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
walletInstance->SetupDescriptorScriptPubKeyMans();
@ -4443,40 +4492,82 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc)
{
auto spk_manager = std::unique_ptr<ScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, desc));
m_spk_managers[id] = std::move(spk_manager);
if (IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
#ifdef ENABLE_EXTERNAL_SIGNER
auto spk_manager = std::unique_ptr<ScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(*this, desc));
m_spk_managers[id] = std::move(spk_manager);
#else
throw std::runtime_error(std::string(__func__) + ": Configure with --enable-external-signer to use external signer wallets");
#endif
} else {
auto spk_manager = std::unique_ptr<ScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, desc));
m_spk_managers[id] = std::move(spk_manager);
}
}
void CWallet::SetupDescriptorScriptPubKeyMans()
{
AssertLockHeld(cs_wallet);
// Make a seed
CKey seed_key;
seed_key.MakeNewKey(true);
CPubKey seed = seed_key.GetPubKey();
assert(seed_key.VerifyPubKey(seed));
if (!IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
// Make a seed
CKey seed_key;
seed_key.MakeNewKey(true);
CPubKey seed = seed_key.GetPubKey();
assert(seed_key.VerifyPubKey(seed));
// Get the extended key
CExtKey master_key;
master_key.SetSeed(seed_key.begin(), seed_key.size());
// Get the extended key
CExtKey master_key;
master_key.SetSeed(seed_key.begin(), seed_key.size());
for (bool internal : {false, true}) {
for (OutputType t : OUTPUT_TYPES) {
auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, internal));
if (IsCrypted()) {
if (IsLocked()) {
throw std::runtime_error(std::string(__func__) + ": Wallet is locked, cannot setup new descriptors");
}
if (!spk_manager->CheckDecryptionKey(vMasterKey) && !spk_manager->Encrypt(vMasterKey, nullptr)) {
throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors");
for (bool internal : {false, true}) {
for (OutputType t : OUTPUT_TYPES) {
auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, internal));
if (IsCrypted()) {
if (IsLocked()) {
throw std::runtime_error(std::string(__func__) + ": Wallet is locked, cannot setup new descriptors");
}
if (!spk_manager->CheckDecryptionKey(vMasterKey) && !spk_manager->Encrypt(vMasterKey, nullptr)) {
throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors");
}
}
spk_manager->SetupDescriptorGeneration(master_key, t);
uint256 id = spk_manager->GetID();
m_spk_managers[id] = std::move(spk_manager);
AddActiveScriptPubKeyMan(id, t, internal);
}
spk_manager->SetupDescriptorGeneration(master_key, t);
uint256 id = spk_manager->GetID();
m_spk_managers[id] = std::move(spk_manager);
AddActiveScriptPubKeyMan(id, t, internal);
}
} else {
#ifdef ENABLE_EXTERNAL_SIGNER
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
// TODO: add account parameter
int account = 0;
UniValue signer_res = signer.GetDescriptors(account);
if (!signer_res.isObject()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
for (bool internal : {false, true}) {
const UniValue& descriptor_vals = find_value(signer_res, internal ? "internal" : "receive");
if (!descriptor_vals.isArray()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
for (const UniValue& desc_val : descriptor_vals.get_array().getValues()) {
std::string desc_str = desc_val.getValStr();
FlatSigningProvider keys;
std::string dummy_error;
std::unique_ptr<Descriptor> desc = Parse(desc_str, keys, dummy_error, false);
if (!desc->GetOutputType()) {
continue;
}
OutputType t = *desc->GetOutputType();
auto spk_manager = std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(*this, internal));
spk_manager->SetupDescriptor(std::move(desc));
uint256 id = spk_manager->GetID();
m_spk_managers[id] = std::move(spk_manager);
AddActiveScriptPubKeyMan(id, t, internal);
}
}
#else
throw std::runtime_error(std::string(__func__) + ": Wallets with external signers require Boost::Process library.");
#endif
}
}

View file

@ -22,6 +22,7 @@
#include <wallet/coinselection.h>
#include <wallet/crypter.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/external_signer.h>
#include <wallet/walletdb.h>
#include <wallet/walletutil.h>
@ -95,7 +96,6 @@ constexpr CAmount DEFAULT_TRANSACTION_MAXFEE{COIN / 10};
constexpr CAmount HIGH_TX_FEE_PER_KB{COIN / 100};
//! -maxtxfee will warn if called with a higher fee than this amount (in satoshis)
constexpr CAmount HIGH_MAX_TX_FEE{100 * HIGH_TX_FEE_PER_KB};
//! Pre-calculated constants for input size estimation in *virtual size*
static constexpr size_t DUMMY_NESTED_P2WPKH_INPUT_SIZE = 91;
@ -115,7 +115,8 @@ static constexpr uint64_t KNOWN_WALLET_FLAGS =
| WALLET_FLAG_BLANK_WALLET
| WALLET_FLAG_KEY_ORIGIN_METADATA
| WALLET_FLAG_DISABLE_PRIVATE_KEYS
| WALLET_FLAG_DESCRIPTORS;
| WALLET_FLAG_DESCRIPTORS
| WALLET_FLAG_EXTERNAL_SIGNER;
static constexpr uint64_t MUTABLE_WALLET_FLAGS =
WALLET_FLAG_AVOID_REUSE;
@ -126,6 +127,7 @@ static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
{"descriptor_wallet", WALLET_FLAG_DESCRIPTORS},
{"external_signer", WALLET_FLAG_EXTERNAL_SIGNER}
};
extern const std::map<uint64_t,std::string> WALLET_FLAG_CAVEATS;
@ -837,6 +839,12 @@ public:
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const;
#ifdef ENABLE_EXTERNAL_SIGNER
ExternalSigner GetExternalSigner() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
#endif
/** Display address on an external signer. Returns false if external signer support is not compiled */
bool DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void LockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

View file

@ -60,6 +60,9 @@ enum WalletFlags : uint64_t {
//! Indicate that this wallet supports DescriptorScriptPubKeyMan
WALLET_FLAG_DESCRIPTORS = (1ULL << 34),
//! Indicates that the wallet needs an external signer
WALLET_FLAG_EXTERNAL_SIGNER = (1ULL << 35),
};
//! Get the path of the wallet directory.

View file

@ -23,3 +23,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
@BUILD_BITCOIND_TRUE@ENABLE_BITCOIND=true
@ENABLE_FUZZ_TRUE@ENABLE_FUZZ=true
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true

102
test/functional/mocks/signer.py Executable file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
# Copyright (c) 2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import argparse
import json
def perform_pre_checks():
mock_result_path = os.path.join(os.getcwd(), "mock_result")
if(os.path.isfile(mock_result_path)):
with open(mock_result_path, "r", encoding="utf8") as f:
mock_result = f.read()
if mock_result[0]:
sys.stdout.write(mock_result[2:])
sys.exit(int(mock_result[0]))
def enumerate(args):
sys.stdout.write(json.dumps([{"fingerprint": "00000001", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}]))
def getdescriptors(args):
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
sys.stdout.write(json.dumps({
"receive": [
"pkh([00000001/44'/1'/" + args.account + "']" + xpub + "/0/*)#vt6w3l3j",
"sh(wpkh([00000001/49'/1'/" + args.account + "']" + xpub + "/0/*))#r0grqw5x",
"wpkh([00000001/84'/1'/" + args.account + "']" + xpub + "/0/*)#x30uthjs"
],
"internal": [
"pkh([00000001/44'/1'/" + args.account + "']" + xpub + "/1/*)#all0v2p2",
"sh(wpkh([00000001/49'/1'/" + args.account + "']" + xpub + "/1/*))#kwx4c3pe",
"wpkh([00000001/84'/1'/" + args.account + "']" + xpub + "/1/*)#h92akzzg"
]
}))
def displayaddress(args):
# Several descriptor formats are acceptable, so allowing for potential
# changes to InferDescriptor:
if args.fingerprint != "00000001":
return sys.stdout.write(json.dumps({"error": "Unexpected fingerprint", "fingerprint": args.fingerprint}))
expected_desc = [
"wpkh([00000001/84'/1'/0'/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)#0yneg42r"
]
if args.desc not in expected_desc:
return sys.stdout.write(json.dumps({"error": "Unexpected descriptor", "desc": args.desc}))
return sys.stdout.write(json.dumps({"address": "bcrt1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th68x4f8g"}))
def signtx(args):
if args.fingerprint != "00000001":
return sys.stdout.write(json.dumps({"error": "Unexpected fingerprint", "fingerprint": args.fingerprint}))
with open(os.path.join(os.getcwd(), "mock_psbt"), "r", encoding="utf8") as f:
mock_psbt = f.read()
if args.fingerprint == "00000001" :
sys.stdout.write(json.dumps({
"psbt": mock_psbt,
"complete": True
}))
else:
sys.stdout.write(json.dumps({"psbt": args.psbt}))
parser = argparse.ArgumentParser(prog='./signer.py', description='External signer mock')
parser.add_argument('--fingerprint')
parser.add_argument('--chain', default='main')
parser.add_argument('--stdin', action='store_true')
subparsers = parser.add_subparsers(description='Commands', dest='command')
subparsers.required = True
parser_enumerate = subparsers.add_parser('enumerate', help='list available signers')
parser_enumerate.set_defaults(func=enumerate)
parser_getdescriptors = subparsers.add_parser('getdescriptors')
parser_getdescriptors.set_defaults(func=getdescriptors)
parser_getdescriptors.add_argument('--account', metavar='account')
parser_displayaddress = subparsers.add_parser('displayaddress', help='display address on signer')
parser_displayaddress.add_argument('--desc', metavar='desc')
parser_displayaddress.set_defaults(func=displayaddress)
parser_signtx = subparsers.add_parser('signtx')
parser_signtx.add_argument('psbt', metavar='psbt')
parser_signtx.set_defaults(func=signtx)
if not sys.stdin.isatty():
buffer = sys.stdin.read()
if buffer and buffer.rstrip() != "":
sys.argv.extend(buffer.rstrip().split(" "))
args = parser.parse_args()
perform_pre_checks()
args.func(args)

View file

@ -105,10 +105,13 @@ class HelpRpcTest(BitcoinTestFramework):
if self.is_wallet_compiled():
components.append('Wallet')
if self.is_external_signer_compiled():
components.append('Signer')
if self.is_zmq_compiled():
components.append('Zmq')
assert_equal(titles, components)
assert_equal(titles, sorted(components))
def dump_help(self):
dump_dir = os.path.join(self.options.tmpdir, 'rpc_help_dump')

View file

@ -827,10 +827,19 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
self.options.previous_releases_path))
return self.options.prev_releases
def skip_if_no_external_signer(self):
"""Skip the running test if external signer support has not been compiled."""
if not self.is_external_signer_compiled():
raise SkipTest("external signer support has not been compiled.")
def is_cli_compiled(self):
"""Checks whether bitcoin-cli was compiled."""
return self.config["components"].getboolean("ENABLE_CLI")
def is_external_signer_compiled(self):
"""Checks whether external signer support was compiled."""
return self.config["components"].getboolean("ENABLE_EXTERNAL_SIGNER")
def is_wallet_compiled(self):
"""Checks whether the wallet module was compiled."""
return self.config["components"].getboolean("ENABLE_WALLET")

View file

@ -678,10 +678,10 @@ class RPCOverloadWrapper():
def __getattr__(self, name):
return getattr(self.rpc, name)
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None):
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None):
if descriptors is None:
descriptors = self.descriptors
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup)
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer)
def importprivkey(self, privkey, label=None, rescan=None):
wallet_info = self.getwalletinfo()

View file

@ -111,6 +111,7 @@ BASE_SCRIPTS = [
'wallet_listtransactions.py --legacy-wallet',
'wallet_listtransactions.py --descriptors',
'feature_taproot.py',
'wallet_signer.py --descriptors',
# vv Tests less than 60s vv
'p2p_sendheaders.py',
'wallet_importmulti.py --legacy-wallet',

217
test/functional/wallet_signer.py Executable file
View file

@ -0,0 +1,217 @@
#!/usr/bin/env python3
# Copyright (c) 2017-2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test external signer.
Verify that a bitcoind node can use an external signer command
"""
import os
import platform
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
class SignerTest(BitcoinTestFramework):
def mock_signer_path(self):
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py')
if platform.system() == "Windows":
return "py " + path
else:
return path
def set_test_params(self):
self.num_nodes = 4
self.extra_args = [
[],
[f"-signer={self.mock_signer_path()}", '-keypool=10'],
[f"-signer={self.mock_signer_path()}", '-keypool=10'],
["-signer=fake.py"],
]
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
self.skip_if_no_external_signer()
def set_mock_result(self, node, res):
with open(os.path.join(node.cwd, "mock_result"), "w", encoding="utf8") as f:
f.write(res)
def clear_mock_result(self, node):
os.remove(os.path.join(node.cwd, "mock_result"))
def run_test(self):
self.log.debug(f"-signer={self.mock_signer_path()}")
assert_raises_rpc_error(-4, 'Error: restart bitcoind with -signer=<cmd>',
self.nodes[0].enumeratesigners
)
# Handle script missing:
assert_raises_rpc_error(-1, 'execve failed: No such file or directory',
self.nodes[3].enumeratesigners
)
# Handle error thrown by script
self.set_mock_result(self.nodes[1], "2")
assert_raises_rpc_error(-1, 'RunCommandParseJSON error',
self.nodes[1].enumeratesigners
)
self.clear_mock_result(self.nodes[1])
self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "error": "fingerprint not found"}]')
assert_raises_rpc_error(-4, 'fingerprint not found',
self.nodes[1].enumeratesigners
)
self.clear_mock_result(self.nodes[1])
# Create new wallets for an external signer.
# disable_private_keys and descriptors must be true:
assert_raises_rpc_error(-4, "Private keys must be disabled when using an external signer", self.nodes[1].createwallet, wallet_name='not_hww', disable_private_keys=False, descriptors=True, external_signer=True)
if self.is_bdb_compiled():
assert_raises_rpc_error(-4, "Descriptor support must be enabled when using an external signer", self.nodes[1].createwallet, wallet_name='not_hww', disable_private_keys=True, descriptors=False, external_signer=True)
else:
assert_raises_rpc_error(-4, "Compiled without bdb support (required for legacy wallets)", self.nodes[1].createwallet, wallet_name='not_hww', disable_private_keys=True, descriptors=False, external_signer=True)
self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, descriptors=True, external_signer=True)
hww = self.nodes[1].get_wallet_rpc('hww')
result = hww.enumeratesigners()
assert_equal(len(result['signers']), 2)
assert_equal(result['signers'][0]["fingerprint"], "00000001")
assert_equal(result['signers'][0]["name"], "trezor_t")
# Flag can't be set afterwards (could be added later for non-blank descriptor based watch-only wallets)
self.nodes[1].createwallet(wallet_name='not_hww', disable_private_keys=True, descriptors=True, external_signer=False)
not_hww = self.nodes[1].get_wallet_rpc('not_hww')
assert_raises_rpc_error(-8, "Wallet flag is immutable: external_signer", not_hww.setwalletflag, "external_signer", True)
# assert_raises_rpc_error(-4, "Multiple signers found, please specify which to use", wallet_name='not_hww', disable_private_keys=True, descriptors=True, external_signer=True)
# TODO: Handle error thrown by script
# self.set_mock_result(self.nodes[1], "2")
# assert_raises_rpc_error(-1, 'Unable to parse JSON',
# self.nodes[1].createwallet, wallet_name='not_hww2', disable_private_keys=True, descriptors=True, external_signer=False
# )
# self.clear_mock_result(self.nodes[1])
assert_equal(hww.getwalletinfo()["keypoolsize"], 30)
address1 = hww.getnewaddress(address_type="bech32")
assert_equal(address1, "bcrt1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th68x4f8g")
address_info = hww.getaddressinfo(address1)
assert_equal(address_info['solvable'], True)
assert_equal(address_info['ismine'], True)
assert_equal(address_info['hdkeypath'], "m/84'/1'/0'/0/0")
address2 = hww.getnewaddress(address_type="p2sh-segwit")
assert_equal(address2, "2N2gQKzjUe47gM8p1JZxaAkTcoHPXV6YyVp")
address_info = hww.getaddressinfo(address2)
assert_equal(address_info['solvable'], True)
assert_equal(address_info['ismine'], True)
assert_equal(address_info['hdkeypath'], "m/49'/1'/0'/0/0")
address3 = hww.getnewaddress(address_type="legacy")
assert_equal(address3, "n1LKejAadN6hg2FrBXoU1KrwX4uK16mco9")
address_info = hww.getaddressinfo(address3)
assert_equal(address_info['solvable'], True)
assert_equal(address_info['ismine'], True)
assert_equal(address_info['hdkeypath'], "m/44'/1'/0'/0/0")
self.log.info('Test signerdisplayaddress')
result = hww.signerdisplayaddress(address1)
assert_equal(result, {"address": address1})
# Handle error thrown by script
self.set_mock_result(self.nodes[1], "2")
assert_raises_rpc_error(-1, 'RunCommandParseJSON error',
hww.signerdisplayaddress, address1
)
self.clear_mock_result(self.nodes[1])
self.log.info('Prepare mock PSBT')
self.nodes[0].sendtoaddress(address1, 1)
self.nodes[0].generate(1)
self.sync_all()
# Load private key into wallet to generate a signed PSBT for the mock
self.nodes[1].createwallet(wallet_name="mock", disable_private_keys=False, blank=True, descriptors=True)
mock_wallet = self.nodes[1].get_wallet_rpc("mock")
assert mock_wallet.getwalletinfo()['private_keys_enabled']
result = mock_wallet.importdescriptors([{
"desc": "wpkh([00000001/84'/1'/0']tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0/*)#rweraev0",
"timestamp": 0,
"range": [0,1],
"internal": False,
"active": True
},
{
"desc": "wpkh([00000001/84'/1'/0']tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/*)#j6uzqvuh",
"timestamp": 0,
"range": [0, 0],
"internal": True,
"active": True
}])
assert_equal(result[0], {'success': True})
assert_equal(result[1], {'success': True})
assert_equal(mock_wallet.getwalletinfo()["txcount"], 1)
dest = self.nodes[0].getnewaddress(address_type='bech32')
mock_psbt = mock_wallet.walletcreatefundedpsbt([], {dest:0.5}, 0, {}, True)['psbt']
mock_psbt_signed = mock_wallet.walletprocesspsbt(psbt=mock_psbt, sign=True, sighashtype="ALL", bip32derivs=True)
mock_psbt_final = mock_wallet.finalizepsbt(mock_psbt_signed["psbt"])
mock_tx = mock_psbt_final["hex"]
assert(mock_wallet.testmempoolaccept([mock_tx])[0]["allowed"])
# # Create a new wallet and populate with specific public keys, in order
# # to work with the mock signed PSBT.
# self.nodes[1].createwallet(wallet_name="hww4", disable_private_keys=True, descriptors=True, external_signer=True)
# hww4 = self.nodes[1].get_wallet_rpc("hww4")
#
# descriptors = [{
# "desc": "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)#x30uthjs",
# "timestamp": "now",
# "range": [0, 1],
# "internal": False,
# "watchonly": True,
# "active": True
# },
# {
# "desc": "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)#h92akzzg",
# "timestamp": "now",
# "range": [0, 0],
# "internal": True,
# "watchonly": True,
# "active": True
# }]
# result = hww4.importdescriptors(descriptors)
# assert_equal(result[0], {'success': True})
# assert_equal(result[1], {'success': True})
assert_equal(hww.getwalletinfo()["txcount"], 1)
assert(hww.testmempoolaccept([mock_tx])[0]["allowed"])
with open(os.path.join(self.nodes[1].cwd, "mock_psbt"), "w", encoding="utf8") as f:
f.write(mock_psbt_signed["psbt"])
self.log.info('Test send using hww1')
res = hww.send(outputs={dest:0.5},options={"add_to_wallet": False})
assert(res["complete"])
assert_equal(res["hex"], mock_tx)
# # Handle error thrown by script
# self.set_mock_result(self.nodes[4], "2")
# assert_raises_rpc_error(-1, 'Unable to parse JSON',
# hww4.signerprocesspsbt, psbt_orig, "00000001"
# )
# self.clear_mock_result(self.nodes[4])
if __name__ == '__main__':
SignerTest().main()