diff options
62 files changed, 1062 insertions, 796 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index e07ff9796e..e0ab1536a0 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -2,6 +2,7 @@ env: # Global defaults PACKAGE_MANAGER_INSTALL: "apt-get update && apt-get install -y" MAKEJOBS: "-j10" TEST_RUNNER_PORT_MIN: "14000" # Must be larger than 12321, which is used for the http cache. See https://cirrus-ci.org/guide/writing-tasks/#http-cache + CI_FAILFAST_TEST_LEAVE_DANGLING: "1" # Cirrus CI does not care about dangling process and setting this variable avoids killing the CI script itself on error CCACHE_SIZE: "200M" CCACHE_DIR: "/tmp/ccache_dir" CCACHE_NOHASHDIR: "1" # Debug info might contain a stale path if the build dir changes, but this is fine diff --git a/ci/test/00_setup_env_native_asan.sh b/ci/test/00_setup_env_native_asan.sh index b03a7edb54..69883e3609 100755 --- a/ci/test/00_setup_env_native_asan.sh +++ b/ci/test/00_setup_env_native_asan.sh @@ -11,4 +11,4 @@ export PACKAGES="clang llvm python3-zmq qtbase5-dev qttools5-dev-tools libevent- export DOCKER_NAME_TAG=ubuntu:22.04 export NO_DEPENDS=1 export GOAL="install" -export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS='-DARENA_DEBUG -DDEBUG_LOCKORDER' --with-sanitizers=address,integer,undefined CC=clang CXX=clang++" +export BITCOIN_CONFIG="--enable-c++20 --enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS='-DARENA_DEBUG -DDEBUG_LOCKORDER' --with-sanitizers=address,integer,undefined CC=clang CXX=clang++" diff --git a/configure.ac b/configure.ac index 63ff6a1ebd..298b72a381 100644 --- a/configure.ac +++ b/configure.ac @@ -78,8 +78,18 @@ AC_ARG_WITH([seccomp], [seccomp_found=$withval], [seccomp_found=auto]) +AC_ARG_ENABLE([c++20], + [AS_HELP_STRING([--enable-c++20], + [enable compilation in c++20 mode (disabled by default)])], + [use_cxx20=$enableval], + [use_cxx20=no]) + dnl Require C++17 compiler (no GNU extensions) +if test "$use_cxx20" = "no"; then AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory]) +else +AX_CXX_COMPILE_STDCXX([20], [noext], [mandatory]) +fi dnl Check if -latomic is required for <std::atomic> CHECK_ATOMIC @@ -96,10 +106,8 @@ fi AC_PROG_OBJCXX ]) -dnl Since libtool 1.5.2 (released 2004-01-25), on Linux libtool no longer -dnl sets RPATH for any directories in the dynamic linker search path. -dnl See more: https://wiki.debian.org/RpathIssue -LT_PREREQ([1.5.2]) +dnl OpenBSD ships with 2.4.2 +LT_PREREQ([2.4.2]) dnl Libtool init checks. LT_INIT([pic-only win32-dll]) diff --git a/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh index 20a8a4f070..4eeb360603 100755 --- a/contrib/guix/libexec/build.sh +++ b/contrib/guix/libexec/build.sh @@ -167,7 +167,6 @@ case "$HOST" in *linux*) glibc_dynamic_linker=$( case "$HOST" in - i686-linux-gnu) echo /lib/ld-linux.so.2 ;; x86_64-linux-gnu) echo /lib64/ld-linux-x86-64.so.2 ;; arm-linux-gnueabihf) echo /lib/ld-linux-armhf.so.3 ;; aarch64-linux-gnu) echo /lib/ld-linux-aarch64.so.1 ;; @@ -204,19 +203,12 @@ make -C depends --jobs="$JOBS" HOST="$HOST" \ ${SOURCES_PATH+SOURCES_PATH="$SOURCES_PATH"} \ ${BASE_CACHE+BASE_CACHE="$BASE_CACHE"} \ ${SDK_PATH+SDK_PATH="$SDK_PATH"} \ - i686_linux_CC=i686-linux-gnu-gcc \ - i686_linux_CXX=i686-linux-gnu-g++ \ - i686_linux_AR=i686-linux-gnu-ar \ - i686_linux_RANLIB=i686-linux-gnu-ranlib \ - i686_linux_NM=i686-linux-gnu-nm \ - i686_linux_STRIP=i686-linux-gnu-strip \ x86_64_linux_CC=x86_64-linux-gnu-gcc \ x86_64_linux_CXX=x86_64-linux-gnu-g++ \ x86_64_linux_AR=x86_64-linux-gnu-ar \ x86_64_linux_RANLIB=x86_64-linux-gnu-ranlib \ x86_64_linux_NM=x86_64-linux-gnu-nm \ x86_64_linux_STRIP=x86_64-linux-gnu-strip \ - qt_config_opts_i686_linux='-platform linux-g++ -xplatform bitcoin-linux-g++' \ qt_config_opts_x86_64_linux='-platform linux-g++ -xplatform bitcoin-linux-g++' \ FORCE_USE_SYSTEM_CLANG=1 diff --git a/contrib/guix/libexec/prelude.bash b/contrib/guix/libexec/prelude.bash index 086df48fbe..f24c120863 100644 --- a/contrib/guix/libexec/prelude.bash +++ b/contrib/guix/libexec/prelude.bash @@ -51,7 +51,7 @@ fi time-machine() { # shellcheck disable=SC2086 guix time-machine --url=https://git.savannah.gnu.org/git/guix.git \ - --commit=ae03f401381e956c4c41b4cf495cbde964fa43d0 \ + --commit=34e9eae68c9583acce5abc4100add3d88932a5ae \ --cores="$JOBS" \ --keep-failed \ --fallback \ diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm index 008531e154..371312be7e 100644 --- a/contrib/guix/manifest.scm +++ b/contrib/guix/manifest.scm @@ -162,17 +162,13 @@ desirable for building Bitcoin Core release binaries." (define (make-gcc-with-pthreads gcc) (package-with-extra-configure-variable gcc "--enable-threads" "posix")) -;; Required to support std::filesystem for mingw-w64 target. -(define (make-gcc-without-newlib gcc) - (package-with-extra-configure-variable gcc "--with-newlib" "no")) - (define (make-mingw-pthreads-cross-toolchain target) "Create a cross-compilation toolchain package for TARGET" (let* ((xbinutils (cross-binutils target)) (pthreads-xlibc mingw-w64-x86_64-winpthreads) (pthreads-xgcc (make-gcc-with-pthreads (cross-gcc target - #:xgcc (make-gcc-without-newlib (make-ssp-fixed-gcc base-gcc)) + #:xgcc (make-ssp-fixed-gcc base-gcc) #:xbinutils xbinutils #:libc pthreads-xlibc)))) ;; Define a meta-package that propagates the resulting XBINUTILS, XLIBC, and @@ -506,8 +502,7 @@ and endian independent.") ("python-certvalidator" ,python-certvalidator) ("python-elfesteem" ,python-elfesteem) ("python-requests" ,python-requests) - ("python-macholib" ,python-macholib) - ("libcrypto" ,openssl))) + ("python-macholib" ,python-macholib))) ;; There are no tests, but attempting to run python setup.py test leads to ;; problems, just disable the test (arguments '(#:tests? #f)) diff --git a/doc/README.md b/doc/README.md index c200ac3753..33f71f4807 100644 --- a/doc/README.md +++ b/doc/README.md @@ -73,6 +73,7 @@ The Bitcoin repo's [root README](/README.md) contains relevant information on th - [Assets Attribution](assets-attribution.md) - [Assumeutxo design](assumeutxo.md) - [bitcoin.conf Configuration File](bitcoin-conf.md) +- [CJDNS Support](cjdns.md) - [Files](files.md) - [Fuzz-testing](fuzzing.md) - [I2P Support](i2p.md) diff --git a/doc/build-netbsd.md b/doc/build-netbsd.md index 762406bf6b..ba9a80f83b 100644 --- a/doc/build-netbsd.md +++ b/doc/build-netbsd.md @@ -27,15 +27,33 @@ git clone https://github.com/bitcoin/bitcoin.git See [dependencies.md](dependencies.md) for a complete overview. -### Building BerkeleyDB +### Building Bitcoin Core + +**Important**: Use `gmake` (the non-GNU `make` will exit with an error). + +#### With descriptor wallet: + +The descriptor wallet uses `sqlite3`. You can install it using: +```bash +pkgin install sqlite3 +``` + +```bash +./autogen.sh +./configure --with-gui=no --without-bdb \ + CPPFLAGS="-I/usr/pkg/include" \ + LDFLAGS="-L/usr/pkg/lib" \ + BOOST_CPPFLAGS="-I/usr/pkg/include" \ + MAKE=gmake +``` -BerkeleyDB is only necessary for the wallet functionality. To skip this, pass -`--disable-wallet` to `./configure` and skip to the next section. +#### With legacy wallet: + +BerkeleyDB is use for legacy wallet functionality. It is recommended to use Berkeley DB 4.8. You cannot use the BerkeleyDB library -from ports, for the same reason as boost above (g++/libstd++ incompatibility). -If you have to build it yourself, you can use [the installation script included -in contrib/](/contrib/install_db4.sh) like so: +from ports. +You can use [the installation script included in contrib/](/contrib/install_db4.sh) like so: ```bash ./contrib/install_db4.sh `pwd` @@ -47,11 +65,6 @@ from the root of the repository. Then set `BDB_PREFIX` for the next section: export BDB_PREFIX="$PWD/db4" ``` -### Building Bitcoin Core - -**Important**: Use `gmake` (the non-GNU `make` will exit with an error). - -With wallet: ```bash ./autogen.sh ./configure --with-gui=no CPPFLAGS="-I/usr/pkg/include" \ @@ -62,7 +75,7 @@ With wallet: MAKE=gmake ``` -Without wallet: +#### Without wallet: ```bash ./autogen.sh ./configure --with-gui=no --disable-wallet \ diff --git a/doc/cjdns.md b/doc/cjdns.md new file mode 100644 index 0000000000..5b2bcaf874 --- /dev/null +++ b/doc/cjdns.md @@ -0,0 +1,95 @@ +# CJDNS support in Bitcoin Core + +It is possible to run Bitcoin Core over CJDNS, an encrypted IPv6 network that +uses public-key cryptography for address allocation and a distributed hash table +for routing. + +## What is CJDNS? + +CJDNS is like a distributed, shared VPN with multiple entry points where every +participant can reach any other participant. All participants use addresses from +the `fc00::/8` network (reserved IPv6 range). Installation and configuration is +done outside of Bitcoin Core, similarly to a VPN (either in the host/OS or on +the network router). + +Compared to IPv4/IPv6, CJDNS provides end-to-end encryption and protects nodes +from traffic analysis and filtering. + +Used with Tor and I2P, CJDNS is a complementary option that can enhance network +redundancy and robustness for both the Bitcoin network and individual nodes. + +Each network has different characteristics. For instance, Tor is widely used but +somewhat centralized. I2P connections have a source address and I2P is slow. +CJDNS is fast but does not hide the sender and the recipient from intermediate +routers. + +## Installing CJDNS and connecting to the network + +To install and set up CJDNS, follow the instructions at +https://github.com/cjdelisle/cjdns#cjdns. + +Don't skip steps +["2. Find a friend"](https://github.com/cjdelisle/cjdns#2-find-a-friend) and +["3. Connect your node to your friend's +node"](https://github.com/cjdelisle/cjdns#3-connect-your-node-to-your-friends-node). +You need to be connected to the CJDNS network before it will work with your +Bitcoin Core node. + +Typically, CJDNS might be launched from its directory with +`sudo ./cjdroute < cjdroute.conf` and it sheds permissions after setting up the +[TUN](https://en.wikipedia.org/wiki/TUN/TAP) interface. You may also [launch it as an +unprivileged user](https://github.com/cjdelisle/cjdns/blob/master/doc/non-root-user.md) +with some additional setup. + +The network connection can be checked by running `./tools/peerStats` from the +CJDNS directory. + +## Run Bitcoin Core with CJDNS + +Once you are connected to the CJDNS network, the following Bitcoin Core +configuration option makes CJDNS peers automatically reachable: + +``` +-cjdnsreachable +``` + +When enabled, this option tells Bitcoin Core that it is running in an +environment where a connection to an `fc00::/8` address will be to the CJDNS +network instead of to an [RFC4193](https://datatracker.ietf.org/doc/html/rfc4193) +IPv6 local network. This helps Bitcoin Core perform better address management: + - Your node can consider incoming `fc00::/8` connections to be from the CJDNS + network rather than from an IPv6 private one. + - If one of your node's local addresses is `fc00::/8`, then it can choose to + gossip that address to peers. + +## Additional configuration options related to CJDNS + +``` +-onlynet=cjdns +``` + +Make automatic outbound connections only to CJDNS addresses. Inbound and manual +connections are not affected by this option. It can be specified multiple times +to allow multiple networks, e.g. onlynet=cjdns, onlynet=i2p, onlynet=onion. + +CJDNS support was added to Bitcoin Core in version 23.0 and there may be fewer +CJDNS peers than Tor or IP ones. You can use `bitcoin-cli -addrinfo` to see the +number of CJDNS addresses known to your node. + +In general, a node can be run with both an onion service and CJDNS (or any/all +of IPv4/IPv6/onion/I2P/CJDNS), which can provide a potential fallback if one of +the networks has issues. There are a number of ways to configure this; see +[doc/tor.md](https://github.com/bitcoin/bitcoin/blob/master/doc/tor.md) for +details. + +## CJDNS-related information in Bitcoin Core + +There are several ways to see your CJDNS address in Bitcoin Core: +- in the "Local addresses" output of CLI `-netinfo` +- in the "localaddresses" output of RPC `getnetworkinfo` + +To see which CJDNS peers your node is connected to, use `bitcoin-cli -netinfo 4` +or the `getpeerinfo` RPC (i.e. `bitcoin-cli getpeerinfo`). + +To see which CJDNS addresses your node knows, use the `getnodeaddresses 0 cjdns` +RPC. diff --git a/doc/i2p.md b/doc/i2p.md index e45b5efb9b..39f65c4e5f 100644 --- a/doc/i2p.md +++ b/doc/i2p.md @@ -80,15 +80,15 @@ phase when syncing up a new node can be very slow. This phase can be sped up by using other networks, for instance `onlynet=onion`, at the same time. In general, a node can be run with both onion and I2P hidden services (or -any/all of IPv4/IPv6/onion/I2P), which can provide a potential fallback if one -of the networks has issues. +any/all of IPv4/IPv6/onion/I2P/CJDNS), which can provide a potential fallback if +one of the networks has issues. ## I2P-related information in Bitcoin Core There are several ways to see your I2P address in Bitcoin Core: -- in the debug log (grep for `AddLocal`, the I2P address ends in `.b32.i2p`) -- in the output of the `getnetworkinfo` RPC in the "localaddresses" section -- in the output of `bitcoin-cli -netinfo` peer connections dashboard +- in the "Local addresses" output of CLI `-netinfo` +- in the "localaddresses" output of RPC `getnetworkinfo` +- in the debug log (grep for `AddLocal`; the I2P address ends in `.b32.i2p`) To see which I2P peers your node is connected to, use `bitcoin-cli -netinfo 4` or the `getpeerinfo` RPC (e.g. `bitcoin-cli getpeerinfo`). diff --git a/doc/tor.md b/doc/tor.md index b7c4f7d425..08d031d084 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -16,9 +16,9 @@ configure Tor. ## How to see information about your Tor configuration via Bitcoin Core There are several ways to see your local onion address in Bitcoin Core: -- in the debug log (grep for "tor:" or "AddLocal") -- in the output of RPC `getnetworkinfo` in the "localaddresses" section -- in the output of the CLI `-netinfo` peer connections dashboard +- in the "Local addresses" output of CLI `-netinfo` +- in the "localaddresses" output of RPC `getnetworkinfo` +- in the debug log (grep for "AddLocal"; the Tor address ends in `.onion`) You may set the `-debug=tor` config logging option to have additional information in the debug log about your Tor configuration. @@ -27,6 +27,9 @@ CLI `-addrinfo` returns the number of addresses known to your node per network. This can be useful to see how many onion peers your node knows, e.g. for `-onlynet=onion`. +To fetch a number of onion addresses that your node knows, for example seven +addresses, use the `getnodeaddresses 7 onion` RPC. + ## 1. Run Bitcoin Core behind a Tor proxy The first step is running Bitcoin Core behind a Tor proxy. This will already anonymize all @@ -58,7 +61,7 @@ outgoing connections, but more is possible. -onlynet=onion Make automatic outbound connections only to .onion addresses. Inbound and manual connections are not affected by this option. It can be specified multiple times to allow multiple networks, - e.g. onlynet=onion, onlynet=i2p. + e.g. onlynet=onion, onlynet=i2p, onlynet=cjdns. In a typical situation, this suffices to run behind a Tor proxy: diff --git a/src/Makefile.am b/src/Makefile.am index e940736b71..af6bcaf4d7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -378,6 +378,7 @@ libbitcoin_node_a_SOURCES = \ rpc/rawtransaction.cpp \ rpc/server.cpp \ rpc/server_util.cpp \ + rpc/txoutproof.cpp \ script/sigcache.cpp \ shutdown.cpp \ signet.cpp \ diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index 609c592d20..16a8948f99 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -13,7 +13,6 @@ using node::NodeContext; using wallet::AttemptSelection; -using wallet::CInputCoin; using wallet::COutput; using wallet::CWallet; using wallet::CWalletTx; @@ -58,14 +57,21 @@ static void CoinSelection(benchmark::Bench& bench) // Create coins std::vector<COutput> coins; for (const auto& wtx : wtxs) { - coins.emplace_back(wallet, *wtx, 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); + coins.emplace_back(COutPoint(wtx->GetHash(), 0), wtx->tx->vout.at(0), /*depth=*/ 6 * 24, GetTxSpendSize(wallet, *wtx, 0), /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, wtx->GetTxTime(), /*from_me=*/ true); } const CoinEligibilityFilter filter_standard(1, 6, 0); - const CoinSelectionParams coin_selection_params(/* change_output_size= */ 34, - /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); + FastRandomContext rand{}; + const CoinSelectionParams coin_selection_params{ + rand, + /* change_output_size= */ 34, + /* change_spend_size= */ 148, + /* effective_feerate= */ CFeeRate(0), + /* long_term_feerate= */ CFeeRate(0), + /* discard_feerate= */ CFeeRate(0), + /* tx_noinputs_size= */ 0, + /* avoid_partial= */ false, + }; bench.run([&] { auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, coins, coin_selection_params); assert(result); @@ -74,17 +80,15 @@ static void CoinSelection(benchmark::Bench& bench) }); } -typedef std::set<CInputCoin> CoinSet; - // Copied from src/wallet/test/coinselector_tests.cpp static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>& set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; - CInputCoin coin(MakeTransactionRef(tx), nInput); + COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 0, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ true); set.emplace_back(); - set.back().Insert(coin, 0, true, 0, 0, false); + set.back().Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false); } // Copied from src/wallet/test/coinselector_tests.cpp static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool) diff --git a/src/core_write.cpp b/src/core_write.cpp index c4b6b8d27e..8ec75880fe 100644 --- a/src/core_write.cpp +++ b/src/core_write.cpp @@ -153,7 +153,9 @@ void ScriptPubKeyToUniv(const CScript& scriptPubKey, UniValue& out, bool include CTxDestination address; out.pushKV("asm", ScriptToAsmStr(scriptPubKey)); - out.pushKV("desc", InferDescriptor(scriptPubKey, DUMMY_SIGNING_PROVIDER)->ToString()); + if (include_address) { + out.pushKV("desc", InferDescriptor(scriptPubKey, DUMMY_SIGNING_PROVIDER)->ToString()); + } if (include_hex) out.pushKV("hex", HexStr(scriptPubKey)); std::vector<std::vector<unsigned char>> solns; @@ -51,12 +51,26 @@ public: // Disallow std::string conversion method to avoid locale-dependent encoding on windows. std::string string() const = delete; + std::string u8string() const + { + const auto& utf8_str{std::filesystem::path::u8string()}; + // utf8_str might either be std::string (C++17) or std::u8string + // (C++20). Convert both to std::string. This method can be removed + // after switching to C++20. + return std::string{utf8_str.begin(), utf8_str.end()}; + } + // Required for path overloads in <fstream>. // See https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=96e0367ead5d8dcac3bec2865582e76e2fbab190 path& make_preferred() { std::filesystem::path::make_preferred(); return *this; } path filename() const { return std::filesystem::path::filename(); } }; +static inline path u8path(const std::string& utf8_str) +{ + return std::filesystem::u8path(utf8_str); +} + // Disallow implicit std::string conversion for absolute to avoid // locale-dependent encoding on windows. static inline path absolute(const path& p) @@ -116,8 +130,8 @@ static inline std::string PathToString(const path& path) // use here, because these methods encode the path using C++'s narrow // multibyte encoding, which on Windows corresponds to the current "code // page", which is unpredictable and typically not able to represent all - // valid paths. So std::filesystem::path::u8string() and - // std::filesystem::u8path() functions are used instead on Windows. On + // valid paths. So fs::path::u8string() and + // fs::u8path() functions are used instead on Windows. On // POSIX, u8string/u8path functions are not safe to use because paths are // not always valid UTF-8, so plain string methods which do not transform // the path there are used. diff --git a/src/init.cpp b/src/init.cpp index e181beab63..de78fdb7b4 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -444,7 +444,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-asmap=<file>", strprintf("Specify asn mapping used for bucketing of the peers (default: %s). Relative paths will be prefixed by the net-specific datadir location.", DEFAULT_ASMAP_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=<n>", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bind=<addr>[:<port>][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, signet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), signetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); - argsman.AddArg("-cjdnsreachable", "If set then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + argsman.AddArg("-cjdnsreachable", "If set, then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network, see doc/cjdns.md) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=<ip>", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-dns", strprintf("Allow DNS lookups for -addnode, -seednode and -connect (default: %u)", DEFAULT_NAME_LOOKUP), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -853,12 +853,14 @@ bool AppInitParameterInteraction(const ArgsManager& args) nLocalServices = ServiceFlags(nLocalServices | NODE_COMPACT_FILTERS); } - // if using block pruning, then disallow txindex and coinstatsindex if (args.GetIntArg("-prune", 0)) { if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) return InitError(_("Prune mode is incompatible with -txindex.")); if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX)) return InitError(_("Prune mode is incompatible with -coinstatsindex.")); + if (args.GetBoolArg("-reindex-chainstate", false)) { + return InitError(_("Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead.")); + } } // If -forcednsseed is set to true, ensure -dnsseed has not been set to false @@ -1300,6 +1302,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } for (int n = 0; n < NET_MAX; n++) { enum Network net = (enum Network)n; + assert(IsReachable(net)); if (!nets.count(net)) SetReachable(net, false); } @@ -99,15 +99,22 @@ struct AddedNodeInfo class CNodeStats; class CClientUIInterface; -struct CSerializedNetMsg -{ +struct CSerializedNetMsg { CSerializedNetMsg() = default; CSerializedNetMsg(CSerializedNetMsg&&) = default; CSerializedNetMsg& operator=(CSerializedNetMsg&&) = default; - // No copying, only moves. + // No implicit copying, only moves. CSerializedNetMsg(const CSerializedNetMsg& msg) = delete; CSerializedNetMsg& operator=(const CSerializedNetMsg&) = delete; + CSerializedNetMsg Copy() const + { + CSerializedNetMsg copy; + copy.data = data; + copy.m_type = m_type; + return copy; + } + std::vector<unsigned char> data; std::string m_type; }; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 77efac3364..34dd7991be 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1624,7 +1624,7 @@ void PeerManagerImpl::NewPoWValidBlock(const CBlockIndex *pindex, const std::sha hashBlock.ToString(), pnode->GetId()); const CSerializedNetMsg& ser_cmpctblock{lazy_ser.get()}; - m_connman.PushMessage(pnode, CSerializedNetMsg{ser_cmpctblock.data, ser_cmpctblock.m_type}); + m_connman.PushMessage(pnode, ser_cmpctblock.Copy()); state.pindexBestHeaderSent = pindex; } }); diff --git a/src/node/miner.h b/src/node/miner.h index 97c55f2864..5fd9abc280 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -45,7 +45,7 @@ struct CTxMemPoolModifiedEntry { nSigOpCostWithAncestors = entry->GetSigOpCostWithAncestors(); } - int64_t GetModifiedFee() const { return iter->GetModifiedFee(); } + CAmount GetModifiedFee() const { return iter->GetModifiedFee(); } uint64_t GetSizeWithAncestors() const { return nSizeWithAncestors; } CAmount GetModFeesWithAncestors() const { return nModFeesWithAncestors; } size_t GetTxSize() const { return iter->GetTxSize(); } diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 5d9ed5bf23..057767eb26 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -151,8 +151,11 @@ void OptionsModel::Init(bool resetSettings) if (!settings.contains("fListen")) settings.setValue("fListen", DEFAULT_LISTEN); - if (!gArgs.SoftSetBoolArg("-listen", settings.value("fListen").toBool())) + if (!gArgs.SoftSetBoolArg("-listen", settings.value("fListen").toBool())) { addOverriddenOption("-listen"); + } else if (!settings.value("fListen").toBool()) { + gArgs.SoftSetBoolArg("-listenonion", false); + } if (!settings.contains("server")) { settings.setValue("server", false); diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 1ef531b293..8e60e82f35 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -11,7 +11,6 @@ #include <core_io.h> #include <index/txindex.h> #include <key_io.h> -#include <merkleblock.h> #include <node/blockstorage.h> #include <node/coin.h> #include <node/context.h> @@ -268,155 +267,6 @@ static RPCHelpMan getrawtransaction() }; } -static RPCHelpMan gettxoutproof() -{ - return RPCHelpMan{"gettxoutproof", - "\nReturns a hex-encoded proof that \"txid\" was included in a block.\n" - "\nNOTE: By default this function only works sometimes. This is when there is an\n" - "unspent output in the utxo for this transaction. To make it always work,\n" - "you need to maintain a transaction index, using the -txindex command line option or\n" - "specify the block in which the transaction is included manually (by blockhash).\n", - { - {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The txids to filter", - { - {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction hash"}, - }, - }, - {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "If specified, looks for txid in the block with this hash"}, - }, - RPCResult{ - RPCResult::Type::STR, "data", "A string that is a serialized, hex-encoded data for the proof." - }, - RPCExamples{""}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::set<uint256> setTxids; - UniValue txids = request.params[0].get_array(); - if (txids.empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'txids' cannot be empty"); - } - for (unsigned int idx = 0; idx < txids.size(); idx++) { - auto ret = setTxids.insert(ParseHashV(txids[idx], "txid")); - if (!ret.second) { - throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated txid: ") + txids[idx].get_str()); - } - } - - const CBlockIndex* pblockindex = nullptr; - uint256 hashBlock; - ChainstateManager& chainman = EnsureAnyChainman(request.context); - if (!request.params[1].isNull()) { - LOCK(cs_main); - hashBlock = ParseHashV(request.params[1], "blockhash"); - pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); - if (!pblockindex) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); - } - } else { - LOCK(cs_main); - CChainState& active_chainstate = chainman.ActiveChainstate(); - - // Loop through txids and try to find which block they're in. Exit loop once a block is found. - for (const auto& tx : setTxids) { - const Coin& coin = AccessByTxid(active_chainstate.CoinsTip(), tx); - if (!coin.IsSpent()) { - pblockindex = active_chainstate.m_chain[coin.nHeight]; - break; - } - } - } - - - // Allow txindex to catch up if we need to query it and before we acquire cs_main. - if (g_txindex && !pblockindex) { - g_txindex->BlockUntilSyncedToCurrentChain(); - } - - LOCK(cs_main); - - if (pblockindex == nullptr) { - const CTransactionRef tx = GetTransaction(/* block_index */ nullptr, /* mempool */ nullptr, *setTxids.begin(), Params().GetConsensus(), hashBlock); - if (!tx || hashBlock.IsNull()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block"); - } - pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); - if (!pblockindex) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt"); - } - } - - CBlock block; - if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); - } - - unsigned int ntxFound = 0; - for (const auto& tx : block.vtx) { - if (setTxids.count(tx->GetHash())) { - ntxFound++; - } - } - if (ntxFound != setTxids.size()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block"); - } - - CDataStream ssMB(SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); - CMerkleBlock mb(block, setTxids); - ssMB << mb; - std::string strHex = HexStr(ssMB); - return strHex; -}, - }; -} - -static RPCHelpMan verifytxoutproof() -{ - return RPCHelpMan{"verifytxoutproof", - "\nVerifies that a proof points to a transaction in a block, returning the transaction it commits to\n" - "and throwing an RPC error if the block is not in our best chain\n", - { - {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded proof generated by gettxoutproof"}, - }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::STR_HEX, "txid", "The txid(s) which the proof commits to, or empty array if the proof cannot be validated."}, - } - }, - RPCExamples{""}, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - CDataStream ssMB(ParseHexV(request.params[0], "proof"), SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); - CMerkleBlock merkleBlock; - ssMB >> merkleBlock; - - UniValue res(UniValue::VARR); - - std::vector<uint256> vMatch; - std::vector<unsigned int> vIndex; - if (merkleBlock.txn.ExtractMatches(vMatch, vIndex) != merkleBlock.header.hashMerkleRoot) - return res; - - ChainstateManager& chainman = EnsureAnyChainman(request.context); - LOCK(cs_main); - - const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(merkleBlock.header.GetHash()); - if (!pindex || !chainman.ActiveChain().Contains(pindex) || pindex->nTx == 0) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain"); - } - - // Check if proof is valid, only add results if so - if (pindex->nTx == merkleBlock.txn.GetNumTransactions()) { - for (const uint256& hash : vMatch) { - res.push_back(hash.GetHex()); - } - } - - return res; -}, - }; -} - static RPCHelpMan createrawtransaction() { return RPCHelpMan{"createrawtransaction", @@ -1121,6 +971,7 @@ static RPCHelpMan decodepsbt() {RPCResult::Type::OBJ, "scriptPubKey", "", { {RPCResult::Type::STR, "asm", "The asm"}, + {RPCResult::Type::STR, "desc", "Inferred descriptor for the output"}, {RPCResult::Type::STR_HEX, "hex", "The hex"}, {RPCResult::Type::STR, "type", "The type, eg 'pubkeyhash'"}, {RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"}, @@ -2089,9 +1940,6 @@ static const CRPCCommand commands[] = { "rawtransactions", &utxoupdatepsbt, }, { "rawtransactions", &joinpsbts, }, { "rawtransactions", &analyzepsbt, }, - - { "blockchain", &gettxoutproof, }, - { "blockchain", &verifytxoutproof, }, }; // clang-format on for (const auto& c : commands) { diff --git a/src/rpc/register.h b/src/rpc/register.h index cc3a5e0a63..5a604ad428 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -9,25 +9,20 @@ * headers for everything under src/rpc/ */ class CRPCTable; -/** Register block chain RPC commands */ void RegisterBlockchainRPCCommands(CRPCTable &tableRPC); -/** Register mempool RPC commands */ void RegisterMempoolRPCCommands(CRPCTable&); -/** Register P2P networking RPC commands */ +void RegisterTxoutProofRPCCommands(CRPCTable&); void RegisterNetRPCCommands(CRPCTable &tableRPC); -/** Register miscellaneous RPC commands */ void RegisterMiscRPCCommands(CRPCTable &tableRPC); -/** Register mining RPC commands */ void RegisterMiningRPCCommands(CRPCTable &tableRPC); -/** Register raw transaction RPC commands */ void RegisterRawTransactionRPCCommands(CRPCTable &tableRPC); -/** Register raw transaction RPC commands */ void RegisterSignerRPCCommands(CRPCTable &tableRPC); static inline void RegisterAllCoreRPCCommands(CRPCTable &t) { RegisterBlockchainRPCCommands(t); RegisterMempoolRPCCommands(t); + RegisterTxoutProofRPCCommands(t); RegisterNetRPCCommands(t); RegisterMiscRPCCommands(t); RegisterMiningRPCCommands(t); diff --git a/src/rpc/txoutproof.cpp b/src/rpc/txoutproof.cpp new file mode 100644 index 0000000000..2700fb400c --- /dev/null +++ b/src/rpc/txoutproof.cpp @@ -0,0 +1,183 @@ +// Copyright (c) 2010 Satoshi Nakamoto +// Copyright (c) 2009-2022 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 <chain.h> +#include <chainparams.h> +#include <coins.h> +#include <index/txindex.h> +#include <merkleblock.h> +#include <node/blockstorage.h> +#include <primitives/transaction.h> +#include <rpc/server.h> +#include <rpc/server_util.h> +#include <rpc/util.h> +#include <univalue.h> +#include <util/strencodings.h> +#include <validation.h> + +using node::GetTransaction; +using node::ReadBlockFromDisk; + +static RPCHelpMan gettxoutproof() +{ + return RPCHelpMan{"gettxoutproof", + "\nReturns a hex-encoded proof that \"txid\" was included in a block.\n" + "\nNOTE: By default this function only works sometimes. This is when there is an\n" + "unspent output in the utxo for this transaction. To make it always work,\n" + "you need to maintain a transaction index, using the -txindex command line option or\n" + "specify the block in which the transaction is included manually (by blockhash).\n", + { + {"txids", RPCArg::Type::ARR, RPCArg::Optional::NO, "The txids to filter", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A transaction hash"}, + }, + }, + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "If specified, looks for txid in the block with this hash"}, + }, + RPCResult{ + RPCResult::Type::STR, "data", "A string that is a serialized, hex-encoded data for the proof." + }, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + std::set<uint256> setTxids; + UniValue txids = request.params[0].get_array(); + if (txids.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Parameter 'txids' cannot be empty"); + } + for (unsigned int idx = 0; idx < txids.size(); idx++) { + auto ret = setTxids.insert(ParseHashV(txids[idx], "txid")); + if (!ret.second) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, duplicated txid: ") + txids[idx].get_str()); + } + } + + const CBlockIndex* pblockindex = nullptr; + uint256 hashBlock; + ChainstateManager& chainman = EnsureAnyChainman(request.context); + if (!request.params[1].isNull()) { + LOCK(cs_main); + hashBlock = ParseHashV(request.params[1], "blockhash"); + pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); + if (!pblockindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + } else { + LOCK(cs_main); + CChainState& active_chainstate = chainman.ActiveChainstate(); + + // Loop through txids and try to find which block they're in. Exit loop once a block is found. + for (const auto& tx : setTxids) { + const Coin& coin = AccessByTxid(active_chainstate.CoinsTip(), tx); + if (!coin.IsSpent()) { + pblockindex = active_chainstate.m_chain[coin.nHeight]; + break; + } + } + } + + + // Allow txindex to catch up if we need to query it and before we acquire cs_main. + if (g_txindex && !pblockindex) { + g_txindex->BlockUntilSyncedToCurrentChain(); + } + + LOCK(cs_main); + + if (pblockindex == nullptr) { + const CTransactionRef tx = GetTransaction(/* block_index */ nullptr, /* mempool */ nullptr, *setTxids.begin(), Params().GetConsensus(), hashBlock); + if (!tx || hashBlock.IsNull()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not yet in block"); + } + pblockindex = chainman.m_blockman.LookupBlockIndex(hashBlock); + if (!pblockindex) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Transaction index corrupt"); + } + } + + CBlock block; + if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); + } + + unsigned int ntxFound = 0; + for (const auto& tx : block.vtx) { + if (setTxids.count(tx->GetHash())) { + ntxFound++; + } + } + if (ntxFound != setTxids.size()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block"); + } + + CDataStream ssMB(SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); + CMerkleBlock mb(block, setTxids); + ssMB << mb; + std::string strHex = HexStr(ssMB); + return strHex; + }, + }; +} + +static RPCHelpMan verifytxoutproof() +{ + return RPCHelpMan{"verifytxoutproof", + "\nVerifies that a proof points to a transaction in a block, returning the transaction it commits to\n" + "and throwing an RPC error if the block is not in our best chain\n", + { + {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex-encoded proof generated by gettxoutproof"}, + }, + RPCResult{ + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::STR_HEX, "txid", "The txid(s) which the proof commits to, or empty array if the proof cannot be validated."}, + } + }, + RPCExamples{""}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue + { + CDataStream ssMB(ParseHexV(request.params[0], "proof"), SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); + CMerkleBlock merkleBlock; + ssMB >> merkleBlock; + + UniValue res(UniValue::VARR); + + std::vector<uint256> vMatch; + std::vector<unsigned int> vIndex; + if (merkleBlock.txn.ExtractMatches(vMatch, vIndex) != merkleBlock.header.hashMerkleRoot) + return res; + + ChainstateManager& chainman = EnsureAnyChainman(request.context); + LOCK(cs_main); + + const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(merkleBlock.header.GetHash()); + if (!pindex || !chainman.ActiveChain().Contains(pindex) || pindex->nTx == 0) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found in chain"); + } + + // Check if proof is valid, only add results if so + if (pindex->nTx == merkleBlock.txn.GetNumTransactions()) { + for (const uint256& hash : vMatch) { + res.push_back(hash.GetHex()); + } + } + + return res; + }, + }; +} + +void RegisterTxoutProofRPCCommands(CRPCTable& t) +{ + static const CRPCCommand commands[]{ + // category actor (function) + // -------- ---------------- + {"blockchain", &gettxoutproof}, + {"blockchain", &verifytxoutproof}, + }; + for (const auto& c : commands) { + t.appendCommand(c.name, &c); + } +} diff --git a/src/scheduler.cpp b/src/scheduler.cpp index 0b2ad3c553..197d009f7c 100644 --- a/src/scheduler.cpp +++ b/src/scheduler.cpp @@ -111,7 +111,7 @@ static void Repeat(CScheduler& s, CScheduler::Function f, std::chrono::milliseco void CScheduler::scheduleEvery(CScheduler::Function f, std::chrono::milliseconds delta) { - scheduleFromNow([=] { Repeat(*this, f, delta); }, delta); + scheduleFromNow([this, f, delta] { Repeat(*this, f, delta); }, delta); } size_t CScheduler::getQueueInfo(std::chrono::system_clock::time_point& first, diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index fcb1a80765..e7c01bd6d0 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -732,26 +732,31 @@ BOOST_AUTO_TEST_CASE(LimitedAndReachable_Network) BOOST_CHECK(IsReachable(NET_IPV6)); BOOST_CHECK(IsReachable(NET_ONION)); BOOST_CHECK(IsReachable(NET_I2P)); + BOOST_CHECK(IsReachable(NET_CJDNS)); SetReachable(NET_IPV4, false); SetReachable(NET_IPV6, false); SetReachable(NET_ONION, false); SetReachable(NET_I2P, false); + SetReachable(NET_CJDNS, false); BOOST_CHECK(!IsReachable(NET_IPV4)); BOOST_CHECK(!IsReachable(NET_IPV6)); BOOST_CHECK(!IsReachable(NET_ONION)); BOOST_CHECK(!IsReachable(NET_I2P)); + BOOST_CHECK(!IsReachable(NET_CJDNS)); SetReachable(NET_IPV4, true); SetReachable(NET_IPV6, true); SetReachable(NET_ONION, true); SetReachable(NET_I2P, true); + SetReachable(NET_CJDNS, true); BOOST_CHECK(IsReachable(NET_IPV4)); BOOST_CHECK(IsReachable(NET_IPV6)); BOOST_CHECK(IsReachable(NET_ONION)); BOOST_CHECK(IsReachable(NET_I2P)); + BOOST_CHECK(IsReachable(NET_CJDNS)); } BOOST_AUTO_TEST_CASE(LimitedAndReachable_NetworkCaseUnroutableAndInternal) diff --git a/src/txmempool.cpp b/src/txmempool.cpp index fb5652d0a0..33918cceab 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -54,16 +54,6 @@ struct update_ancestor_state int64_t modifySigOpsCost; }; -struct update_fee_delta -{ - explicit update_fee_delta(int64_t _feeDelta) : feeDelta(_feeDelta) { } - - void operator() (CTxMemPoolEntry &e) { e.UpdateFeeDelta(feeDelta); } - -private: - int64_t feeDelta; -}; - bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp) { AssertLockHeld(cs_main); @@ -99,7 +89,7 @@ CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& tx, CAmount fee, nModFeesWithAncestors{nFee}, nSigOpCostWithAncestors{sigOpCost} {} -void CTxMemPoolEntry::UpdateFeeDelta(int64_t newFeeDelta) +void CTxMemPoolEntry::UpdateFeeDelta(CAmount newFeeDelta) { nModFeesWithDescendants += newFeeDelta - feeDelta; nModFeesWithAncestors += newFeeDelta - feeDelta; @@ -496,7 +486,7 @@ void CTxMemPool::addUnchecked(const CTxMemPoolEntry &entry, setEntries &setAnces CAmount delta{0}; ApplyDelta(entry.GetTx().GetHash(), delta); if (delta) { - mapTx.modify(newit, update_fee_delta(delta)); + mapTx.modify(newit, [&delta](CTxMemPoolEntry& e) { e.UpdateFeeDelta(delta); }); } // Update cachedInnerUsage to include contained transaction's usage. @@ -931,7 +921,7 @@ void CTxMemPool::PrioritiseTransaction(const uint256& hash, const CAmount& nFeeD delta += nFeeDelta; txiter it = mapTx.find(hash); if (it != mapTx.end()) { - mapTx.modify(it, update_fee_delta(delta)); + mapTx.modify(it, [&delta](CTxMemPoolEntry& e) { e.UpdateFeeDelta(delta); }); // Now update all ancestors' modified fees with descendants setEntries setAncestors; uint64_t nNoLimit = std::numeric_limits<uint64_t>::max(); diff --git a/src/txmempool.h b/src/txmempool.h index e7e5a3c402..f5d5abc62e 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -101,7 +101,7 @@ private: const unsigned int entryHeight; //!< Chain height when entering the mempool const bool spendsCoinbase; //!< keep track of transactions that spend a coinbase const int64_t sigOpCost; //!< Total sigop cost - int64_t feeDelta{0}; //!< Used for determining the priority of the transaction for mining in a block + CAmount feeDelta{0}; //!< Used for determining the priority of the transaction for mining in a block LockPoints lockPoints; //!< Track the height and time at which tx was final // Information about descendants of this transaction that are in the @@ -131,7 +131,7 @@ public: std::chrono::seconds GetTime() const { return std::chrono::seconds{nTime}; } unsigned int GetHeight() const { return entryHeight; } int64_t GetSigOpCost() const { return sigOpCost; } - int64_t GetModifiedFee() const { return nFee + feeDelta; } + CAmount GetModifiedFee() const { return nFee + feeDelta; } size_t DynamicMemoryUsage() const { return nUsageSize; } const LockPoints& GetLockPoints() const { return lockPoints; } @@ -140,8 +140,8 @@ public: // Adjusts the ancestor state void UpdateAncestorState(int64_t modifySize, CAmount modifyFee, int64_t modifyCount, int64_t modifySigOps); // Updates the fee delta used for mining priority score, and the - // modified fees with descendants. - void UpdateFeeDelta(int64_t feeDelta); + // modified fees with descendants/ancestors. + void UpdateFeeDelta(CAmount newFeeDelta); // Update the LockPoints after a reorg void UpdateLockPoints(const LockPoints& lp); diff --git a/src/wallet/bdb.cpp b/src/wallet/bdb.cpp index 49f0abf9e7..0d0456af4b 100644 --- a/src/wallet/bdb.cpp +++ b/src/wallet/bdb.cpp @@ -60,12 +60,12 @@ bool WalletDatabaseFileId::operator==(const WalletDatabaseFileId& rhs) const * erases the weak pointer from the g_dbenvs map. * @post A new BerkeleyEnvironment weak pointer is inserted into g_dbenvs if the directory path key was not already in the map. */ -std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory) +std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory, bool use_shared_memory) { LOCK(cs_db); auto inserted = g_dbenvs.emplace(fs::PathToString(env_directory), std::weak_ptr<BerkeleyEnvironment>()); if (inserted.second) { - auto env = std::make_shared<BerkeleyEnvironment>(env_directory); + auto env = std::make_shared<BerkeleyEnvironment>(env_directory, use_shared_memory); inserted.first->second = env; return env; } @@ -113,7 +113,7 @@ void BerkeleyEnvironment::Reset() fMockDb = false; } -BerkeleyEnvironment::BerkeleyEnvironment(const fs::path& dir_path) : strPath(fs::PathToString(dir_path)) +BerkeleyEnvironment::BerkeleyEnvironment(const fs::path& dir_path, bool use_shared_memory) : strPath(fs::PathToString(dir_path)), m_use_shared_memory(use_shared_memory) { Reset(); } @@ -145,8 +145,9 @@ bool BerkeleyEnvironment::Open(bilingual_str& err) LogPrintf("BerkeleyEnvironment::Open: LogDir=%s ErrorFile=%s\n", fs::PathToString(pathLogDir), fs::PathToString(pathErrorFile)); unsigned int nEnvFlags = 0; - if (gArgs.GetBoolArg("-privdb", DEFAULT_WALLET_PRIVDB)) + if (!m_use_shared_memory) { nEnvFlags |= DB_PRIVATE; + } dbenv->set_lg_dir(fs::PathToString(pathLogDir).c_str()); dbenv->set_cachesize(0, 0x100000, 1); // 1 MiB should be enough for just the wallet @@ -188,7 +189,7 @@ bool BerkeleyEnvironment::Open(bilingual_str& err) } //! Construct an in-memory mock Berkeley environment for testing -BerkeleyEnvironment::BerkeleyEnvironment() +BerkeleyEnvironment::BerkeleyEnvironment() : m_use_shared_memory(false) { Reset(); @@ -377,7 +378,7 @@ void BerkeleyBatch::Flush() nMinutes = 1; if (env) { // env is nullptr for dummy databases (i.e. in tests). Don't actually flush if env is nullptr so we don't segfault - env->dbenv->txn_checkpoint(nMinutes ? gArgs.GetIntArg("-dblogsize", DEFAULT_WALLET_DBLOGSIZE) * 1024 : 0, nMinutes, 0); + env->dbenv->txn_checkpoint(nMinutes ? m_database.m_max_log_mb * 1024 : 0, nMinutes, 0); } } @@ -831,13 +832,13 @@ std::unique_ptr<BerkeleyDatabase> MakeBerkeleyDatabase(const fs::path& path, con { LOCK(cs_db); // Lock env.m_databases until insert in BerkeleyDatabase constructor std::string data_filename = fs::PathToString(data_file.filename()); - std::shared_ptr<BerkeleyEnvironment> env = GetBerkeleyEnv(data_file.parent_path()); + std::shared_ptr<BerkeleyEnvironment> env = GetBerkeleyEnv(data_file.parent_path(), options.use_shared_memory); if (env->m_databases.count(data_filename)) { error = Untranslated(strprintf("Refusing to load database. Data file '%s' is already loaded.", fs::PathToString(env->Directory() / data_filename))); status = DatabaseStatus::FAILED_ALREADY_LOADED; return nullptr; } - db = std::make_unique<BerkeleyDatabase>(std::move(env), std::move(data_filename)); + db = std::make_unique<BerkeleyDatabase>(std::move(env), std::move(data_filename), options); } if (options.verify && !db->Verify(error)) { diff --git a/src/wallet/bdb.h b/src/wallet/bdb.h index b924890d81..fd6c76183e 100644 --- a/src/wallet/bdb.h +++ b/src/wallet/bdb.h @@ -32,8 +32,6 @@ struct bilingual_str; namespace wallet { -static const unsigned int DEFAULT_WALLET_DBLOGSIZE = 100; -static const bool DEFAULT_WALLET_PRIVDB = true; struct WalletDatabaseFileId { u_int8_t value[DB_FILE_ID_LEN]; @@ -56,8 +54,9 @@ public: std::map<std::string, std::reference_wrapper<BerkeleyDatabase>> m_databases; std::unordered_map<std::string, WalletDatabaseFileId> m_fileids; std::condition_variable_any m_db_in_use; + bool m_use_shared_memory; - explicit BerkeleyEnvironment(const fs::path& env_directory); + explicit BerkeleyEnvironment(const fs::path& env_directory, bool use_shared_memory); BerkeleyEnvironment(); ~BerkeleyEnvironment(); void Reset(); @@ -85,7 +84,7 @@ public: }; /** Get BerkeleyEnvironment given a directory path. */ -std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory); +std::shared_ptr<BerkeleyEnvironment> GetBerkeleyEnv(const fs::path& env_directory, bool use_shared_memory); class BerkeleyBatch; @@ -98,8 +97,8 @@ public: BerkeleyDatabase() = delete; /** Create DB handle to real database */ - BerkeleyDatabase(std::shared_ptr<BerkeleyEnvironment> env, std::string filename) : - WalletDatabase(), env(std::move(env)), strFile(std::move(filename)) + BerkeleyDatabase(std::shared_ptr<BerkeleyEnvironment> env, std::string filename, const DatabaseOptions& options) : + WalletDatabase(), env(std::move(env)), strFile(std::move(filename)), m_max_log_mb(options.max_log_mb) { auto inserted = this->env->m_databases.emplace(strFile, std::ref(*this)); assert(inserted.second); @@ -160,6 +159,7 @@ public: std::unique_ptr<Db> m_db; std::string strFile; + int64_t m_max_log_mb; /** Make a BerkeleyBatch connected to this database */ std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override; diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index 20891a3e28..38c5939232 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -50,8 +50,8 @@ struct { * The Branch and Bound algorithm is described in detail in Murch's Master Thesis: * https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf * - * @param const std::vector<CInputCoin>& utxo_pool The set of UTXOs that we are choosing from. - * These UTXOs will be sorted in descending order by effective value and the CInputCoins' + * @param const std::vector<OutputGroup>& utxo_pool The set of UTXO groups that we are choosing from. + * These UTXO groups will be sorted in descending order by effective value and the OutputGroups' * values are their effective values. * @param const CAmount& selection_target This is the value that we want to select. It is the lower * bound of the range. @@ -165,14 +165,14 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo return result; } -std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value) +std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng) { SelectionResult result(target_value); std::vector<size_t> indexes; indexes.resize(utxo_pool.size()); std::iota(indexes.begin(), indexes.end(), 0); - Shuffle(indexes.begin(), indexes.end(), FastRandomContext()); + Shuffle(indexes.begin(), indexes.end(), rng); CAmount selected_eff_value = 0; for (const size_t i : indexes) { @@ -187,7 +187,7 @@ std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& ut return std::nullopt; } -static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue, +static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue, std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000) { std::vector<char> vfIncluded; @@ -195,8 +195,6 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const vfBest.assign(groups.size(), true); nBest = nTotalLower; - FastRandomContext insecure_rand; - for (int nRep = 0; nRep < iterations && nBest != nTargetValue; nRep++) { vfIncluded.assign(groups.size(), false); @@ -233,7 +231,7 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const } } -std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue) +std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng) { SelectionResult result(nTargetValue); @@ -242,7 +240,7 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, std::vector<OutputGroup> applicable_groups; CAmount nTotalLower = 0; - Shuffle(groups.begin(), groups.end(), FastRandomContext()); + Shuffle(groups.begin(), groups.end(), rng); for (const OutputGroup& group : groups) { if (group.GetSelectionAmount() == nTargetValue) { @@ -274,9 +272,9 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, std::vector<char> vfBest; CAmount nBest; - ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue, vfBest, nBest); + ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest); if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) { - ApproximateBestSubset(applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest); + ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest); } // If we have a bigger coin and (either the stochastic approximation didn't find a good solution, @@ -311,29 +309,29 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, ******************************************************************************/ -void OutputGroup::Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only) { +void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only) { // Compute the effective value first - const CAmount coin_fee = output.m_input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.m_input_bytes); + const CAmount coin_fee = output.input_bytes < 0 ? 0 : m_effective_feerate.GetFee(output.input_bytes); const CAmount ev = output.txout.nValue - coin_fee; // Filter for positive only here before adding the coin if (positive_only && ev <= 0) return; m_outputs.push_back(output); - CInputCoin& coin = m_outputs.back(); + COutput& coin = m_outputs.back(); - coin.m_fee = coin_fee; - fee += coin.m_fee; + coin.fee = coin_fee; + fee += coin.fee; - coin.m_long_term_fee = coin.m_input_bytes < 0 ? 0 : m_long_term_feerate.GetFee(coin.m_input_bytes); - long_term_fee += coin.m_long_term_fee; + coin.long_term_fee = coin.input_bytes < 0 ? 0 : m_long_term_feerate.GetFee(coin.input_bytes); + long_term_fee += coin.long_term_fee; coin.effective_value = ev; effective_value += coin.effective_value; - m_from_me &= from_me; - m_value += output.txout.nValue; - m_depth = std::min(m_depth, depth); + m_from_me &= coin.from_me; + m_value += coin.txout.nValue; + m_depth = std::min(m_depth, coin.depth); // ancestors here express the number of ancestors the new coin will end up having, which is // the sum, rather than the max; this will overestimate in the cases where multiple inputs // have common ancestors @@ -355,7 +353,7 @@ CAmount OutputGroup::GetSelectionAmount() const return m_subtract_fee_outputs ? m_value : effective_value; } -CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value) +CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value) { // This function should not be called with empty inputs as that would mean the selection failed assert(!inputs.empty()); @@ -363,8 +361,8 @@ CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cos // Always consider the cost of spending an input now vs in the future. CAmount waste = 0; CAmount selected_effective_value = 0; - for (const CInputCoin& coin : inputs) { - waste += coin.m_fee - coin.m_long_term_fee; + for (const COutput& coin : inputs) { + waste += coin.fee - coin.long_term_fee; selected_effective_value += use_effective_value ? coin.effective_value : coin.txout.nValue; } @@ -409,14 +407,14 @@ void SelectionResult::AddInput(const OutputGroup& group) m_use_effective = !group.m_subtract_fee_outputs; } -const std::set<CInputCoin>& SelectionResult::GetInputSet() const +const std::set<COutput>& SelectionResult::GetInputSet() const { return m_selected_inputs; } -std::vector<CInputCoin> SelectionResult::GetShuffledInputVector() const +std::vector<COutput> SelectionResult::GetShuffledInputVector() const { - std::vector<CInputCoin> coins(m_selected_inputs.begin(), m_selected_inputs.end()); + std::vector<COutput> coins(m_selected_inputs.begin(), m_selected_inputs.end()); Shuffle(coins.begin(), coins.end(), FastRandomContext()); return coins; } @@ -428,4 +426,9 @@ bool SelectionResult::operator<(SelectionResult other) const // As this operator is only used in std::min_element, we want the result that has more inputs when waste are equal. return *m_waste < *other.m_waste || (*m_waste == *other.m_waste && m_selected_inputs.size() > other.m_selected_inputs.size()); } + +std::string COutput::ToString() const +{ + return strprintf("COutput(%s, %d, %d) [%s]", outpoint.hash.ToString(), outpoint.n, depth, FormatMoney(txout.nValue)); +} } // namespace wallet diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 496a026999..504d57aa78 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -19,62 +19,77 @@ static constexpr CAmount MIN_CHANGE{COIN / 100}; static const CAmount MIN_FINAL_CHANGE = MIN_CHANGE/2; /** A UTXO under consideration for use in funding a new transaction. */ -class CInputCoin { +class COutput +{ public: - CInputCoin(const CTransactionRef& tx, unsigned int i) - { - if (!tx) - throw std::invalid_argument("tx should not be null"); - if (i >= tx->vout.size()) - throw std::out_of_range("The output index is out of range"); - - outpoint = COutPoint(tx->GetHash(), i); - txout = tx->vout[i]; - effective_value = txout.nValue; - } + /** The outpoint identifying this UTXO */ + COutPoint outpoint; - CInputCoin(const CTransactionRef& tx, unsigned int i, int input_bytes) : CInputCoin(tx, i) - { - m_input_bytes = input_bytes; - } + /** The output itself */ + CTxOut txout; - CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in) - { - outpoint = outpoint_in; - txout = txout_in; - effective_value = txout.nValue; - } + /** + * Depth in block chain. + * If > 0: the tx is on chain and has this many confirmations. + * If = 0: the tx is waiting confirmation. + * If < 0: a conflicting tx is on chain and has this many confirmations. */ + int depth; - CInputCoin(const COutPoint& outpoint_in, const CTxOut& txout_in, int input_bytes) : CInputCoin(outpoint_in, txout_in) - { - m_input_bytes = input_bytes; - } + /** Pre-computed estimated size of this output as a fully-signed input in a transaction. Can be -1 if it could not be calculated */ + int input_bytes; - COutPoint outpoint; - CTxOut txout; + /** Whether we have the private keys to spend this output */ + bool spendable; + + /** Whether we know how to spend this output, ignoring the lack of keys */ + bool solvable; + + /** + * Whether this output is considered safe to spend. Unconfirmed transactions + * from outside keys and unconfirmed replacement transactions are considered + * unsafe and will not be used to fund new spending transactions. + */ + bool safe; + + /** The time of the transaction containing this output as determined by CWalletTx::nTimeSmart */ + int64_t time; + + /** Whether the transaction containing this output is sent from the owning wallet */ + bool from_me; + + /** The output's value minus fees required to spend it. Initialized as the output's absolute value. */ CAmount effective_value; - CAmount m_fee{0}; - CAmount m_long_term_fee{0}; - /** Pre-computed estimated size of this output as a fully-signed input in a transaction. Can be -1 if it could not be calculated */ - int m_input_bytes{-1}; + /** The fee required to spend this output at the transaction's target feerate. */ + CAmount fee{0}; - bool operator<(const CInputCoin& rhs) const { - return outpoint < rhs.outpoint; - } + /** The fee required to spend this output at the consolidation feerate. */ + CAmount long_term_fee{0}; - bool operator!=(const CInputCoin& rhs) const { - return outpoint != rhs.outpoint; - } + COutput(const COutPoint& outpoint, const CTxOut& txout, int depth, int input_bytes, bool spendable, bool solvable, bool safe, int64_t time, bool from_me) + : outpoint(outpoint), + txout(txout), + depth(depth), + input_bytes(input_bytes), + spendable(spendable), + solvable(solvable), + safe(safe), + time(time), + from_me(from_me), + effective_value(txout.nValue) + {} - bool operator==(const CInputCoin& rhs) const { - return outpoint == rhs.outpoint; + std::string ToString() const; + + bool operator<(const COutput& rhs) const { + return outpoint < rhs.outpoint; } }; /** Parameters for one iteration of Coin Selection. */ -struct CoinSelectionParams -{ +struct CoinSelectionParams { + /** Randomness to use in the context of coin selection. */ + FastRandomContext& rng_fast; /** Size of a change output in bytes, determined by the output type. */ size_t change_output_size = 0; /** Size of the input to spend a change output in virtual bytes. */ @@ -100,17 +115,20 @@ struct CoinSelectionParams * reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */ bool m_avoid_partial_spends = false; - CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate, - CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) : - change_output_size(change_output_size), - change_spend_size(change_spend_size), - m_effective_feerate(effective_feerate), - m_long_term_feerate(long_term_feerate), - m_discard_feerate(discard_feerate), - tx_noinputs_size(tx_noinputs_size), - m_avoid_partial_spends(avoid_partial) - {} - CoinSelectionParams() {} + CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate, + CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) + : rng_fast{rng_fast}, + change_output_size(change_output_size), + change_spend_size(change_spend_size), + m_effective_feerate(effective_feerate), + m_long_term_feerate(long_term_feerate), + m_discard_feerate(discard_feerate), + tx_noinputs_size(tx_noinputs_size), + m_avoid_partial_spends(avoid_partial) + { + } + CoinSelectionParams(FastRandomContext& rng_fast) + : rng_fast{rng_fast} {} }; /** Parameters for filtering which OutputGroups we may use in coin selection. @@ -139,7 +157,7 @@ struct CoinEligibilityFilter struct OutputGroup { /** The list of UTXOs contained in this output group. */ - std::vector<CInputCoin> m_outputs; + std::vector<COutput> m_outputs; /** Whether the UTXOs were sent by the wallet to itself. This is relevant because we may want at * least a certain number of confirmations on UTXOs received from outside wallets while trusting * our own UTXOs more. */ @@ -176,7 +194,7 @@ struct OutputGroup m_subtract_fee_outputs(params.m_subtract_fee_outputs) {} - void Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only); + void Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only); bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const; CAmount GetSelectionAmount() const; }; @@ -198,13 +216,13 @@ struct OutputGroup * @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false). * @return The waste */ -[[nodiscard]] CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true); +[[nodiscard]] CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true); struct SelectionResult { private: /** Set of inputs selected by the algorithm to use in the transaction */ - std::set<CInputCoin> m_selected_inputs; + std::set<COutput> m_selected_inputs; /** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */ const CAmount m_target; /** Whether the input values for calculations should be the effective value (true) or normal value (false) */ @@ -230,9 +248,9 @@ public: [[nodiscard]] CAmount GetWaste() const; /** Get m_selected_inputs */ - const std::set<CInputCoin>& GetInputSet() const; - /** Get the vector of CInputCoins that will be used to fill in a CTransaction's vin */ - std::vector<CInputCoin> GetShuffledInputVector() const; + const std::set<COutput>& GetInputSet() const; + /** Get the vector of COutputs that will be used to fill in a CTransaction's vin */ + std::vector<COutput> GetShuffledInputVector() const; bool operator<(SelectionResult other) const; }; @@ -246,10 +264,10 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo * @param[in] target_value The target value to select for * @returns If successful, a SelectionResult, otherwise, std::nullopt */ -std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value); +std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng); // Original coin selection algorithm as a fallback -std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue); +std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng); } // namespace wallet #endif // BITCOIN_WALLET_COINSELECTION_H diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 0ed2658129..8e79ee678e 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -6,6 +6,7 @@ #include <chainparams.h> #include <fs.h> #include <logging.h> +#include <util/system.h> #include <wallet/db.h> #include <exception> @@ -137,4 +138,13 @@ bool IsSQLiteFile(const fs::path& path) // Check the application id matches our network magic return memcmp(Params().MessageStart(), app_id, 4) == 0; } + +void ReadDatabaseArgs(const ArgsManager& args, DatabaseOptions& options) +{ + // Override current options with args values, if any were specified + options.use_unsafe_sync = args.GetBoolArg("-unsafesqlitesync", options.use_unsafe_sync); + options.use_shared_memory = !args.GetBoolArg("-privdb", !options.use_shared_memory); + options.max_log_mb = args.GetIntArg("-dblogsize", options.max_log_mb); +} + } // namespace wallet diff --git a/src/wallet/db.h b/src/wallet/db.h index 5825b00e3a..f09844c37e 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -16,6 +16,7 @@ #include <optional> #include <string> +class ArgsManager; struct bilingual_str; namespace wallet { @@ -207,7 +208,12 @@ struct DatabaseOptions { std::optional<DatabaseFormat> require_format; uint64_t create_flags = 0; SecureString create_passphrase; - bool verify = true; + + // Specialized options. Not every option is supported by every backend. + bool verify = true; //!< Check data integrity on load. + bool use_unsafe_sync = false; //!< Disable file sync for faster performance. + bool use_shared_memory = false; //!< Let other processes access the database. + int64_t max_log_mb = 100; //!< Max log size to allow before consolidating. }; enum class DatabaseStatus { @@ -227,6 +233,7 @@ enum class DatabaseStatus { /** Recursively list database paths in directory. */ std::vector<fs::path> ListDatabases(const fs::path& path); +void ReadDatabaseArgs(const ArgsManager& args, DatabaseOptions& options); std::unique_ptr<WalletDatabase> MakeDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); fs::path BDBDataFile(const fs::path& path); diff --git a/src/wallet/dump.cpp b/src/wallet/dump.cpp index 6d8508fc72..d80c3e25b0 100644 --- a/src/wallet/dump.cpp +++ b/src/wallet/dump.cpp @@ -19,10 +19,10 @@ namespace wallet { static const std::string DUMP_MAGIC = "BITCOIN_CORE_WALLET_DUMP"; uint32_t DUMP_VERSION = 1; -bool DumpWallet(CWallet& wallet, bilingual_str& error) +bool DumpWallet(const ArgsManager& args, CWallet& wallet, bilingual_str& error) { // Get the dumpfile - std::string dump_filename = gArgs.GetArg("-dumpfile", ""); + std::string dump_filename = args.GetArg("-dumpfile", ""); if (dump_filename.empty()) { error = _("No dump file provided. To use dump, -dumpfile=<filename> must be provided."); return false; @@ -114,10 +114,10 @@ static void WalletToolReleaseWallet(CWallet* wallet) delete wallet; } -bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings) +bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings) { // Get the dumpfile - std::string dump_filename = gArgs.GetArg("-dumpfile", ""); + std::string dump_filename = args.GetArg("-dumpfile", ""); if (dump_filename.empty()) { error = _("No dump file provided. To use createfromdump, -dumpfile=<filename> must be provided."); return false; @@ -171,7 +171,7 @@ bool CreateFromDump(const std::string& name, const fs::path& wallet_path, biling return false; } // Get the data file format with format_value as the default - std::string file_format = gArgs.GetArg("-format", format_value); + std::string file_format = args.GetArg("-format", format_value); if (file_format.empty()) { error = _("No wallet file format provided. To use createfromdump, -format=<format> must be provided."); return false; @@ -193,6 +193,7 @@ bool CreateFromDump(const std::string& name, const fs::path& wallet_path, biling DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(args, options); options.require_create = true; options.require_format = data_format; std::unique_ptr<WalletDatabase> database = MakeDatabase(wallet_path, options, status, error); diff --git a/src/wallet/dump.h b/src/wallet/dump.h index a879c4db35..bf683e9843 100644 --- a/src/wallet/dump.h +++ b/src/wallet/dump.h @@ -11,11 +11,12 @@ #include <vector> struct bilingual_str; +class ArgsManager; namespace wallet { class CWallet; -bool DumpWallet(CWallet& wallet, bilingual_str& error); -bool CreateFromDump(const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings); +bool DumpWallet(const ArgsManager& args, CWallet& wallet, bilingual_str& error); +bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::path& wallet_path, bilingual_str& error, std::vector<bilingual_str>& warnings); } // namespace wallet #endif // BITCOIN_WALLET_DUMP_H diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index 3552c14160..bc53180c0e 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -233,12 +233,6 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo // Write back transaction mtx = CMutableTransaction(*tx_new); - // Mark new tx not replaceable, if requested. - if (!coin_control.m_signal_bip125_rbf.value_or(wallet.m_signal_rbf)) { - for (auto& input : mtx.vin) { - if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe; - } - } return Result::OK; } diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 7a83dbc35d..7e21126298 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -80,9 +80,9 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const argsman.AddArg("-walletrbf", strprintf("Send transactions with full-RBF opt-in enabled (RPC only, default: %u)", DEFAULT_WALLET_RBF), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); #ifdef USE_BDB - argsman.AddArg("-dblogsize=<n>", strprintf("Flush wallet database activity from memory to disk log every <n> megabytes (default: %u)", DEFAULT_WALLET_DBLOGSIZE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST); + argsman.AddArg("-dblogsize=<n>", strprintf("Flush wallet database activity from memory to disk log every <n> megabytes (default: %u)", DatabaseOptions().max_log_mb), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST); argsman.AddArg("-flushwallet", strprintf("Run a thread to flush wallet periodically (default: %u)", DEFAULT_FLUSHWALLET), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST); - argsman.AddArg("-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db environment (default: %u)", DEFAULT_WALLET_PRIVDB), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST); + argsman.AddArg("-privdb", strprintf("Sets the DB_PRIVATE flag in the wallet db environment (default: %u)", !DatabaseOptions().use_shared_memory), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST); #else argsman.AddHiddenArgs({"-dblogsize", "-flushwallet", "-privdb"}); #endif diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 9083c304b2..98e843385c 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -111,6 +111,17 @@ WalletTxOut MakeWalletTxOut(const CWallet& wallet, return result; } +WalletTxOut MakeWalletTxOut(const CWallet& wallet, + const COutput& output) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +{ + WalletTxOut result; + result.txout = output.txout; + result.time = output.time; + result.depth_in_main_chain = output.depth; + result.is_spent = wallet.IsSpent(output.outpoint.hash, output.outpoint.n); + return result; +} + class WalletImpl : public Wallet { public: @@ -419,8 +430,8 @@ public: for (const auto& entry : ListCoins(*m_wallet)) { auto& group = result[entry.first]; for (const auto& coin : entry.second) { - group.emplace_back(COutPoint(coin.tx->GetHash(), coin.i), - MakeWalletTxOut(*m_wallet, *coin.tx, coin.i, coin.nDepth)); + group.emplace_back(coin.outpoint, + MakeWalletTxOut(*m_wallet, coin)); } } return result; @@ -544,6 +555,7 @@ public: std::shared_ptr<CWallet> wallet; DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(*m_context.args, options); options.require_create = true; options.create_flags = wallet_creation_flags; options.create_passphrase = passphrase; @@ -553,6 +565,7 @@ public: { DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(*m_context.args, options); options.require_existing = true; return MakeWallet(m_context, LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings)); } diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 633d8c5450..98ce95dcd1 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -57,6 +57,7 @@ bool VerifyWallets(WalletContext& context) if (!args.IsArgSet("wallet")) { DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(args, options); bilingual_str error_string; options.require_existing = true; options.verify = false; @@ -84,6 +85,7 @@ bool VerifyWallets(WalletContext& context) DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(args, options); options.require_existing = true; options.verify = true; bilingual_str error_string; @@ -112,6 +114,7 @@ bool LoadWallets(WalletContext& context) } DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(*context.args, options); options.require_existing = true; options.verify = false; // No need to verify, assuming verified earlier in VerifyWallets() bilingual_str error; diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp index 035541babd..781ae3c6e0 100644 --- a/src/wallet/rpc/coins.cpp +++ b/src/wallet/rpc/coins.cpp @@ -648,16 +648,16 @@ RPCHelpMan listunspent() for (const COutput& out : vecOutputs) { CTxDestination address; - const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey; + const CScript& scriptPubKey = out.txout.scriptPubKey; bool fValidAddress = ExtractDestination(scriptPubKey, address); - bool reused = avoid_reuse && pwallet->IsSpentKey(out.tx->GetHash(), out.i); + bool reused = avoid_reuse && pwallet->IsSpentKey(out.outpoint.hash, out.outpoint.n); if (destinations.size() && (!fValidAddress || !destinations.count(address))) continue; UniValue entry(UniValue::VOBJ); - entry.pushKV("txid", out.tx->GetHash().GetHex()); - entry.pushKV("vout", out.i); + entry.pushKV("txid", out.outpoint.hash.GetHex()); + entry.pushKV("vout", (int)out.outpoint.n); if (fValidAddress) { entry.pushKV("address", EncodeDestination(address)); @@ -702,21 +702,21 @@ RPCHelpMan listunspent() } entry.pushKV("scriptPubKey", HexStr(scriptPubKey)); - entry.pushKV("amount", ValueFromAmount(out.tx->tx->vout[out.i].nValue)); - entry.pushKV("confirmations", out.nDepth); - if (!out.nDepth) { + entry.pushKV("amount", ValueFromAmount(out.txout.nValue)); + entry.pushKV("confirmations", out.depth); + if (!out.depth) { size_t ancestor_count, descendant_count, ancestor_size; CAmount ancestor_fees; - pwallet->chain().getTransactionAncestry(out.tx->GetHash(), ancestor_count, descendant_count, &ancestor_size, &ancestor_fees); + pwallet->chain().getTransactionAncestry(out.outpoint.hash, ancestor_count, descendant_count, &ancestor_size, &ancestor_fees); if (ancestor_count) { entry.pushKV("ancestorcount", uint64_t(ancestor_count)); entry.pushKV("ancestorsize", uint64_t(ancestor_size)); entry.pushKV("ancestorfees", uint64_t(ancestor_fees)); } } - entry.pushKV("spendable", out.fSpendable); - entry.pushKV("solvable", out.fSolvable); - if (out.fSolvable) { + entry.pushKV("spendable", out.spendable); + entry.pushKV("solvable", out.solvable); + if (out.solvable) { std::unique_ptr<SigningProvider> provider = pwallet->GetSolvingProvider(scriptPubKey); if (provider) { auto descriptor = InferDescriptor(scriptPubKey, *provider); @@ -724,7 +724,7 @@ RPCHelpMan listunspent() } } if (avoid_reuse) entry.pushKV("reused", reused); - entry.pushKV("safe", out.fSafe); + entry.pushKV("safe", out.safe); results.push_back(entry); } diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 1380f1a77a..804331eb5d 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -8,6 +8,7 @@ #include <rpc/server.h> #include <rpc/util.h> #include <util/translation.h> +#include <wallet/context.h> #include <wallet/receive.h> #include <wallet/rpc/wallet.h> #include <wallet/rpc/util.h> @@ -220,6 +221,7 @@ static RPCHelpMan loadwallet() DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(*context.args, options); options.require_existing = true; bilingual_str error; std::vector<bilingual_str> warnings; @@ -381,6 +383,7 @@ static RPCHelpMan createwallet() DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(*context.args, options); options.require_create = true; options.create_flags = flags; options.create_passphrase = passphrase; diff --git a/src/wallet/salvage.cpp b/src/wallet/salvage.cpp index 1ecc96fe0e..9ba3c7fd2c 100644 --- a/src/wallet/salvage.cpp +++ b/src/wallet/salvage.cpp @@ -23,10 +23,11 @@ static bool KeyFilter(const std::string& type) return WalletBatch::IsKeyType(type) || type == DBKeys::HDCHAIN; } -bool RecoverDatabaseFile(const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings) +bool RecoverDatabaseFile(const ArgsManager& args, const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings) { DatabaseOptions options; DatabaseStatus status; + ReadDatabaseArgs(args, options); options.require_existing = true; options.verify = false; options.require_format = DatabaseFormat::BERKELEY; diff --git a/src/wallet/salvage.h b/src/wallet/salvage.h index 332aceb262..e4822c3c75 100644 --- a/src/wallet/salvage.h +++ b/src/wallet/salvage.h @@ -9,10 +9,11 @@ #include <fs.h> #include <streams.h> +class ArgsManager; struct bilingual_str; namespace wallet { -bool RecoverDatabaseFile(const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings); +bool RecoverDatabaseFile(const ArgsManager& args, const fs::path& file_path, bilingual_str& error, std::vector<bilingual_str>& warnings); } // namespace wallet #endif // BITCOIN_WALLET_SALVAGE_H diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index a707ef89d2..304a9f10ca 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -29,11 +29,6 @@ int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out return CalculateMaximumSignedInputSize(wtx.tx->vout[out], &wallet, use_max_sig); } -std::string COutput::ToString() const -{ - return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); -} - int CalculateMaximumSignedInputSize(const CTxOut& txout, const SigningProvider* provider, bool use_max_sig) { CMutableTransaction txn; @@ -158,6 +153,8 @@ void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const C continue; } + bool tx_from_me = CachedTxIsFromMe(wallet, wtx, ISMINE_ALL); + for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { // Only consider selected coins if add_inputs is false if (coinControl && !coinControl->m_add_inputs && !coinControl->IsSelected(COutPoint(entry.first, i))) { @@ -190,8 +187,9 @@ void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const C bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false; bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); + int input_bytes = GetTxSpendSize(wallet, wtx, i, (coinControl && coinControl->fAllowWatchOnly)); - vCoins.push_back(COutput(wallet, wtx, i, nDepth, spendable, solvable, safeTx, (coinControl && coinControl->fAllowWatchOnly))); + vCoins.emplace_back(COutPoint(wtx.GetHash(), i), wtx.tx->vout.at(i), nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me); // Checks the sum amount of all UTXO's. if (nMinimumSumAmount != MAX_MONEY) { @@ -218,8 +216,8 @@ CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinContr std::vector<COutput> vCoins; AvailableCoins(wallet, vCoins, coinControl); for (const COutput& out : vCoins) { - if (out.fSpendable) { - balance += out.tx->tx->vout[out.i].nValue; + if (out.spendable) { + balance += out.txout.nValue; } } return balance; @@ -243,6 +241,12 @@ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransactio return ptx->vout[n]; } +const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint& outpoint) +{ + AssertLockHeld(wallet.cs_wallet); + return FindNonChangeParentOutput(wallet, *wallet.GetWalletTx(outpoint.hash)->tx, outpoint.n); +} + std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) { AssertLockHeld(wallet.cs_wallet); @@ -254,8 +258,8 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) for (const COutput& coin : availableCoins) { CTxDestination address; - if ((coin.fSpendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.fSolvable)) && - ExtractDestination(FindNonChangeParentOutput(wallet, *coin.tx->tx, coin.i).scriptPubKey, address)) { + if ((coin.spendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.solvable)) && + ExtractDestination(FindNonChangeParentOutput(wallet, coin.outpoint).scriptPubKey, address)) { result[address].emplace_back(std::move(coin)); } } @@ -268,14 +272,15 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) for (const COutPoint& output : lockedCoins) { auto it = wallet.mapWallet.find(output.hash); if (it != wallet.mapWallet.end()) { - int depth = wallet.GetTxDepthInMainChain(it->second); - if (depth >= 0 && output.n < it->second.tx->vout.size() && - wallet.IsMine(it->second.tx->vout[output.n]) == is_mine_filter + const auto& wtx = it->second; + int depth = wallet.GetTxDepthInMainChain(wtx); + if (depth >= 0 && output.n < wtx.tx->vout.size() && + wallet.IsMine(wtx.tx->vout[output.n]) == is_mine_filter ) { CTxDestination address; - if (ExtractDestination(FindNonChangeParentOutput(wallet, *it->second.tx, output.n).scriptPubKey, address)) { + if (ExtractDestination(FindNonChangeParentOutput(wallet, *wtx.tx, output.n).scriptPubKey, address)) { result[address].emplace_back( - wallet, it->second, output.n, depth, true /* spendable */, true /* solvable */, false /* safe */); + COutPoint(wtx.GetHash(), output.n), wtx.tx->vout.at(output.n), depth, GetTxSpendSize(wallet, wtx, output.n), /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ false, wtx.GetTxTime(), CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)); } } } @@ -292,15 +297,14 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C // Allowing partial spends means no grouping. Each COutput gets its own OutputGroup. for (const COutput& output : outputs) { // Skip outputs we cannot spend - if (!output.fSpendable) continue; + if (!output.spendable) continue; size_t ancestors, descendants; - wallet.chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); - CInputCoin input_coin = output.GetInputCoin(); + wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants); // Make an OutputGroup containing just this output OutputGroup group{coin_sel_params}; - group.Insert(input_coin, output.nDepth, CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL), ancestors, descendants, positive_only); + group.Insert(output, ancestors, descendants, positive_only); // Check the OutputGroup's eligibility. Only add the eligible ones. if (positive_only && group.GetSelectionAmount() <= 0) continue; @@ -312,18 +316,17 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C // We want to combine COutputs that have the same scriptPubKey into single OutputGroups // except when there are more than OUTPUT_GROUP_MAX_ENTRIES COutputs grouped in an OutputGroup. // To do this, we maintain a map where the key is the scriptPubKey and the value is a vector of OutputGroups. - // For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput's CInputCoin is added + // For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput is added // to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has - // OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the end of the vector. + // OUTPUT_GROUP_MAX_ENTRIES COutputs, a new OutputGroup is added to the end of the vector. std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map; for (const auto& output : outputs) { // Skip outputs we cannot spend - if (!output.fSpendable) continue; + if (!output.spendable) continue; size_t ancestors, descendants; - wallet.chain().getTransactionAncestry(output.tx->GetHash(), ancestors, descendants); - CInputCoin input_coin = output.GetInputCoin(); - CScript spk = input_coin.txout.scriptPubKey; + wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants); + CScript spk = output.txout.scriptPubKey; std::vector<OutputGroup>& groups = spk_to_groups_map[spk]; @@ -332,7 +335,7 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C groups.emplace_back(coin_sel_params); } - // Get the last OutputGroup in the vector so that we can add the CInputCoin to it + // Get the last OutputGroup in the vector so that we can add the COutput to it // A pointer is used here so that group can be reassigned later if it is full. OutputGroup* group = &groups.back(); @@ -344,8 +347,8 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C group = &groups.back(); } - // Add the input_coin to group - group->Insert(input_coin, output.nDepth, CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL), ancestors, descendants, positive_only); + // Add the output to group + group->Insert(output, ancestors, descendants, positive_only); } // Now we go through the entire map and pull out the OutputGroups @@ -386,7 +389,7 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm std::vector<OutputGroup> all_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, false /* positive_only */); // While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output. // So we need to include that for KnapsackSolver as well, as we are expecting to create a change output. - if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee)}) { + if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee, coin_selection_params.rng_fast)}) { knapsack_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change); results.push_back(*knapsack_result); } @@ -394,7 +397,7 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm // We include the minimum final change for SRD as we do want to avoid making really small change. // KnapsackSolver does not need this because it includes MIN_CHANGE internally. const CAmount srd_target = nTargetValue + coin_selection_params.m_change_fee + MIN_FINAL_CHANGE; - if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target)}) { + if (auto srd_result{SelectCoinsSRD(positive_groups, srd_target, coin_selection_params.rng_fast)}) { srd_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change); results.push_back(*srd_result); } @@ -421,11 +424,11 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs) { for (const COutput& out : vCoins) { - if (!out.fSpendable) continue; - /* Set depth, from_me, ancestors, and descendants to 0 or false as these don't matter for preset inputs as no actual selection is being done. + if (!out.spendable) continue; + /* Set ancestors and descendants to 0 as these don't matter for preset inputs as no actual selection is being done. * positive_only is set to false because we want to include all preset inputs, even if they are dust. */ - preset_inputs.Insert(out.GetInputCoin(), 0, false, 0, 0, false); + preset_inputs.Insert(out, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false); } SelectionResult result(nTargetValue); result.AddInput(preset_inputs); @@ -434,7 +437,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec } // calculate value from preset inputs and store them - std::set<CInputCoin> setPresetCoins; + std::set<COutPoint> preset_coins; std::vector<COutPoint> vPresetInputs; coin_control.ListSelected(vPresetInputs); @@ -462,27 +465,29 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec input_bytes = GetVirtualTransactionSize(coin_control.GetInputWeight(outpoint), 0, 0); } - CInputCoin coin(outpoint, txout, input_bytes); - if (coin.m_input_bytes == -1) { + if (input_bytes == -1) { return std::nullopt; // Not solvable, can't estimate size for fee } - coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes); + + /* Set some defaults for depth, spendable, solvable, safe, time, and from_me as these don't matter for preset inputs since no selection is being done. */ + COutput output(outpoint, txout, /*depth=*/ 0, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false); + output.effective_value = output.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(output.input_bytes); if (coin_selection_params.m_subtract_fee_outputs) { - value_to_select -= coin.txout.nValue; + value_to_select -= output.txout.nValue; } else { - value_to_select -= coin.effective_value; + value_to_select -= output.effective_value; } - setPresetCoins.insert(coin); - /* Set depth, from_me, ancestors, and descendants to 0 or false as don't matter for preset inputs as no actual selection is being done. + preset_coins.insert(outpoint); + /* Set ancestors and descendants to 0 as they don't matter for preset inputs since no actual selection is being done. * positive_only is set to false because we want to include all preset inputs, even if they are dust. */ - preset_inputs.Insert(coin, 0, false, 0, 0, false); + preset_inputs.Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false); } // remove preset inputs from vCoins so that Coin Selection doesn't pick them. for (std::vector<COutput>::iterator it = vCoins.begin(); it != vCoins.end() && coin_control.HasSelected();) { - if (setPresetCoins.count(it->GetInputCoin())) + if (preset_coins.count(it->outpoint)) it = vCoins.erase(it); else ++it; @@ -501,7 +506,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec // Cases where we have 101+ outputs all pointing to the same destination may result in // privacy leaks as they will potentially be deterministically sorted. We solve that by // explicitly shuffling the outputs before processing - Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext()); + Shuffle(vCoins.begin(), vCoins.end(), coin_selection_params.rng_fast); } // Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the @@ -585,7 +590,8 @@ static bool IsCurrentForAntiFeeSniping(interfaces::Chain& chain, const uint256& * Set a height-based locktime for new transactions (uses the height of the * current chain tip unless we are not synced with the current chain */ -static void DiscourageFeeSniping(CMutableTransaction& tx, interfaces::Chain& chain, const uint256& block_hash, int block_height) +static void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast, + interfaces::Chain& chain, const uint256& block_hash, int block_height) { // All inputs must be added by now assert(!tx.vin.empty()); @@ -616,8 +622,8 @@ static void DiscourageFeeSniping(CMutableTransaction& tx, interfaces::Chain& cha // that transactions that are delayed after signing for whatever reason, // e.g. high-latency mix networks and some CoinJoin implementations, have // better privacy. - if (GetRandInt(10) == 0) { - tx.nLockTime = std::max(0, int(tx.nLockTime) - GetRandInt(100)); + if (rng_fast.randrange(10) == 0) { + tx.nLockTime = std::max(0, int(tx.nLockTime) - int(rng_fast.randrange(100))); } } else { // If our chain is lagging behind, we can't discourage fee sniping nor help @@ -653,9 +659,10 @@ static bool CreateTransactionInternal( { AssertLockHeld(wallet.cs_wallet); + FastRandomContext rng_fast; CMutableTransaction txNew; // The resulting transaction that we make - CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy + CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends; // Set the long term feerate estimate to the wallet's consolidate feerate @@ -782,10 +789,9 @@ static bool CreateTransactionInternal( assert(change_and_fee >= 0); CTxOut newTxOut(change_and_fee, scriptChange); - if (nChangePosInOut == -1) - { + if (nChangePosInOut == -1) { // Insert change txn at random position: - nChangePosInOut = GetRandInt(txNew.vout.size()+1); + nChangePosInOut = rng_fast.randrange(txNew.vout.size() + 1); } else if ((unsigned int)nChangePosInOut > txNew.vout.size()) { @@ -797,7 +803,7 @@ static bool CreateTransactionInternal( auto change_position = txNew.vout.insert(txNew.vout.begin() + nChangePosInOut, newTxOut); // Shuffle selected coins and fill in final vin - std::vector<CInputCoin> selected_coins = result->GetShuffledInputVector(); + std::vector<COutput> selected_coins = result->GetShuffledInputVector(); // The sequence number is set to non-maxint so that DiscourageFeeSniping // works. @@ -811,7 +817,7 @@ static bool CreateTransactionInternal( for (const auto& coin : selected_coins) { txNew.vin.push_back(CTxIn(coin.outpoint, CScript(), nSequence)); } - DiscourageFeeSniping(txNew, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); + DiscourageFeeSniping(txNew, rng_fast, wallet.chain(), wallet.GetLastBlockHash(), wallet.GetLastBlockHeight()); // Calculate the transaction fee TxSize tx_sizes = CalculateMaximumSignedTxSize(CTransaction(txNew), &wallet, &coin_control); diff --git a/src/wallet/spend.h b/src/wallet/spend.h index 4453fb2762..e43aac5273 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -11,61 +11,11 @@ #include <wallet/wallet.h> namespace wallet { -/** Get the marginal bytes if spending the specified output from this transaction */ +/** Get the marginal bytes if spending the specified output from this transaction. + * use_max_sig indicates whether to use the maximum sized, 72 byte signature when calculating the + * size of the input spend. This should only be set when watch-only outputs are allowed */ int GetTxSpendSize(const CWallet& wallet, const CWalletTx& wtx, unsigned int out, bool use_max_sig = false); -class COutput -{ -public: - const CWalletTx *tx; - - /** Index in tx->vout. */ - int i; - - /** - * Depth in block chain. - * If > 0: the tx is on chain and has this many confirmations. - * If = 0: the tx is waiting confirmation. - * If < 0: a conflicting tx is on chain and has this many confirmations. */ - int nDepth; - - /** Pre-computed estimated size of this output as a fully-signed input in a transaction. Can be -1 if it could not be calculated */ - int nInputBytes; - - /** Whether we have the private keys to spend this output */ - bool fSpendable; - - /** Whether we know how to spend this output, ignoring the lack of keys */ - bool fSolvable; - - /** Whether to use the maximum sized, 72 byte signature when calculating the size of the input spend. This should only be set when watch-only outputs are allowed */ - bool use_max_sig; - - /** - * Whether this output is considered safe to spend. Unconfirmed transactions - * from outside keys and unconfirmed replacement transactions are considered - * unsafe and will not be used to fund new spending transactions. - */ - bool fSafe; - - COutput(const CWallet& wallet, const CWalletTx& wtx, int iIn, int nDepthIn, bool fSpendableIn, bool fSolvableIn, bool fSafeIn, bool use_max_sig_in = false) - { - tx = &wtx; i = iIn; nDepth = nDepthIn; fSpendable = fSpendableIn; fSolvable = fSolvableIn; fSafe = fSafeIn; nInputBytes = -1; use_max_sig = use_max_sig_in; - // If known and signable by the given wallet, compute nInputBytes - // Failure will keep this value -1 - if (fSpendable) { - nInputBytes = GetTxSpendSize(wallet, wtx, i, use_max_sig); - } - } - - std::string ToString() const; - - inline CInputCoin GetInputCoin() const - { - return CInputCoin(tx->tx, i, nInputBytes); - } -}; - //Get the marginal bytes of spending the specified output int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* pwallet, bool use_max_sig = false); int CalculateMaximumSignedInputSize(const CTxOut& txout, const SigningProvider* pwallet, bool use_max_sig = false); @@ -93,6 +43,7 @@ CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinContr * Find non-change parent output. */ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const CTransaction& tx, int output) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); +const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); /** * Return list of available coins and locked coins grouped by non-change output address. diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp index 55949e6e7a..3f860289f9 100644 --- a/src/wallet/sqlite.cpp +++ b/src/wallet/sqlite.cpp @@ -83,8 +83,8 @@ static void SetPragma(sqlite3* db, const std::string& key, const std::string& va } } -SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, bool mock) - : WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)) +SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock) + : WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_use_unsafe_sync(options.use_unsafe_sync) { { LOCK(g_sqlite_mutex); @@ -255,7 +255,7 @@ void SQLiteDatabase::Open() // Enable fullfsync for the platforms that use it SetPragma(m_db, "fullfsync", "true", "Failed to enable fullfsync"); - if (gArgs.GetBoolArg("-unsafesqlitesync", false)) { + if (m_use_unsafe_sync) { // Use normal synchronous mode for the journal LogPrintf("WARNING SQLite is configured to not wait for data to be flushed to disk. Data loss and corruption may occur.\n"); SetPragma(m_db, "synchronous", "OFF", "Failed to set synchronous mode to OFF"); @@ -546,7 +546,7 @@ std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const D { try { fs::path data_file = SQLiteDataFile(path); - auto db = std::make_unique<SQLiteDatabase>(data_file.parent_path(), data_file); + auto db = std::make_unique<SQLiteDatabase>(data_file.parent_path(), data_file, options); if (options.verify && !db->Verify(error)) { status = DatabaseStatus::FAILED_VERIFY; return nullptr; diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h index 3ed598d0d2..47b7ebb0ec 100644 --- a/src/wallet/sqlite.h +++ b/src/wallet/sqlite.h @@ -69,7 +69,7 @@ public: SQLiteDatabase() = delete; /** Create DB handle to real database */ - SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, bool mock = false); + SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock = false); ~SQLiteDatabase(); @@ -113,6 +113,7 @@ public: std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override; sqlite3* m_db{nullptr}; + bool m_use_unsafe_sync; }; std::unique_ptr<SQLiteDatabase> MakeSQLiteDatabase(const fs::path& path, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error); diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index b9f12158ca..14bec8ca4c 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -28,20 +28,20 @@ BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup) // we repeat those tests this many times and only complain if all iterations of the test fail #define RANDOM_REPEATS 5 -typedef std::set<CInputCoin> CoinSet; +typedef std::set<COutput> CoinSet; static const CoinEligibilityFilter filter_standard(1, 6, 0); static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); static int nextLockTime = 0; -static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set) +static void add_coin(const CAmount& nValue, int nInput, std::vector<COutput>& set) { CMutableTransaction tx; tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; tx.nLockTime = nextLockTime++; // so all transactions get different hashes - set.emplace_back(MakeTransactionRef(tx), nInput); + set.emplace_back(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false); } static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result) @@ -50,9 +50,9 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result) tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; tx.nLockTime = nextLockTime++; // so all transactions get different hashes - CInputCoin coin(MakeTransactionRef(tx), nInput); + COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false); OutputGroup group; - group.Insert(coin, 1, false, 0, 0, true); + group.Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ true); result.AddInput(group); } @@ -62,10 +62,10 @@ static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fe tx.vout.resize(nInput + 1); tx.vout[nInput].nValue = nValue; tx.nLockTime = nextLockTime++; // so all transactions get different hashes - CInputCoin coin(MakeTransactionRef(tx), nInput); + COutput coin(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false); coin.effective_value = nValue - fee; - coin.m_fee = fee; - coin.m_long_term_fee = long_term_fee; + coin.fee = fee; + coin.long_term_fee = long_term_fee; set.insert(coin); } @@ -82,24 +82,13 @@ static void add_coin(std::vector<COutput>& coins, CWallet& wallet, const CAmount assert(destination_ok); tx.vout[nInput].scriptPubKey = GetScriptForDestination(dest); } - if (fIsFromMe) { - // IsFromMe() returns (GetDebit() > 0), and GetDebit() is 0 if vin.empty(), - // so stop vin being empty, and cache a non-zero Debit to fake out IsFromMe() - tx.vin.resize(1); - } uint256 txid = tx.GetHash(); LOCK(wallet.cs_wallet); auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{})); assert(ret.second); CWalletTx& wtx = (*ret.first).second; - if (fIsFromMe) - { - wtx.m_amounts[CWalletTx::DEBIT].Set(ISMINE_SPENDABLE, 1); - wtx.m_is_cache_empty = false; - } - COutput output(wallet, wtx, nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */); - coins.push_back(output); + coins.emplace_back(COutPoint(wtx.GetHash(), nInput), wtx.tx->vout.at(nInput), nAge, GetTxSpendSize(wallet, wtx, nInput), /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, wtx.GetTxTime(), fIsFromMe); } /** Check if SelectionResult a is equivalent to SelectionResult b. @@ -124,11 +113,14 @@ static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b) /** Check if this selection is equal to another one. Equal means same inputs (i.e same value and prevout) */ static bool EqualResult(const SelectionResult& a, const SelectionResult& b) { - std::pair<CoinSet::iterator, CoinSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin()); + std::pair<CoinSet::iterator, CoinSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(), + [](const COutput& a, const COutput& b) { + return a.outpoint == b.outpoint; + }); return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end(); } -static CAmount make_hard_case(int utxos, std::vector<CInputCoin>& utxo_pool) +static CAmount make_hard_case(int utxos, std::vector<COutput>& utxo_pool) { utxo_pool.clear(); CAmount target = 0; @@ -140,34 +132,30 @@ static CAmount make_hard_case(int utxos, std::vector<CInputCoin>& utxo_pool) return target; } -inline std::vector<OutputGroup>& GroupCoins(const std::vector<CInputCoin>& coins) -{ - static std::vector<OutputGroup> static_groups; - static_groups.clear(); - for (auto& coin : coins) { - static_groups.emplace_back(); - static_groups.back().Insert(coin, 0, true, 0, 0, false); - } - return static_groups; -} - inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& coins) { static std::vector<OutputGroup> static_groups; static_groups.clear(); for (auto& coin : coins) { static_groups.emplace_back(); - static_groups.back().Insert(coin.GetInputCoin(), coin.nDepth, coin.tx->m_amounts[CWalletTx::DEBIT].m_cached[ISMINE_SPENDABLE] && coin.tx->m_amounts[CWalletTx::DEBIT].m_value[ISMINE_SPENDABLE] == 1 /* HACK: we can't figure out the is_me flag so we use the conditions defined above; perhaps set safe to false for !fIsFromMe in add_coin() */, 0, 0, false); + static_groups.back().Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false); } return static_groups; } inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput>& coins, CWallet& wallet, const CoinEligibilityFilter& filter) { - CoinSelectionParams coin_selection_params(/* change_output_size= */ 0, - /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); + FastRandomContext rand{}; + CoinSelectionParams coin_selection_params{ + rand, + /* change_output_size= */ 0, + /* change_spend_size= */ 0, + /* effective_feerate= */ CFeeRate(0), + /* long_term_feerate= */ CFeeRate(0), + /* discard_feerate= */ CFeeRate(0), + /* tx_noinputs_size= */ 0, + /* avoid_partial= */ false, + }; static std::vector<OutputGroup> static_groups; static_groups = GroupOutputs(wallet, coins, coin_selection_params, filter, /*positive_only=*/false); return static_groups; @@ -176,8 +164,9 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const std::vector<COutput> // Branch and bound coin selection tests BOOST_AUTO_TEST_CASE(bnb_search_test) { + FastRandomContext rand{}; // Setup - std::vector<CInputCoin> utxo_pool; + std::vector<COutput> utxo_pool; SelectionResult expected_result(CAmount(0)); ///////////////////////// @@ -301,10 +290,16 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) } // Make sure that effective value is working in AttemptSelection when BnB is used - CoinSelectionParams coin_selection_params_bnb(/* change_output_size= */ 0, - /* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000), - /* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000), - /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); + CoinSelectionParams coin_selection_params_bnb{ + rand, + /* change_output_size= */ 0, + /* change_spend_size= */ 0, + /* effective_feerate= */ CFeeRate(3000), + /* long_term_feerate= */ CFeeRate(1000), + /* discard_feerate= */ CFeeRate(1000), + /* tx_noinputs_size= */ 0, + /* avoid_partial= */ false, + }; { std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase()); wallet->LoadWallet(); @@ -315,13 +310,13 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) std::vector<COutput> coins; add_coin(coins, *wallet, 1); - coins.at(0).nInputBytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail + coins.at(0).input_bytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail BOOST_CHECK(!SelectCoinsBnB(GroupCoins(coins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change)); // Test fees subtracted from output: coins.clear(); add_coin(coins, *wallet, 1 * CENT); - coins.at(0).nInputBytes = 40; + coins.at(0).input_bytes = 40; coin_selection_params_bnb.m_subtract_fee_outputs = true; const auto result9 = SelectCoinsBnB(GroupCoins(coins), 1 * CENT, coin_selection_params_bnb.m_cost_of_change); BOOST_CHECK(result9); @@ -342,7 +337,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(coins, *wallet, 2 * CENT, 6 * 24, false, 0, true); CCoinControl coin_control; coin_control.fAllowOtherInputs = true; - coin_control.Select(COutPoint(coins.at(0).tx->GetHash(), coins.at(0).i)); + coin_control.Select(coins.at(0).outpoint); coin_selection_params_bnb.m_effective_feerate = CFeeRate(0); const auto result10 = SelectCoins(*wallet, coins, 10 * CENT, coin_control, coin_selection_params_bnb); BOOST_CHECK(result10); @@ -351,6 +346,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) BOOST_AUTO_TEST_CASE(knapsack_solver_test) { + FastRandomContext rand{}; + const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v) { return KnapsackSolver(g, v, rand); }}; + const auto KnapsackSolver{temp1}; std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase()); wallet->LoadWallet(); LOCK(wallet->cs_wallet); @@ -660,6 +658,7 @@ BOOST_AUTO_TEST_CASE(knapsack_solver_test) BOOST_AUTO_TEST_CASE(ApproximateBestSubset) { + FastRandomContext rand{}; std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", m_args, CreateMockWalletDatabase()); wallet->LoadWallet(); LOCK(wallet->cs_wallet); @@ -673,7 +672,7 @@ BOOST_AUTO_TEST_CASE(ApproximateBestSubset) add_coin(coins, *wallet, 1000 * COIN); add_coin(coins, *wallet, 3 * COIN); - const auto result = KnapsackSolver(KnapsackGroupOutputs(coins, *wallet, filter_standard), 1003 * COIN); + const auto result = KnapsackSolver(KnapsackGroupOutputs(coins, *wallet, filter_standard), 1003 * COIN, rand); BOOST_CHECK(result); BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN); BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U); @@ -714,10 +713,16 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test) CAmount target = rand.randrange(balance - 1000) + 1000; // Perform selection - CoinSelectionParams cs_params(/* change_output_size= */ 34, - /* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0), - /* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0), - /* tx_noinputs_size= */ 0, /* avoid_partial= */ false); + CoinSelectionParams cs_params{ + rand, + /* change_output_size= */ 34, + /* change_spend_size= */ 148, + /* effective_feerate= */ CFeeRate(0), + /* long_term_feerate= */ CFeeRate(0), + /* discard_feerate= */ CFeeRate(0), + /* tx_noinputs_size= */ 0, + /* avoid_partial= */ false, + }; CCoinControl cc; const auto result = SelectCoins(*wallet, coins, target, cc, cs_params); BOOST_CHECK(result); diff --git a/src/wallet/test/db_tests.cpp b/src/wallet/test/db_tests.cpp index 35ae3707f8..fbf1e0efd3 100644 --- a/src/wallet/test/db_tests.cpp +++ b/src/wallet/test/db_tests.cpp @@ -19,13 +19,13 @@ static std::shared_ptr<BerkeleyEnvironment> GetWalletEnv(const fs::path& path, s { fs::path data_file = BDBDataFile(path); database_filename = fs::PathToString(data_file.filename()); - return GetBerkeleyEnv(data_file.parent_path()); + return GetBerkeleyEnv(data_file.parent_path(), false); } BOOST_AUTO_TEST_CASE(getwalletenv_file) { std::string test_name = "test_name.dat"; - const fs::path datadir = gArgs.GetDataDirNet(); + const fs::path datadir = m_args.GetDataDirNet(); fs::path file_path = datadir / test_name; std::ofstream f{file_path}; f.close(); @@ -39,7 +39,7 @@ BOOST_AUTO_TEST_CASE(getwalletenv_file) BOOST_AUTO_TEST_CASE(getwalletenv_directory) { std::string expected_name = "wallet.dat"; - const fs::path datadir = gArgs.GetDataDirNet(); + const fs::path datadir = m_args.GetDataDirNet(); std::string filename; std::shared_ptr<BerkeleyEnvironment> env = GetWalletEnv(datadir, filename); @@ -49,8 +49,8 @@ BOOST_AUTO_TEST_CASE(getwalletenv_directory) BOOST_AUTO_TEST_CASE(getwalletenv_g_dbenvs_multiple) { - fs::path datadir = gArgs.GetDataDirNet() / "1"; - fs::path datadir_2 = gArgs.GetDataDirNet() / "2"; + fs::path datadir = m_args.GetDataDirNet() / "1"; + fs::path datadir_2 = m_args.GetDataDirNet() / "2"; std::string filename; std::shared_ptr<BerkeleyEnvironment> env_1 = GetWalletEnv(datadir, filename); diff --git a/src/wallet/test/init_test_fixture.cpp b/src/wallet/test/init_test_fixture.cpp index be38cebafd..34c22a9c0d 100644 --- a/src/wallet/test/init_test_fixture.cpp +++ b/src/wallet/test/init_test_fixture.cpp @@ -15,12 +15,12 @@ namespace wallet { InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainName) : BasicTestingSetup(chainName) { - m_wallet_loader = MakeWalletLoader(*m_node.chain, *Assert(m_node.args)); + m_wallet_loader = MakeWalletLoader(*m_node.chain, m_args); std::string sep; sep += fs::path::preferred_separator; - m_datadir = gArgs.GetDataDirNet(); + m_datadir = m_args.GetDataDirNet(); m_cwd = fs::current_path(); m_walletdir_path_cases["default"] = m_datadir / "wallets"; @@ -42,14 +42,11 @@ InitWalletDirTestingSetup::InitWalletDirTestingSetup(const std::string& chainNam InitWalletDirTestingSetup::~InitWalletDirTestingSetup() { - gArgs.LockSettings([&](util::Settings& settings) { - settings.forced_settings.erase("walletdir"); - }); fs::current_path(m_cwd); } void InitWalletDirTestingSetup::SetWalletDir(const fs::path& walletdir_path) { - gArgs.ForceSetArg("-walletdir", fs::PathToString(walletdir_path)); + m_args.ForceSetArg("-walletdir", fs::PathToString(walletdir_path)); } } // namespace wallet diff --git a/src/wallet/test/init_tests.cpp b/src/wallet/test/init_tests.cpp index 7fdecc5642..fb0a07e181 100644 --- a/src/wallet/test/init_tests.cpp +++ b/src/wallet/test/init_tests.cpp @@ -18,7 +18,7 @@ BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_default) SetWalletDir(m_walletdir_path_cases["default"]); bool result = m_wallet_loader->verify(); BOOST_CHECK(result == true); - fs::path walletdir = gArgs.GetPathArg("-walletdir"); + fs::path walletdir = m_args.GetPathArg("-walletdir"); fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]); BOOST_CHECK_EQUAL(walletdir, expected_path); } @@ -28,7 +28,7 @@ BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_custom) SetWalletDir(m_walletdir_path_cases["custom"]); bool result = m_wallet_loader->verify(); BOOST_CHECK(result == true); - fs::path walletdir = gArgs.GetPathArg("-walletdir"); + fs::path walletdir = m_args.GetPathArg("-walletdir"); fs::path expected_path = fs::canonical(m_walletdir_path_cases["custom"]); BOOST_CHECK_EQUAL(walletdir, expected_path); } @@ -68,7 +68,7 @@ BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_no_trailing) SetWalletDir(m_walletdir_path_cases["trailing"]); bool result = m_wallet_loader->verify(); BOOST_CHECK(result == true); - fs::path walletdir = gArgs.GetPathArg("-walletdir"); + fs::path walletdir = m_args.GetPathArg("-walletdir"); fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]); BOOST_CHECK_EQUAL(walletdir, expected_path); } @@ -78,7 +78,7 @@ BOOST_AUTO_TEST_CASE(walletinit_verify_walletdir_no_trailing2) SetWalletDir(m_walletdir_path_cases["trailing2"]); bool result = m_wallet_loader->verify(); BOOST_CHECK(result == true); - fs::path walletdir = gArgs.GetPathArg("-walletdir"); + fs::path walletdir = m_args.GetPathArg("-walletdir"); fs::path expected_path = fs::canonical(m_walletdir_path_cases["default"]); BOOST_CHECK_EQUAL(walletdir, expected_path); } diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index c59f7e6f05..c1fa2c3c60 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -221,7 +221,7 @@ BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); WalletContext context; - context.args = &gArgs; + context.args = &m_args; AddWallet(context, wallet); UniValue keys; keys.setArray(); @@ -277,12 +277,12 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) SetMockTime(KEY_TIME); m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); - std::string backup_file = fs::PathToString(gArgs.GetDataDirNet() / "wallet.backup"); + std::string backup_file = fs::PathToString(m_args.GetDataDirNet() / "wallet.backup"); // Import key into wallet and call dumpwallet to create backup file. { WalletContext context; - context.args = &gArgs; + context.args = &m_args; const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", m_args, CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); @@ -310,7 +310,7 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) wallet->SetupLegacyScriptPubKeyMan(); WalletContext context; - context.args = &gArgs; + context.args = &m_args; JSONRPCRequest request; request.context = &context; request.params.setArray(); @@ -588,7 +588,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) for (const auto& group : list) { for (const auto& coin : group.second) { LOCK(wallet->cs_wallet); - wallet->LockCoin(COutPoint(coin.tx->GetHash(), coin.i)); + wallet->LockCoin(coin.outpoint); } } { @@ -716,10 +716,10 @@ BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) //! rescanning where new transactions in new blocks could be lost. BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) { - gArgs.ForceSetArg("-unsafesqlitesync", "1"); + m_args.ForceSetArg("-unsafesqlitesync", "1"); // Create new wallet with known key and unload it. WalletContext context; - context.args = &gArgs; + context.args = &m_args; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; @@ -812,7 +812,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) { WalletContext context; - context.args = &gArgs; + context.args = &m_args; auto wallet = TestLoadWallet(context); BOOST_CHECK(wallet); UnloadWallet(std::move(wallet)); @@ -820,9 +820,9 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { - gArgs.ForceSetArg("-unsafesqlitesync", "1"); + m_args.ForceSetArg("-unsafesqlitesync", "1"); WalletContext context; - context.args = &gArgs; + context.args = &m_args; context.chain = m_node.chain.get(); auto wallet = TestLoadWallet(context); CKey key; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 261d042529..be64b4cdbb 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -366,6 +366,7 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string& std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& backup_file, const std::string& wallet_name, std::optional<bool> load_on_start, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings) { DatabaseOptions options; + ReadDatabaseArgs(*context.args, options); options.require_existing = true; if (!fs::exists(backup_file)) { diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 6e100524a4..1251c9f37e 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -1189,10 +1189,11 @@ std::unique_ptr<WalletDatabase> CreateDummyWalletDatabase() /** Return object for accessing temporary in-memory database. */ std::unique_ptr<WalletDatabase> CreateMockWalletDatabase() { + DatabaseOptions options; #ifdef USE_SQLITE - return std::make_unique<SQLiteDatabase>("", "", true); + return std::make_unique<SQLiteDatabase>("", "", options, true); #elif USE_BDB - return std::make_unique<BerkeleyDatabase>(std::make_shared<BerkeleyEnvironment>(), ""); + return std::make_unique<BerkeleyDatabase>(std::make_shared<BerkeleyEnvironment>(), "", options); #endif } } // namespace wallet diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 9cd18dd0a5..769175b5a8 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -140,6 +140,7 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) if (command == "create") { DatabaseOptions options; + ReadDatabaseArgs(args, options); options.require_create = true; // If -legacy is set, use it. Otherwise default to false. bool make_legacy = args.GetBoolArg("-legacy", false); @@ -165,6 +166,7 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) } } else if (command == "info") { DatabaseOptions options; + ReadDatabaseArgs(args, options); options.require_existing = true; const std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, args, options); if (!wallet_instance) return false; @@ -174,7 +176,7 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) #ifdef USE_BDB bilingual_str error; std::vector<bilingual_str> warnings; - bool ret = RecoverDatabaseFile(path, error, warnings); + bool ret = RecoverDatabaseFile(args, path, error, warnings); if (!ret) { for (const auto& warning : warnings) { tfm::format(std::cerr, "%s\n", warning.original); @@ -190,11 +192,12 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) #endif } else if (command == "dump") { DatabaseOptions options; + ReadDatabaseArgs(args, options); options.require_existing = true; const std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, args, options); if (!wallet_instance) return false; bilingual_str error; - bool ret = DumpWallet(*wallet_instance, error); + bool ret = DumpWallet(args, *wallet_instance, error); if (!ret && !error.empty()) { tfm::format(std::cerr, "%s\n", error.original); return ret; @@ -204,7 +207,7 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) } else if (command == "createfromdump") { bilingual_str error; std::vector<bilingual_str> warnings; - bool ret = CreateFromDump(name, path, error, warnings); + bool ret = CreateFromDump(args, name, path, error, warnings); for (const auto& warning : warnings) { tfm::format(std::cout, "%s\n", warning.original); } diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index fb0f6d7cb7..14308b3fd1 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -30,6 +30,11 @@ addnode connect to generic DNS name addnode connect to a CJDNS address - Test getnetworkinfo for each node + +- Test passing invalid -proxy +- Test passing invalid -onion +- Test passing -onlynet=onion without -proxy or -onion +- Test passing -onlynet=onion with -onion=0 and with -noonion """ import socket @@ -263,12 +268,13 @@ class ProxyTest(BitcoinTestFramework): n2 = networks_dict(self.nodes[2].getnetworkinfo()) assert_equal(NETWORKS, n2.keys()) + proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' for net in NETWORKS: if net == NET_I2P: expected_proxy = '' expected_randomize = False else: - expected_proxy = f'{self.conf2.addr[0]}:{self.conf2.addr[1]}' + expected_proxy = proxy expected_randomize = True assert_equal(n2[net]['proxy'], expected_proxy) assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) @@ -279,11 +285,9 @@ class ProxyTest(BitcoinTestFramework): if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) assert_equal(NETWORKS, n3.keys()) + proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' for net in NETWORKS: - if net == NET_I2P or net == NET_ONION: - expected_proxy = '' - else: - expected_proxy = f'[{self.conf3.addr[0]}]:{self.conf3.addr[1]}' + expected_proxy = '' if net == NET_I2P or net == NET_ONION else proxy assert_equal(n3[net]['proxy'], expected_proxy) assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) @@ -305,6 +309,32 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n4['i2p']['reachable'], False) assert_equal(n4['cjdns']['reachable'], True) + self.stop_node(1) + + self.log.info("Test passing invalid -proxy raises expected init error") + self.nodes[1].extra_args = ["-proxy=abc:def"] + msg = "Error: Invalid -proxy address or hostname: 'abc:def'" + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + self.log.info("Test passing invalid -onion raises expected init error") + self.nodes[1].extra_args = ["-onion=xyz:abc"] + msg = "Error: Invalid -onion address or hostname: 'xyz:abc'" + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + msg = ( + "Error: Outbound connections restricted to Tor (-onlynet=onion) but " + "the proxy for reaching the Tor network is not provided (no -proxy= " + "and no -onion= given) or it is explicitly forbidden (-onion=0)" + ) + self.log.info("Test passing -onlynet=onion without -proxy or -onion raises expected init error") + self.nodes[1].extra_args = ["-onlynet=onion"] + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + + self.log.info("Test passing -onlynet=onion with -onion=0/-noonion raises expected init error") + for arg in ["-onion=0", "-noonion"]: + self.nodes[1].extra_args = ["-onlynet=onion", arg] + self.nodes[1].assert_start_raises_init_error(expected_msg=msg) + if __name__ == '__main__': ProxyTest().main() diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index ba3c5053cb..bf19384279 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -141,6 +141,10 @@ class PruneTest(BitcoinTestFramework): expected_msg='Error: Prune mode is incompatible with -coinstatsindex.', extra_args=['-prune=550', '-coinstatsindex'], ) + self.nodes[0].assert_start_raises_init_error( + expected_msg='Error: Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead.', + extra_args=['-prune=550', '-reindex-chainstate'], + ) def test_height_min(self): assert os.path.isfile(os.path.join(self.prunedir, "blk00000.dat")), "blk00000.dat is missing, pruning too early" diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index 1ee12c0040..7d8d10589b 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -23,6 +23,9 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, ) +from test_framework.wallet import ( + MiniWallet, +) from test_framework.netutil import test_ipv6_local from io import BytesIO from time import sleep @@ -100,8 +103,6 @@ class ZMQTestSetupBlock: class ZMQTest (BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 - if self.is_wallet_compiled(): - self.requires_wallet = True # This test isn't testing txn relay/timing, so set whitelist on the # peers for instant txn relay. This speeds up the test run time 2-3x. self.extra_args = [["-whitelist=noban@127.0.0.1"]] * self.num_nodes @@ -111,6 +112,7 @@ class ZMQTest (BitcoinTestFramework): self.skip_if_no_bitcoind_zmq() def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) self.ctx = zmq.Context() try: self.test_basic() @@ -211,25 +213,25 @@ class ZMQTest (BitcoinTestFramework): assert_equal([txid.hex()], self.nodes[1].getblock(hash)["tx"]) - if self.is_wallet_compiled(): - self.log.info("Wait for tx from second node") - payment_txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1.0) - self.sync_all() - - # Should receive the broadcasted txid. - txid = hashtx.receive() - assert_equal(payment_txid, txid.hex()) + self.wallet.rescan_utxos() + self.log.info("Wait for tx from second node") + payment_tx = self.wallet.send_self_transfer(from_node=self.nodes[1]) + payment_txid = payment_tx['txid'] + self.sync_all() + # Should receive the broadcasted txid. + txid = hashtx.receive() + assert_equal(payment_txid, txid.hex()) - # Should receive the broadcasted raw transaction. - hex = rawtx.receive() - assert_equal(payment_txid, hash256_reversed(hex).hex()) + # Should receive the broadcasted raw transaction. + hex = rawtx.receive() + assert_equal(payment_tx['wtxid'], hash256_reversed(hex).hex()) - # Mining the block with this tx should result in second notification - # after coinbase tx notification - self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) - hashtx.receive() - txid = hashtx.receive() - assert_equal(payment_txid, txid.hex()) + # Mining the block with this tx should result in second notification + # after coinbase tx notification + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) + hashtx.receive() + txid = hashtx.receive() + assert_equal(payment_txid, txid.hex()) self.log.info("Test the getzmqnotifications RPC") @@ -243,9 +245,6 @@ class ZMQTest (BitcoinTestFramework): assert_equal(self.nodes[1].getzmqnotifications(), []) def test_reorg(self): - if not self.is_wallet_compiled(): - self.log.info("Skipping reorg test because wallet is disabled") - return address = 'tcp://127.0.0.1:28333' @@ -256,7 +255,7 @@ class ZMQTest (BitcoinTestFramework): self.disconnect_nodes(0, 1) # Generate 1 block in nodes[0] with 1 mempool tx and receive all notifications - payment_txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1.0) + payment_txid = self.wallet.send_self_transfer(from_node=self.nodes[0])['txid'] disconnect_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE, sync_fun=self.no_op)[0] disconnect_cb = self.nodes[0].getblock(disconnect_block)["tx"][0] assert_equal(self.nodes[0].getbestblockhash(), hashblock.receive().hex()) @@ -325,126 +324,124 @@ class ZMQTest (BitcoinTestFramework): assert_equal((self.nodes[1].getblockhash(block_count-1), "C", None), seq.receive_sequence()) assert_equal((self.nodes[1].getblockhash(block_count), "C", None), seq.receive_sequence()) - # Rest of test requires wallet functionality - if self.is_wallet_compiled(): - self.log.info("Wait for tx from second node") - payment_txid = self.nodes[1].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=5.0, replaceable=True) - self.sync_all() - self.log.info("Testing sequence notifications with mempool sequence values") - - # Should receive the broadcasted txid. - assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) - seq_num += 1 - - self.log.info("Testing RBF notification") - # Replace it to test eviction/addition notification - rbf_info = self.nodes[1].bumpfee(payment_txid) - self.sync_all() - assert_equal((payment_txid, "R", seq_num), seq.receive_sequence()) - seq_num += 1 - assert_equal((rbf_info["txid"], "A", seq_num), seq.receive_sequence()) - seq_num += 1 - - # Doesn't get published when mined, make a block and tx to "flush" the possibility - # though the mempool sequence number does go up by the number of transactions - # removed from the mempool by the block mining it. - mempool_size = len(self.nodes[0].getrawmempool()) - c_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0] - # Make sure the number of mined transactions matches the number of txs out of mempool - mempool_size_delta = mempool_size - len(self.nodes[0].getrawmempool()) - assert_equal(len(self.nodes[0].getblock(c_block)["tx"])-1, mempool_size_delta) - seq_num += mempool_size_delta - payment_txid_2 = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1.0) - self.sync_all() - assert_equal((c_block, "C", None), seq.receive_sequence()) - assert_equal((payment_txid_2, "A", seq_num), seq.receive_sequence()) - seq_num += 1 - - # Spot check getrawmempool results that they only show up when asked for - assert type(self.nodes[0].getrawmempool()) is list - assert type(self.nodes[0].getrawmempool(mempool_sequence=False)) is list - assert "mempool_sequence" not in self.nodes[0].getrawmempool(verbose=True) - assert_raises_rpc_error(-8, "Verbose results cannot contain mempool sequence values.", self.nodes[0].getrawmempool, True, True) - assert_equal(self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"], seq_num) - - self.log.info("Testing reorg notifications") - # Manually invalidate the last block to test mempool re-entry - # N.B. This part could be made more lenient in exact ordering - # since it greatly depends on inner-workings of blocks/mempool - # during "deep" re-orgs. Probably should "re-construct" - # blockchain/mempool state from notifications instead. - block_count = self.nodes[0].getblockcount() - best_hash = self.nodes[0].getbestblockhash() - self.nodes[0].invalidateblock(best_hash) - sleep(2) # Bit of room to make sure transaction things happened - - # Make sure getrawmempool mempool_sequence results aren't "queued" but immediately reflective - # of the time they were gathered. - assert self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"] > seq_num - - assert_equal((best_hash, "D", None), seq.receive_sequence()) - assert_equal((rbf_info["txid"], "A", seq_num), seq.receive_sequence()) - seq_num += 1 - - # Other things may happen but aren't wallet-deterministic so we don't test for them currently - self.nodes[0].reconsiderblock(best_hash) - self.generatetoaddress(self.nodes[1], 1, ADDRESS_BCRT1_UNSPENDABLE) - - self.log.info("Evict mempool transaction by block conflict") - orig_txid = self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=1.0, replaceable=True) - - # More to be simply mined - more_tx = [] - for _ in range(5): - more_tx.append(self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 0.1)) - - raw_tx = self.nodes[0].getrawtransaction(orig_txid) - bump_info = self.nodes[0].bumpfee(orig_txid) - # Mine the pre-bump tx - txs_to_add = [raw_tx] + [self.nodes[0].getrawtransaction(txid) for txid in more_tx] - block = create_block(int(self.nodes[0].getbestblockhash(), 16), create_coinbase(self.nodes[0].getblockcount()+1), txlist=txs_to_add) - add_witness_commitment(block) - block.solve() - assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None) - tip = self.nodes[0].getbestblockhash() - assert_equal(int(tip, 16), block.sha256) - orig_txid_2 = self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=1.0, replaceable=True) - - # Flush old notifications until evicted tx original entry + self.log.info("Wait for tx from second node") + payment_tx = self.wallet.send_self_transfer(from_node=self.nodes[1]) + payment_txid = payment_tx['txid'] + self.sync_all() + self.log.info("Testing sequence notifications with mempool sequence values") + + # Should receive the broadcasted txid. + assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) + seq_num += 1 + + self.log.info("Testing RBF notification") + # Replace it to test eviction/addition notification + payment_tx['tx'].vout[0].nValue -= 1000 + rbf_txid = self.nodes[1].sendrawtransaction(payment_tx['tx'].serialize().hex()) + self.sync_all() + assert_equal((payment_txid, "R", seq_num), seq.receive_sequence()) + seq_num += 1 + assert_equal((rbf_txid, "A", seq_num), seq.receive_sequence()) + seq_num += 1 + + # Doesn't get published when mined, make a block and tx to "flush" the possibility + # though the mempool sequence number does go up by the number of transactions + # removed from the mempool by the block mining it. + mempool_size = len(self.nodes[0].getrawmempool()) + c_block = self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE)[0] + # Make sure the number of mined transactions matches the number of txs out of mempool + mempool_size_delta = mempool_size - len(self.nodes[0].getrawmempool()) + assert_equal(len(self.nodes[0].getblock(c_block)["tx"])-1, mempool_size_delta) + seq_num += mempool_size_delta + payment_txid_2 = self.wallet.send_self_transfer(from_node=self.nodes[1])['txid'] + self.sync_all() + assert_equal((c_block, "C", None), seq.receive_sequence()) + assert_equal((payment_txid_2, "A", seq_num), seq.receive_sequence()) + seq_num += 1 + + # Spot check getrawmempool results that they only show up when asked for + assert type(self.nodes[0].getrawmempool()) is list + assert type(self.nodes[0].getrawmempool(mempool_sequence=False)) is list + assert "mempool_sequence" not in self.nodes[0].getrawmempool(verbose=True) + assert_raises_rpc_error(-8, "Verbose results cannot contain mempool sequence values.", self.nodes[0].getrawmempool, True, True) + assert_equal(self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"], seq_num) + + self.log.info("Testing reorg notifications") + # Manually invalidate the last block to test mempool re-entry + # N.B. This part could be made more lenient in exact ordering + # since it greatly depends on inner-workings of blocks/mempool + # during "deep" re-orgs. Probably should "re-construct" + # blockchain/mempool state from notifications instead. + block_count = self.nodes[0].getblockcount() + best_hash = self.nodes[0].getbestblockhash() + self.nodes[0].invalidateblock(best_hash) + sleep(2) # Bit of room to make sure transaction things happened + + # Make sure getrawmempool mempool_sequence results aren't "queued" but immediately reflective + # of the time they were gathered. + assert self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"] > seq_num + + assert_equal((best_hash, "D", None), seq.receive_sequence()) + assert_equal((rbf_txid, "A", seq_num), seq.receive_sequence()) + seq_num += 1 + + # Other things may happen but aren't wallet-deterministic so we don't test for them currently + self.nodes[0].reconsiderblock(best_hash) + self.generatetoaddress(self.nodes[1], 1, ADDRESS_BCRT1_UNSPENDABLE) + + self.log.info("Evict mempool transaction by block conflict") + orig_tx = self.wallet.send_self_transfer(from_node=self.nodes[0]) + orig_txid = orig_tx['txid'] + + # More to be simply mined + more_tx = [] + for _ in range(5): + more_tx.append(self.wallet.send_self_transfer(from_node=self.nodes[0])) + + orig_tx['tx'].vout[0].nValue -= 1000 + bump_txid = self.nodes[0].sendrawtransaction(orig_tx['tx'].serialize().hex()) + # Mine the pre-bump tx + txs_to_add = [orig_tx['hex']] + [tx['hex'] for tx in more_tx] + block = create_block(int(self.nodes[0].getbestblockhash(), 16), create_coinbase(self.nodes[0].getblockcount()+1), txlist=txs_to_add) + add_witness_commitment(block) + block.solve() + assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None) + tip = self.nodes[0].getbestblockhash() + assert_equal(int(tip, 16), block.sha256) + orig_txid_2 = self.wallet.send_self_transfer(from_node=self.nodes[0])['txid'] + + # Flush old notifications until evicted tx original entry + (hash_str, label, mempool_seq) = seq.receive_sequence() + while hash_str != orig_txid: (hash_str, label, mempool_seq) = seq.receive_sequence() - while hash_str != orig_txid: - (hash_str, label, mempool_seq) = seq.receive_sequence() - mempool_seq += 1 + mempool_seq += 1 - # Added original tx - assert_equal(label, "A") - # More transactions to be simply mined - for i in range(len(more_tx)): - assert_equal((more_tx[i], "A", mempool_seq), seq.receive_sequence()) - mempool_seq += 1 - # Bumped by rbf - assert_equal((orig_txid, "R", mempool_seq), seq.receive_sequence()) - mempool_seq += 1 - assert_equal((bump_info["txid"], "A", mempool_seq), seq.receive_sequence()) + # Added original tx + assert_equal(label, "A") + # More transactions to be simply mined + for i in range(len(more_tx)): + assert_equal((more_tx[i]['txid'], "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 - # Conflict announced first, then block - assert_equal((bump_info["txid"], "R", mempool_seq), seq.receive_sequence()) - mempool_seq += 1 - assert_equal((tip, "C", None), seq.receive_sequence()) - mempool_seq += len(more_tx) - # Last tx - assert_equal((orig_txid_2, "A", mempool_seq), seq.receive_sequence()) - mempool_seq += 1 - self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) - self.sync_all() # want to make sure we didn't break "consensus" for other tests + # Bumped by rbf + assert_equal((orig_txid, "R", mempool_seq), seq.receive_sequence()) + mempool_seq += 1 + assert_equal((bump_txid, "A", mempool_seq), seq.receive_sequence()) + mempool_seq += 1 + # Conflict announced first, then block + assert_equal((bump_txid, "R", mempool_seq), seq.receive_sequence()) + mempool_seq += 1 + assert_equal((tip, "C", None), seq.receive_sequence()) + mempool_seq += len(more_tx) + # Last tx + assert_equal((orig_txid_2, "A", mempool_seq), seq.receive_sequence()) + mempool_seq += 1 + self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) + self.sync_all() # want to make sure we didn't break "consensus" for other tests def test_mempool_sync(self): """ Use sequence notification plus getrawmempool sequence results to "sync mempool" """ - if not self.is_wallet_compiled(): - self.log.info("Skipping mempool sync test") - return self.log.info("Testing 'mempool sync' usage of sequence notifier") [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")]) @@ -455,10 +452,10 @@ class ZMQTest (BitcoinTestFramework): # Some transactions have been happening but we aren't consuming zmq notifications yet # or we lost a ZMQ message somehow and want to start over - txids = [] + txs = [] num_txs = 5 for _ in range(num_txs): - txids.append(self.nodes[1].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=1.0, replaceable=True)) + txs.append(self.wallet.send_self_transfer(from_node=self.nodes[1])) self.sync_all() # 1) Consume backlog until we get a mempool sequence number @@ -484,11 +481,12 @@ class ZMQTest (BitcoinTestFramework): # Things continue to happen in the "interim" while waiting for snapshot results # We have node 0 do all these to avoid p2p races with RBF announcements for _ in range(num_txs): - txids.append(self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=0.1, replaceable=True)) - self.nodes[0].bumpfee(txids[-1]) + txs.append(self.wallet.send_self_transfer(from_node=self.nodes[0])) + txs[-1]['tx'].vout[0].nValue -= 1000 + self.nodes[0].sendrawtransaction(txs[-1]['tx'].serialize().hex()) self.sync_all() self.generatetoaddress(self.nodes[0], 1, ADDRESS_BCRT1_UNSPENDABLE) - final_txid = self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=0.1, replaceable=True) + final_txid = self.wallet.send_self_transfer(from_node=self.nodes[0])['txid'] # 3) Consume ZMQ backlog until we get to "now" for the mempool snapshot while True: diff --git a/test/functional/mempool_package_onemore.py b/test/functional/mempool_package_onemore.py index a6fb1dcf35..423a5bf2ee 100755 --- a/test/functional/mempool_package_onemore.py +++ b/test/functional/mempool_package_onemore.py @@ -7,74 +7,68 @@ size. """ -from decimal import Decimal - -from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_rpc_error, - chain_transaction, ) +from test_framework.wallet import MiniWallet + MAX_ANCESTORS = 25 MAX_DESCENDANTS = 25 + class MempoolPackagesTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [["-maxorphantx=1000"]] - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + def chain_tx(self, utxos_to_spend, *, num_outputs=1): + return self.wallet.send_self_transfer_multi( + from_node=self.nodes[0], + utxos_to_spend=utxos_to_spend, + num_outputs=num_outputs)['new_utxos'] def run_test(self): - # Mine some blocks and have them mature. - self.generate(self.nodes[0], COINBASE_MATURITY + 1) - utxo = self.nodes[0].listunspent(10) - txid = utxo[0]['txid'] - vout = utxo[0]['vout'] - value = utxo[0]['amount'] + self.wallet = MiniWallet(self.nodes[0]) + self.wallet.rescan_utxos() - fee = Decimal("0.0002") # MAX_ANCESTORS transactions off a confirmed tx should be fine chain = [] + utxo = self.wallet.get_utxo() for _ in range(4): - (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [vout], value, fee, 2) - vout = 0 - value = sent_value - chain.append([txid, value]) + utxo, utxo2 = self.chain_tx([utxo], num_outputs=2) + chain.append(utxo2) for _ in range(MAX_ANCESTORS - 4): - (txid, sent_value) = chain_transaction(self.nodes[0], [txid], [0], value, fee, 1) - value = sent_value - chain.append([txid, value]) - (second_chain, second_chain_value) = chain_transaction(self.nodes[0], [utxo[1]['txid']], [utxo[1]['vout']], utxo[1]['amount'], fee, 1) + utxo, = self.chain_tx([utxo]) + chain.append(utxo) + second_chain, = self.chain_tx([self.wallet.get_utxo()]) # Check mempool has MAX_ANCESTORS + 1 transactions in it assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 1) # Adding one more transaction on to the chain should fail. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", chain_transaction, self.nodes[0], [txid], [0], value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25]", self.chain_tx, [utxo]) # ...even if it chains on from some point in the middle of the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[2][0]], [1], chain[2][1], fee, 1) - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[1][0]], [1], chain[1][1], fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[2]]) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[1]]) # ...even if it chains on to two parent transactions with one in the chain. - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0], second_chain], [1, 0], chain[0][1] + second_chain_value, fee, 1) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0], second_chain]) # ...especially if its > 40k weight - assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", chain_transaction, self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 350) + assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0]], num_outputs=350) # But not if it chains directly off the first transaction - (replacable_txid, replacable_orig_value) = chain_transaction(self.nodes[0], [chain[0][0]], [1], chain[0][1], fee, 1) + replacable_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[chain[0]])['tx'] # and the second chain should work just fine - chain_transaction(self.nodes[0], [second_chain], [0], second_chain_value, fee, 1) + self.chain_tx([second_chain]) # Make sure we can RBF the chain which used our carve-out rule - second_tx_outputs = {self.nodes[0].getrawtransaction(replacable_txid, True)["vout"][0]['scriptPubKey']['address']: replacable_orig_value - (Decimal(1) / Decimal(100))} - second_tx = self.nodes[0].createrawtransaction([{'txid': chain[0][0], 'vout': 1}], second_tx_outputs) - signed_second_tx = self.nodes[0].signrawtransactionwithwallet(second_tx) - self.nodes[0].sendrawtransaction(signed_second_tx['hex']) + replacable_tx.vout[0].nValue -= 1000000 + self.nodes[0].sendrawtransaction(replacable_tx.serialize().hex()) # Finally, check that we added two transactions assert_equal(len(self.nodes[0].getrawmempool()), MAX_ANCESTORS + 3) + if __name__ == '__main__': MempoolPackagesTest().main() diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index 1a3d14100f..1695acaaa8 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -18,15 +18,18 @@ from test_framework.util import ( assert_equal, ) from test_framework.wallet_util import bytes_to_wif +from test_framework.wallet import ( + MiniWallet, + getnewdestination, +) class RpcCreateMultiSigTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.supports_cli = False - - def skip_test_if_missing_module(self): - self.skip_if_no_wallet() + if self.is_bdb_compiled(): + self.requires_wallet = True def get_keys(self): self.pub = [] @@ -37,15 +40,20 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): k.generate() self.pub.append(k.get_pubkey().get_bytes().hex()) self.priv.append(bytes_to_wif(k.get_bytes(), k.is_compressed)) - self.final = node2.getnewaddress() + if self.is_bdb_compiled(): + self.final = node2.getnewaddress() + else: + self.final = getnewdestination()[2] def run_test(self): node0, node1, node2 = self.nodes + self.wallet = MiniWallet(test_node=node0) - self.check_addmultisigaddress_errors() + if self.is_bdb_compiled(): + self.check_addmultisigaddress_errors() self.log.info('Generating blocks ...') - self.generate(node0, 149) + self.generate(self.wallet, 149) self.moved = 0 for self.nkeys in [3, 5]: @@ -53,14 +61,14 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): for self.output_type in ["bech32", "p2sh-segwit", "legacy"]: self.get_keys() self.do_multisig() - - self.checkbalances() + if self.is_bdb_compiled(): + self.checkbalances() # Test mixed compressed and uncompressed pubkeys self.log.info('Mixed compressed and uncompressed multisigs are not allowed') - pk0 = node0.getaddressinfo(node0.getnewaddress())['pubkey'] - pk1 = node1.getaddressinfo(node1.getnewaddress())['pubkey'] - pk2 = node2.getaddressinfo(node2.getnewaddress())['pubkey'] + pk0 = getnewdestination()[0].hex() + pk1 = getnewdestination()[0].hex() + pk2 = getnewdestination()[0].hex() # decompress pk2 pk_obj = ECPubKey() @@ -68,26 +76,30 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): pk_obj.compressed = False pk2 = pk_obj.get_bytes().hex() - node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) - wmulti0 = node0.get_wallet_rpc('wmulti0') + if self.is_bdb_compiled(): + node0.createwallet(wallet_name='wmulti0', disable_private_keys=True) + wmulti0 = node0.get_wallet_rpc('wmulti0') # Check all permutations of keys because order matters apparently for keys in itertools.permutations([pk0, pk1, pk2]): # Results should be the same as this legacy one legacy_addr = node0.createmultisig(2, keys, 'legacy')['address'] - result = wmulti0.addmultisigaddress(2, keys, '', 'legacy') - assert_equal(legacy_addr, result['address']) - assert 'warnings' not in result + + if self.is_bdb_compiled(): + result = wmulti0.addmultisigaddress(2, keys, '', 'legacy') + assert_equal(legacy_addr, result['address']) + assert 'warnings' not in result # Generate addresses with the segwit types. These should all make legacy addresses for addr_type in ['bech32', 'p2sh-segwit']: - result = wmulti0.createmultisig(2, keys, addr_type) + result = self.nodes[0].createmultisig(2, keys, addr_type) assert_equal(legacy_addr, result['address']) assert_equal(result['warnings'], ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]) - result = wmulti0.addmultisigaddress(2, keys, '', addr_type) - assert_equal(legacy_addr, result['address']) - assert_equal(result['warnings'], ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]) + if self.is_bdb_compiled(): + result = wmulti0.addmultisigaddress(2, keys, '', addr_type) + assert_equal(legacy_addr, result['address']) + assert_equal(result['warnings'], ["Unable to make chosen address type, please ensure no uncompressed public keys are present."]) self.log.info('Testing sortedmulti descriptors with BIP 67 test vectors') with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/rpc_bip67.json'), encoding='utf-8') as f: @@ -126,26 +138,29 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): bal0 = node0.getbalance() bal1 = node1.getbalance() bal2 = node2.getbalance() + balw = self.wallet.get_balance() height = node0.getblockchaininfo()["blocks"] assert 150 < height < 350 total = 149 * 50 + (height - 149 - 100) * 25 assert bal1 == 0 assert bal2 == self.moved - assert bal0 + bal1 + bal2 == total + assert_equal(bal0 + bal1 + bal2 + balw, total) def do_multisig(self): node0, node1, node2 = self.nodes - if 'wmulti' not in node1.listwallets(): - try: - node1.loadwallet('wmulti') - except JSONRPCException as e: - path = os.path.join(self.options.tmpdir, "node1", "regtest", "wallets", "wmulti") - if e.error['code'] == -18 and "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path) in e.error['message']: - node1.createwallet(wallet_name='wmulti', disable_private_keys=True) - else: - raise - wmulti = node1.get_wallet_rpc('wmulti') + + if self.is_bdb_compiled(): + if 'wmulti' not in node1.listwallets(): + try: + node1.loadwallet('wmulti') + except JSONRPCException as e: + path = os.path.join(self.options.tmpdir, "node1", "regtest", "wallets", "wmulti") + if e.error['code'] == -18 and "Wallet file verification failed. Failed to load database path '{}'. Path does not exist.".format(path) in e.error['message']: + node1.createwallet(wallet_name='wmulti', disable_private_keys=True) + else: + raise + wmulti = node1.get_wallet_rpc('wmulti') # Construct the expected descriptor desc = 'multi({},{})'.format(self.nsigs, ','.join(self.pub)) @@ -164,17 +179,19 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): if self.output_type == 'bech32': assert madd[0:4] == "bcrt" # actually a bech32 address - # compare against addmultisigaddress - msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None, self.output_type) - maddw = msigw["address"] - mredeemw = msigw["redeemScript"] - assert_equal(desc, drop_origins(msigw['descriptor'])) - # addmultisigiaddress and createmultisig work the same - assert maddw == madd - assert mredeemw == mredeem - - txid = node0.sendtoaddress(madd, 40) - + if self.is_bdb_compiled(): + # compare against addmultisigaddress + msigw = wmulti.addmultisigaddress(self.nsigs, self.pub, None, self.output_type) + maddw = msigw["address"] + mredeemw = msigw["redeemScript"] + assert_equal(desc, drop_origins(msigw['descriptor'])) + # addmultisigiaddress and createmultisig work the same + assert maddw == madd + assert mredeemw == mredeem + wmulti.unloadwallet() + + spk = bytes.fromhex(node0.validateaddress(madd)["scriptPubKey"]) + txid, _ = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=spk, amount=1300) tx = node0.getrawtransaction(txid, True) vout = [v["n"] for v in tx["vout"] if madd == v["scriptPubKey"]["address"]] assert len(vout) == 1 @@ -225,8 +242,6 @@ class RpcCreateMultiSigTest(BitcoinTestFramework): txinfo = node0.getrawtransaction(tx, True, blk) self.log.info("n/m=%d/%d %s size=%d vsize=%d weight=%d" % (self.nsigs, self.nkeys, self.output_type, txinfo["size"], txinfo["vsize"], txinfo["weight"])) - wmulti.unloadwallet() - if __name__ == '__main__': RpcCreateMultiSigTest().main() diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py index a650df70dd..37b8a2294d 100644 --- a/test/functional/test_framework/wallet.py +++ b/test/functional/test_framework/wallet.py @@ -96,6 +96,9 @@ class MiniWallet: self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true() self._scriptPubKey = bytes.fromhex(self._test_node.validateaddress(self._address)['scriptPubKey']) + def get_balance(self): + return sum(u['value'] for u in self._utxos) + def rescan_utxos(self): """Drop all utxos and rescan the utxo set""" self._utxos = [] @@ -188,6 +191,46 @@ class MiniWallet: txid = self.sendrawtransaction(from_node=from_node, tx_hex=tx.serialize().hex()) return txid, 1 + def send_self_transfer_multi(self, **kwargs): + """ + Create and send a transaction that spends the given UTXOs and creates a + certain number of outputs with equal amounts. + + Returns a dictionary with + - txid + - serialized transaction in hex format + - transaction as CTransaction instance + - list of newly created UTXOs, ordered by vout index + """ + tx = self.create_self_transfer_multi(**kwargs) + txid = self.sendrawtransaction(from_node=kwargs['from_node'], tx_hex=tx.serialize().hex()) + return {'new_utxos': [self.get_utxo(txid=txid, vout=vout) for vout in range(len(tx.vout))], + 'txid': txid, 'hex': tx.serialize().hex(), 'tx': tx} + + def create_self_transfer_multi(self, *, from_node, utxos_to_spend, num_outputs=1, fee_per_output=1000): + """ + Create and return a transaction that spends the given UTXOs and creates a + certain number of outputs with equal amounts. + """ + # create simple tx template (1 input, 1 output) + tx = self.create_self_transfer(fee_rate=0, from_node=from_node, utxo_to_spend=utxos_to_spend[0], mempool_valid=False)['tx'] + + # duplicate inputs, witnesses and outputs + tx.vin = [deepcopy(tx.vin[0]) for _ in range(len(utxos_to_spend))] + tx.wit.vtxinwit = [deepcopy(tx.wit.vtxinwit[0]) for _ in range(len(utxos_to_spend))] + tx.vout = [deepcopy(tx.vout[0]) for _ in range(num_outputs)] + + # adapt input prevouts + for i, utxo in enumerate(utxos_to_spend): + tx.vin[i] = CTxIn(COutPoint(int(utxo['txid'], 16), utxo['vout'])) + + # adapt output amounts (use fixed fee per output) + inputs_value_total = sum([int(COIN * utxo['value']) for utxo in utxos_to_spend]) + outputs_value_total = inputs_value_total - fee_per_output * num_outputs + for i in range(num_outputs): + tx.vout[i].nValue = outputs_value_total // num_outputs + return tx + def create_self_transfer(self, *, fee_rate=Decimal("0.003"), from_node=None, utxo_to_spend=None, mempool_valid=True, locktime=0, sequence=0): """Create and return a tx with the specified fee_rate. Fee may be exact or at most one satoshi higher than needed.""" from_node = from_node or self._test_node diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index b0f24e3b97..306c8e7ff0 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -188,8 +188,7 @@ BASE_SCRIPTS = [ 'rpc_decodescript.py', 'rpc_blockchain.py', 'rpc_deprecated.py', - 'wallet_disable.py --legacy-wallet', - 'wallet_disable.py --descriptors', + 'wallet_disable.py', 'p2p_addr_relay.py', 'p2p_getaddr_caching.py', 'p2p_getdata.py', @@ -225,8 +224,7 @@ BASE_SCRIPTS = [ 'feature_rbf.py --descriptors', 'mempool_packages.py', 'mempool_package_onemore.py', - 'rpc_createmultisig.py --legacy-wallet', - 'rpc_createmultisig.py --descriptors', + 'rpc_createmultisig.py', 'rpc_packages.py', 'mempool_package_limits.py', 'feature_versionbits_warning.py', @@ -309,8 +307,7 @@ BASE_SCRIPTS = [ 'feature_txindex_compatibility.py', 'feature_logging.py', 'feature_anchors.py', - 'feature_coinstatsindex.py --legacy-wallet', - 'feature_coinstatsindex.py --descriptors', + 'feature_coinstatsindex.py', 'wallet_orphanedreward.py', 'wallet_timelock.py', 'p2p_node_network_limited.py', @@ -589,11 +586,12 @@ def run_tests(*, test_list, src_dir, build_dir, tmpdir, jobs=1, enable_coverage= # Clean up dangling processes if any. This may only happen with --failfast option. # Killing the process group will also terminate the current process but that is # not an issue - if len(job_queue.jobs): + if not os.getenv("CI_FAILFAST_TEST_LEAVE_DANGLING") and len(job_queue.jobs): os.killpg(os.getpgid(0), signal.SIGKILL) sys.exit(not all_passed) + def print_results(test_results, max_len_name, runtime): results = "\n" + BOLD[1] + "%s | %s | %s\n\n" % ("TEST".ljust(max_len_name), "STATUS ", "DURATION") + BOLD[0] |