diff options
42 files changed, 1407 insertions, 96 deletions
diff --git a/.travis.yml b/.travis.yml index ea589b4059..1e49815a4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -149,6 +149,11 @@ jobs: FILE_ENV="./ci/test/00_setup_env_native_valgrind.sh" - stage: test + name: 'x86_64 Linux [GOAL: install] [bionic] [no depends, only system libs, c++17]' + env: >- + FILE_ENV="./ci/test/00_setup_env_native_cxx17.sh" + + - stage: test name: 'x86_64 Linux [GOAL: install] [focal] [no depends, only system libs, sanitizers: fuzzer,address,undefined]' env: >- FILE_ENV="./ci/test/00_setup_env_native_fuzz.sh" diff --git a/build-aux/m4/ax_cxx_compile_stdcxx.m4 b/build-aux/m4/ax_cxx_compile_stdcxx.m4 index f147cee3b1..43087b2e68 100644 --- a/build-aux/m4/ax_cxx_compile_stdcxx.m4 +++ b/build-aux/m4/ax_cxx_compile_stdcxx.m4 @@ -1,5 +1,5 @@ # =========================================================================== -# http://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html # =========================================================================== # # SYNOPSIS @@ -33,21 +33,23 @@ # Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com> # Copyright (c) 2015 Paul Norman <penorman@mac.com> # Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu> +# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com> +# Copyright (c) 2019 Enji Cooper <yaneurabeya@gmail.com> # # Copying and distribution of this file, with or without modification, are # permitted in any medium without royalty provided the copyright notice # and this notice are preserved. This file is offered as-is, without any # warranty. -#serial 4 +#serial 11 dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro dnl (serial version number 13). AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl - m4_if([$1], [11], [], - [$1], [14], [], - [$1], [17], [m4_fatal([support for C++17 not yet implemented in AX_CXX_COMPILE_STDCXX])], + m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], + [$1], [14], [ax_cxx_compile_alternatives="14 1y"], + [$1], [17], [ax_cxx_compile_alternatives="17 1z"], [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl m4_if([$2], [], [], [$2], [ext], [], @@ -57,26 +59,13 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true], [$3], [optional], [ax_cxx_compile_cxx$1_required=false], [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])]) - m4_if([$4], [], [ax_cxx_compile_cxx$1_try_default=true], - [$4], [default], [ax_cxx_compile_cxx$1_try_default=true], - [$4], [nodefault], [ax_cxx_compile_cxx$1_try_default=false], - [m4_fatal([invalid fourth argument `$4' to AX_CXX_COMPILE_STDCXX])]) AC_LANG_PUSH([C++])dnl ac_success=no - m4_if([$4], [nodefault], [], [dnl - AC_CACHE_CHECK(whether $CXX supports C++$1 features by default, - ax_cv_cxx_compile_cxx$1, - [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], - [ax_cv_cxx_compile_cxx$1=yes], - [ax_cv_cxx_compile_cxx$1=no])]) - if test x$ax_cv_cxx_compile_cxx$1 = xyes; then - ac_success=yes - fi]) - m4_if([$2], [noext], [], [dnl if test x$ac_success = xno; then - for switch in -std=gnu++$1 -std=gnu++0x; do + for alternative in ${ax_cxx_compile_alternatives}; do + switch="-std=gnu++${alternative}" cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, $cachevar, @@ -102,22 +91,27 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl dnl HP's aCC needs +std=c++11 according to: dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf dnl Cray's crayCC needs "-h std=c++11" - for switch in -std=c++$1 -std=c++0x +std=c++$1 "-h std=c++$1"; do - cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) - AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, - $cachevar, - [ac_save_CXX="$CXX" - CXX="$CXX $switch" - AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], - [eval $cachevar=yes], - [eval $cachevar=no]) - CXX="$ac_save_CXX"]) - if eval test x\$$cachevar = xyes; then - CXX="$CXX $switch" - if test -n "$CXXCPP" ; then - CXXCPP="$CXXCPP $switch" + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break fi - ac_success=yes + done + if test x$ac_success = xyes; then break fi done @@ -154,6 +148,11 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 ) +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 +) dnl Tests for new features in C++11 @@ -191,11 +190,13 @@ namespace cxx11 struct Base { + virtual ~Base() {} virtual void f() {} }; struct Derived : public Base { + virtual ~Derived() override {} virtual void f() override {} }; @@ -524,7 +525,7 @@ namespace cxx14 } - namespace test_digit_seperators + namespace test_digit_separators { constexpr auto ten_million = 100'000'000; @@ -566,3 +567,385 @@ namespace cxx14 #endif // __cplusplus >= 201402L ]]) + + +dnl Tests for new features in C++17 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include <initializer_list> +#include <utility> +#include <type_traits> + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template<typename... Args> + int multiply(Args... args) + { + return (args * ... * 1); + } + + template<typename... Args> + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value); + static_assert(std::is_same<int, decltype(bar)>::value); + } + + namespace test_typename_in_template_template_parameter + { + + template<template<typename> typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template <bool cond> + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template <typename T1, typename T2> + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template <auto n> + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair<int, int> pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair<int, int>& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template<typename T> + Bad + f(T*, T*); + + template<typename T1, typename T2> + Good + f(T1*, T2*); + + static_assert (std::is_same_v<Good, decltype(f(g1, g2))>); + + } + + namespace test_inline_variables + { + + template<class T> void f(T) + {} + + template<class T> inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // __cplusplus < 201703L + +]]) diff --git a/ci/test/00_setup_env.sh b/ci/test/00_setup_env.sh index 887083de1c..dae61c5e34 100755 --- a/ci/test/00_setup_env.sh +++ b/ci/test/00_setup_env.sh @@ -59,4 +59,4 @@ export DOCKER_PACKAGES=${DOCKER_PACKAGES:-build-essential libtool autotools-dev export GOAL=${GOAL:-install} export DIR_QA_ASSETS=${DIR_QA_ASSETS:-${BASE_SCRATCH_DIR}/qa-assets} export PATH=${BASE_ROOT_DIR}/ci/retry:$PATH -export CI_RETRY_EXE=${CI_RETRY_EXE:retry} +export CI_RETRY_EXE=${CI_RETRY_EXE:-"retry --"} diff --git a/ci/test/00_setup_env_native_cxx17.sh b/ci/test/00_setup_env_native_cxx17.sh new file mode 100644 index 0000000000..9a75276420 --- /dev/null +++ b/ci/test/00_setup_env_native_cxx17.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C.UTF-8 + +export CONTAINER_NAME=ci_native_cxx17 +export PACKAGES="python3-zmq libevent-dev bsdmainutils libboost-system-dev libboost-filesystem-dev libboost-test-dev libboost-thread-dev libdb5.3++-dev libminiupnpc-dev libzmq3-dev" +export NO_DEPENDS=1 +export GOAL="install" +export BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --enable-c++17" diff --git a/ci/test/04_install.sh b/ci/test/04_install.sh index b428a2e5b2..5dbf1b82f1 100755 --- a/ci/test/04_install.sh +++ b/ci/test/04_install.sh @@ -58,6 +58,7 @@ else bash -c "export PATH=$BASE_SCRATCH_DIR/bins/:\$PATH && cd $P_CI_DIR && $*" } fi +export -f DOCKER_EXEC if [ -n "$DPKG_ADD_ARCH" ]; then DOCKER_EXEC dpkg --add-architecture "$DPKG_ADD_ARCH" diff --git a/ci/test/05_before_script.sh b/ci/test/05_before_script.sh index 17853e75dd..ab9d673101 100755 --- a/ci/test/05_before_script.sh +++ b/ci/test/05_before_script.sh @@ -37,6 +37,6 @@ if [ -z "$NO_DEPENDS" ]; then fi if [ "$TEST_PREVIOUS_RELEASES" = "true" ]; then BEGIN_FOLD previous-versions - DOCKER_EXEC contrib/devtools/previous_release.sh -b -t "$PREVIOUS_RELEASES_DIR" v0.17.1 v0.18.1 v0.19.0.1 + DOCKER_EXEC contrib/devtools/previous_release.sh -b -t "$PREVIOUS_RELEASES_DIR" v0.15.2 v0.16.3 v0.17.1 v0.18.1 v0.19.0.1 END_FOLD fi diff --git a/configure.ac b/configure.ac index 473fe2064a..b681f3f6de 100644 --- a/configure.ac +++ b/configure.ac @@ -61,8 +61,20 @@ case $host in lt_cv_deplibs_check_method="pass_all" ;; esac -dnl Require C++11 compiler (no GNU extensions) -AX_CXX_COMPILE_STDCXX([11], [noext], [mandatory], [nodefault]) + +AC_ARG_ENABLE([c++17], + [AS_HELP_STRING([--enable-c++17], + [enable compilation in c++17 mode (disabled by default)])], + [use_cxx17=$enableval], + [use_cxx17=no]) + +dnl Require C++11 or C++17 compiler (no GNU extensions) +if test "x$use_cxx17" = xyes; then + AX_CXX_COMPILE_STDCXX([17], [noext], [mandatory]) +else + AX_CXX_COMPILE_STDCXX([11], [noext], [mandatory]) +fi + dnl Check if -latomic is required for <std::atomic> CHECK_ATOMIC @@ -846,6 +858,22 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <malloc.h>]], [ AC_MSG_RESULT(no)] ) +dnl Check for posix_fallocate +AC_MSG_CHECKING(for posix_fallocate) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ + // same as in src/util/system.cpp + #ifdef __linux__ + #ifdef _POSIX_C_SOURCE + #undef _POSIX_C_SOURCE + #endif + #define _POSIX_C_SOURCE 200112L + #endif // __linux__ + #include <fcntl.h>]], + [[ int f = posix_fallocate(0, 0, 0); ]])], + [ AC_MSG_RESULT(yes); AC_DEFINE(HAVE_POSIX_FALLOCATE, 1,[Define this symbol if you have posix_fallocate]) ], + [ AC_MSG_RESULT(no)] +) + AC_MSG_CHECKING([for visibility attribute]) AC_LINK_IFELSE([AC_LANG_SOURCE([ int foo_def( void ) __attribute__((visibility("default"))); diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 06dc59cf5c..48db60f086 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -49,6 +49,7 @@ FUZZ_TARGETS = \ test/fuzz/key \ test/fuzz/key_io \ test/fuzz/key_origin_info_deserialize \ + test/fuzz/kitchen_sink \ test/fuzz/locale \ test/fuzz/merkle_block_deserialize \ test/fuzz/merkleblock \ @@ -64,13 +65,12 @@ FUZZ_TARGETS = \ test/fuzz/parse_numbers \ test/fuzz/parse_script \ test/fuzz/parse_univalue \ - test/fuzz/prevector \ test/fuzz/partial_merkle_tree_deserialize \ test/fuzz/partially_signed_transaction_deserialize \ test/fuzz/pow \ test/fuzz/prefilled_transaction_deserialize \ + test/fuzz/prevector \ test/fuzz/primitives_transaction \ - test/fuzz/process_messages \ test/fuzz/process_message \ test/fuzz/process_message_addr \ test/fuzz/process_message_block \ @@ -96,6 +96,7 @@ FUZZ_TARGETS = \ test/fuzz/process_message_tx \ test/fuzz/process_message_verack \ test/fuzz/process_message_version \ + test/fuzz/process_messages \ test/fuzz/protocol \ test/fuzz/psbt \ test/fuzz/psbt_input_deserialize \ @@ -116,6 +117,7 @@ FUZZ_TARGETS = \ test/fuzz/string \ test/fuzz/strprintf \ test/fuzz/sub_net_deserialize \ + test/fuzz/system \ test/fuzz/timedata \ test/fuzz/transaction \ test/fuzz/tx_in \ @@ -567,6 +569,12 @@ test_fuzz_key_origin_info_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON) test_fuzz_key_origin_info_deserialize_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) test_fuzz_key_origin_info_deserialize_SOURCES = test/fuzz/deserialize.cpp +test_fuzz_kitchen_sink_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) +test_fuzz_kitchen_sink_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) +test_fuzz_kitchen_sink_LDADD = $(FUZZ_SUITE_LD_COMMON) +test_fuzz_kitchen_sink_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) +test_fuzz_kitchen_sink_SOURCES = test/fuzz/kitchen_sink.cpp + test_fuzz_locale_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) test_fuzz_locale_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) test_fuzz_locale_LDADD = $(FUZZ_SUITE_LD_COMMON) @@ -969,6 +977,12 @@ test_fuzz_sub_net_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON) test_fuzz_sub_net_deserialize_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) test_fuzz_sub_net_deserialize_SOURCES = test/fuzz/deserialize.cpp +test_fuzz_system_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) +test_fuzz_system_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) +test_fuzz_system_LDADD = $(FUZZ_SUITE_LD_COMMON) +test_fuzz_system_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) +test_fuzz_system_SOURCES = test/fuzz/system.cpp + test_fuzz_timedata_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) test_fuzz_timedata_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) test_fuzz_timedata_LDADD = $(FUZZ_SUITE_LD_COMMON) diff --git a/src/bench/block_assemble.cpp b/src/bench/block_assemble.cpp index 1a0084c915..268f67cada 100644 --- a/src/bench/block_assemble.cpp +++ b/src/bench/block_assemble.cpp @@ -16,7 +16,14 @@ static void AssembleBlock(benchmark::State& state) { - RegTestingSetup test_setup; + TestingSetup test_setup{ + CBaseChainParams::REGTEST, + /* extra_args */ { + "-nodebuglogfile", + "-nodebug", + }, + }; + const std::vector<unsigned char> op_true{OP_TRUE}; CScriptWitness witness; witness.stack.push_back(op_true); diff --git a/src/bench/duplicate_inputs.cpp b/src/bench/duplicate_inputs.cpp index 57673ccb84..e87f15042b 100644 --- a/src/bench/duplicate_inputs.cpp +++ b/src/bench/duplicate_inputs.cpp @@ -14,7 +14,13 @@ static void DuplicateInputs(benchmark::State& state) { - RegTestingSetup test_setup; + TestingSetup test_setup{ + CBaseChainParams::REGTEST, + /* extra_args */ { + "-nodebuglogfile", + "-nodebug", + }, + }; const CScript SCRIPT_PUB{CScript(OP_TRUE)}; diff --git a/src/bench/mempool_eviction.cpp b/src/bench/mempool_eviction.cpp index 7df024def6..69483f2914 100644 --- a/src/bench/mempool_eviction.cpp +++ b/src/bench/mempool_eviction.cpp @@ -16,8 +16,8 @@ static void AddTx(const CTransactionRef& tx, const CAmount& nFee, CTxMemPool& po unsigned int sigOpCost = 4; LockPoints lp; pool.addUnchecked(CTxMemPoolEntry( - tx, nFee, nTime, nHeight, - spendsCoinbase, sigOpCost, lp)); + tx, nFee, nTime, nHeight, + spendsCoinbase, sigOpCost, lp)); } // Right now this is only testing eviction performance in an extremely small @@ -25,7 +25,13 @@ static void AddTx(const CTransactionRef& tx, const CAmount& nFee, CTxMemPool& po // unique transactions for a more meaningful performance measurement. static void MempoolEviction(benchmark::State& state) { - RegTestingSetup test_setup; + TestingSetup test_setup{ + CBaseChainParams::REGTEST, + /* extra_args */ { + "-nodebuglogfile", + "-nodebug", + }, + }; CMutableTransaction tx1 = CMutableTransaction(); tx1.vin.resize(1); diff --git a/src/bench/wallet_balance.cpp b/src/bench/wallet_balance.cpp index 05d61fca22..810c344ab5 100644 --- a/src/bench/wallet_balance.cpp +++ b/src/bench/wallet_balance.cpp @@ -14,7 +14,14 @@ static void WalletBalance(benchmark::State& state, const bool set_dirty, const bool add_watchonly, const bool add_mine) { - RegTestingSetup test_setup; + TestingSetup test_setup{ + CBaseChainParams::REGTEST, + /* extra_args */ { + "-nodebuglogfile", + "-nodebug", + }, + }; + const auto& ADDRESS_WATCHONLY = ADDRESS_BCRT1_UNSPENDABLE; NodeContext node; @@ -25,7 +32,7 @@ static void WalletBalance(benchmark::State& state, const bool set_dirty, const b bool first_run; if (wallet.LoadWallet(first_run) != DBErrors::LOAD_OK) assert(false); } - auto handler = chain->handleNotifications({ &wallet, [](CWallet*) {} }); + auto handler = chain->handleNotifications({&wallet, [](CWallet*) {}}); const Optional<std::string> address_mine{add_mine ? Optional<std::string>{getnewaddress(wallet)} : nullopt}; if (add_watchonly) importaddress(wallet, ADDRESS_WATCHONLY); diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 26327ac6eb..50a8a8a882 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -810,6 +810,19 @@ void PeerLogicValidation::InitializeNode(CNode *pnode) { PushNodeVersion(pnode, connman, GetTime()); } +void PeerLogicValidation::ReattemptInitialBroadcast(CScheduler& scheduler) const +{ + std::set<uint256> unbroadcast_txids = m_mempool.GetUnbroadcastTxs(); + + for (const uint256& txid : unbroadcast_txids) { + RelayTransaction(txid, *connman); + } + + // schedule next run for 10-15 minutes in the future + const std::chrono::milliseconds delta = std::chrono::minutes{10} + GetRandMillis(std::chrono::minutes{5}); + scheduler.scheduleFromNow([&] { ReattemptInitialBroadcast(scheduler); }, delta); +} + void PeerLogicValidation::FinalizeNode(NodeId nodeid, bool& fUpdateConnectionTime) { fUpdateConnectionTime = false; LOCK(cs_main); @@ -1159,6 +1172,10 @@ PeerLogicValidation::PeerLogicValidation(CConnman* connmanIn, BanMan* banman, CS // timer. static_assert(EXTRA_PEER_CHECK_INTERVAL < STALE_CHECK_INTERVAL, "peer eviction timer should be less than stale tip check timer"); scheduler.scheduleEvery([this, consensusParams] { this->CheckForStaleTipAndEvictPeers(consensusParams); }, std::chrono::seconds{EXTRA_PEER_CHECK_INTERVAL}); + + // schedule next run for 10-15 minutes in the future + const std::chrono::milliseconds delta = std::chrono::minutes{10} + GetRandMillis(std::chrono::minutes{5}); + scheduler.scheduleFromNow([&] { ReattemptInitialBroadcast(scheduler); }, delta); } /** @@ -1587,7 +1604,7 @@ void static ProcessGetBlockData(CNode* pfrom, const CChainParams& chainparams, c } } -void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnman* connman, const CTxMemPool& mempool, const std::atomic<bool>& interruptMsgProc) LOCKS_EXCLUDED(cs_main) +void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnman* connman, CTxMemPool& mempool, const std::atomic<bool>& interruptMsgProc) LOCKS_EXCLUDED(cs_main) { AssertLockNotHeld(cs_main); @@ -1636,7 +1653,13 @@ void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnm push = true; } } - if (!push) { + + if (push) { + // We interpret fulfilling a GETDATA for a transaction as a + // successful initial broadcast and remove it from our + // unbroadcast set. + mempool.RemoveUnbroadcastTx(inv.hash); + } else { vNotFound.push_back(inv); } } diff --git a/src/net_processing.h b/src/net_processing.h index 3d9bc65a3a..a85d5e7c70 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -76,6 +76,8 @@ public: void CheckForStaleTipAndEvictPeers(const Consensus::Params &consensusParams); /** If we have extra outbound peers, try to disconnect the one with the oldest block announcement */ void EvictExtraOutboundPeers(int64_t time_in_seconds) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Retrieve unbroadcast transactions from the mempool and reattempt sending to peers */ + void ReattemptInitialBroadcast(CScheduler& scheduler) const; private: int64_t m_stale_tip_check_time; //!< Next time to check for stale tip diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp index 201406ce3b..3841d8687d 100644 --- a/src/node/transaction.cpp +++ b/src/node/transaction.cpp @@ -78,6 +78,10 @@ TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef t } if (relay) { + // the mempool tracks locally submitted transactions to make a + // best-effort of initial broadcast + node.mempool->AddUnbroadcastTx(hashTx); + RelayTransaction(hashTx, *node.connman); } diff --git a/src/random.cpp b/src/random.cpp index 765239e1f5..b408b1e13e 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -592,6 +592,11 @@ std::chrono::microseconds GetRandMicros(std::chrono::microseconds duration_max) return std::chrono::microseconds{GetRand(duration_max.count())}; } +std::chrono::milliseconds GetRandMillis(std::chrono::milliseconds duration_max) noexcept +{ + return std::chrono::milliseconds{GetRand(duration_max.count())}; +} + int GetRandInt(int nMax) noexcept { return GetRand(nMax); diff --git a/src/random.h b/src/random.h index 0c5008685b..690125079b 100644 --- a/src/random.h +++ b/src/random.h @@ -69,6 +69,7 @@ void GetRandBytes(unsigned char* buf, int num) noexcept; uint64_t GetRand(uint64_t nMax) noexcept; std::chrono::microseconds GetRandMicros(std::chrono::microseconds duration_max) noexcept; +std::chrono::milliseconds GetRandMillis(std::chrono::milliseconds duration_max) noexcept; int GetRandInt(int nMax) noexcept; uint256 GetRandHash() noexcept; diff --git a/src/rest.cpp b/src/rest.cpp index b389bf2028..5f99e26bad 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -607,7 +607,7 @@ static bool rest_blockhash_by_height(HTTPRequest* req, std::string height_str; const RetFormat rf = ParseDataFormat(height_str, str_uri_part); - int32_t blockheight; + int32_t blockheight = -1; // Initialization done only to prevent valgrind false positive, see https://github.com/bitcoin/bitcoin/pull/18785 if (!ParseInt32(height_str, &blockheight) || blockheight < 0) { return RESTERR(req, HTTP_BAD_REQUEST, "Invalid height: " + SanitizeString(height_str)); } diff --git a/src/span.h b/src/span.h index 664ea8d4c6..73b37e35f3 100644 --- a/src/span.h +++ b/src/span.h @@ -25,6 +25,23 @@ public: constexpr Span(C* data, std::ptrdiff_t size) noexcept : m_data(data), m_size(size) {} constexpr Span(C* data, C* end) noexcept : m_data(data), m_size(end - data) {} + /** Implicit conversion of spans between compatible types. + * + * Specifically, if a pointer to an array of type O can be implicitly converted to a pointer to an array of type + * C, then permit implicit conversion of Span<O> to Span<C>. This matches the behavior of the corresponding + * C++20 std::span constructor. + * + * For example this means that a Span<T> can be converted into a Span<const T>. + */ + template <typename O, typename std::enable_if<std::is_convertible<O (*)[], C (*)[]>::value, int>::type = 0> + constexpr Span(const Span<O>& other) noexcept : m_data(other.m_data), m_size(other.m_size) {} + + /** Default copy constructor. */ + constexpr Span(const Span&) noexcept = default; + + /** Default assignment operator. */ + Span& operator=(const Span& other) noexcept = default; + constexpr C* data() const noexcept { return m_data; } constexpr C* begin() const noexcept { return m_data; } constexpr C* end() const noexcept { return m_data + m_size; } @@ -44,6 +61,8 @@ public: friend constexpr bool operator<=(const Span& a, const Span& b) noexcept { return !(b < a); } friend constexpr bool operator>(const Span& a, const Span& b) noexcept { return (b < a); } friend constexpr bool operator>=(const Span& a, const Span& b) noexcept { return !(a < b); } + + template <typename O> friend class Span; }; /** Create a span to a container exposing data() and size(). diff --git a/src/test/fuzz/fees.cpp b/src/test/fuzz/fees.cpp index 090994263e..f29acace23 100644 --- a/src/test/fuzz/fees.cpp +++ b/src/test/fuzz/fees.cpp @@ -8,6 +8,7 @@ #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> +#include <util/fees.h> #include <cstdint> #include <string> @@ -23,4 +24,6 @@ void test_one_input(const std::vector<uint8_t>& buffer) const CAmount rounded_fee = fee_filter_rounder.round(current_minimum_fee); assert(MoneyRange(rounded_fee)); } + const FeeReason fee_reason = fuzzed_data_provider.PickValueInArray({FeeReason::NONE, FeeReason::HALF_ESTIMATE, FeeReason::FULL_ESTIMATE, FeeReason::DOUBLE_ESTIMATE, FeeReason::CONSERVATIVE, FeeReason::MEMPOOL_MIN, FeeReason::PAYTXFEE, FeeReason::FALLBACK, FeeReason::REQUIRED}); + (void)StringForFeeReason(fee_reason); } diff --git a/src/test/fuzz/integer.cpp b/src/test/fuzz/integer.cpp index 9dbf0fcc90..d5b5ec3c9a 100644 --- a/src/test/fuzz/integer.cpp +++ b/src/test/fuzz/integer.cpp @@ -24,8 +24,8 @@ #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> -#include <time.h> #include <uint256.h> +#include <util/check.h> #include <util/moneystr.h> #include <util/strencodings.h> #include <util/string.h> @@ -35,6 +35,7 @@ #include <cassert> #include <chrono> +#include <ctime> #include <limits> #include <set> #include <vector> @@ -287,8 +288,12 @@ void test_one_input(const std::vector<uint8_t>& buffer) try { const uint64_t deserialized_u64 = ReadCompactSize(stream); assert(u64 == deserialized_u64 && stream.empty()); + } catch (const std::ios_base::failure&) { } - catch (const std::ios_base::failure&) { - } + } + + try { + CHECK_NONFATAL(b); + } catch (const NonFatalCheckError&) { } } diff --git a/src/test/fuzz/kitchen_sink.cpp b/src/test/fuzz/kitchen_sink.cpp new file mode 100644 index 0000000000..af6dc71322 --- /dev/null +++ b/src/test/fuzz/kitchen_sink.cpp @@ -0,0 +1,25 @@ +// Copyright (c) 2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <rpc/util.h> +#include <test/fuzz/FuzzedDataProvider.h> +#include <test/fuzz/fuzz.h> +#include <test/fuzz/util.h> +#include <util/error.h> + +#include <cstdint> +#include <vector> + +// The fuzzing kitchen sink: Fuzzing harness for functions that need to be +// fuzzed but a.) don't belong in any existing fuzzing harness file, and +// b.) are not important enough to warrant their own fuzzing harness file. +void test_one_input(const std::vector<uint8_t>& buffer) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + + const TransactionError transaction_error = fuzzed_data_provider.PickValueInArray<TransactionError>({TransactionError::OK, TransactionError::MISSING_INPUTS, TransactionError::ALREADY_IN_CHAIN, TransactionError::P2P_DISABLED, TransactionError::MEMPOOL_REJECTED, TransactionError::MEMPOOL_ERROR, TransactionError::INVALID_PSBT, TransactionError::PSBT_MISMATCH, TransactionError::SIGHASH_MISMATCH, TransactionError::MAX_FEE_EXCEEDED}); + (void)JSONRPCTransactionError(transaction_error); + (void)RPCErrorFromTransactionError(transaction_error); + (void)TransactionErrorString(transaction_error); +} diff --git a/src/test/fuzz/parse_hd_keypath.cpp b/src/test/fuzz/parse_hd_keypath.cpp index 9a23f4b2d4..f668ca8c48 100644 --- a/src/test/fuzz/parse_hd_keypath.cpp +++ b/src/test/fuzz/parse_hd_keypath.cpp @@ -2,12 +2,22 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> +#include <test/fuzz/util.h> #include <util/bip32.h> +#include <cstdint> +#include <vector> + void test_one_input(const std::vector<uint8_t>& buffer) { const std::string keypath_str(buffer.begin(), buffer.end()); std::vector<uint32_t> keypath; (void)ParseHDKeypath(keypath_str, keypath); + + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + const std::vector<uint32_t> random_keypath = ConsumeRandomLengthIntegralVector<uint32_t>(fuzzed_data_provider); + (void)FormatHDKeypath(random_keypath); + (void)WriteHDKeypath(random_keypath); } diff --git a/src/test/fuzz/string.cpp b/src/test/fuzz/string.cpp index 3de0cf8db7..49bee0e81f 100644 --- a/src/test/fuzz/string.cpp +++ b/src/test/fuzz/string.cpp @@ -115,4 +115,8 @@ void test_one_input(const std::vector<uint8_t>& buffer) assert(data_stream.empty()); assert(deserialized_string == random_string_1); } + { + int64_t amount_out; + (void)ParseFixedPoint(random_string_1, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 1024), &amount_out); + } } diff --git a/src/test/fuzz/system.cpp b/src/test/fuzz/system.cpp new file mode 100644 index 0000000000..7f378c2b13 --- /dev/null +++ b/src/test/fuzz/system.cpp @@ -0,0 +1,123 @@ +// Copyright (c) 2020 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <test/fuzz/FuzzedDataProvider.h> +#include <test/fuzz/fuzz.h> +#include <test/fuzz/util.h> +#include <util/system.h> + +#include <cstdint> +#include <string> +#include <vector> + +namespace { +std::string GetArgumentName(const std::string& name) +{ + size_t idx = name.find('='); + if (idx == std::string::npos) { + idx = name.size(); + } + return name.substr(0, idx); +} +} // namespace + +void test_one_input(const std::vector<uint8_t>& buffer) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + ArgsManager args_manager{}; + + if (fuzzed_data_provider.ConsumeBool()) { + SetupHelpOptions(args_manager); + } + + while (fuzzed_data_provider.ConsumeBool()) { + switch (fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 7)) { + case 0: { + args_manager.SelectConfigNetwork(fuzzed_data_provider.ConsumeRandomLengthString(16)); + break; + } + case 1: { + args_manager.SoftSetArg(fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeRandomLengthString(16)); + break; + } + case 2: { + args_manager.ForceSetArg(fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeRandomLengthString(16)); + break; + } + case 3: { + args_manager.SoftSetBoolArg(fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeBool()); + break; + } + case 4: { + const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray<OptionsCategory>({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::HIDDEN}); + // Avoid hitting: + // util/system.cpp:425: void ArgsManager::AddArg(const std::string &, const std::string &, unsigned int, const OptionsCategory &): Assertion `ret.second' failed. + const std::string argument_name = GetArgumentName(fuzzed_data_provider.ConsumeRandomLengthString(16)); + if (args_manager.GetArgFlags(argument_name) != nullopt) { + break; + } + args_manager.AddArg(argument_name, fuzzed_data_provider.ConsumeRandomLengthString(16), fuzzed_data_provider.ConsumeIntegral<unsigned int>(), options_category); + break; + } + case 5: { + // Avoid hitting: + // util/system.cpp:425: void ArgsManager::AddArg(const std::string &, const std::string &, unsigned int, const OptionsCategory &): Assertion `ret.second' failed. + const std::vector<std::string> names = ConsumeRandomLengthStringVector(fuzzed_data_provider); + std::vector<std::string> hidden_arguments; + for (const std::string& name : names) { + const std::string hidden_argument = GetArgumentName(name); + if (args_manager.GetArgFlags(hidden_argument) != nullopt) { + continue; + } + if (std::find(hidden_arguments.begin(), hidden_arguments.end(), hidden_argument) != hidden_arguments.end()) { + continue; + } + hidden_arguments.push_back(hidden_argument); + } + args_manager.AddHiddenArgs(hidden_arguments); + break; + } + case 6: { + args_manager.ClearArgs(); + break; + } + case 7: { + const std::vector<std::string> random_arguments = ConsumeRandomLengthStringVector(fuzzed_data_provider); + std::vector<const char*> argv; + argv.resize(random_arguments.size()); + for (const std::string& random_argument : random_arguments) { + argv.push_back(random_argument.c_str()); + } + try { + std::string error; + (void)args_manager.ParseParameters(argv.size(), argv.data(), error); + } catch (const std::logic_error&) { + } + break; + } + } + } + + const std::string s1 = fuzzed_data_provider.ConsumeRandomLengthString(16); + const std::string s2 = fuzzed_data_provider.ConsumeRandomLengthString(16); + const int64_t i64 = fuzzed_data_provider.ConsumeIntegral<int64_t>(); + const bool b = fuzzed_data_provider.ConsumeBool(); + + (void)args_manager.GetArg(s1, i64); + (void)args_manager.GetArg(s1, s2); + (void)args_manager.GetArgFlags(s1); + (void)args_manager.GetArgs(s1); + (void)args_manager.GetBoolArg(s1, b); + try { + (void)args_manager.GetChainName(); + } catch (const std::runtime_error&) { + } + (void)args_manager.GetHelpMessage(); + (void)args_manager.GetUnrecognizedSections(); + (void)args_manager.GetUnsuitableSectionOnlyArgs(); + (void)args_manager.IsArgNegated(s1); + (void)args_manager.IsArgSet(s1); + + (void)HelpRequested(args_manager); +} diff --git a/src/test/util/logging.cpp b/src/test/util/logging.cpp index fe2e69104b..65a64f2384 100644 --- a/src/test/util/logging.cpp +++ b/src/test/util/logging.cpp @@ -11,13 +11,13 @@ #include <stdexcept> -DebugLogHelper::DebugLogHelper(std::string message) - : m_message{std::move(message)} +DebugLogHelper::DebugLogHelper(std::string message, MatchFn match) + : m_message{std::move(message)}, m_match(std::move(match)) { m_print_connection = LogInstance().PushBackCallback( [this](const std::string& s) { if (m_found) return; - m_found = s.find(m_message) != std::string::npos; + m_found = s.find(m_message) != std::string::npos && m_match(&s); }); noui_test_redirect(); } @@ -26,7 +26,7 @@ void DebugLogHelper::check_found() { noui_reconnect(); LogInstance().DeleteCallback(m_print_connection); - if (!m_found) { + if (!m_found && m_match(nullptr)) { throw std::runtime_error(strprintf("'%s' not found in debug log\n", m_message)); } } diff --git a/src/test/util/logging.h b/src/test/util/logging.h index 45ec44173c..1fcf7ca305 100644 --- a/src/test/util/logging.h +++ b/src/test/util/logging.h @@ -17,10 +17,22 @@ class DebugLogHelper bool m_found{false}; std::list<std::function<void(const std::string&)>>::iterator m_print_connection; + //! Custom match checking function. + //! + //! Invoked with pointers to lines containing matching strings, and with + //! null if check_found() is called without any successful match. + //! + //! Can return true to enable default DebugLogHelper behavior of: + //! (1) ending search after first successful match, and + //! (2) raising an error in check_found if no match was found + //! Can return false to do the opposite in either case. + using MatchFn = std::function<bool(const std::string* line)>; + MatchFn m_match; + void check_found(); public: - DebugLogHelper(std::string message); + DebugLogHelper(std::string message, MatchFn match = [](const std::string*){ return true; }); ~DebugLogHelper() { check_found(); } }; diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 102e50dc5c..c5c0208d8f 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -417,6 +417,8 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason) for (const CTxIn& txin : it->GetTx().vin) mapNextTx.erase(txin.prevout); + RemoveUnbroadcastTx(hash, true /* add logging because unchecked */ ); + if (vTxHashes.size() > 1) { vTxHashes[it->vTxHashesIdx] = std::move(vTxHashes.back()); vTxHashes[it->vTxHashesIdx].second->vTxHashesIdx = it->vTxHashesIdx; @@ -919,6 +921,15 @@ size_t CTxMemPool::DynamicMemoryUsage() const { return memusage::MallocUsage(sizeof(CTxMemPoolEntry) + 12 * sizeof(void*)) * mapTx.size() + memusage::DynamicUsage(mapNextTx) + memusage::DynamicUsage(mapDeltas) + memusage::DynamicUsage(mapLinks) + memusage::DynamicUsage(vTxHashes) + cachedInnerUsage; } +void CTxMemPool::RemoveUnbroadcastTx(const uint256& txid, const bool unchecked) { + LOCK(cs); + + if (m_unbroadcast_txids.erase(txid)) + { + LogPrint(BCLog::MEMPOOL, "Removed %i from set of unbroadcast txns%s\n", txid.GetHex(), (unchecked ? " before confirmation that txn was sent out" : "")); + } +} + void CTxMemPool::RemoveStaged(setEntries &stage, bool updateDescendants, MemPoolRemovalReason reason) { AssertLockHeld(cs); UpdateForRemoveFromMempool(stage, updateDescendants); diff --git a/src/txmempool.h b/src/txmempool.h index 3dae0a04c7..4bee78b8d6 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -549,6 +549,9 @@ private: std::vector<indexed_transaction_set::const_iterator> GetSortedDepthAndScore() const EXCLUSIVE_LOCKS_REQUIRED(cs); + /** track locally submitted transactions to periodically retry initial broadcast */ + std::set<uint256> m_unbroadcast_txids GUARDED_BY(cs); + public: indirectmap<COutPoint, const CTransaction*> mapNextTx GUARDED_BY(cs); std::map<uint256, CAmount> mapDeltas; @@ -698,6 +701,21 @@ public: size_t DynamicMemoryUsage() const; + /** Adds a transaction to the unbroadcast set */ + void AddUnbroadcastTx(const uint256& txid) { + LOCK(cs); + m_unbroadcast_txids.insert(txid); + } + + /** Removes a transaction from the unbroadcast set */ + void RemoveUnbroadcastTx(const uint256& txid, const bool unchecked = false); + + /** Returns transactions in unbroadcast set */ + const std::set<uint256> GetUnbroadcastTxs() const { + LOCK(cs); + return m_unbroadcast_txids; + } + private: /** UpdateForDescendants is used by UpdateTransactionsFromBlock to update * the descendants for a single transaction that has been added to the diff --git a/src/util/system.cpp b/src/util/system.cpp index 0790ea0d48..2013b416db 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -17,7 +17,7 @@ #endif #ifndef WIN32 -// for posix_fallocate +// for posix_fallocate, in configure.ac we check if it is present after this #ifdef __linux__ #ifdef _POSIX_C_SOURCE @@ -1019,7 +1019,7 @@ void AllocateFileRange(FILE *file, unsigned int offset, unsigned int length) { } ftruncate(fileno(file), static_cast<off_t>(offset) + length); #else - #if defined(__linux__) + #if defined(HAVE_POSIX_FALLOCATE) // Version using posix_fallocate off_t nEndPos = (off_t)offset + length; if (0 == posix_fallocate(fileno(file), 0, nEndPos)) return; diff --git a/src/validation.cpp b/src/validation.cpp index 25975e3e31..d18803b342 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4998,6 +4998,7 @@ bool LoadMempool(CTxMemPool& pool) int64_t expired = 0; int64_t failed = 0; int64_t already_there = 0; + int64_t unbroadcast = 0; int64_t nNow = GetTime(); try { @@ -5051,12 +5052,21 @@ bool LoadMempool(CTxMemPool& pool) for (const auto& i : mapDeltas) { pool.PrioritiseTransaction(i.first, i.second); } + + std::set<uint256> unbroadcast_txids; + file >> unbroadcast_txids; + unbroadcast = unbroadcast_txids.size(); + + for (const auto& txid : unbroadcast_txids) { + pool.AddUnbroadcastTx(txid); + } + } catch (const std::exception& e) { LogPrintf("Failed to deserialize mempool data on disk: %s. Continuing anyway.\n", e.what()); return false; } - LogPrintf("Imported mempool transactions from disk: %i succeeded, %i failed, %i expired, %i already there\n", count, failed, expired, already_there); + LogPrintf("Imported mempool transactions from disk: %i succeeded, %i failed, %i expired, %i already there, %i waiting for initial broadcast\n", count, failed, expired, already_there, unbroadcast); return true; } @@ -5066,6 +5076,7 @@ bool DumpMempool(const CTxMemPool& pool) std::map<uint256, CAmount> mapDeltas; std::vector<TxMempoolInfo> vinfo; + std::set<uint256> unbroadcast_txids; static Mutex dump_mutex; LOCK(dump_mutex); @@ -5076,6 +5087,7 @@ bool DumpMempool(const CTxMemPool& pool) mapDeltas[i.first] = i.second; } vinfo = pool.infoAll(); + unbroadcast_txids = pool.GetUnbroadcastTxs(); } int64_t mid = GetTimeMicros(); @@ -5100,6 +5112,10 @@ bool DumpMempool(const CTxMemPool& pool) } file << mapDeltas; + + LogPrintf("Writing %d unbroadcast transactions to disk.\n", unbroadcast_txids.size()); + file << unbroadcast_txids; + if (!FileCommit(file.Get())) throw std::runtime_error("FileCommit failed"); file.fclose(); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 2bdeb89f3c..1215956bb4 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -4,6 +4,7 @@ #include <wallet/wallet.h> +#include <future> #include <memory> #include <stdint.h> #include <vector> @@ -12,6 +13,7 @@ #include <node/context.h> #include <policy/policy.h> #include <rpc/server.h> +#include <test/util/logging.h> #include <test/util/setup_common.h> #include <validation.h> #include <wallet/coincontrol.h> @@ -26,6 +28,36 @@ extern UniValue importwallet(const JSONRPCRequest& request); BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) +static std::shared_ptr<CWallet> TestLoadWallet(interfaces::Chain& chain) +{ + std::string error; + std::vector<std::string> warnings; + auto wallet = CWallet::CreateWalletFromFile(chain, WalletLocation(""), error, warnings); + wallet->postInitProcess(); + return wallet; +} + +static void TestUnloadWallet(std::shared_ptr<CWallet>&& wallet) +{ + SyncWithValidationInterfaceQueue(); + wallet->m_chain_notifications_handler.reset(); + UnloadWallet(std::move(wallet)); +} + +static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey) +{ + CMutableTransaction mtx; + mtx.vout.push_back({from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey}); + mtx.vin.push_back({CTxIn{from.GetHash(), index}}); + FillableSigningProvider keystore; + keystore.AddKey(key); + std::map<COutPoint, Coin> coins; + coins[mtx.vin[0].prevout].out = from.vout[index]; + std::map<int, std::string> input_errors; + BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors)); + return mtx; +} + static void AddKey(CWallet& wallet, const CKey& key) { auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); @@ -658,4 +690,86 @@ BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor); } +//! Test CreateWalletFromFile function and its behavior handling potential race +//! conditions if it's called the same time an incoming transaction shows up in +//! the mempool or a new block. +//! +//! It isn't possible for a unit test to totally verify there aren't race +//! conditions without hooking into the implementation more, so this test just +//! verifies that new transactions are detected during loading without any +//! notifications at all, to infer that timing of notifications shouldn't +//! matter. The test could be extended to cover other scenarios in the future. +BOOST_FIXTURE_TEST_CASE(CreateWalletFromFile, TestChain100Setup) +{ + // Create new wallet with known key and unload it. + auto chain = interfaces::MakeChain(m_node); + auto wallet = TestLoadWallet(*chain); + CKey key; + key.MakeNewKey(true); + AddKey(*wallet, key); + TestUnloadWallet(std::move(wallet)); + + // Add log hook to detect AddToWallet events from rescans, blockConnected, + // and transactionAddedToMempool notifications + int addtx_count = 0; + DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) { + if (s) ++addtx_count; + return false; + }); + + bool rescan_completed = false; + DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) { + if (s) { + // For now, just assert that cs_main is being held during the + // rescan, ensuring that a new block couldn't be connected + // that the wallet would miss. After + // https://github.com/bitcoin/bitcoin/pull/16426 when cs_main is no + // longer held, the test can be extended to append a new block here + // and check it's handled correctly. + AssertLockHeld(::cs_main); + rescan_completed = true; + } + return false; + }); + + // Block the queue to prevent the wallet receiving blockConnected and + // transactionAddedToMempool notifications, and create block and mempool + // transactions paying to the wallet + std::promise<void> promise; + CallFunctionInValidationInterfaceQueue([&promise] { + promise.get_future().wait(); + }); + std::string error; + m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); + auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); + BOOST_CHECK(chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); + + // Reload wallet and make sure new transactions are detected despite events + // being blocked + wallet = TestLoadWallet(*chain); + BOOST_CHECK(rescan_completed); + BOOST_CHECK_EQUAL(addtx_count, 2); + unsigned int block_tx_time, mempool_tx_time; + { + LOCK(wallet->cs_wallet); + block_tx_time = wallet->mapWallet.at(block_tx.GetHash()).nTimeReceived; + mempool_tx_time = wallet->mapWallet.at(mempool_tx.GetHash()).nTimeReceived; + } + + // Unblock notification queue and make sure stale blockConnected and + // transactionAddedToMempool events are processed + promise.set_value(); + SyncWithValidationInterfaceQueue(); + BOOST_CHECK_EQUAL(addtx_count, 4); + { + LOCK(wallet->cs_wallet); + BOOST_CHECK_EQUAL(block_tx_time, wallet->mapWallet.at(block_tx.GetHash()).nTimeReceived); + BOOST_CHECK_EQUAL(mempool_tx_time, wallet->mapWallet.at(mempool_tx.GetHash()).nTimeReceived); + } + + TestUnloadWallet(std::move(wallet)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index b6f25de64e..6d46841cee 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1993,7 +1993,8 @@ void CWallet::ResendWalletTransactions() // that these are our transactions. if (GetTime() < nNextResend || !fBroadcastTransactions) return; bool fFirst = (nNextResend == 0); - nNextResend = GetTime() + GetRand(30 * 60); + // resend 12-36 hours from now, ~1 day on average. + nNextResend = GetTime() + (12 * 60 * 60) + GetRand(24 * 60 * 60); if (fFirst) return; // Only do it if there's been a new block since last time diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index e1671624a8..99003d2d1f 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -40,10 +40,13 @@ import os import time from test_framework.test_framework import BitcoinTestFramework +from test_framework.mininode import P2PTxInvStore from test_framework.util import ( assert_equal, assert_greater_than_or_equal, assert_raises_rpc_error, + connect_nodes, + disconnect_nodes, wait_until, ) @@ -80,6 +83,11 @@ class MempoolPersistTest(BitcoinTestFramework): assert_greater_than_or_equal(tx_creation_time, tx_creation_time_lower) assert_greater_than_or_equal(tx_creation_time_higher, tx_creation_time) + # disconnect nodes & make a txn that remains in the unbroadcast set. + disconnect_nodes(self.nodes[0], 2) + self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), Decimal("12")) + connect_nodes(self.nodes[0], 2) + self.log.debug("Stop-start the nodes. Verify that node0 has the transactions in its mempool and node1 does not. Verify that node2 calculates its balance correctly after loading wallet transactions.") self.stop_nodes() # Give this node a head-start, so we can be "extra-sure" that it didn't load anything later @@ -89,7 +97,7 @@ class MempoolPersistTest(BitcoinTestFramework): self.start_node(2) wait_until(lambda: self.nodes[0].getmempoolinfo()["loaded"], timeout=1) wait_until(lambda: self.nodes[2].getmempoolinfo()["loaded"], timeout=1) - assert_equal(len(self.nodes[0].getrawmempool()), 5) + assert_equal(len(self.nodes[0].getrawmempool()), 6) assert_equal(len(self.nodes[2].getrawmempool()), 5) # The others have loaded their mempool. If node_1 loaded anything, we'd probably notice by now: assert_equal(len(self.nodes[1].getrawmempool()), 0) @@ -105,9 +113,10 @@ class MempoolPersistTest(BitcoinTestFramework): self.nodes[2].syncwithvalidationinterfacequeue() # Flush mempool to wallet assert_equal(node2_balance, self.nodes[2].getbalance()) + # start node0 with wallet disabled so wallet transactions don't get resubmitted self.log.debug("Stop-start node0 with -persistmempool=0. Verify that it doesn't load its mempool.dat file.") self.stop_nodes() - self.start_node(0, extra_args=["-persistmempool=0"]) + self.start_node(0, extra_args=["-persistmempool=0", "-disablewallet"]) wait_until(lambda: self.nodes[0].getmempoolinfo()["loaded"]) assert_equal(len(self.nodes[0].getrawmempool()), 0) @@ -115,7 +124,7 @@ class MempoolPersistTest(BitcoinTestFramework): self.stop_nodes() self.start_node(0) wait_until(lambda: self.nodes[0].getmempoolinfo()["loaded"]) - assert_equal(len(self.nodes[0].getrawmempool()), 5) + assert_equal(len(self.nodes[0].getrawmempool()), 6) mempooldat0 = os.path.join(self.nodes[0].datadir, self.chain, 'mempool.dat') mempooldat1 = os.path.join(self.nodes[1].datadir, self.chain, 'mempool.dat') @@ -124,12 +133,12 @@ class MempoolPersistTest(BitcoinTestFramework): self.nodes[0].savemempool() assert os.path.isfile(mempooldat0) - self.log.debug("Stop nodes, make node1 use mempool.dat from node0. Verify it has 5 transactions") + self.log.debug("Stop nodes, make node1 use mempool.dat from node0. Verify it has 6 transactions") os.rename(mempooldat0, mempooldat1) self.stop_nodes() self.start_node(1, extra_args=[]) wait_until(lambda: self.nodes[1].getmempoolinfo()["loaded"]) - assert_equal(len(self.nodes[1].getrawmempool()), 5) + assert_equal(len(self.nodes[1].getrawmempool()), 6) self.log.debug("Prevent bitcoind from writing mempool.dat to disk. Verify that `savemempool` fails") # to test the exception we are creating a tmp folder called mempool.dat.new @@ -139,6 +148,27 @@ class MempoolPersistTest(BitcoinTestFramework): assert_raises_rpc_error(-1, "Unable to dump mempool to disk", self.nodes[1].savemempool) os.rmdir(mempooldotnew1) + self.test_persist_unbroadcast() + + def test_persist_unbroadcast(self): + node0 = self.nodes[0] + self.start_node(0) + + # clear out mempool + node0.generate(1) + + # disconnect nodes to make a txn that remains in the unbroadcast set. + disconnect_nodes(node0, 1) + node0.sendtoaddress(self.nodes[1].getnewaddress(), Decimal("12")) + + # shutdown, then startup with wallet disabled + self.stop_nodes() + self.start_node(0, extra_args=["-disablewallet"]) + + # check that txn gets broadcast due to unbroadcast logic + conn = node0.add_p2p_connection(P2PTxInvStore()) + node0.mockscheduler(16*60) # 15 min + 1 for buffer + wait_until(lambda: len(conn.get_invs()) == 1) if __name__ == '__main__': MempoolPersistTest().main() diff --git a/test/functional/mempool_unbroadcast.py b/test/functional/mempool_unbroadcast.py new file mode 100755 index 0000000000..a561f28b91 --- /dev/null +++ b/test/functional/mempool_unbroadcast.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test that the mempool ensures transaction delivery by periodically sending +to peers until a GETDATA is received.""" + +import time + +from test_framework.mininode import P2PTxInvStore +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + connect_nodes, + create_confirmed_utxos, + disconnect_nodes, +) + + +class MempoolUnbroadcastTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.test_broadcast() + self.test_txn_removal() + + def test_broadcast(self): + self.log.info("Test that mempool reattempts delivery of locally submitted transaction") + node = self.nodes[0] + + min_relay_fee = node.getnetworkinfo()["relayfee"] + utxos = create_confirmed_utxos(min_relay_fee, node, 10) + + disconnect_nodes(node, 1) + + self.log.info("Generate transactions that only node 0 knows about") + + # generate a wallet txn + addr = node.getnewaddress() + wallet_tx_hsh = node.sendtoaddress(addr, 0.0001) + + # generate a txn using sendrawtransaction + us0 = utxos.pop() + inputs = [{"txid": us0["txid"], "vout": us0["vout"]}] + outputs = {addr: 0.0001} + tx = node.createrawtransaction(inputs, outputs) + node.settxfee(min_relay_fee) + txF = node.fundrawtransaction(tx) + txFS = node.signrawtransactionwithwallet(txF["hex"]) + rpc_tx_hsh = node.sendrawtransaction(txFS["hex"]) + + # check that second node doesn't have these two txns + mempool = self.nodes[1].getrawmempool() + assert rpc_tx_hsh not in mempool + assert wallet_tx_hsh not in mempool + + # ensure that unbroadcast txs are persisted to mempool.dat + self.restart_node(0) + + self.log.info("Reconnect nodes & check if they are sent to node 1") + connect_nodes(node, 1) + + # fast forward into the future & ensure that the second node has the txns + node.mockscheduler(15 * 60) # 15 min in seconds + self.sync_mempools(timeout=30) + mempool = self.nodes[1].getrawmempool() + assert rpc_tx_hsh in mempool + assert wallet_tx_hsh in mempool + + self.log.info("Add another connection & ensure transactions aren't broadcast again") + + conn = node.add_p2p_connection(P2PTxInvStore()) + node.mockscheduler(15 * 60) + time.sleep(5) + assert_equal(len(conn.get_invs()), 0) + + def test_txn_removal(self): + self.log.info("Test that transactions removed from mempool are removed from unbroadcast set") + node = self.nodes[0] + disconnect_nodes(node, 1) + node.disconnect_p2ps + + # since the node doesn't have any connections, it will not receive + # any GETDATAs & thus the transaction will remain in the unbroadcast set. + addr = node.getnewaddress() + txhsh = node.sendtoaddress(addr, 0.0001) + + # check transaction was removed from unbroadcast set due to presence in + # a block + removal_reason = "Removed {} from set of unbroadcast txns before confirmation that txn was sent out".format(txhsh) + with node.assert_debug_log([removal_reason]): + node.generate(1) + +if __name__ == "__main__": + MempoolUnbroadcastTest().main() diff --git a/test/functional/mempool_updatefromblock.py b/test/functional/mempool_updatefromblock.py new file mode 100755 index 0000000000..8a703ef009 --- /dev/null +++ b/test/functional/mempool_updatefromblock.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test mempool descendants/ancestors information update. + +Test mempool update of transaction descendants/ancestors information (count, size) +when transactions have been re-added from a disconnected block to the mempool. +""" +import time + +from decimal import Decimal +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + + +class MempoolUpdateFromBlockTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [['-limitdescendantsize=1000', '-limitancestorsize=1000']] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def transaction_graph_test(self, size, n_tx_to_mine=None, start_input_txid='', end_address='', fee=Decimal(0.00100000)): + """Create an acyclic tournament (a type of directed graph) of transactions and use it for testing. + + Keyword arguments: + size -- the order N of the tournament which is equal to the number of the created transactions + n_tx_to_mine -- the number of transaction that should be mined into a block + + If all of the N created transactions tx[0]..tx[N-1] reside in the mempool, + the following holds: + the tx[K] transaction: + - has N-K descendants (including this one), and + - has K+1 ancestors (including this one) + + More details: https://en.wikipedia.org/wiki/Tournament_(graph_theory) + """ + + if not start_input_txid: + start_input_txid = self.nodes[0].getblock(self.nodes[0].getblockhash(1))['tx'][0] + + if not end_address: + end_address = self.nodes[0].getnewaddress() + + first_block_hash = '' + tx_id = [] + tx_size = [] + self.log.info('Creating {} transactions...'.format(size)) + for i in range(0, size): + self.log.debug('Preparing transaction #{}...'.format(i)) + # Prepare inputs. + if i == 0: + inputs = [{'txid': start_input_txid, 'vout': 0}] + inputs_value = self.nodes[0].gettxout(start_input_txid, 0)['value'] + else: + inputs = [] + inputs_value = 0 + for j, tx in enumerate(tx_id[0:i]): + # Transaction tx[K] is a child of each of previous transactions tx[0]..tx[K-1] at their output K-1. + vout = i - j - 1 + inputs.append({'txid': tx_id[j], 'vout': vout}) + inputs_value += self.nodes[0].gettxout(tx, vout)['value'] + + self.log.debug('inputs={}'.format(inputs)) + self.log.debug('inputs_value={}'.format(inputs_value)) + + # Prepare outputs. + tx_count = i + 1 + if tx_count < size: + # Transaction tx[K] is an ancestor of each of subsequent transactions tx[K+1]..tx[N-1]. + n_outputs = size - tx_count + output_value = ((inputs_value - fee) / Decimal(n_outputs)).quantize(Decimal('0.00000001')) + outputs = {} + for n in range(0, n_outputs): + outputs[self.nodes[0].getnewaddress()] = output_value + else: + output_value = (inputs_value - fee).quantize(Decimal('0.00000001')) + outputs = {end_address: output_value} + + self.log.debug('output_value={}'.format(output_value)) + self.log.debug('outputs={}'.format(outputs)) + + # Create a new transaction. + unsigned_raw_tx = self.nodes[0].createrawtransaction(inputs, outputs) + signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(unsigned_raw_tx) + tx_id.append(self.nodes[0].sendrawtransaction(signed_raw_tx['hex'])) + tx_size.append(self.nodes[0].getrawmempool(True)[tx_id[-1]]['vsize']) + + if tx_count in n_tx_to_mine: + # The created transactions are mined into blocks by batches. + self.log.info('The batch of {} transactions has been accepted into the mempool.'.format(len(self.nodes[0].getrawmempool()))) + block_hash = self.nodes[0].generate(1)[0] + if not first_block_hash: + first_block_hash = block_hash + assert_equal(len(self.nodes[0].getrawmempool()), 0) + self.log.info('All of the transactions from the current batch have been mined into a block.') + elif tx_count == size: + # At the end all of the mined blocks are invalidated, and all of the created + # transactions should be re-added from disconnected blocks to the mempool. + self.log.info('The last batch of {} transactions has been accepted into the mempool.'.format(len(self.nodes[0].getrawmempool()))) + start = time.time() + self.nodes[0].invalidateblock(first_block_hash) + end = time.time() + assert_equal(len(self.nodes[0].getrawmempool()), size) + self.log.info('All of the recently mined transactions have been re-added into the mempool in {} seconds.'.format(end - start)) + + self.log.info('Checking descendants/ancestors properties of all of the in-mempool transactions...') + for k, tx in enumerate(tx_id): + self.log.debug('Check transaction #{}.'.format(k)) + assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantcount'], size - k) + assert_equal(self.nodes[0].getrawmempool(True)[tx]['descendantsize'], sum(tx_size[k:size])) + assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorcount'], k + 1) + assert_equal(self.nodes[0].getrawmempool(True)[tx]['ancestorsize'], sum(tx_size[0:(k + 1)])) + + def run_test(self): + # Use batch size limited by DEFAULT_ANCESTOR_LIMIT = 25 to not fire "too many unconfirmed parents" error. + self.transaction_graph_test(size=100, n_tx_to_mine=[25, 50, 75]) + + +if __name__ == '__main__': + MempoolUpdateFromBlockTest().main() diff --git a/test/functional/p2p_filter.py b/test/functional/p2p_filter.py index a8b768c144..15955a938c 100755 --- a/test/functional/p2p_filter.py +++ b/test/functional/p2p_filter.py @@ -64,19 +64,40 @@ class FilterTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() - def run_test(self): - filter_node = self.nodes[0].add_p2p_connection(FilterNode()) - + def test_size_limits(self, filter_node): self.log.info('Check that too large filter is rejected') with self.nodes[0].assert_debug_log(['Misbehaving']): - filter_node.send_and_ping(msg_filterload(data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS+1)) - with self.nodes[0].assert_debug_log(['Misbehaving']): filter_node.send_and_ping(msg_filterload(data=b'\xbb'*(MAX_BLOOM_FILTER_SIZE+1))) + self.log.info('Check that max size filter is accepted') + with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']): + filter_node.send_and_ping(msg_filterload(data=b'\xbb'*(MAX_BLOOM_FILTER_SIZE))) + filter_node.send_and_ping(msg_filterclear()) + + self.log.info('Check that filter with too many hash functions is rejected') + with self.nodes[0].assert_debug_log(['Misbehaving']): + filter_node.send_and_ping(msg_filterload(data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS+1)) + + self.log.info('Check that filter with max hash functions is accepted') + with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']): + filter_node.send_and_ping(msg_filterload(data=b'\xaa', nHashFuncs=MAX_BLOOM_HASH_FUNCS)) + # Don't send filterclear until next two filteradd checks are done + + self.log.info('Check that max size data element to add to the filter is accepted') + with self.nodes[0].assert_debug_log([], unexpected_msgs=['Misbehaving']): + filter_node.send_and_ping(msg_filteradd(data=b'\xcc'*(MAX_SCRIPT_ELEMENT_SIZE))) + self.log.info('Check that too large data element to add to the filter is rejected') with self.nodes[0].assert_debug_log(['Misbehaving']): filter_node.send_and_ping(msg_filteradd(data=b'\xcc'*(MAX_SCRIPT_ELEMENT_SIZE+1))) + filter_node.send_and_ping(msg_filterclear()) + + def run_test(self): + filter_node = self.nodes[0].add_p2p_connection(FilterNode()) + + self.test_size_limits(filter_node) + self.log.info('Add filtered P2P connection to the node') filter_node.send_and_ping(filter_node.watch_filter_init) filter_address = self.nodes[0].decodescript(filter_node.watch_script_pubkey)['addresses'][0] diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py index 6aa73623e6..257499fcb9 100755 --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -12,7 +12,10 @@ found in the mini-node branch of http://github.com/jgarzik/pynode. P2PConnection: A low-level connection object to a node's P2P interface P2PInterface: A high-level interface object for communicating to a node over P2P P2PDataStore: A p2p interface class that keeps a store of transactions and blocks - and can respond correctly to getdata and getheaders messages""" + and can respond correctly to getdata and getheaders messages +P2PTxInvStore: A p2p interface class that inherits from P2PDataStore, and keeps + a count of how many times each txid has been announced.""" + import asyncio from collections import defaultdict from io import BytesIO @@ -627,3 +630,20 @@ class P2PDataStore(P2PInterface): # Check that none of the txs are now in the mempool for tx in txs: assert tx.hash not in raw_mempool, "{} tx found in mempool".format(tx.hash) + +class P2PTxInvStore(P2PInterface): + """A P2PInterface which stores a count of how many times each txid has been announced.""" + def __init__(self): + super().__init__() + self.tx_invs_received = defaultdict(int) + + def on_inv(self, message): + # Store how many times invs have been received for each tx. + for i in message.inv: + if i.type == MSG_TX: + # save txid + self.tx_invs_received[i.hash] += 1 + + def get_invs(self): + with mininode_lock: + return list(self.tx_invs_received.keys()) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 64e1aa3bbc..eb9f6528b3 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -326,6 +326,13 @@ def initialize_datadir(dirname, n, chain): os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True) return datadir +def adjust_bitcoin_conf_for_pre_17(conf_file): + with open(conf_file,'r', encoding='utf8') as conf: + conf_data = conf.read() + with open(conf_file, 'w', encoding='utf8') as conf: + conf_data_changed = conf_data.replace('[regtest]', '') + conf.write(conf_data_changed) + def get_datadir_path(dirname, n): return os.path.join(dirname, "node" + str(n)) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index b8523e16b7..6f5f61de2c 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -93,6 +93,7 @@ BASE_SCRIPTS = [ 'p2p_segwit.py', 'p2p_timeouts.py', 'p2p_tx_download.py', + 'mempool_updatefromblock.py', 'wallet_dump.py', 'wallet_listtransactions.py', # vv Tests less than 60s vv @@ -190,6 +191,7 @@ BASE_SCRIPTS = [ 'wallet_import_rescan.py', 'wallet_import_with_label.py', 'wallet_importdescriptors.py', + 'wallet_upgradewallet.py', 'rpc_bind.py --ipv4', 'rpc_bind.py --ipv6', 'rpc_bind.py --nonloopback', @@ -220,6 +222,7 @@ BASE_SCRIPTS = [ 'p2p_unrequested_blocks.py', 'feature_includeconf.py', 'feature_asmap.py', + 'mempool_unbroadcast.py', 'rpc_deriveaddresses.py', 'rpc_deriveaddresses.py --usecli', 'rpc_scantxoutset.py', diff --git a/test/functional/wallet_resendwallettransactions.py b/test/functional/wallet_resendwallettransactions.py index db5902f820..b384998d56 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -3,29 +3,14 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test that the wallet resends transactions periodically.""" -from collections import defaultdict import time from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import ToHex -from test_framework.mininode import P2PInterface, mininode_lock +from test_framework.mininode import P2PTxInvStore, mininode_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, wait_until - -class P2PStoreTxInvs(P2PInterface): - def __init__(self): - super().__init__() - self.tx_invs_received = defaultdict(int) - - def on_inv(self, message): - # Store how many times invs have been received for each tx. - for i in message.inv: - if i.type == 1: - # save txid - self.tx_invs_received[i.hash] += 1 - - class ResendWalletTransactionsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -36,7 +21,7 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): def run_test(self): node = self.nodes[0] # alias - node.add_p2p_connection(P2PStoreTxInvs()) + node.add_p2p_connection(P2PTxInvStore()) self.log.info("Create a new transaction and wait until it's broadcast") txid = int(node.sendtoaddress(node.getnewaddress(), 1), 16) @@ -51,7 +36,7 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): wait_until(lambda: node.p2p.tx_invs_received[txid] >= 1, lock=mininode_lock) # Add a second peer since txs aren't rebroadcast to the same peer (see filterInventoryKnown) - node.add_p2p_connection(P2PStoreTxInvs()) + node.add_p2p_connection(P2PTxInvStore()) self.log.info("Create a block") # Create and submit a block without the transaction. @@ -69,9 +54,10 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): node.p2ps[1].sync_with_ping() assert_equal(node.p2ps[1].tx_invs_received[txid], 0) - self.log.info("Transaction should be rebroadcast after 30 minutes") - # Use mocktime and give an extra 5 minutes to be sure. - rebroadcast_time = int(time.time()) + 41 * 60 + self.log.info("Bump time & check that transaction is rebroadcast") + # Transaction should be rebroadcast approximately 24 hours in the future, + # but can range from 12-36. So bump 36 hours to be sure. + rebroadcast_time = int(time.time()) + 36 * 60 * 60 node.setmocktime(rebroadcast_time) wait_until(lambda: node.p2ps[1].tx_invs_received[txid] >= 1, lock=mininode_lock) diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py new file mode 100755 index 0000000000..d04bc4ce44 --- /dev/null +++ b/test/functional/wallet_upgradewallet.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""upgradewallet RPC functional test + +Test upgradewallet RPC. Download v0.15.2 v0.16.3 node binaries: + +contrib/devtools/previous_release.sh -b v0.15.2 v0.16.3 +""" + +import os +import shutil + +from test_framework.test_framework import BitcoinTestFramework, SkipTest +from test_framework.util import ( + adjust_bitcoin_conf_for_pre_17, + assert_equal, + assert_greater_than, + assert_is_hex_string +) + +class UpgradeWalletTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 3 + self.extra_args = [ + ["-addresstype=bech32"], # current wallet version + ["-usehd=1"], # v0.16.3 wallet + ["-usehd=0"] # v0.15.2 wallet + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def setup_network(self): + self.setup_nodes() + + def setup_nodes(self): + if os.getenv("TEST_PREVIOUS_RELEASES") == "false": + raise SkipTest("upgradewallet RPC tests") + + releases_path = os.getenv("PREVIOUS_RELEASES_DIR") or os.getcwd() + "/releases" + if not os.path.isdir(releases_path): + if os.getenv("TEST_PREVIOUS_RELEASES") == "true": + raise AssertionError("TEST_PREVIOUS_RELEASES=1 but releases missing: " + releases_path) + raise SkipTest("This test requires binaries for previous releases") + + self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[ + None, + 160300, + 150200 + ], binary=[ + self.options.bitcoind, + releases_path + "/v0.16.3/bin/bitcoind", + releases_path + "/v0.15.2/bin/bitcoind", + ], binary_cli=[ + self.options.bitcoincli, + releases_path + "/v0.16.3/bin/bitcoin-cli", + releases_path + "/v0.15.2/bin/bitcoin-cli", + ]) + # adapt bitcoin.conf, because older bitcoind's don't recognize config sections + adjust_bitcoin_conf_for_pre_17(self.nodes[1].bitcoinconf) + adjust_bitcoin_conf_for_pre_17(self.nodes[2].bitcoinconf) + self.start_nodes() + + def dumb_sync_blocks(self): + """ + Little helper to sync older wallets. + Notice that v0.15.2's regtest is hardforked, so there is + no sync for it. + v0.15.2 is only being used to test for version upgrade + and master hash key presence. + v0.16.3 is being used to test for version upgrade and balances. + Further info: https://github.com/bitcoin/bitcoin/pull/18774#discussion_r416967844 + """ + node_from = self.nodes[0] + v16_3_node = self.nodes[1] + to_height = node_from.getblockcount() + height = self.nodes[1].getblockcount() + for i in range(height, to_height+1): + b = node_from.getblock(blockhash=node_from.getblockhash(i), verbose=0) + v16_3_node.submitblock(b) + assert_equal(v16_3_node.getblockcount(), to_height) + + def run_test(self): + self.nodes[0].generatetoaddress(101, self.nodes[0].getnewaddress()) + self.dumb_sync_blocks() + # # Sanity check the test framework: + res = self.nodes[0].getblockchaininfo() + assert_equal(res['blocks'], 101) + node_master = self.nodes[0] + v16_3_node = self.nodes[1] + v15_2_node = self.nodes[2] + + # Send coins to old wallets for later conversion checks. + v16_3_wallet = v16_3_node.get_wallet_rpc('wallet.dat') + v16_3_address = v16_3_wallet.getnewaddress() + node_master.generatetoaddress(101, v16_3_address) + self.dumb_sync_blocks() + v16_3_balance = v16_3_wallet.getbalance() + + self.log.info("Test upgradewallet RPC...") + # Prepare for copying of the older wallet + node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets") + v16_3_wallet = os.path.join(v16_3_node.datadir, "regtest/wallets/wallet.dat") + v15_2_wallet = os.path.join(v15_2_node.datadir, "regtest/wallet.dat") + self.stop_nodes() + + # Copy the 0.16.3 wallet to the last Bitcoin Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v16_3_wallet, + node_master_wallet_dir + ) + self.restart_node(0, ['-nowallet']) + node_master.loadwallet('') + + wallet = node_master.get_wallet_rpc('') + old_version = wallet.getwalletinfo()["walletversion"] + + # calling upgradewallet without version arguments + # should return nothing if successful + assert_equal(wallet.upgradewallet(), "") + new_version = wallet.getwalletinfo()["walletversion"] + # upgraded wallet version should be greater than older one + assert_greater_than(new_version, old_version) + # wallet should still contain the same balance + assert_equal(wallet.getbalance(), v16_3_balance) + + self.stop_node(0) + # Copy the 0.15.2 wallet to the last Bitcoin Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v15_2_wallet, + node_master_wallet_dir + ) + self.restart_node(0, ['-nowallet']) + node_master.loadwallet('') + + wallet = node_master.get_wallet_rpc('') + # should have no master key hash before conversion + assert_equal('hdseedid' in wallet.getwalletinfo(), False) + # calling upgradewallet with explicit version number + # should return nothing if successful + assert_equal(wallet.upgradewallet(169900), "") + new_version = wallet.getwalletinfo()["walletversion"] + # upgraded wallet should have version 169900 + assert_equal(new_version, 169900) + # after conversion master key hash should be present + assert_is_hex_string(wallet.getwalletinfo()['hdseedid']) + +if __name__ == '__main__': + UpgradeWalletTest().main() |