diff --git a/.fuzzbuzz.yml b/.fuzzbuzz.yml new file mode 100644 index 0000000000..d44ac27eb9 --- /dev/null +++ b/.fuzzbuzz.yml @@ -0,0 +1,16 @@ +base: ubuntu:16.04 +language: c++ +engine: libFuzzer +environment: + - CXXFLAGS=-fcoverage-mapping -fno-omit-frame-pointer -fprofile-instr-generate -gline-tables-only -O1 +setup: + - sudo apt-get update + - sudo apt-get install -y autoconf bsdmainutils clang git libboost-all-dev libboost-program-options-dev libc++1 libc++abi1 libc++abi-dev libc++-dev libclang1 libclang-dev libdb5.3++ libevent-dev libllvm-ocaml-dev libomp5 libomp-dev libprotobuf-dev libqt5core5a libqt5dbus5 libqt5gui5 libssl-dev libtool llvm llvm-dev llvm-runtime pkg-config protobuf-compiler qttools5-dev qttools5-dev-tools software-properties-common + - ./autogen.sh + - CC=clang CXX=clang++ ./configure --enable-fuzz --with-sanitizers=address,fuzzer,undefined + - make + - git clone https://github.com/bitcoin-core/qa-assets +auto_targets: + find_targets_command: find src/test/fuzz/ -executable -type f ! -name "*.cpp" ! -name "*.h" + base_corpus_dir: qa-assets/fuzz_seed_corpus/ + memory_limit: none diff --git a/Makefile.am b/Makefile.am index cd33808f28..3b92da88f7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,7 +41,7 @@ OSX_DEPLOY_SCRIPT=$(top_srcdir)/contrib/macdeploy/macdeployqtplus OSX_FANCY_PLIST=$(top_srcdir)/contrib/macdeploy/fancy.plist OSX_INSTALLER_ICONS=$(top_srcdir)/src/qt/res/icons/dash.icns OSX_PLIST=$(top_builddir)/share/qt/Info.plist #not installed -OSX_QT_TRANSLATIONS = da,de,es,hu,ru,uk,zh_CN,zh_TW +OSX_QT_TRANSLATIONS = ar,bg,ca,cs,da,de,es,fa,fi,fr,gd,gl,he,hu,it,ja,ko,lt,lv,pl,pt,ru,sk,sl,sv,uk,zh_CN,zh_TW DIST_DOCS = $(wildcard doc/*.md) $(wildcard doc/release-notes/*.md) DIST_CONTRIB = $(top_srcdir)/contrib/dash-cli.bash-completion \ diff --git a/build-aux/m4/ax_boost_base.m4 b/build-aux/m4/ax_boost_base.m4 index 16fa69b41f..2ae33f7140 100644 --- a/build-aux/m4/ax_boost_base.m4 +++ b/build-aux/m4/ax_boost_base.m4 @@ -33,7 +33,7 @@ # and this notice are preserved. This file is offered as-is, without any # warranty. -#serial 47 +#serial 48 # example boost program (need to pass version) m4_define([_AX_BOOST_BASE_PROGRAM], @@ -123,6 +123,7 @@ AC_DEFUN([_AX_BOOST_BASE_RUNDETECT],[ dnl are almost assuredly the ones desired. AS_CASE([${host_cpu}], [i?86],[multiarch_libsubdir="lib/i386-${host_os}"], + [armv7l],[multiarch_libsubdir="lib/arm-${host_os}"], [multiarch_libsubdir="lib/${host_cpu}-${host_os}"] ) diff --git a/configure.ac b/configure.ac index aaf71d8cb4..49a637bf16 100644 --- a/configure.ac +++ b/configure.ac @@ -34,14 +34,14 @@ dnl faketime breaks configure and is only needed for make. Disable it here. unset FAKETIME dnl Automake init set-up and checks -AM_INIT_AUTOMAKE([no-define subdir-objects foreign]) +AM_INIT_AUTOMAKE([1.13 no-define subdir-objects foreign]) dnl faketime messes with timestamps and causes configure to be re-run. dnl --disable-maintainer-mode can be used to bypass this. AM_MAINTAINER_MODE([enable]) dnl make the compilation flags quiet unless V=1 is used -m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) +AM_SILENT_RULES([yes]) dnl Compiler checks (here before libtool). if test "x${CXXFLAGS+set}" = "xset"; then @@ -409,6 +409,7 @@ if test "x$CXXFLAGS_overridden" = "xno"; then AX_CHECK_COMPILE_FLAG([-Wunused-local-typedef],[NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-unused-local-typedef"],,[[$CXXFLAG_WERROR]]) AX_CHECK_COMPILE_FLAG([-Wdeprecated-register],[NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-deprecated-register"],,[[$CXXFLAG_WERROR]]) AX_CHECK_COMPILE_FLAG([-Wimplicit-fallthrough],[NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-implicit-fallthrough"],,[[$CXXFLAG_WERROR]]) + AX_CHECK_COMPILE_FLAG([-Wdeprecated-copy],[NOWARN_CXXFLAGS="$NOWARN_CXXFLAGS -Wno-deprecated-copy"],,[[$CXXFLAG_WERROR]]) fi enable_sse42=no @@ -1425,9 +1426,6 @@ if test "x$use_ccache" != "xno"; then fi AC_MSG_RESULT($use_ccache) fi -if test "x$use_ccache" = "xyes"; then - AX_CHECK_PREPROC_FLAG([-Qunused-arguments],[CPPFLAGS="-Qunused-arguments $CPPFLAGS"]) -fi dnl enable wallet AC_MSG_CHECKING([if wallet should be enabled]) diff --git a/depends/packages/qt.mk b/depends/packages/qt.mk index 33b5b623b8..e5a8c2438d 100644 --- a/depends/packages/qt.mk +++ b/depends/packages/qt.mk @@ -10,6 +10,7 @@ $(package)_build_subdir=qtbase $(package)_qt_libs=corelib network widgets gui plugins testlib $(package)_patches=fix_qt_pkgconfig.patch mac-qmake.conf fix_configure_mac.patch fix_no_printer.patch fix_rcc_determinism.patch xkb-default.patch no-xlib.patch +# Update OSX_QT_TRANSLATIONS when this is updated $(package)_qttranslations_file_name=qttranslations-$($(package)_suffix) $(package)_qttranslations_sha256_hash=9822084f8e2d2939ba39f4af4c0c2320e45d5996762a9423f833055607604ed8 diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 5b5f20c0aa..4db13f0c42 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -197,7 +197,7 @@ endif %.cpp.test: %.cpp @echo Running tests: `cat $< | grep -E "(BOOST_FIXTURE_TEST_SUITE\\(|BOOST_AUTO_TEST_SUITE\\()" | cut -d '(' -f 2 | cut -d ',' -f 1 | cut -d ')' -f 1` from $< - $(AM_V_at)$(TEST_BINARY) -l test_suite -t "`cat $< | grep -E "(BOOST_FIXTURE_TEST_SUITE\\(|BOOST_AUTO_TEST_SUITE\\()" | cut -d '(' -f 2 | cut -d ',' -f 1 | cut -d ')' -f 1`" > $<.log 2>&1 || (cat $<.log && false) + $(AM_V_at)$(TEST_BINARY) --catch_system_errors=no -l test_suite -t "`cat $< | grep -E "(BOOST_FIXTURE_TEST_SUITE\\(|BOOST_AUTO_TEST_SUITE\\()" | cut -d '(' -f 2 | cut -d ',' -f 1 | cut -d ')' -f 1`" > $<.log 2>&1 || (cat $<.log && false) test/data/%.json.h: test/data/%.json @$(MKDIR_P) $(@D) diff --git a/src/coins.h b/src/coins.h index dc34309798..56faf02704 100644 --- a/src/coins.h +++ b/src/coins.h @@ -58,7 +58,7 @@ public: template void Serialize(Stream &s) const { assert(!IsSpent()); - uint32_t code = nHeight * 2 + fCoinBase; + uint32_t code = nHeight * uint32_t{2} + fCoinBase; ::Serialize(s, VARINT(code)); ::Serialize(s, Using(out)); } diff --git a/src/fs.cpp b/src/fs.cpp index a146107c4c..e80c0141a0 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -2,6 +2,9 @@ #ifndef WIN32 #include +#include +#include +#include #else #include #include @@ -40,20 +43,38 @@ FileLock::~FileLock() } } +static bool IsWSL() +{ + struct utsname uname_data; + return uname(&uname_data) == 0 && std::string(uname_data.version).find("Microsoft") != std::string::npos; +} + bool FileLock::TryLock() { if (fd == -1) { return false; } - struct flock lock; - lock.l_type = F_WRLCK; - lock.l_whence = SEEK_SET; - lock.l_start = 0; - lock.l_len = 0; - if (fcntl(fd, F_SETLK, &lock) == -1) { - reason = GetErrorReason(); - return false; + + // Exclusive file locking is broken on WSL using fcntl (issue #18622) + // This workaround can be removed once the bug on WSL is fixed + static const bool is_wsl = IsWSL(); + if (is_wsl) { + if (flock(fd, LOCK_EX | LOCK_NB) == -1) { + reason = GetErrorReason(); + return false; + } + } else { + struct flock lock; + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + lock.l_start = 0; + lock.l_len = 0; + if (fcntl(fd, F_SETLK, &lock) == -1) { + reason = GetErrorReason(); + return false; + } } + return true; } #else diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 2305c9d704..e1e30de553 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -218,7 +218,7 @@ static bool InitRPCAuthentication() { if (gArgs.GetArg("-rpcpassword", "") == "") { - LogPrintf("No rpcpassword set - using random cookie authentication.\n"); + LogPrintf("Using random cookie authentication.\n"); if (!GenerateAuthCookie(&strRPCUserColonPass)) { uiInterface.ThreadSafeMessageBox( _("Error: A fatal internal error occurred, see debug.log for details"), // Same message as AbortNode diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 127edc2594..7119a67142 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -242,7 +242,7 @@ static void http_request_cb(struct evhttp_request* req, void* arg) if (hreq->GetRequestMethod() == HTTPRequest::UNKNOWN) { LogPrint(BCLog::HTTP, "HTTP request from %s rejected: Unknown HTTP request method\n", hreq->GetPeer().ToString()); - hreq->WriteReply(HTTP_BADMETHOD); + hreq->WriteReply(HTTP_BAD_METHOD); return; } @@ -274,10 +274,10 @@ static void http_request_cb(struct evhttp_request* req, void* arg) item.release(); /* if true, queue took ownership */ else { LogPrintf("WARNING: request rejected because http work queue depth exceeded, it can be increased with the -rpcworkqueue= setting\n"); - item->req->WriteReply(HTTP_INTERNAL, "Work queue depth exceeded"); + item->req->WriteReply(HTTP_INTERNAL_SERVER_ERROR, "Work queue depth exceeded"); } } else { - hreq->WriteReply(HTTP_NOTFOUND); + hreq->WriteReply(HTTP_NOT_FOUND); } } @@ -533,7 +533,7 @@ HTTPRequest::~HTTPRequest() if (!replySent) { // Keep track of whether reply was sent to avoid request leaks LogPrintf("%s: Unhandled request\n", __func__); - WriteReply(HTTP_INTERNAL, "Unhandled request"); + WriteReply(HTTP_INTERNAL_SERVER_ERROR, "Unhandled request"); } // evhttpd cleans up the request, as long as a reply was sent. } diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index 03221d7d88..f63b36eb75 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -9,6 +9,8 @@ #include #include +#include + std::string COutPoint::ToString() const { return strprintf("COutPoint(%s, %u)", hash.ToString()/*.substr(0,10)*/, n); @@ -99,10 +101,11 @@ CAmount CTransaction::GetValueOut() const { CAmount nValueOut = 0; for (const auto& tx_out : vout) { - nValueOut += tx_out.nValue; - if (!MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut)) + if (!MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut + tx_out.nValue)) throw std::runtime_error(std::string(__func__) + ": value out of range"); + nValueOut += tx_out.nValue; } + assert(MoneyRange(nValueOut)); return nValueOut; } diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index 47a13dfc05..7f621211aa 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -692,8 +692,7 @@ void CoinControlDialog::updateView() int nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); for (const auto& coins : model->wallet().listCoins()) { - CCoinControlWidgetItem *itemWalletAddress = new CCoinControlWidgetItem(); - itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); + CCoinControlWidgetItem* itemWalletAddress{nullptr}; QString sWalletAddress = QString::fromStdString(EncodeDestination(coins.first)); QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress); if (sWalletLabel.isEmpty()) @@ -702,7 +701,7 @@ void CoinControlDialog::updateView() if (treeMode) { // wallet address - ui->treeWidget->addTopLevelItem(itemWalletAddress); + itemWalletAddress = new CCoinControlWidgetItem(ui->treeWidget); itemWalletAddress->setFlags(flgTristate); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index dd89113b90..4e9af90d37 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -30,7 +30,6 @@ class CCoinControlWidgetItem : public QTreeWidgetItem { public: explicit CCoinControlWidgetItem(QTreeWidget *parent, int type = Type) : QTreeWidgetItem(parent, type) {} - explicit CCoinControlWidgetItem(int type = Type) : QTreeWidgetItem(type) {} explicit CCoinControlWidgetItem(QTreeWidgetItem *parent, int type = Type) : QTreeWidgetItem(parent, type) {} bool operator<(const QTreeWidgetItem &other) const override; diff --git a/src/qt/trafficgraphwidget.cpp b/src/qt/trafficgraphwidget.cpp index 55196407d2..e1d73c6e5f 100644 --- a/src/qt/trafficgraphwidget.cpp +++ b/src/qt/trafficgraphwidget.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 2a996b2577..ad47f078c2 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -683,7 +683,7 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const return details; } case ConfirmedRole: - return rec->status.countsForBalance; + return rec->status.status == TransactionStatus::Status::Confirming || rec->status.status == TransactionStatus::Status::Confirmed; case FormattedAmountRole: // Used for copy/export, so don't include separators return formatTxAmount(rec, false, BitcoinUnits::separatorNever); diff --git a/src/rest.cpp b/src/rest.cpp index bf36907368..37b0281182 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -580,7 +580,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/script/script_error.cpp b/src/script/script_error.cpp index 094455e6ff..807e6ff126 100644 --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -73,7 +73,7 @@ const char* ScriptErrorString(const ScriptError serror) case SCRIPT_ERR_MINIMALDATA: return "Data push larger than necessary"; case SCRIPT_ERR_SIG_PUSHONLY: - return "Only non-push operators allowed in signatures"; + return "Only push operators allowed in signatures"; case SCRIPT_ERR_SIG_HIGH_S: return "Non-canonical signature: S value is unnecessarily high"; case SCRIPT_ERR_SIG_NULLDUMMY: diff --git a/src/script/standard.h b/src/script/standard.h index 96c0828982..a4baf26232 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -45,8 +45,7 @@ extern unsigned nMaxDatacarrierBytes; /** * Mandatory script verification flags that all new blocks must comply with for * them to be valid. (but old blocks may not comply with) Currently just P2SH, - * but in the future other flags may be added, such as a soft-fork to enforce - * strict DER encoding. + * but in the future other flags may be added. * * Failing one of these tests may trigger a DoS ban - see CheckInputs() for * details. diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp index 945fd80687..da57e4fa94 100644 --- a/src/support/lockedpool.cpp +++ b/src/support/lockedpool.cpp @@ -257,6 +257,11 @@ void *PosixLockedPageAllocator::AllocateLocked(size_t len, bool *lockingSuccess) } if (addr) { *lockingSuccess = mlock(addr, len) == 0; +#if defined(MADV_DONTDUMP) // Linux + madvise(addr, len, MADV_DONTDUMP); +#elif defined(MADV_NOCORE) // FreeBSD + madvise(addr, len, MADV_NOCORE); +#endif } return addr; } diff --git a/src/sync.h b/src/sync.h index 6572e7e70a..d5798e8f5e 100644 --- a/src/sync.h +++ b/src/sync.h @@ -10,9 +10,9 @@ #include #include -#include #include - +#include +#include ///////////////////////////////////////////////// // // diff --git a/src/test/raii_event_tests.cpp b/src/test/raii_event_tests.cpp index 6a6055fb52..8b03382b2b 100644 --- a/src/test/raii_event_tests.cpp +++ b/src/test/raii_event_tests.cpp @@ -4,9 +4,6 @@ #include -#ifdef EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED -// It would probably be ideal to define dummy test(s) that report skipped, but boost::test doesn't seem to make that practical (at least not in versions available with common distros) - #include #include @@ -18,6 +15,10 @@ #include +BOOST_FIXTURE_TEST_SUITE(raii_event_tests, BasicTestingSetup) + +#ifdef EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED + static std::map tags; static std::map orders; static uint16_t tagSequence = 0; @@ -36,8 +37,6 @@ static void tag_free(void* mem) { free(mem); } -BOOST_FIXTURE_TEST_SUITE(raii_event_tests, BasicTestingSetup) - BOOST_AUTO_TEST_CASE(raii_event_creation) { event_set_mem_functions(tag_malloc, realloc, tag_free); @@ -89,6 +88,14 @@ BOOST_AUTO_TEST_CASE(raii_event_order) event_set_mem_functions(malloc, realloc, free); } -BOOST_AUTO_TEST_SUITE_END() +#else + +BOOST_AUTO_TEST_CASE(raii_event_tests_SKIPPED) +{ + // It would probably be ideal to report skipped, but boost::test doesn't seem to make that practical (at least not in versions available with common distros) + BOOST_TEST_MESSAGE("Skipping raii_event_tess: libevent doesn't support event_set_mem_functions"); +} #endif // EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/scheduler_tests.cpp b/src/test/scheduler_tests.cpp index 8dac13d8fd..dc0b17f682 100644 --- a/src/test/scheduler_tests.cpp +++ b/src/test/scheduler_tests.cpp @@ -112,6 +112,24 @@ BOOST_AUTO_TEST_CASE(manythreads) BOOST_CHECK_EQUAL(counterSum, 200); } +BOOST_AUTO_TEST_CASE(wait_until_past) +{ + std::condition_variable condvar; + Mutex mtx; + WAIT_LOCK(mtx, lock); + + const auto no_wait= [&](const std::chrono::seconds& d) { + return condvar.wait_until(lock, std::chrono::system_clock::now() - d); + }; + + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::seconds{1})); + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::minutes{1})); + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::hours{1})); + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::hours{10})); + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::hours{100})); + BOOST_CHECK(std::cv_status::timeout == no_wait(std::chrono::hours{1000})); +} + BOOST_AUTO_TEST_CASE(singlethreadedscheduler_ordered) { CScheduler scheduler; diff --git a/src/undo.h b/src/undo.h index 91656ce904..bb5a129888 100644 --- a/src/undo.h +++ b/src/undo.h @@ -24,7 +24,7 @@ struct TxInUndoFormatter { template void Ser(Stream &s, const Coin& txout) { - ::Serialize(s, VARINT(txout.nHeight * 2 + (txout.fCoinBase ? 1u : 0u))); + ::Serialize(s, VARINT(txout.nHeight * uint32_t{2} + txout.fCoinBase )); if (txout.nHeight > 0) { // Required to maintain compatibility with older undo format. ::Serialize(s, (unsigned char)0); @@ -34,9 +34,9 @@ struct TxInUndoFormatter template void Unser(Stream &s, Coin& txout) { - unsigned int nCode = 0; + uint32_t nCode = 0; ::Unserialize(s, VARINT(nCode)); - txout.nHeight = nCode / 2; + txout.nHeight = nCode >> 1; txout.fCoinBase = nCode & 1; if (txout.nHeight > 0) { // Old versions stored the version number for the last spend of diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index 00ac4ba3df..8499344e1e 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -580,7 +580,7 @@ class FullBlockTest(BitcoinTestFramework): self.move_tip(44) b47 = self.next_block(47, solve=False) target = uint256_from_compact(b47.nBits) - while b47.sha256 < target: + while b47.sha256 <= target: b47.nNonce += 1 b47.rehash() self.send_blocks([b47], False, request_block=False) @@ -1257,6 +1257,8 @@ class FullBlockTest(BitcoinTestFramework): block.hashMerkleRoot = block.calc_merkle_root() if solve: block.solve() + else: + block.rehash() self.tip = block self.block_heights[block.sha256] = height assert number not in self.blocks diff --git a/test/functional/mempool_expiry.py b/test/functional/mempool_expiry.py new file mode 100755 index 0000000000..8b9b7b155a --- /dev/null +++ b/test/functional/mempool_expiry.py @@ -0,0 +1,100 @@ +#!/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. +"""Tests that a mempool transaction expires after a given timeout and that its +children are removed as well. + +Both the default expiry timeout defined by DEFAULT_MEMPOOL_EXPIRY and a user +definable expiry timeout via the '-mempoolexpiry=' command line argument +( is the timeout in hours) are tested. +""" + +from datetime import timedelta + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + find_vout_for_address, +) + +DEFAULT_MEMPOOL_EXPIRY = 336 # hours +CUSTOM_MEMPOOL_EXPIRY = 10 # hours + + +class MempoolExpiryTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def test_transaction_expiry(self, timeout): + """Tests that a transaction expires after the expiry timeout and its + children are removed as well.""" + node = self.nodes[0] + + # Send a parent transaction that will expire. + parent_address = node.getnewaddress() + parent_txid = node.sendtoaddress(parent_address, 1.0) + + # Set the mocktime to the arrival time of the parent transaction. + entry_time = node.getmempoolentry(parent_txid)['time'] + node.setmocktime(entry_time) + + # Create child transaction spending the parent transaction + vout = find_vout_for_address(node, parent_txid, parent_address) + inputs = [{'txid': parent_txid, 'vout': vout}] + outputs = {node.getnewaddress(): 0.99} + child_raw = node.createrawtransaction(inputs, outputs) + child_signed = node.signrawtransactionwithwallet(child_raw)['hex'] + + # Let half of the timeout elapse and broadcast the child transaction. + half_expiry_time = entry_time + int(60 * 60 * timeout/2) + node.setmocktime(half_expiry_time) + child_txid = node.sendrawtransaction(child_signed) + self.log.info('Broadcast child transaction after {} hours.'.format( + timedelta(seconds=(half_expiry_time-entry_time)))) + + # Let most of the timeout elapse and check that the parent tx is still + # in the mempool. + nearly_expiry_time = entry_time + 60 * 60 * timeout - 5 + node.setmocktime(nearly_expiry_time) + # Expiry of mempool transactions is only checked when a new transaction + # is added to the to the mempool. + node.sendtoaddress(node.getnewaddress(), 1.0) + self.log.info('Test parent tx not expired after {} hours.'.format( + timedelta(seconds=(nearly_expiry_time-entry_time)))) + assert_equal(entry_time, node.getmempoolentry(parent_txid)['time']) + + # Transaction should be evicted from the mempool after the expiry time + # has passed. + expiry_time = entry_time + 60 * 60 * timeout + 5 + node.setmocktime(expiry_time) + # Expiry of mempool transactions is only checked when a new transaction + # is added to the to the mempool. + node.sendtoaddress(node.getnewaddress(), 1.0) + self.log.info('Test parent tx expiry after {} hours.'.format( + timedelta(seconds=(expiry_time-entry_time)))) + assert_raises_rpc_error(-5, 'Transaction not in mempool', + node.getmempoolentry, parent_txid) + + # The child transaction should be removed from the mempool as well. + self.log.info('Test child tx is evicted as well.') + assert_raises_rpc_error(-5, 'Transaction not in mempool', + node.getmempoolentry, child_txid) + + def run_test(self): + self.log.info('Test default mempool expiry timeout of %d hours.' % + DEFAULT_MEMPOOL_EXPIRY) + self.test_transaction_expiry(DEFAULT_MEMPOOL_EXPIRY) + + self.log.info('Test custom mempool expiry timeout of %d hours.' % + CUSTOM_MEMPOOL_EXPIRY) + self.restart_node(0, ['-mempoolexpiry=%d' % CUSTOM_MEMPOOL_EXPIRY]) + self.test_transaction_expiry(CUSTOM_MEMPOOL_EXPIRY) + + +if __name__ == '__main__': + MempoolExpiryTest().main() diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 77eb1d3787..38c4b9e3e3 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -434,7 +434,11 @@ def connect_nodes(from_connection, node_num): from_connection.addnode(ip_port, "onetry") # poll until version handshake complete to avoid race conditions # with transaction relaying - wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) + # See comments in net_processing: + # * Must have a version message before anything else + # * Must have a verack message before anything else + wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo())) + wait_until(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo())) def connect_nodes_bi(nodes, a, b): connect_nodes(nodes[a], b) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index ed8279d574..1f42f7b0ce 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -156,6 +156,7 @@ BASE_SCRIPTS = [ 'rpc_signmessage.py', 'feature_nulldummy.py', 'mempool_accept.py', + 'mempool_expiry.py', 'wallet_import_rescan.py', 'rpc_bind.py --ipv4', 'rpc_bind.py --ipv6',