diff --git a/ci/test/00_setup_env_arm.sh b/ci/test/00_setup_env_arm.sh index af66a99b5111..edbc9334928f 100755 --- a/ci/test/00_setup_env_arm.sh +++ b/ci/test/00_setup_env_arm.sh @@ -25,4 +25,4 @@ export RUN_FUNCTIONAL_TESTS=false export GOAL="install" # -Wno-psabi is to disable ABI warnings: "note: parameter passing for argument of type ... changed in GCC 7.1" # This could be removed once the ABI change warning does not show up by default -export BITCOIN_CONFIG="--enable-reduce-exports CXXFLAGS=-Wno-psabi --with-boost-process" +export BITCOIN_CONFIG="--enable-reduce-exports CXXFLAGS=-Wno-psabi" diff --git a/ci/test/00_setup_env_mac.sh b/ci/test/00_setup_env_mac.sh index aba312bf78af..ca04e916fc2a 100755 --- a/ci/test/00_setup_env_mac.sh +++ b/ci/test/00_setup_env_mac.sh @@ -14,4 +14,4 @@ export XCODE_BUILD_ID=15A240d export RUN_UNIT_TESTS=false export RUN_FUNCTIONAL_TESTS=false export GOAL="all deploy" -export BITCOIN_CONFIG="--with-gui --enable-reduce-exports --disable-miner --with-boost-process" +export BITCOIN_CONFIG="--with-gui --enable-reduce-exports --disable-miner" diff --git a/ci/test/00_setup_env_mac_native_x86_64.sh b/ci/test/00_setup_env_mac_native_x86_64.sh index 252b95c2178f..b20efb2bde37 100755 --- a/ci/test/00_setup_env_mac_native_x86_64.sh +++ b/ci/test/00_setup_env_mac_native_x86_64.sh @@ -10,7 +10,7 @@ export CONTAINER_NAME=ci_macos export HOST=x86_64-apple-darwin export PIP_PACKAGES="zmq lief" export GOAL="install" -export BITCOIN_CONFIG="--with-gui --enable-reduce-exports --disable-miner --with-boost-process" +export BITCOIN_CONFIG="--with-gui --enable-reduce-exports --disable-miner" export CI_OS_NAME="macos" export NO_DEPENDS=1 export OSX_SDK="" diff --git a/ci/test/00_setup_env_native_asan.sh b/ci/test/00_setup_env_native_asan.sh index 5f1507e5b7e2..ac78c511139e 100755 --- a/ci/test/00_setup_env_native_asan.sh +++ b/ci/test/00_setup_env_native_asan.sh @@ -12,4 +12,4 @@ export TEST_RUNNER_EXTRA="--timeout-factor=4" # Increase timeout because saniti export FUNCTIONAL_TESTS_CONFIG="--exclude wallet_multiwallet.py" # Temporarily suppress ASan heap-use-after-free (see issue #14163) export RUN_BENCH=true export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=address,integer,undefined CC=clang CXX=clang++ --with-boost-process" +export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=address,integer,undefined CC=clang CXX=clang++" diff --git a/ci/test/00_setup_env_native_fuzz.sh b/ci/test/00_setup_env_native_fuzz.sh index 586496215358..71356f213582 100755 --- a/ci/test/00_setup_env_native_fuzz.sh +++ b/ci/test/00_setup_env_native_fuzz.sh @@ -15,4 +15,4 @@ export RUN_UNIT_TESTS=false export RUN_FUNCTIONAL_TESTS=false export RUN_FUZZ_TESTS=true export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --disable-ccache --enable-fuzz --with-sanitizers=fuzzer,address,undefined,integer CC='clang-19 -ftrivial-auto-var-init=pattern' CXX='clang++-19 -ftrivial-auto-var-init=pattern' --with-boost-process" +export BITCOIN_CONFIG="--enable-zmq --disable-ccache --enable-fuzz --with-sanitizers=fuzzer,address,undefined,integer CC='clang-19 -ftrivial-auto-var-init=pattern' CXX='clang++-19 -ftrivial-auto-var-init=pattern'" diff --git a/ci/test/00_setup_env_native_multiprocess.sh b/ci/test/00_setup_env_native_multiprocess.sh index f0e6e532b162..4583a86737a0 100755 --- a/ci/test/00_setup_env_native_multiprocess.sh +++ b/ci/test/00_setup_env_native_multiprocess.sh @@ -13,7 +13,7 @@ export DEP_OPTS="MULTIPROCESS=1 CC=clang-19 CXX=clang++-19" export RUN_TIDY=true export GOAL="install" export TEST_RUNNER_EXTRA="--v2transport" -export BITCOIN_CONFIG="--with-boost-process --enable-debug CC=clang-19 CXX=clang++-19" # Use clang to avoid OOM +export BITCOIN_CONFIG="--enable-debug CC=clang-19 CXX=clang++-19" # Use clang to avoid OOM # Additional flags for RUN_TIDY export BITCOIN_CONFIG="${BITCOIN_CONFIG} --disable-hardening CFLAGS='-O0 -g0' CXXFLAGS='-O0 -g0 -Wno-error=documentation'" export BITCOIND=dash-node # Used in functional tests diff --git a/ci/test/00_setup_env_native_nowallet.sh b/ci/test/00_setup_env_native_nowallet.sh index 3637d3b8d0da..3e083c3d4d62 100755 --- a/ci/test/00_setup_env_native_nowallet.sh +++ b/ci/test/00_setup_env_native_nowallet.sh @@ -11,4 +11,4 @@ export HOST=x86_64-pc-linux-gnu export PACKAGES="python3-zmq" export DEP_OPTS="NO_WALLET=1 CC=gcc-14 CXX=g++-14" export GOAL="install" -export BITCOIN_CONFIG="--enable-reduce-exports --with-boost-process CC=gcc-14 CXX=g++-14" +export BITCOIN_CONFIG="--enable-reduce-exports CC=gcc-14 CXX=g++-14" diff --git a/ci/test/00_setup_env_native_qt5.sh b/ci/test/00_setup_env_native_qt5.sh index a2ff43d07e02..1b41c7d4644e 100755 --- a/ci/test/00_setup_env_native_qt5.sh +++ b/ci/test/00_setup_env_native_qt5.sh @@ -15,4 +15,4 @@ export RUN_UNIT_TESTS_SEQUENTIAL="true" export RUN_UNIT_TESTS="false" export GOAL="install" export DOWNLOAD_PREVIOUS_RELEASES="true" -export BITCOIN_CONFIG="--enable-zmq --with-libs=no --enable-reduce-exports LDFLAGS=-static-libstdc++ --with-boost-process" +export BITCOIN_CONFIG="--enable-zmq --with-libs=no --enable-reduce-exports LDFLAGS=-static-libstdc++" diff --git a/ci/test/00_setup_env_native_tsan.sh b/ci/test/00_setup_env_native_tsan.sh index 2feddf269806..798ded8afd16 100755 --- a/ci/test/00_setup_env_native_tsan.sh +++ b/ci/test/00_setup_env_native_tsan.sh @@ -12,6 +12,6 @@ export DEP_OPTS="CC=clang-19 CXX='clang++-19 -stdlib=libc++'" export TEST_RUNNER_EXTRA="--extended --exclude feature_pruning,feature_dbcrash,wallet_multiwallet.py" # Temporarily suppress ASan heap-use-after-free (see issue #14163) export TEST_RUNNER_EXTRA="${TEST_RUNNER_EXTRA} --timeout-factor=4" # Increase timeout because sanitizers slow down export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --with-sanitizers=thread CC=clang-19 CXX=clang++-19 CXXFLAGS='-g' --with-boost-process" +export BITCOIN_CONFIG="--enable-zmq --with-sanitizers=thread CC=clang-19 CXX=clang++-19 CXXFLAGS='-g'" export CPPFLAGS="-DARENA_DEBUG -DDEBUG_LOCKORDER -DDEBUG_LOCKCONTENTION" export PYZMQ=true diff --git a/ci/test/00_setup_env_s390x.sh b/ci/test/00_setup_env_s390x.sh index 5d95535daf92..2bb4d5e57562 100755 --- a/ci/test/00_setup_env_s390x.sh +++ b/ci/test/00_setup_env_s390x.sh @@ -22,4 +22,4 @@ export RUN_UNIT_TESTS=true export TEST_RUNNER_EXTRA="--exclude rpc_bind,feature_bind_extra" # Excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547 export RUN_FUNCTIONAL_TESTS=true export GOAL="install" -export BITCOIN_CONFIG="--enable-reduce-exports --with-boost-process" +export BITCOIN_CONFIG="--enable-reduce-exports" diff --git a/ci/test/00_setup_env_win64.sh b/ci/test/00_setup_env_win64.sh index fa39b532213e..2010c111818d 100755 --- a/ci/test/00_setup_env_win64.sh +++ b/ci/test/00_setup_env_win64.sh @@ -16,5 +16,5 @@ export GOAL="deploy" # Prior to 11.0.0, the mingw-w64 headers were missing noreturn attributes, causing warnings when # cross-compiling for Windows. https://sourceforge.net/p/mingw-w64/bugs/306/ # https://github.com/mingw-w64/mingw-w64/commit/1690994f515910a31b9fb7c7bd3a52d4ba987abe -export BITCOIN_CONFIG="--enable-gui --enable-reduce-exports --disable-miner --without-boost-process CXXFLAGS='-Wno-return-type -Wno-error=maybe-uninitialized -Wno-error=array-bounds'" +export BITCOIN_CONFIG="--enable-gui --enable-reduce-exports --disable-miner CXXFLAGS='-Wno-return-type -Wno-error=maybe-uninitialized -Wno-error=array-bounds'" export DIRECT_WINE_EXEC_TESTS=true diff --git a/configure.ac b/configure.ac index 6eccd119c840..c2723841e083 100644 --- a/configure.ac +++ b/configure.ac @@ -319,10 +319,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 auto, requires Boost::Process)])], + [use_external_signer=$enableval], + [use_external_signer=auto]) AC_LANG_PUSH([C++]) @@ -1437,6 +1437,7 @@ if test "$enable_fuzz" = "yes"; then bitcoin_enable_qt_dbus=no use_bench=no use_tests=no + use_external_signer=no use_upnp=no use_natpmp=no use_zmq=no @@ -1578,18 +1579,18 @@ if test "$use_natpmp" != "no"; then fi if test "$build_bitcoin_wallet$build_bitcoin_cli$build_bitcoin_tx$build_bitcoind$bitcoin_enable_qt$use_tests$use_bench$enable_fuzz_binary" = "nononononononono"; then - use_boost=no + use_boost=no else - use_boost=yes + use_boost=yes fi if test "$use_boost" = "yes"; then -dnl Check for Boost headers -AX_BOOST_BASE([1.73.0],[],[AC_MSG_ERROR([Boost is not available!])]) -if test "$want_boost" = "no"; then - AC_MSG_ERROR([[only libdashconsensus can be built without boost]]) -fi + dnl Check for Boost headers + AX_BOOST_BASE([1.73.0],[],[AC_MSG_ERROR([Boost is not available!])]) + if test "$want_boost" = "no"; then + AC_MSG_ERROR([[only libdashconsensus can be built without boost]]) + fi dnl we don't use multi_index serialization BOOST_CPPFLAGS="$BOOST_CPPFLAGS -DBOOST_MULTI_INDEX_DISABLE_SERIALIZATION" @@ -1598,27 +1599,36 @@ fi BOOST_CPPFLAGS="$BOOST_CPPFLAGS -DBOOST_MULTI_INDEX_ENABLE_SAFE_MODE" fi -dnl Prevent use of std::unary_function, which was removed in C++17, -dnl and will generate warnings with newer compilers for Boost -dnl older than 1.80. -dnl See: https://github.com/boostorg/config/pull/430. -AX_CHECK_PREPROC_FLAG([-DBOOST_NO_CXX98_FUNCTION_BASE], [BOOST_CPPFLAGS="$BOOST_CPPFLAGS -DBOOST_NO_CXX98_FUNCTION_BASE"], [], [$CXXFLAG_WERROR], - [AC_LANG_PROGRAM([[#include ]])]) - -dnl Opt-in to Boost Process -if test "$boost_process" != "no"; then -AC_MSG_CHECKING(for Boost Process) -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include ]], - [[ 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!])] -) -fi + dnl Prevent use of std::unary_function, which was removed in C++17, + dnl and will generate warnings with newer compilers for Boost + dnl older than 1.80. + dnl See: https://github.com/boostorg/config/pull/430. + AX_CHECK_PREPROC_FLAG([-DBOOST_NO_CXX98_FUNCTION_BASE], [BOOST_CPPFLAGS="$BOOST_CPPFLAGS -DBOOST_NO_CXX98_FUNCTION_BASE"], [], [$CXXFLAG_WERROR], + [AC_LANG_PROGRAM([[#include ]])]) if test "$suppress_external_warnings" != "no"; then BOOST_CPPFLAGS=SUPPRESS_WARNINGS($BOOST_CPPFLAGS) + fi fi + +if test "$use_external_signer" != "no"; then + case $host in + *mingw*) + dnl Boost Process uses Boost Filesystem when targeting Windows. Also, + dnl since Boost 1.71.0, Process does not work with mingw-w64 without + dnl workarounds. See 67669ab425b52a2b6be3d2f3b3b7e3939b676a2c. + if test "$use_external_signer" = "yes"; then + AC_MSG_ERROR([External signing is not supported on Windows]) + fi + use_external_signer="no"; + ;; + *) + use_external_signer="yes" + AC_DEFINE([ENABLE_EXTERNAL_SIGNER], [1], [Define if external signer support is enabled]) + ;; + esac fi +AM_CONDITIONAL([ENABLE_EXTERNAL_SIGNER], [test "$use_external_signer" = "yes"]) dnl Check for reduced exports if test "$use_reduce_exports" = "yes"; then @@ -2012,6 +2022,7 @@ AC_SUBST(ARM_SHANI_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(TESTDEFS) @@ -2079,7 +2090,7 @@ esac echo echo "Options used to compile and link:" -echo " boost process = $with_boost_process" +echo " external signer = $use_external_signer" echo " multiprocess = $build_multiprocess" echo " with libs = $build_bitcoin_libs" echo " with wallet = $enable_wallet" @@ -2089,36 +2100,36 @@ if test "$enable_wallet" != "no"; then fi echo " with gui / qt = $bitcoin_enable_qt" if test $bitcoin_enable_qt != "no"; then - echo " with qr = $use_qr" + echo " with qr = $use_qr" fi -echo " with zmq = $use_zmq" +echo " with zmq = $use_zmq" if test $enable_fuzz = "no"; then - echo " with test = $use_tests" + echo " with test = $use_tests" else - echo " with test = not building test_dash because fuzzing is enabled" + echo " with test = not building test_dash because fuzzing is enabled" fi -echo " with fuzz binary = $enable_fuzz_binary" -echo " with bench = $use_bench" -echo " with upnp = $use_upnp" -echo " with natpmp = $use_natpmp" -echo " USDT tracing = $use_usdt" -echo " sanitizers = $use_sanitizers" -echo " debug enabled = $enable_debug" -echo " stacktraces enabled = $enable_stacktraces" -echo " crash hooks enabled = $enable_crashhooks" -echo " miner enabled = $enable_miner" -echo " gprof enabled = $enable_gprof" -echo " werror = $enable_werror" +echo " with fuzz binary= $enable_fuzz_binary" +echo " with bench = $use_bench" +echo " with upnp = $use_upnp" +echo " with natpmp = $use_natpmp" +echo " USDT tracing = $use_usdt" +echo " sanitizers = $use_sanitizers" +echo " debug enabled = $enable_debug" +echo " stacktraces = $enable_stacktraces" +echo " crash hooks = $enable_crashhooks" +echo " miner enabled = $enable_miner" +echo " gprof enabled = $enable_gprof" +echo " werror = $enable_werror" echo -echo " target os = $host_os" -echo " build os = $build_os" +echo " target os = $host_os" +echo " build os = $build_os" echo -echo " CC = $CC" -echo " CFLAGS = $DEBUG_CFLAGS $PTHREAD_CFLAGS $BACKTRACE_FLAGS $CFLAGS" -echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CORE_CPPFLAGS $CPPFLAGS" -echo " CXX = $CXX" -echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CORE_CXXFLAGS $BACKTRACE_FLAGS $CXXFLAGS" -echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $CORE_LDFLAGS $BACKTRACE_LDFLAGS $LDFLAGS" -echo " AR = $AR" -echo " ARFLAGS = $ARFLAGS" +echo " CC = $CC" +echo " CFLAGS = $DEBUG_CFLAGS $PTHREAD_CFLAGS $BACKTRACE_FLAGS $CFLAGS" +echo " CPPFLAGS = $DEBUG_CPPFLAGS $HARDENED_CPPFLAGS $CORE_CPPFLAGS $CPPFLAGS" +echo " CXX = $CXX" +echo " CXXFLAGS = $DEBUG_CXXFLAGS $HARDENED_CXXFLAGS $WARN_CXXFLAGS $NOWARN_CXXFLAGS $ERROR_CXXFLAGS $GPROF_CXXFLAGS $CORE_CXXFLAGS $BACKTRACE_FLAGS $CXXFLAGS" +echo " LDFLAGS = $PTHREAD_LIBS $HARDENED_LDFLAGS $GPROF_LDFLAGS $CORE_LDFLAGS $BACKTRACE_LDFLAGS $LDFLAGS" +echo " AR = $AR" +echo " ARFLAGS = $ARFLAGS" echo diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 8d54cb41ef7e..611e4941a07e 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -2093,7 +2093,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 diff --git a/doc/external-signer.md b/doc/external-signer.md new file mode 100644 index 000000000000..fc69fb4391b2 --- /dev/null +++ b/doc/external-signer.md @@ -0,0 +1,171 @@ +# Support for signing transactions outside of Dash Core + +Dash Core can be launched with `-signer=` where `` 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/dashpay/HWI) tool. Version 2.0 or newer is required. Although this tool is hosted under the Dash Core GitHub organization and maintained by Dash Core developers, it should be used with caution. It is considered experimental and has far less review than Dash 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 Dash Core. + +Start Dash Core: + +```sh +$ dashd -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/dashpay/HWI). + +### Create wallet and import keys + +Get a list of signing devices / services: + +``` +$ dash-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 +$ dash-cli createwallet "hww" true true "" true true true +``` + +### Verify an address + +Display an address on the device: + +```sh +$ dash-cli -rpcwallet= getnewaddress +$ dash-cli -rpcwallet= walletdisplayaddress
+``` + +Replace `
` with the result of `getnewaddress`. + +### Spending + +Under the hood this uses a [Partially Signed Blockchain Transaction](psbt.md). + +```sh +$ dash-cli -rpcwallet= sendtoaddress
+``` + +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": } +``` + +## Signer API + +In order to be compatible with Dash 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 Blockchain Transaction ([PSBT](psbt.md)) + +### `enumerate` (required) + +Usage: +``` +$ 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 `` knows a signer exists but can't currently reach it. + +### `signtransaction` (required) + +Usage: +``` +$ --fingerprint= (--testnet) signtransaction +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: + +``` +$ --fingerprint= (--testnet) getdescriptors + +``` + +Returns descriptors supported by the device. Example: + +``` +$ --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: +``` + --fingerprint= (--testnet) displayaddress --desc descriptor +``` + +Example, display the first native SegWit receive address on Testnet: + +``` + --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 contains a master key fingerprint, the command MUST fail if it does not match the fingerprint known by the device. + +If 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 Dash Core uses the Signer API + +The `enumeratesigners` RPC simply calls ` enumerate`. + +The `createwallet` RPC calls: + +* ` --fingerprint=00000000 getdescriptors 0` + +It then imports descriptors for all support address types, in a BIP44/49/84 compatible manner. + +The `walletdisplayaddress` RPC reuses some code from `getaddressinfo` on the provided address and obtains the inferred descriptor. It then calls ` --fingerprint=00000000 displayaddress --desc=`. + +`sendtoaddress` and `sendmany` check `inputs->bip32_derivs` to see if any inputs have the same `master_fingerprint` as the signer. If so, it calls ` --fingerprint=00000000 signtransaction `. It waits for the device to return a (partially) signed psbt, tries to finalize it and broadcasts the transaction. diff --git a/src/Makefile.am b/src/Makefile.am index 9a3b18c65482..2ed2fb3a1943 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -215,6 +215,7 @@ BITCOIN_CORE_H = \ evo/specialtx_filter.h \ evo/specialtxman.h \ evo/types.h \ + external_signer.h \ dsnotificationinterface.h \ governance/classes.h \ governance/common.h \ @@ -430,6 +431,7 @@ BITCOIN_CORE_H = \ wallet/crypter.h \ wallet/db.h \ wallet/dump.h \ + wallet/external_signer_scriptpubkeyman.h \ wallet/fees.h \ wallet/hdchain.h \ wallet/ismine.h \ @@ -643,6 +645,7 @@ libbitcoin_wallet_a_SOURCES = \ wallet/crypter.cpp \ wallet/db.cpp \ wallet/dump.cpp \ + wallet/external_signer_scriptpubkeyman.cpp \ wallet/fees.cpp \ wallet/hdchain.cpp \ wallet/interfaces.cpp \ @@ -902,6 +905,7 @@ libbitcoin_common_a_SOURCES = \ deploymentinfo.cpp \ evo/core_write.cpp \ evo/netinfo.cpp \ + external_signer.cpp \ governance/common.cpp \ init/common.cpp \ key.cpp \ @@ -919,6 +923,7 @@ libbitcoin_common_a_SOURCES = \ psbt.cpp \ rpc/evo_util.cpp \ rpc/rawtransaction_util.cpp \ + rpc/external_signer.cpp \ rpc/util.cpp \ saltedhasher.cpp \ scheduler.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 194957d28f2b..a6cc2ce79318 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -43,10 +43,10 @@ FUZZ_SUITE_LD_COMMON = \ $(LIBTEST_FUZZ) \ $(LIBTEST_UTIL) \ $(LIBBITCOIN_NODE) \ + $(LIBBITCOIN_WALLET) \ $(LIBBITCOIN_COMMON) \ $(LIBBITCOIN_UTIL) \ $(LIBBITCOIN_CONSENSUS) \ - $(LIBBITCOIN_WALLET) \ $(LIBBITCOIN_CRYPTO) \ $(LIBBITCOIN_CLI) \ $(LIBDASHBLS) \ @@ -222,7 +222,6 @@ BITCOIN_TESTS += \ wallet/test/scriptpubkeyman_tests.cpp FUZZ_SUITE_LD_COMMON +=\ - $(LIBBITCOIN_WALLET) \ $(SQLITE_LIBS) \ $(BDB_LIBS) diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp index e02c4b505149..5ad3b2ee23ee 100644 --- a/src/dummywallet.cpp +++ b/src/dummywallet.cpp @@ -51,6 +51,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const "-maxtxfee=", "-rescan=", "-salvagewallet", + "-signer=", "-spendzeroconfchange", "-wallet=", "-walletbackupsdir=", diff --git a/src/external_signer.cpp b/src/external_signer.cpp new file mode 100644 index 000000000000..3057fa687e0c --- /dev/null +++ b/src/external_signer.cpp @@ -0,0 +1,117 @@ +// 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +ExternalSigner::ExternalSigner(const std::string& command, const std::string chain, const std::string& fingerprint, const std::string name): m_command(command), m_chain(chain), m_fingerprint(fingerprint), m_name(name) {} + +std::string ExternalSigner::NetworkArg() const +{ + return " --chain " + m_chain; +} + +bool ExternalSigner::Enumerate(const std::string& command, std::vector& signers, const std::string chain) +{ + // Call enumerate + const UniValue result = RunCommandParseJSON(command + " enumerate"); + if (!result.isArray()) { + throw std::runtime_error(strprintf("'%s' received invalid response, expected array of signers", command)); + } + for (UniValue signer : result.getValues()) { + // Check for error + const UniValue& error = signer.find_value("error"); + if (!error.isNull()) { + if (!error.isStr()) { + throw std::runtime_error(strprintf("'%s' error", command)); + } + throw std::runtime_error(strprintf("'%s' error: %s", command, error.getValStr())); + } + // Check if fingerprint is present + const UniValue& fingerprint = signer.find_value("fingerprint"); + if (fingerprint.isNull()) { + throw std::runtime_error(strprintf("'%s' received invalid response, missing signer fingerprint", command)); + } + const std::string fingerprintStr = fingerprint.get_str(); + // Skip duplicate signer + bool duplicate = false; + for (const ExternalSigner& signer : signers) { + if (signer.m_fingerprint.compare(fingerprintStr) == 0) duplicate = true; + } + if (duplicate) break; + std::string name; + const UniValue& model_field = signer.find_value("model"); + if (model_field.isStr() && model_field.getValStr() != "") { + name += model_field.getValStr(); + } + signers.push_back(ExternalSigner(command, chain, fingerprintStr, 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(const 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; + // parse ExternalSigner master fingerprint + std::vector parsed_m_fingerprint = ParseHex(m_fingerprint); + // Check if signer fingerprint matches any input master key fingerprint + auto matches_signer_fingerprint = [&](const PSBTInput& input) { + for (const auto& entry : input.hd_keypaths) { + if (parsed_m_fingerprint == MakeUCharSpan(entry.second.fingerprint)) return true; + } + return false; + }; + + if (!std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), matches_signer_fingerprint)) { + error = "Signer fingerprint " + m_fingerprint + " does not match any of the inputs:\n" + EncodeBase64(ssTx.str()); + return false; + } + + const std::string command = m_command + " --stdin --fingerprint \"" + m_fingerprint + "\"" + NetworkArg(); + const std::string stdinStr = "signtx \"" + EncodeBase64(ssTx.str()) + "\""; + + const UniValue signer_result = RunCommandParseJSON(command, stdinStr); + + if (signer_result.find_value("error").isStr()) { + error = signer_result.find_value("error").get_str(); + return false; + } + + if (!signer_result.find_value("psbt").isStr()) { + error = "Unexpected result from signer"; + return false; + } + + PartiallySignedTransaction signer_psbtx; + std::string signer_psbt_error; + if (!DecodeBase64PSBT(signer_psbtx, signer_result.find_value("psbt").get_str(), signer_psbt_error)) { + error = strprintf("TX decode failed %s", signer_psbt_error); + return false; + } + + psbtx = signer_psbtx; + + return true; +} diff --git a/src/external_signer.h b/src/external_signer.h new file mode 100644 index 000000000000..cf301d2780bc --- /dev/null +++ b/src/external_signer.h @@ -0,0 +1,66 @@ +// 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_EXTERNAL_SIGNER_H +#define BITCOIN_EXTERNAL_SIGNER_H + +#include +#include + +#include +#include + +struct PartiallySignedTransaction; + +//! 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; + + //! Dash mainnet, testnet, etc + std::string m_chain; + + std::string NetworkArg() const; + +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 chain, const std::string& fingerprint, const std::string name); + + //! Master key fingerprint of the signer + std::string m_fingerprint; + + //! Name of signer + std::string m_name; + + //! Obtain a list of signers. Calls ` 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" + //! @returns success + static bool Enumerate(const std::string& command, std::vector& signers, const std::string chain); + + //! Display address on the device. Calls ` displayaddress --desc `. + //! @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 ` getdescriptors --account ` + //! @param[in] account which BIP32 account to use (e.g. `m/44'/0'/account'`) + //! @returns see doc/external-signer.md + UniValue GetDescriptors(const int account); + + //! Sign PartiallySignedTransaction on the device. + //! Calls ` signtransaction` and passes the PSBT via stdin. + //! @param[in,out] psbt PartiallySignedTransaction to be signed + bool SignTransaction(PartiallySignedTransaction& psbt, std::string& error); +}; + +#endif // BITCOIN_EXTERNAL_SIGNER_H diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 7646fc7f1ea1..e3429afcd888 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -158,6 +158,16 @@ struct BlockAndHeaderTipInfo double verification_progress; }; +//! External signer interface used by the GUI. +class ExternalSigner +{ +public: + virtual ~ExternalSigner() {}; + + //! Get signer display name + virtual std::string getName() = 0; +}; + //! Top-level interface for a dash node (dashd process). class Node { @@ -240,6 +250,9 @@ class Node //! Disconnect node by id. virtual bool disconnectById(NodeId id) = 0; + //! Return list of external signers (attached devices which can sign transactions). + virtual std::vector> listExternalSigners() = 0; + //! Get total bytes recv. virtual int64_t getTotalBytesRecv() = 0; diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 56a2f595e666..e9f209df28f4 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -151,6 +151,9 @@ class Wallet //! Save or remove receive request. virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0; + //! Display address on external signer + virtual bool displayAddress(const CTxDestination& dest) = 0; + //! Lock coin. virtual bool lockCoin(const COutPoint& output, const bool write_to_db) = 0; @@ -288,6 +291,9 @@ class Wallet // Return whether private keys enabled. virtual bool privateKeysDisabled() = 0; + // Return whether wallet uses an external signer. + virtual bool hasExternalSigner() = 0; + //! Get max tx fee. virtual CAmount getDefaultMaxTxFee() = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 8d8579b62e2f..f2fc6401e329 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -398,6 +399,17 @@ class CoinJoinOptionsImpl : public CoinJoin::Options } }; +#ifdef ENABLE_EXTERNAL_SIGNER +class ExternalSignerImpl : public interfaces::ExternalSigner +{ +public: + ExternalSignerImpl(::ExternalSigner signer) : m_signer(std::move(signer)) {} + std::string getName() override { return m_signer.m_name; } +private: + ::ExternalSigner m_signer; +}; +#endif + class NodeImpl : public Node { private: @@ -555,6 +567,28 @@ class NodeImpl : public Node } return false; } + std::vector> listExternalSigners() override + { +#ifdef ENABLE_EXTERNAL_SIGNER + std::vector signers = {}; + const std::string command = gArgs.GetArg("-signer", ""); + if (command == "") return {}; + ExternalSigner::Enumerate(command, signers, Params().NetworkIDString()); + std::vector> result; + for (auto& signer : signers) { + result.emplace_back(std::make_unique(std::move(signer))); + } + return result; +#else + // This result is undistinguisable from a succesful call that returns + // no signers. For the current GUI this doesn't matter, because the wallet + // creation dialog disables the external signer checkbox in both + // cases. The return type could be changed to std::optional + // (or something that also includes error messages) if this distinction + // becomes important. + return {}; +#endif // ENABLE_EXTERNAL_SIGNER + } int64_t getTotalBytesRecv() override { return m_context->connman ? m_context->connman->GetTotalBytesRecv() : 0; } int64_t getTotalBytesSent() override { return m_context->connman ? m_context->connman->GetTotalBytesSent() : 0; } size_t getMempoolSize() override { return m_context->mempool ? m_context->mempool->size() : 0; } diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index e10eacfa3d5e..8b19e927ba72 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -6,6 +6,7 @@ #include #endif +#include #include #include @@ -39,14 +40,40 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : }); connect(ui->encrypt_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) { - // Disable the disable_privkeys_checkbox when isEncryptWalletChecked is + // Disable the disable_privkeys_checkbox and external_signer_checkbox when isEncryptWalletChecked is // set to true, enable it when isEncryptWalletChecked is false. ui->disable_privkeys_checkbox->setEnabled(!checked); - +#ifdef ENABLE_EXTERNAL_SIGNER + ui->external_signer_checkbox->setEnabled(m_has_signers && !checked); +#endif // When the disable_privkeys_checkbox is disabled, uncheck it. if (!ui->disable_privkeys_checkbox->isEnabled()) { ui->disable_privkeys_checkbox->setChecked(false); } + + // When the external_signer_checkbox box is disabled, uncheck it. + if (!ui->external_signer_checkbox->isEnabled()) { + ui->external_signer_checkbox->setChecked(false); + } + + }); + + connect(ui->external_signer_checkbox, &QCheckBox::toggled, [this](bool checked) { + ui->encrypt_wallet_checkbox->setEnabled(!checked); + ui->blank_wallet_checkbox->setEnabled(!checked); + ui->disable_privkeys_checkbox->setEnabled(!checked); + ui->descriptor_checkbox->setEnabled(!checked); + + // The external signer checkbox is only enabled when a device is detected. + // In that case it is checked by default. Toggling it restores the other + // options to their default. + ui->descriptor_checkbox->setChecked(checked); + ui->encrypt_wallet_checkbox->setChecked(false); + ui->disable_privkeys_checkbox->setChecked(checked); + // The blank check box is ambiguous. This flag is always true for a + // watch-only wallet, even though we immedidately fetch keys from the + // external signer. + ui->blank_wallet_checkbox->setChecked(checked); }); connect(ui->disable_privkeys_checkbox, &QCheckBox::toggled, [this](bool checked) { // Disable the encrypt_wallet_checkbox when isDisablePrivateKeysChecked is @@ -74,11 +101,22 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : ui->descriptor_checkbox->setToolTip(tr("Compiled without sqlite support (required for descriptor wallets)")); ui->descriptor_checkbox->setEnabled(false); ui->descriptor_checkbox->setChecked(false); + ui->external_signer_checkbox->setEnabled(false); + ui->external_signer_checkbox->setChecked(false); #endif + #ifndef USE_BDB ui->descriptor_checkbox->setEnabled(false); ui->descriptor_checkbox->setChecked(true); #endif + +#ifndef ENABLE_EXTERNAL_SIGNER + //: "External signing" means using devices such as hardware wallets. + ui->external_signer_checkbox->setToolTip(tr("Compiled without external signing support (required for external signing)")); + ui->external_signer_checkbox->setEnabled(false); + ui->external_signer_checkbox->setChecked(false); +#endif + } CreateWalletDialog::~CreateWalletDialog() @@ -86,6 +124,27 @@ CreateWalletDialog::~CreateWalletDialog() delete ui; } +void CreateWalletDialog::setSigners(const std::vector>& signers) +{ + m_has_signers = !signers.empty(); + if (m_has_signers) { + ui->external_signer_checkbox->setEnabled(true); + ui->external_signer_checkbox->setChecked(true); + ui->encrypt_wallet_checkbox->setEnabled(false); + ui->encrypt_wallet_checkbox->setChecked(false); + // The order matters, because connect() is called when toggling a checkbox: + ui->blank_wallet_checkbox->setEnabled(false); + ui->blank_wallet_checkbox->setChecked(false); + ui->disable_privkeys_checkbox->setEnabled(false); + ui->disable_privkeys_checkbox->setChecked(true); + const std::string label = signers[0]->getName(); + ui->wallet_name_line_edit->setText(QString::fromStdString(label)); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + } else { + ui->external_signer_checkbox->setEnabled(false); + } +} + QString CreateWalletDialog::walletName() const { return ui->wallet_name_line_edit->text(); @@ -110,3 +169,8 @@ bool CreateWalletDialog::isDescriptorWalletChecked() const { return ui->descriptor_checkbox->isChecked(); } + +bool CreateWalletDialog::isExternalSignerChecked() const +{ + return ui->external_signer_checkbox->isChecked(); +} diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 5fca441eba47..939b82ff78c4 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -7,6 +7,12 @@ #include +#include + +namespace interfaces { +class ExternalSigner; +} // namespace interfaces + class WalletModel; namespace Ui { @@ -23,14 +29,18 @@ class CreateWalletDialog : public QDialog explicit CreateWalletDialog(QWidget* parent); virtual ~CreateWalletDialog(); + void setSigners(const std::vector>& signers); + QString walletName() const; bool isEncryptWalletChecked() const; bool isDisablePrivateKeysChecked() const; bool isMakeBlankWalletChecked() const; bool isDescriptorWalletChecked() const; + bool isExternalSignerChecked() const; private: Ui::CreateWalletDialog *ui; + bool m_has_signers = false; }; #endif // BITCOIN_QT_CREATEWALLETDIALOG_H diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index 26c93d861838..48310006fc36 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -155,6 +155,16 @@ + + + + Use an external signing device such as a hardware wallet. Configure the external signer script in wallet preferences first. + + + External signer + + + @@ -188,6 +198,7 @@ encrypt_wallet_checkbox disable_privkeys_checkbox blank_wallet_checkbox + external_signer_checkbox diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index bca7379c75f5..b46cf51b7ce1 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -409,6 +409,36 @@ + + + + External Signer (e.g. hardware wallet) + + + + + + + + &External signer script path + + + externalSignerPath + + + + + + + Full path to a Dash Core compatible script (e.g. C:\Downloads\hwi.exe or /Users/you/Downloads/hwi.py). Beware: malware can steal your coins! + + + + + + + + diff --git a/src/qt/forms/receiverequestdialog.ui b/src/qt/forms/receiverequestdialog.ui index 7d95a8bc907c..70a7cf71de9b 100644 --- a/src/qt/forms/receiverequestdialog.ui +++ b/src/qt/forms/receiverequestdialog.ui @@ -254,6 +254,19 @@ + + + + &Verify + + + Verify this address on e.g. a hardware wallet screen + + + false + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 32ace0a1a275..da72b71b0675 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -139,6 +139,11 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) : showPage(0); +#ifndef ENABLE_EXTERNAL_SIGNER + //: "External signing" means using devices such as hardware wallets. + ui->externalSignerPath->setToolTip(tr("Compiled without external signing support (required for external signing)")); + ui->externalSignerPath->setEnabled(false); +#endif /* Display elements init */ /* Number of displayed decimal digits selector */ @@ -270,6 +275,7 @@ void OptionsDialog::setModel(OptionsModel *_model) connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning); connect(ui->pruneSize, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); connect(ui->databaseCache, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); + connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); }); connect(ui->threadsScriptVerif, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); /* Wallet */ connect(ui->showMasternodesTab, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); @@ -336,6 +342,7 @@ void OptionsDialog::setMapper() /* Wallet */ mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures); + mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath); mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount); mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls); mapper->addMapping(ui->keepChangeAddress, OptionsModel::KeepChangeAddress); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 719df0456b76..5fcf94edb9e7 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -223,6 +223,13 @@ void OptionsModel::Init(bool resetSettings) if (!gArgs.SoftSetBoolArg("-spendzeroconfchange", settings.value("bSpendZeroConfChange").toBool())) addOverriddenOption("-spendzeroconfchange"); + if (!settings.contains("external_signer_path")) + settings.setValue("external_signer_path", ""); + + if (!gArgs.SoftSetArg("-signer", settings.value("external_signer_path").toString().toStdString())) { + addOverriddenOption("-signer"); + } + if (!settings.contains("SubFeeFromAmount")) { settings.setValue("SubFeeFromAmount", false); } @@ -496,6 +503,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const #ifdef ENABLE_WALLET case SpendZeroConfChange: return settings.value("bSpendZeroConfChange"); + case ExternalSignerPath: + return settings.value("external_signer_path"); case SubFeeFromAmount: return m_sub_fee_from_amount; case ShowMasternodesTab: @@ -671,6 +680,12 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in setRestartRequired(true); } break; + case ExternalSignerPath: + if (settings.value("external_signer_path") != value.toString()) { + settings.setValue("external_signer_path", value.toString()); + setRestartRequired(true); + } + break; case ShowMasternodesTab: if (settings.value("fShowMasternodesTab") != value) { settings.setValue("fShowMasternodesTab", value); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 290ff3bd3dba..8d6d3c2c73f3 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -73,6 +73,7 @@ class OptionsModel : public QAbstractListModel Prune, // bool PruneSize, // int DatabaseCache, // int + ExternalSignerPath, // QString SpendZeroConfChange, // bool ShowMasternodesTab, // bool ShowGovernanceTab, // bool diff --git a/src/qt/receiverequestdialog.cpp b/src/qt/receiverequestdialog.cpp index 2961084f3714..bb1a90d45c26 100644 --- a/src/qt/receiverequestdialog.cpp +++ b/src/qt/receiverequestdialog.cpp @@ -93,6 +93,12 @@ void ReceiveRequestDialog::setInfo(const SendCoinsRecipient &_info) ui->wallet_tag->hide(); ui->wallet_content->hide(); } + + ui->btnVerify->setVisible(model->wallet().hasExternalSigner()); + + connect(ui->btnVerify, &QPushButton::clicked, [this] { + model->displayAddress(info.address.toStdString()); + }); } void ReceiveRequestDialog::updateDisplayUnit() diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 26ffc2309a3d..146ed5cefe8c 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -226,7 +226,18 @@ void SendCoinsDialog::setModel(WalletModel *_model) updateFeeSectionControls(); updateSmartFeeLabel(); - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().hasExternalSigner()) { + //: "device" usually means a hardware wallet + ui->sendButton->setText(tr("Sign on device")); + if (gArgs.GetArg("-signer", "") != "") { + ui->sendButton->setEnabled(true); + ui->sendButton->setToolTip(tr("Connect your hardware wallet first.")); + } else { + ui->sendButton->setEnabled(false); + //: "External signer" means using devices such as hardware wallets. + ui->sendButton->setToolTip(tr("Set external signer script path in Options -> Wallet")); + } + } else if (model->wallet().privateKeysDisabled()) { ui->sendButton->setText(tr("Cr&eate Unsigned")); ui->sendButton->setToolTip(tr("Creates a Partially Signed Blockchain Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); } @@ -369,10 +380,8 @@ bool SendCoinsDialog::send(const QList& recipients, QString& that the displayed transaction details represent the transaction the user intends to create. */ question_string.append(tr("Do you want to create this transaction?")); question_string.append("
"); - // TODO: re-enable it when external signer will be backported - // if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) { - const bool external_signer_available{false}; - if (external_signer_available) { + if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) { + question_string.append(tr("Do you want to draft this transaction?")); /*: Text to inform a user attempting to create a transaction of their current options. At this stage, a user can only create a PSBT. This string is displayed when private keys are disabled and an external signer is not available. */ @@ -503,9 +512,52 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())}; PartiallySignedTransaction psbtx(mtx); bool complete = false; - const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, nullptr, psbtx, complete); + // Always fill without signing first. This prevents an external signer + // from being called prematurely and is not expensive. + TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, nullptr, psbtx, complete); assert(!complete); assert(err == TransactionError::OK); + if (model->wallet().hasExternalSigner()) { + try { + err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, nullptr, psbtx, complete); + } catch (const std::runtime_error& e) { + QMessageBox::critical(nullptr, tr("Sign failed"), e.what()); + send_failure = true; + return; + } + if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) { + //: "External signer" means using devices such as hardware wallets. + QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found"); + send_failure = true; + return; + } + if (err == TransactionError::EXTERNAL_SIGNER_FAILED) { + //: "External signer" means using devices such as hardware wallets. + QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure"); + send_failure = true; + return; + } + if (err != TransactionError::OK) { + tfm::format(std::cerr, "Failed to sign PSBT"); + processSendCoinsReturn(WalletModel::TransactionCreationFailed); + send_failure = true; + return; + } + // fillPSBT does not always properly finalize + complete = FinalizeAndExtractPSBT(psbtx, mtx); + } + + // Broadcast transaction if complete (even with an external signer this + // is not always the case, e.g. in a multisig wallet). + if (complete) { + const CTransactionRef tx = MakeTransactionRef(mtx); + m_current_transaction->setWtx(tx); + model->sendCoins(*m_current_transaction, m_coin_control->IsUsingCoinJoin()); + return; + } + + // Copy PSBT to clipboard and offer to save + assert(!complete); // Serialize the PSBT CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; @@ -547,7 +599,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) break; default: assert(false); - } + } // msgBox.exec() } else { assert(!model->wallet().privateKeysDisabled()); // now send the prepared transaction @@ -715,7 +767,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances) if(model && model->getOptionsModel()) { CAmount balance = 0; - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().hasExternalSigner()) { + ui->labelBalanceName->setText(tr("External balance:")); + } else if (model->wallet().privateKeysDisabled()) { balance = balances.watch_only_balance; ui->labelBalanceName->setText(tr("Watch-only balance:")); } else if (m_coin_control->IsUsingCoinJoin()) { @@ -801,7 +855,7 @@ void SendCoinsDialog::on_buttonMinimizeFee_clicked() void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry) { // Include watch-only for wallets without private key - m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled(); + m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); // Calculate available amount to send. CAmount amount = model->wallet().getAvailableBalance(*m_coin_control); @@ -855,7 +909,7 @@ void SendCoinsDialog::updateCoinControlState() // Either custom fee will be used or if not selected, the confirmation target from dropdown box m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex()); // Include watch-only for wallets without private key - m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled(); + m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); } void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, const QString& blockHash, double nVerificationProgress, bool header, SynchronizationState sync_state) { diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 79df868be617..a6b071ef1b2f 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -35,6 +36,7 @@ using wallet::WALLET_FLAG_BLANK_WALLET; using wallet::WALLET_FLAG_DESCRIPTORS; +using wallet::WALLET_FLAG_EXTERNAL_SIGNER; using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS; WalletController::WalletController(ClientModel& client_model, QObject* parent) @@ -258,6 +260,9 @@ void CreateWalletActivity::createWallet() if (m_create_wallet_dialog->isDescriptorWalletChecked()) { flags |= WALLET_FLAG_DESCRIPTORS; } + if (m_create_wallet_dialog->isExternalSignerChecked()) { + flags |= WALLET_FLAG_EXTERNAL_SIGNER; + } QTimer::singleShot(500ms, worker(), [this, name, flags] { auto wallet{node().walletLoader().createWallet(name, m_passphrase, flags, m_warning_message)}; @@ -364,6 +369,15 @@ void CreateWalletActivity::finish() void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); + + std::vector> signers; + try { + signers = node().listExternalSigners(); + } catch (const std::runtime_error& e) { + QMessageBox::critical(nullptr, tr("Can't list signers"), e.what()); + } + m_create_wallet_dialog->setSigners(signers); + m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal); m_create_wallet_dialog->show(); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 0d448689c297..9b03c9cf9d5c 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -578,6 +579,18 @@ WalletModel::UnlockContext::~UnlockContext() } } +bool WalletModel::displayAddress(std::string sAddress) +{ + CTxDestination dest = DecodeDestination(sAddress); + bool res = false; + try { + res = m_wallet->displayAddress(dest); + } catch (const std::runtime_error& e) { + QMessageBox::critical(nullptr, tr("Can't display address"), e.what()); + } + return res; +} + bool WalletModel::isWalletEnabled() { return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 523c07ad91b9..b33e74a43ada 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -141,6 +141,8 @@ class WalletModel : public QObject UnlockContext requestUnlock(bool fForMixingOnly = false); + bool displayAddress(std::string sAddress); + static bool isWalletEnabled(); int getNumISLocks() const; diff --git a/src/qt/walletmodeltransaction.cpp b/src/qt/walletmodeltransaction.cpp index aed16d919262..c3dc38fa27c9 100644 --- a/src/qt/walletmodeltransaction.cpp +++ b/src/qt/walletmodeltransaction.cpp @@ -26,6 +26,11 @@ CTransactionRef& WalletModelTransaction::getWtx() return wtx; } +void WalletModelTransaction::setWtx(const CTransactionRef& newTx) +{ + wtx = newTx; +} + unsigned int WalletModelTransaction::getTransactionSize() { return wtx != nullptr ? ::GetSerializeSize(*wtx, PROTOCOL_VERSION) : 0; diff --git a/src/qt/walletmodeltransaction.h b/src/qt/walletmodeltransaction.h index 05973f621e34..67effb8e2908 100644 --- a/src/qt/walletmodeltransaction.h +++ b/src/qt/walletmodeltransaction.h @@ -27,6 +27,8 @@ class WalletModelTransaction QList getRecipients() const; CTransactionRef& getWtx(); + void setWtx(const CTransactionRef&); + unsigned int getTransactionSize(); void setTransactionFee(const CAmount& newFee); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 9742b33fbc59..636ca29d6163 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -239,6 +239,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createwallet", 4, "avoid_reuse"}, { "createwallet", 5, "descriptors"}, { "createwallet", 6, "load_on_startup"}, + { "createwallet", 7, "external_signer"}, { "restorewallet", 2, "load_on_startup"}, { "loadwallet", 1, "load_on_startup"}, { "unloadwallet", 1, "load_on_startup"}, diff --git a/src/rpc/external_signer.cpp b/src/rpc/external_signer.cpp new file mode 100644 index 000000000000..91a21f4e4d5b --- /dev/null +++ b/src/rpc/external_signer.cpp @@ -0,0 +1,71 @@ +// 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 +#include +#include +#include +#include + +#include +#include + +#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{ + HelpExampleCli("enumeratesigners", "") + + HelpExampleRpc("enumeratesigners", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + const std::string command = gArgs.GetArg("-signer", ""); + if (command == "") throw JSONRPCError(RPC_MISC_ERROR, "Error: restart dashd with -signer="); + const std::string chain = gArgs.GetChainName(); + UniValue signers_res = UniValue::VARR; + try { + std::vector signers; + ExternalSigner::Enumerate(command, signers, chain); + for (const 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 std::exception& e) { + throw JSONRPCError(RPC_MISC_ERROR, e.what()); + } + UniValue result(UniValue::VOBJ); + result.pushKV("signers", signers_res); + return result; + } + }; +} + +void RegisterSignerRPCCommands(CRPCTable& t) +{ + static const CRPCCommand commands[]{ + {"signer", &enumeratesigners}, + }; + for (const auto& c : commands) { + t.appendCommand(c.name, &c); + } +} + +#endif // ENABLE_EXTERNAL_SIGNER diff --git a/src/rpc/register.h b/src/rpc/register.h index f6bbd57a7c60..04aede8e16b2 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -22,6 +22,8 @@ void RegisterNetRPCCommands(CRPCTable&); void RegisterOutputScriptRPCCommands(CRPCTable&); void RegisterRawTransactionRPCCommands(CRPCTable &tableRPC); void RegisterSignMessageRPCCommands(CRPCTable&); +/** Register raw transaction RPC commands */ +void RegisterSignerRPCCommands(CRPCTable &tableRPC); void RegisterTxoutProofRPCCommands(CRPCTable&); void RegisterMasternodeRPCCommands(CRPCTable &tableRPC); void RegisterCoinJoinRPCCommands(CRPCTable &tableRPC); @@ -48,6 +50,9 @@ static inline void RegisterAllCoreRPCCommands(CRPCTable &t) RegisterOutputScriptRPCCommands(t); RegisterRawTransactionRPCCommands(t); RegisterSignMessageRPCCommands(t); +#ifdef ENABLE_EXTERNAL_SIGNER + RegisterSignerRPCCommands(t); +#endif // ENABLE_EXTERNAL_SIGNER RegisterTxoutProofRPCCommands(t); RegisterMasternodeRPCCommands(t); RegisterCoinJoinRPCCommands(t); diff --git a/src/test/system_tests.cpp b/src/test/system_tests.cpp index ae8b23e4a734..c2e7a77e73a8 100644 --- a/src/test/system_tests.cpp +++ b/src/test/system_tests.cpp @@ -6,22 +6,27 @@ #include #include -#ifdef HAVE_BOOST_PROCESS +#ifdef ENABLE_EXTERNAL_SIGNER +#if defined(WIN32) && !defined(__kernel_entry) +// A workaround for boost 1.71 incompatibility with mingw-w64 compiler. +// For details see https://github.com/bitcoin/bitcoin/pull/22348. +#define __kernel_entry +#endif #include -#endif // HAVE_BOOST_PROCESS +#endif // ENABLE_EXTERNAL_SIGNER #include 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 BOOST_AUTO_TEST_CASE(run_command) { @@ -96,6 +101,6 @@ BOOST_AUTO_TEST_CASE(run_command) } #endif } -#endif // HAVE_BOOST_PROCESS +#endif // ENABLE_EXTERNAL_SIGNER BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/error.cpp b/src/util/error.cpp index 5dc2a89b30d1..9a6002209e3e 100644 --- a/src/util/error.cpp +++ b/src/util/error.cpp @@ -33,6 +33,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); diff --git a/src/util/error.h b/src/util/error.h index 4cb519adf193..42f19cf3cb21 100644 --- a/src/util/error.h +++ b/src/util/error.h @@ -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); diff --git a/src/util/system.cpp b/src/util/system.cpp index 1b79ef2a1832..50c761b8418a 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -8,9 +8,9 @@ #include -#ifdef HAVE_BOOST_PROCESS +#ifdef ENABLE_EXTERNAL_SIGNER #include -#endif // HAVE_BOOST_PROCESS +#endif // ENABLE_EXTERNAL_SIGNER #include #include @@ -1416,9 +1416,9 @@ void RenameThreadPool(ctpl::thread_pool& tp, const char* baseName) } } -#ifdef HAVE_BOOST_PROCESS UniValue RunCommandParseJSON(const std::string& str_command, const std::string& str_std_in) { +#ifdef ENABLE_EXTERNAL_SIGNER namespace bp = boost::process; UniValue result_json; @@ -1450,8 +1450,10 @@ UniValue RunCommandParseJSON(const std::string& str_command, const std::string& if (!result_json.read(result)) throw std::runtime_error("Unable to parse JSON: " + result); return result_json; +#else + throw std::runtime_error("Compiled without external signing support (required for external signing)."); +#endif // ENABLE_EXTERNAL_SIGNER } -#endif // HAVE_BOOST_PROCESS void SetupEnvironment() { diff --git a/src/util/system.h b/src/util/system.h index bcf9ba0865cf..51bae62d70fe 100644 --- a/src/util/system.h +++ b/src/util/system.h @@ -113,7 +113,6 @@ std::string ShellEscape(const std::string& arg); #if HAVE_SYSTEM void runCommand(const std::string& strCommand); #endif -#ifdef HAVE_BOOST_PROCESS /** * Execute a command which returns JSON, and parse the result. * @@ -122,7 +121,6 @@ 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 /** * Most paths passed as configuration arguments are treated as relative to diff --git a/src/wallet/external_signer_scriptpubkeyman.cpp b/src/wallet/external_signer_scriptpubkeyman.cpp new file mode 100644 index 000000000000..2d50c09b3574 --- /dev/null +++ b/src/wallet/external_signer_scriptpubkeyman.cpp @@ -0,0 +1,86 @@ +// 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 +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace wallet { +bool ExternalSignerScriptPubKeyMan::SetupDescriptor(std::unique_ptr 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 dashd with -signer="); + std::vector 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, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const +{ + if (!sign) { + return DescriptorScriptPubKeyMan::FillPSBT(psbt, txdata, sighash_type, false, bip32derivs, n_signed, finalize); + } + + // 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; + } + if (finalize) FinalizePSBT(psbt); // This won't work in a multisig setup + return TransactionError::OK; +} +} // namespace wallet diff --git a/src/wallet/external_signer_scriptpubkeyman.h b/src/wallet/external_signer_scriptpubkeyman.h new file mode 100644 index 000000000000..efb1b797f137 --- /dev/null +++ b/src/wallet/external_signer_scriptpubkeyman.h @@ -0,0 +1,36 @@ +// 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 + +#include + +#include + +namespace wallet { +class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan +{ + public: + ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor) + : DescriptorScriptPubKeyMan(storage, descriptor) + {} + ExternalSignerScriptPubKeyMan(WalletStorage& storage) + : DescriptorScriptPubKeyMan(storage) + {} + + /** Provide a descriptor at setup time + * Returns false if already setup or setup fails, true if setup is successful + */ + bool SetupDescriptor(std::unique_ptrdesc); + + static ExternalSigner GetExternalSigner(); + + bool DisplayAddress(const CScript scriptPubKey, const ExternalSigner &signer) const; + + TransactionError FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override; +}; +} // namespace wallet + +#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 4aa657beb8d3..444e066b0021 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -67,6 +67,9 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const argsman.AddArg("-keypool=", strprintf("Set key pool size to (default: %u). Warning: Smaller sizes may increase the risk of losing funds when restoring from an old backup, if none of the addresses in the original keypool have been used.", DEFAULT_KEYPOOL_SIZE), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); argsman.AddArg("-rescan=", "Rescan the block chain for missing wallet transactions on startup" " (1 = start from wallet creation time, 2 = start from genesis block)", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); +#ifdef ENABLE_EXTERNAL_SIGNER + argsman.AddArg("-signer=", "External signing tool, see doc/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("-wallet=", "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 . 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 .", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET); argsman.AddArg("-walletbackupsdir=", "Specify full path to directory for automatic wallet backups (must exist)", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index a0f55c32fb2a..6d087a276594 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -267,6 +267,11 @@ class WalletImpl : public Wallet WalletBatch batch{m_wallet->GetDatabase()}; return m_wallet->SetAddressReceiveRequest(batch, dest, id, value); } + bool displayAddress(const CTxDestination& dest) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->DisplayAddress(dest); + } bool lockCoin(const COutPoint& output, const bool write_to_db) override { LOCK(m_wallet->cs_wallet); @@ -518,6 +523,7 @@ class WalletImpl : public Wallet unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; } bool hdEnabled() override { return m_wallet->IsHDEnabled(); } bool canGetAddresses() override { return m_wallet->CanGetAddresses(); } + bool hasExternalSigner() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER); } bool privateKeysDisabled() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; } void remove() override diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index 6058e621aa6f..f4a95c0bf25a 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -665,4 +665,47 @@ RPCHelpMan listlabels() }, }; } + +#ifdef ENABLE_EXTERNAL_SIGNER +RPCHelpMan walletdisplayaddress() +{ + return RPCHelpMan{"walletdisplayaddress", + "Display address on an external signer for verification.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "dash address to display"}, + }, + RPCResult{ + RPCResult::Type::OBJ,"","", + { + {RPCResult::Type::STR, "address", "The address as confirmed by the signer"}, + } + }, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + std::shared_ptr 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_MISC_ERROR, "Failed to display address"); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("address", request.params[0].get_str()); + return result; + } + }; +} +#endif // ENABLE_EXTERNAL_SIGNER + } // namespace wallet diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 055aceff5ea7..0802f0d46a17 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1954,7 +1954,7 @@ RPCHelpMan importdescriptors() { "block from time %d, which is after or within %d seconds of key creation, and " "could contain transactions pertaining to the desc. As a result, transactions " "and coins using this desc may not appear in the wallet. This error could be " - "caused by pruning or data corruption (see bitcoind log for details) and could " + "caused by pruning or data corruption (see dashd log for details) and could " "be dealt with by downloading and rescanning the relevant blocks (see -reindex " "and -rescan options).", GetImportTimestamp(request, now), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW))); diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 746dfc4bcdb3..5feea820add2 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -904,8 +904,10 @@ 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; + (void)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); diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index cdc91d451913..df765bd52ed4 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -191,6 +191,7 @@ static RPCHelpMan getwalletinfo() {RPCResult::Type::NUM, "progress", "scanning progress percentage [0.0, 1.0]"}, }}, {RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"}, + {RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"}, }, }, RPCExamples{ @@ -268,6 +269,7 @@ static RPCHelpMan getwalletinfo() obj.pushKV("scanning", false); } obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); + obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)); return obj; }, }; @@ -623,6 +625,7 @@ static RPCHelpMan createwallet() {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{false}, "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, {"descriptors", RPCArg::Type::BOOL, RPCArg::Default{false}, "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation. This feature is well-tested but still considered experimental."}, {"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED_NAMED_ARG, "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, RPCArg::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, "", "", @@ -672,6 +675,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, "Compiled without external signing support (required for external signing)"); +#endif + } #ifndef USE_BDB if (!(flags & WALLET_FLAG_DESCRIPTORS)) { @@ -1094,6 +1104,9 @@ RPCHelpMan keypoolrefill(); RPCHelpMan newkeypool(); RPCHelpMan getaddressesbylabel(); RPCHelpMan listlabels(); +#ifdef ENABLE_EXTERNAL_SIGNER +RPCHelpMan walletdisplayaddress(); +#endif // ENABLE_EXTERNAL_SIGNER // backup RPCHelpMan dumpprivkey(); @@ -1217,6 +1230,9 @@ Span GetWalletRPCCommands() {"wallet", &unloadwallet}, {"wallet", &upgradewallet}, {"wallet", &upgradetohd}, +#ifdef ENABLE_EXTERNAL_SIGNER + {"wallet", &walletdisplayaddress}, +#endif // ENABLE_EXTERNAL_SIGNER {"wallet", &walletlock}, {"wallet", &walletpassphrasechange}, {"wallet", &walletpassphrase}, diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 7f4ee75c413b..b30b1bd6c554 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -512,8 +512,6 @@ class LegacySigningProvider : public SigningProvider class DescriptorScriptPubKeyMan : public ScriptPubKeyMan { private: - WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man); - using ScriptPubKeyMap = std::map; // Map of scripts to descriptor range index using PubKeyMap = std::map; // Map of pubkeys involved in scripts to descriptor range index using CryptedKeyMap = std::map>>; @@ -550,6 +548,9 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan // Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions. std::unique_ptr 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), @@ -584,6 +585,11 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan //! Setup descriptors based on the given CExtkey bool SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, PathDerivationType type); + /** Provide a descriptor at setup time + * Returns false if already setup or setup fails, true if setup is successful + */ + bool SetupDescriptor(std::unique_ptrdesc); + bool HavePrivateKeys() const override; std::optional GetOldestKeyPoolTime() const override; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 24c4a2e5ff1d..20a1376e0b51 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include