diff options
28 files changed, 400 insertions, 155 deletions
diff --git a/depends/packages/native_mac_alias.mk b/depends/packages/native_mac_alias.mk index 488ec8b59c..306c835656 100644 --- a/depends/packages/native_mac_alias.mk +++ b/depends/packages/native_mac_alias.mk @@ -1,14 +1,9 @@ package=native_mac_alias -$(package)_version=2.0.6 +$(package)_version=2.0.7 $(package)_download_path=https://github.com/al45tair/mac_alias/archive/ $(package)_file_name=v$($(package)_version).tar.gz -$(package)_sha256_hash=78a3332d9a597eebf09ae652d38ad1e263b28db5c2e6dd725fad357b03b77371 +$(package)_sha256_hash=6f606d3b6bccd2112aeabf1a063f5b5ece87005a5d7e97c8faca23b916e88838 $(package)_install_libdir=$(build_prefix)/lib/python/dist-packages -$(package)_patches=python3.patch - -define $(package)_preprocess_cmds - patch -p1 < $($(package)_patch_dir)/python3.patch -endef define $(package)_build_cmds python setup.py build diff --git a/depends/patches/native_mac_alias/python3.patch b/depends/patches/native_mac_alias/python3.patch deleted file mode 100644 index 6f2f5534a2..0000000000 --- a/depends/patches/native_mac_alias/python3.patch +++ /dev/null @@ -1,56 +0,0 @@ -diff -dur a/mac_alias/alias.py b/mac_alias/alias.py ---- a/mac_alias/alias.py -+++ b/mac_alias/alias.py -@@ -258,10 +258,10 @@ - alias = Alias() - alias.appinfo = appinfo - -- alias.volume = VolumeInfo (volname.replace('/',':'), -+ alias.volume = VolumeInfo (volname.decode().replace('/',':'), - voldate, fstype, disktype, - volattrs, volfsid) -- alias.target = TargetInfo (kind, filename.replace('/',':'), -+ alias.target = TargetInfo (kind, filename.decode().replace('/',':'), - folder_cnid, cnid, - crdate, creator_code, type_code) - alias.target.levels_from = levels_from -@@ -276,9 +276,9 @@ - b.read(1) - - if tag == TAG_CARBON_FOLDER_NAME: -- alias.target.folder_name = value.replace('/',':') -+ alias.target.folder_name = value.decode().replace('/',':') - elif tag == TAG_CNID_PATH: -- alias.target.cnid_path = struct.unpack(b'>%uI' % (length // 4), -+ alias.target.cnid_path = struct.unpack('>%uI' % (length // 4), - value) - elif tag == TAG_CARBON_PATH: - alias.target.carbon_path = value -@@ -313,9 +313,9 @@ - alias.target.creation_date \ - = mac_epoch + datetime.timedelta(seconds=seconds) - elif tag == TAG_POSIX_PATH: -- alias.target.posix_path = value -+ alias.target.posix_path = value.decode() - elif tag == TAG_POSIX_PATH_TO_MOUNTPOINT: -- alias.volume.posix_path = value -+ alias.volume.posix_path = value.decode() - elif tag == TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE: - alias.volume.disk_image_alias = Alias.from_bytes(value) - elif tag == TAG_USER_HOME_LENGTH_PREFIX: -@@ -467,12 +467,12 @@ - - b.write(struct.pack(b'>hhQhhQ', - TAG_HIGH_RES_VOLUME_CREATION_DATE, -- 8, long(voldate * 65536), -+ 8, int(voldate * 65536), - TAG_HIGH_RES_CREATION_DATE, -- 8, long(crdate * 65536))) -+ 8, int(crdate * 65536))) - - if self.target.cnid_path: -- cnid_path = struct.pack(b'>%uI' % len(self.target.cnid_path), -+ cnid_path = struct.pack('>%uI' % len(self.target.cnid_path), - *self.target.cnid_path) - b.write(struct.pack(b'>hh', TAG_CNID_PATH, - len(cnid_path))) diff --git a/doc/build-windows.md b/doc/build-windows.md index 8d4afdc817..a10654c7ee 100644 --- a/doc/build-windows.md +++ b/doc/build-windows.md @@ -34,10 +34,9 @@ Full instructions to install WSL are available on the above link. To install WSL on Windows 10 with Fall Creators Update installed (version >= 16215.0) do the following: 1. Enable the Windows Subsystem for Linux feature - * From Start, search for "Turn Windows features on or off" (type 'turn') - * Select Windows Subsystem for Linux - * Click OK - * Restart if necessary + * Open the Windows Features dialog (`OptionalFeatures.exe`) + * Enable 'Windows Susbsystem for Linux' + * Click 'OK' and restart if necessary 2. Install Ubuntu * Open Microsoft Store and search for Ubuntu or use [this link](https://www.microsoft.com/store/productId/9NBLGGH4MSV6) * Click Install diff --git a/doc/release-notes.md b/doc/release-notes.md index 0292eaa4d2..528cb81a38 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -56,6 +56,13 @@ frequently tested on them. Notable changes =============== +RPC changes +------------ + +### Low-level changes + +- The `fundrawtransaction` rpc will reject the previously deprecated `reserveChangeKey` option. + Credits ======= diff --git a/src/clientversion.cpp b/src/clientversion.cpp index 7aa0a8d660..662fbb6e77 100644 --- a/src/clientversion.cpp +++ b/src/clientversion.cpp @@ -43,7 +43,7 @@ const std::string CLIENT_NAME("Satoshi"); //! git will put "#define GIT_ARCHIVE 1" on the next line inside archives. $Format:%n#define GIT_ARCHIVE 1$ #ifdef GIT_ARCHIVE -#define GIT_COMMIT_ID "$Format:%h$" +#define GIT_COMMIT_ID "$Format:%H$" #define GIT_COMMIT_DATE "$Format:%cD$" #endif diff --git a/src/init.cpp b/src/init.cpp index b28baba779..895a5358f4 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -680,11 +680,13 @@ void ThreadImport(std::vector<fs::path> vImportFiles) if (!ActivateBestChain(state, chainparams)) { LogPrintf("Failed to connect best block"); StartShutdown(); + return; } if (gArgs.GetBoolArg("-stopafterblockimport", DEFAULT_STOPAFTERBLOCKIMPORT)) { LogPrintf("Stopping after block import\n"); StartShutdown(); + return; } } // End scope of CImportingNow if (gArgs.GetArg("-persistmempool", DEFAULT_PERSIST_MEMPOOL)) { diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 74f0db3520..ffb5bff4de 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -393,11 +393,8 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con } // Add entry - { - LOCK(wallet->cs_wallet); - wallet->SetAddressBook(DecodeDestination(strAddress), strLabel, - (type == Send ? "send" : "receive")); - } + wallet->SetAddressBook(DecodeDestination(strAddress), strLabel, + (type == Send ? "send" : "receive")); return QString::fromStdString(strAddress); } @@ -411,10 +408,7 @@ bool AddressTableModel::removeRows(int row, int count, const QModelIndex &parent // Also refuse to remove receiving addresses. return false; } - { - LOCK(wallet->cs_wallet); - wallet->DelAddressBook(DecodeDestination(rec->address.toStdString())); - } + wallet->DelAddressBook(DecodeDestination(rec->address.toStdString())); return true; } diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 541114e5fe..34954a6bfa 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -42,6 +42,7 @@ WalletModel::WalletModel(const PlatformStyle *platformStyle, CWallet *_wallet, O transactionTableModel(0), recentRequestsTableModel(0), cachedBalance(0), cachedUnconfirmedBalance(0), cachedImmatureBalance(0), + cachedWatchOnlyBalance{0}, cachedWatchUnconfBalance{0}, cachedWatchImmatureBalance{0}, cachedEncryptionStatus(Unencrypted), cachedNumBlocks(0) { diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 189da6ae48..f1352a13cf 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1434,7 +1434,7 @@ UniValue preciousblock(const JSONRPCRequest& request) PreciousBlock(state, Params(), pblockindex); if (!state.IsValid()) { - throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason()); + throw JSONRPCError(RPC_DATABASE_ERROR, FormatStateMessage(state)); } return NullUniValue; @@ -1472,7 +1472,7 @@ UniValue invalidateblock(const JSONRPCRequest& request) } if (!state.IsValid()) { - throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason()); + throw JSONRPCError(RPC_DATABASE_ERROR, FormatStateMessage(state)); } return NullUniValue; @@ -1509,7 +1509,7 @@ UniValue reconsiderblock(const JSONRPCRequest& request) ActivateBestChain(state, Params()); if (!state.IsValid()) { - throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason()); + throw JSONRPCError(RPC_DATABASE_ERROR, FormatStateMessage(state)); } return NullUniValue; @@ -1563,7 +1563,7 @@ UniValue getchaintxstats(const JSONRPCRequest& request) pindex = chainActive.Tip(); } } - + assert(pindex != nullptr); if (request.params[0].isNull()) { diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index dd74095b62..3f3bfa0cfd 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -264,11 +264,11 @@ static UniValue BIP22ValidationResult(const CValidationState& state) if (state.IsValid()) return NullUniValue; - std::string strRejectReason = state.GetRejectReason(); if (state.IsError()) - throw JSONRPCError(RPC_VERIFY_ERROR, strRejectReason); + throw JSONRPCError(RPC_VERIFY_ERROR, FormatStateMessage(state)); if (state.IsInvalid()) { + std::string strRejectReason = state.GetRejectReason(); if (strRejectReason.empty()) return "rejected"; return strRejectReason; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 24f2431efc..803cd28649 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -981,12 +981,12 @@ UniValue sendrawtransaction(const JSONRPCRequest& request) if (!AcceptToMemoryPool(mempool, state, std::move(tx), &fMissingInputs, nullptr /* plTxnReplaced */, false /* bypass_limits */, nMaxRawTxFee)) { if (state.IsInvalid()) { - throw JSONRPCError(RPC_TRANSACTION_REJECTED, strprintf("%i: %s", state.GetRejectCode(), state.GetRejectReason())); + throw JSONRPCError(RPC_TRANSACTION_REJECTED, FormatStateMessage(state)); } else { if (fMissingInputs) { throw JSONRPCError(RPC_TRANSACTION_ERROR, "Missing inputs"); } - throw JSONRPCError(RPC_TRANSACTION_ERROR, state.GetRejectReason()); + throw JSONRPCError(RPC_TRANSACTION_ERROR, FormatStateMessage(state)); } } else { // If wallet is enabled, ensure that the wallet has been made aware diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 55d60d95e9..4b2da3e219 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -13,6 +13,10 @@ #include <stdint.h> #include <vector> +#ifndef WIN32 +#include <sys/types.h> +#include <sys/wait.h> +#endif #include <boost/test/unit_test.hpp> @@ -603,4 +607,130 @@ BOOST_AUTO_TEST_CASE(test_ParseFixedPoint) BOOST_CHECK(!ParseFixedPoint("1.", 8, &amount)); } +static void TestOtherThread(fs::path dirname, std::string lockname, bool *result) +{ + *result = LockDirectory(dirname, lockname); +} + +#ifndef WIN32 // Cannot do this test on WIN32 due to lack of fork() +static constexpr char LockCommand = 'L'; +static constexpr char UnlockCommand = 'U'; +static constexpr char ExitCommand = 'X'; + +static void TestOtherProcess(fs::path dirname, std::string lockname, int fd) +{ + char ch; + int rv; + while (true) { + rv = read(fd, &ch, 1); // Wait for command + assert(rv == 1); + switch(ch) { + case LockCommand: + ch = LockDirectory(dirname, lockname); + rv = write(fd, &ch, 1); + assert(rv == 1); + break; + case UnlockCommand: + ReleaseDirectoryLocks(); + ch = true; // Always succeeds + rv = write(fd, &ch, 1); + break; + case ExitCommand: + close(fd); + exit(0); + default: + assert(0); + } + } +} +#endif + +BOOST_AUTO_TEST_CASE(test_LockDirectory) +{ + fs::path dirname = fs::temp_directory_path() / fs::unique_path(); + const std::string lockname = ".lock"; +#ifndef WIN32 + // Revert SIGCHLD to default, otherwise boost.test will catch and fail on + // it: there is BOOST_TEST_IGNORE_SIGCHLD but that only works when defined + // at build-time of the boost library + void (*old_handler)(int) = signal(SIGCHLD, SIG_DFL); + + // Fork another process for testing before creating the lock, so that we + // won't fork while holding the lock (which might be undefined, and is not + // relevant as test case as that is avoided with -daemonize). + int fd[2]; + BOOST_CHECK_EQUAL(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); + pid_t pid = fork(); + if (!pid) { + BOOST_CHECK_EQUAL(close(fd[1]), 0); // Child: close parent end + TestOtherProcess(dirname, lockname, fd[0]); + } + BOOST_CHECK_EQUAL(close(fd[0]), 0); // Parent: close child end +#endif + // Lock on non-existent directory should fail + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), false); + + fs::create_directories(dirname); + + // Probing lock on new directory should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Persistent lock on new directory should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true); + + // Another lock on the directory from the same thread should succeed + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname), true); + + // Another lock on the directory from a different thread within the same process should succeed + bool threadresult; + std::thread thr(TestOtherThread, dirname, lockname, &threadresult); + thr.join(); + BOOST_CHECK_EQUAL(threadresult, true); +#ifndef WIN32 + // Try to aquire lock in child process while we're holding it, this should fail. + char ch; + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, false); + + // Give up our lock + ReleaseDirectoryLocks(); + // Probing lock from our side now should succeed, but not hold on to the lock. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Try to acquire the lock in the child process, this should be succesful. + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, true); + + // When we try to probe the lock now, it should fail. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), false); + + // Unlock the lock in the child process + BOOST_CHECK_EQUAL(write(fd[1], &UnlockCommand, 1), 1); + BOOST_CHECK_EQUAL(read(fd[1], &ch, 1), 1); + BOOST_CHECK_EQUAL((bool)ch, true); + + // When we try to probe the lock now, it should succeed. + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Re-lock the lock in the child process, then wait for it to exit, check + // successful return. After that, we check that exiting the process + // has released the lock as we would expect by probing it. + int processstatus; + BOOST_CHECK_EQUAL(write(fd[1], &LockCommand, 1), 1); + BOOST_CHECK_EQUAL(write(fd[1], &ExitCommand, 1), 1); + BOOST_CHECK_EQUAL(waitpid(pid, &processstatus, 0), pid); + BOOST_CHECK_EQUAL(processstatus, 0); + BOOST_CHECK_EQUAL(LockDirectory(dirname, lockname, true), true); + + // Restore SIGCHLD + signal(SIGCHLD, old_handler); + BOOST_CHECK_EQUAL(close(fd[1]), 0); // Close our side of the socketpair +#endif + // Clean up + ReleaseDirectoryLocks(); + fs::remove_all(dirname); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util.cpp b/src/util.cpp index 6738bbc6e4..dcf7ed38b1 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -373,20 +373,37 @@ int LogPrintStr(const std::string &str) return ret; } +/** A map that contains all the currently held directory locks. After + * successful locking, these will be held here until the global destructor + * cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks + * is called. + */ +static std::map<std::string, std::unique_ptr<boost::interprocess::file_lock>> dir_locks; +/** Mutex to protect dir_locks. */ +static std::mutex cs_dir_locks; + bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only) { + std::lock_guard<std::mutex> ulock(cs_dir_locks); fs::path pathLockFile = directory / lockfile_name; - FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist. + + // If a lock for this directory already exists in the map, don't try to re-lock it + if (dir_locks.count(pathLockFile.string())) { + return true; + } + + // Create empty lock file if it doesn't exist. + FILE* file = fsbridge::fopen(pathLockFile, "a"); if (file) fclose(file); try { - static std::map<std::string, boost::interprocess::file_lock> locks; - boost::interprocess::file_lock& lock = locks.emplace(pathLockFile.string(), pathLockFile.string().c_str()).first->second; - if (!lock.try_lock()) { + auto lock = MakeUnique<boost::interprocess::file_lock>(pathLockFile.string().c_str()); + if (!lock->try_lock()) { return false; } - if (probe_only) { - lock.unlock(); + if (!probe_only) { + // Lock successful and we're not just probing, put it into the map + dir_locks.emplace(pathLockFile.string(), std::move(lock)); } } catch (const boost::interprocess::interprocess_exception& e) { return error("Error while attempting to lock directory %s: %s", directory.string(), e.what()); @@ -394,6 +411,12 @@ bool LockDirectory(const fs::path& directory, const std::string lockfile_name, b return true; } +void ReleaseDirectoryLocks() +{ + std::lock_guard<std::mutex> ulock(cs_dir_locks); + dir_locks.clear(); +} + /** Interpret string as boolean, for argument parsing */ static bool InterpretBool(const std::string& strValue) { diff --git a/src/util.h b/src/util.h index 05138a9bfe..9490a5678f 100644 --- a/src/util.h +++ b/src/util.h @@ -174,6 +174,12 @@ int RaiseFileDescriptorLimit(int nMinFD); void AllocateFileRange(FILE *file, unsigned int offset, unsigned int length); bool RenameOver(fs::path src, fs::path dest); bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only=false); + +/** Release all directory locks. This is used for unit testing only, at runtime + * the global destructor will take care of the locks. + */ +void ReleaseDirectoryLocks(); + bool TryCreateDirectories(const fs::path& p); fs::path GetDefaultDataDir(); const fs::path &GetDataDir(bool fNetSpecific = true); diff --git a/src/validation.cpp b/src/validation.cpp index dabf7e279e..e809f66e25 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -712,7 +712,7 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool CAmount mempoolRejectFee = pool.GetMinFee(gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000).GetFee(nSize); if (!bypass_limits && mempoolRejectFee > 0 && nModifiedFees < mempoolRejectFee) { - return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "mempool min fee not met", false, strprintf("%d < %d", nFees, mempoolRejectFee)); + return state.DoS(0, false, REJECT_INSUFFICIENTFEE, "mempool min fee not met", false, strprintf("%d < %d", nModifiedFees, mempoolRejectFee)); } // No transactions are allowed below minRelayTxFee except from disconnected blocks diff --git a/src/wallet/feebumper.cpp b/src/wallet/feebumper.cpp index 5234a69710..9cae660c60 100644 --- a/src/wallet/feebumper.cpp +++ b/src/wallet/feebumper.cpp @@ -274,7 +274,7 @@ Result CommitTransaction(CWallet* wallet, const uint256& txid, CMutableTransacti CValidationState state; if (!wallet->CommitTransaction(wtxBumped, reservekey, g_connman.get(), state)) { // NOTE: CommitTransaction never returns false, so this should never happen. - errors.push_back(strprintf("The transaction was rejected: %s", state.GetRejectReason())); + errors.push_back(strprintf("The transaction was rejected: %s", FormatStateMessage(state))); return Result::WALLET_ERROR; } @@ -297,4 +297,3 @@ Result CommitTransaction(CWallet* wallet, const uint256& txid, CMutableTransacti } } // namespace feebumper - diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index b466cf1a81..8f378acd1a 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -435,7 +435,7 @@ static void SendMoney(CWallet * const pwallet, const CTxDestination &address, CA } CValidationState state; if (!pwallet->CommitTransaction(wtxNew, reservekey, g_connman.get(), state)) { - strError = strprintf("Error: The transaction was rejected! Reason given: %s", state.GetRejectReason()); + strError = strprintf("Error: The transaction was rejected! Reason given: %s", FormatStateMessage(state)); throw JSONRPCError(RPC_WALLET_ERROR, strError); } } @@ -1155,7 +1155,7 @@ UniValue sendmany(const JSONRPCRequest& request) throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason); CValidationState state; if (!pwallet->CommitTransaction(wtx, keyChange, g_connman.get(), state)) { - strFailReason = strprintf("Transaction commit failed:: %s", state.GetRejectReason()); + strFailReason = strprintf("Transaction commit failed:: %s", FormatStateMessage(state)); throw JSONRPCError(RPC_WALLET_ERROR, strFailReason); } @@ -3129,7 +3129,6 @@ UniValue fundrawtransaction(const JSONRPCRequest& request) {"change_type", UniValueType(UniValue::VSTR)}, {"includeWatching", UniValueType(UniValue::VBOOL)}, {"lockUnspents", UniValueType(UniValue::VBOOL)}, - {"reserveChangeKey", UniValueType(UniValue::VBOOL)}, // DEPRECATED (and ignored), should be removed in 0.16 or so. {"feeRate", UniValueType()}, // will be checked below {"subtractFeeFromOutputs", UniValueType(UniValue::VARR)}, {"replaceable", UniValueType(UniValue::VBOOL)}, diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 161372784b..9db5d63922 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -451,6 +451,9 @@ BOOST_FIXTURE_TEST_CASE(rescan, TestChain100Setup) // than or equal to key birthday. BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) { + g_address_type = OUTPUT_TYPE_DEFAULT; + g_change_type = OUTPUT_TYPE_DEFAULT; + // Create two blocks with same timestamp to verify that importwallet rescan // will pick up both blocks, not just the first. const int64_t BLOCK_TIME = chainActive.Tip()->GetBlockTimeMax() + 5; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 513819606b..408a01c50b 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3092,7 +3092,7 @@ bool CWallet::CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey, CCon { // Broadcast if (!wtx.AcceptToMemoryPool(maxTxFee, state)) { - LogPrintf("CommitTransaction(): Transaction cannot be broadcast immediately, %s\n", state.GetRejectReason()); + LogPrintf("CommitTransaction(): Transaction cannot be broadcast immediately, %s\n", FormatStateMessage(state)); // TODO: if we expect the failure to be long term or permanent, instead delete wtx from the wallet and return failure. } else { wtx.RelayWalletTransaction(connman); @@ -4179,11 +4179,6 @@ int CMerkleTx::GetBlocksToMaturity() const bool CWalletTx::AcceptToMemoryPool(const CAmount& nAbsurdFee, CValidationState& state) { - // Quick check to avoid re-setting fInMempool to false - if (mempool.exists(tx->GetHash())) { - return false; - } - // We must set fInMempool here - while it will be re-set to true by the // entered-mempool callback, if we did not there would be a race where a // user could call sendmoney in a loop and hit spurious out of funds errors @@ -4191,7 +4186,7 @@ bool CWalletTx::AcceptToMemoryPool(const CAmount& nAbsurdFee, CValidationState& // unavailable as we're not yet aware its in mempool. bool ret = ::AcceptToMemoryPool(mempool, state, tx, nullptr /* pfMissingInputs */, nullptr /* plTxnReplaced */, false /* bypass_limits */, nAbsurdFee); - fInMempool = ret; + fInMempool |= ret; return ret; } diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py index e4f413cc2a..740c498ce6 100755 --- a/test/functional/feature_nulldummy.py +++ b/test/functional/feature_nulldummy.py @@ -21,7 +21,7 @@ from test_framework.script import CScript from io import BytesIO import time -NULLDUMMY_ERROR = "64: non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" +NULLDUMMY_ERROR = "non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero) (code 64)" def trueDummy(tx): scriptSig = CScript(tx.vin[0].scriptSig) diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index e7ce3820d2..7e01663c96 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -50,5 +50,15 @@ class MempoolLimitTest(BitcoinTestFramework): assert_equal(self.nodes[0].getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) assert_greater_than(self.nodes[0].getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + self.log.info('Create a mempool tx that will not pass mempoolminfee') + us0 = utxos.pop() + inputs = [{ "txid" : us0["txid"], "vout" : us0["vout"]}] + outputs = {self.nodes[0].getnewaddress() : 0.0001} + tx = self.nodes[0].createrawtransaction(inputs, outputs) + # specifically fund this tx with a fee < mempoolminfee, >= than minrelaytxfee + txF = self.nodes[0].fundrawtransaction(tx, {'feeRate': relayfee}) + txFS = self.nodes[0].signrawtransaction(txF['hex']) + assert_raises_rpc_error(-26, "mempool min fee not met, 166 < 411 (code 66)", self.nodes[0].sendrawtransaction, txFS['hex']) + if __name__ == '__main__': MempoolLimitTest().main() diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py index 57954ce321..8cea9c2783 100755 --- a/test/functional/mining_prioritisetransaction.py +++ b/test/functional/mining_prioritisetransaction.py @@ -120,7 +120,7 @@ class PrioritiseTransactionTest(BitcoinTestFramework): tx_id = self.nodes[0].decoderawtransaction(tx_hex)["txid"] # This will raise an exception due to min relay fee not being met - assert_raises_rpc_error(-26, "66: min relay fee not met", self.nodes[0].sendrawtransaction, tx_hex) + assert_raises_rpc_error(-26, "min relay fee not met (code 66)", self.nodes[0].sendrawtransaction, tx_hex) assert(tx_id not in self.nodes[0].getrawmempool()) # This is a less than 1000-byte transaction, so just set the fee diff --git a/test/functional/p2p_invalid_tx.py b/test/functional/p2p_invalid_tx.py index 9c1100e070..64fada38e2 100755 --- a/test/functional/p2p_invalid_tx.py +++ b/test/functional/p2p_invalid_tx.py @@ -4,68 +4,50 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid transactions. -In this test we connect to one node over p2p, and test tx requests. -""" +In this test we connect to one node over p2p, and test tx requests.""" +from test_framework.blocktools import create_block, create_coinbase, create_transaction +from test_framework.messages import COIN +from test_framework.mininode import network_thread_start, P2PDataStore +from test_framework.test_framework import BitcoinTestFramework -from test_framework.test_framework import ComparisonTestFramework -from test_framework.comptool import TestManager, TestInstance, RejectResult -from test_framework.blocktools import * -import time +class InvalidTxRequestTest(BitcoinTestFramework): - - -# Use the ComparisonTestFramework with 1 node: only use --testbinary. -class InvalidTxRequestTest(ComparisonTestFramework): - - ''' Can either run this test as 1 node with expected answers, or two and compare them. - Change the "outcome" variable from each TestInstance object to only do the comparison. ''' def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True + self.extra_args = [["-whitelist=127.0.0.1"]] def run_test(self): - test = TestManager(self, self.options.tmpdir) - test.add_all_connections(self.nodes) - self.tip = None - self.block_time = None + # Add p2p connection to node0 + node = self.nodes[0] # convenience reference to the node + node.add_p2p_connection(P2PDataStore()) + network_thread_start() - test.run() + node.p2p.wait_for_verack() - def get_tests(self): - if self.tip is None: - self.tip = int("0x" + self.nodes[0].getbestblockhash(), 0) - self.block_time = int(time.time())+1 + best_block = self.nodes[0].getbestblockhash() + tip = int(best_block, 16) + best_block_time = self.nodes[0].getblock(best_block)['time'] + block_time = best_block_time + 1 - ''' - Create a new block with an anyone-can-spend coinbase - ''' + self.log.info("Create a new block with an anyone-can-spend coinbase.") height = 1 - block = create_block(self.tip, create_coinbase(height), self.block_time) - self.block_time += 1 + block = create_block(tip, create_coinbase(height), block_time) + block_time += 1 block.solve() # Save the coinbase for later - self.block1 = block - self.tip = block.sha256 + block1 = block + tip = block.sha256 height += 1 - yield TestInstance([[block, True]]) + node.p2p.send_blocks_and_test([block], node, success=True) - ''' - Now we need that block to mature so we can spend the coinbase. - ''' - test = TestInstance(sync_every_block=False) - for i in range(100): - block = create_block(self.tip, create_coinbase(height), self.block_time) - block.solve() - self.tip = block.sha256 - self.block_time += 1 - test.blocks_and_transactions.append([block, True]) - height += 1 - yield test + self.log.info("Mature the block.") + self.nodes[0].generate(100) # b'\x64' is OP_NOTIF # Transaction will be rejected with code 16 (REJECT_INVALID) - tx1 = create_transaction(self.block1.vtx[0], 0, b'\x64', 50 * COIN - 12000) - yield TestInstance([[tx1, RejectResult(16, b'mandatory-script-verify-flag-failed')]]) + tx1 = create_transaction(block1.vtx[0], 0, b'\x64', 50 * COIN - 12000) + node.p2p.send_txs_and_test([tx1], node, success=False, reject_code=16, reject_reason=b'mandatory-script-verify-flag-failed (Invalid OP_IF construction)') # TODO: test further transactions... diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 6b9c9c15b7..4d3be18516 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -181,6 +181,9 @@ class RawTransactionsTest(BitcoinTestFramework): assert_raises_rpc_error(-3, "Unexpected key foo", self.nodes[2].fundrawtransaction, rawtx, {'foo':'bar'}) + # reserveChangeKey was deprecated and is now removed + assert_raises_rpc_error(-3, "Unexpected key reserveChangeKey", lambda: self.nodes[2].fundrawtransaction(hexstring=rawtx, options={'reserveChangeKey': True})) + ############################################################ # test a fundrawtransaction with an invalid change address # ############################################################ diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index a54a0299c7..46ef7521e0 100644 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -44,6 +44,11 @@ NODE_UNSUPPORTED_SERVICE_BIT_5 = (1 << 5) NODE_UNSUPPORTED_SERVICE_BIT_7 = (1 << 7) NODE_NETWORK_LIMITED = (1 << 10) +MSG_TX = 1 +MSG_BLOCK = 2 +MSG_WITNESS_FLAG = 1 << 30 +MSG_TYPE_MASK = 0xffffffff >> 2 + # Serialization/deserialization tools def sha256(s): return hashlib.new('sha256', s).digest() @@ -204,8 +209,6 @@ class CAddress(): return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, self.ip, self.port) -MSG_WITNESS_FLAG = 1<<30 - class CInv(): typemap = { 0: "Error", diff --git a/test/functional/test_framework/mininode.py b/test/functional/test_framework/mininode.py index 4d9661de37..99d0abc3f9 100755 --- a/test/functional/test_framework/mininode.py +++ b/test/functional/test_framework/mininode.py @@ -10,7 +10,9 @@ This python code was modified from ArtForz' public domain half-a-node, as 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""" +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""" import asyncore from collections import defaultdict from io import BytesIO @@ -86,7 +88,7 @@ class P2PConnection(asyncore.dispatcher): self.network = net self.disconnect = False - logger.info('Connecting to Bitcoin Node: %s:%d' % (self.dstaddr, self.dstport)) + logger.debug('Connecting to Bitcoin Node: %s:%d' % (self.dstaddr, self.dstport)) try: self.connect((dstaddr, dstport)) @@ -356,10 +358,22 @@ class P2PInterface(P2PConnection): wait_until(test_function, timeout=timeout, lock=mininode_lock) def wait_for_getdata(self, timeout=60): + """Waits for a getdata message. + + Receiving any getdata message will satisfy the predicate. the last_message["getdata"] + value must be explicitly cleared before calling this method, or this will return + immediately with success. TODO: change this method to take a hash value and only + return true if the correct block/tx has been requested.""" test_function = lambda: self.last_message.get("getdata") wait_until(test_function, timeout=timeout, lock=mininode_lock) def wait_for_getheaders(self, timeout=60): + """Waits for a getheaders message. + + Receiving any getheaders message will satisfy the predicate. the last_message["getheaders"] + value must be explicitly cleared before calling this method, or this will return + immediately with success. TODO: change this method to take a hash value and only + return true if the correct block header has been requested.""" test_function = lambda: self.last_message.get("getheaders") wait_until(test_function, timeout=timeout, lock=mininode_lock) @@ -440,3 +454,138 @@ def network_thread_join(timeout=10): for thread in network_threads: thread.join(timeout) assert not thread.is_alive() + +class P2PDataStore(P2PInterface): + """A P2P data store class. + + Keeps a block and transaction store and responds correctly to getdata and getheaders requests.""" + + def __init__(self): + super().__init__() + self.reject_code_received = None + self.reject_reason_received = None + # store of blocks. key is block hash, value is a CBlock object + self.block_store = {} + self.last_block_hash = '' + # store of txs. key is txid, value is a CTransaction object + self.tx_store = {} + self.getdata_requests = [] + + def on_getdata(self, message): + """Check for the tx/block in our stores and if found, reply with an inv message.""" + for inv in message.inv: + self.getdata_requests.append(inv.hash) + if (inv.type & MSG_TYPE_MASK) == MSG_TX and inv.hash in self.tx_store.keys(): + self.send_message(msg_tx(self.tx_store[inv.hash])) + elif (inv.type & MSG_TYPE_MASK) == MSG_BLOCK and inv.hash in self.block_store.keys(): + self.send_message(msg_block(self.block_store[inv.hash])) + else: + logger.debug('getdata message type {} received.'.format(hex(inv.type))) + + def on_getheaders(self, message): + """Search back through our block store for the locator, and reply with a headers message if found.""" + + locator, hash_stop = message.locator, message.hashstop + + # Assume that the most recent block added is the tip + if not self.block_store: + return + + headers_list = [self.block_store[self.last_block_hash]] + maxheaders = 2000 + while headers_list[-1].sha256 not in locator.vHave: + # Walk back through the block store, adding headers to headers_list + # as we go. + prev_block_hash = headers_list[-1].hashPrevBlock + if prev_block_hash in self.block_store: + prev_block_header = self.block_store[prev_block_hash] + headers_list.append(prev_block_header) + if prev_block_header.sha256 == hash_stop: + # if this is the hashstop header, stop here + break + else: + logger.debug('block hash {} not found in block store'.format(hex(prev_block_hash))) + break + + # Truncate the list if there are too many headers + headers_list = headers_list[:-maxheaders - 1:-1] + response = msg_headers(headers_list) + + if response is not None: + self.send_message(response) + + def on_reject(self, message): + """Store reject reason and code for testing.""" + self.reject_code_received = message.code + self.reject_reason_received = message.reason + + def send_blocks_and_test(self, blocks, rpc, success=True, request_block=True, reject_code=None, reject_reason=None, timeout=60): + """Send blocks to test node and test whether the tip advances. + + - add all blocks to our block_store + - send a headers message for the final block + - the on_getheaders handler will ensure that any getheaders are responded to + - if request_block is True: wait for getdata for each of the blocks. The on_getdata handler will + ensure that any getdata messages are responded to + - if success is True: assert that the node's tip advances to the most recent block + - if success is False: assert that the node's tip doesn't advance + - if reject_code and reject_reason are set: assert that the correct reject message is received""" + + with mininode_lock: + self.reject_code_received = None + self.reject_reason_received = None + + for block in blocks: + self.block_store[block.sha256] = block + self.last_block_hash = block.sha256 + + self.send_message(msg_headers([blocks[-1]])) + + if request_block: + wait_until(lambda: blocks[-1].sha256 in self.getdata_requests, timeout=timeout, lock=mininode_lock) + + if success: + wait_until(lambda: rpc.getbestblockhash() == blocks[-1].hash, timeout=timeout) + else: + assert rpc.getbestblockhash() != blocks[-1].hash + + if reject_code is not None: + wait_until(lambda: self.reject_code_received == reject_code, lock=mininode_lock) + if reject_reason is not None: + wait_until(lambda: self.reject_reason_received == reject_reason, lock=mininode_lock) + + def send_txs_and_test(self, txs, rpc, success=True, reject_code=None, reject_reason=None): + """Send txs to test node and test whether they're accepted to the mempool. + + - add all txs to our tx_store + - send tx messages for all txs + - if success is True: assert that the tx is accepted to the mempool + - if success is False: assert that the tx is not accepted to the mempool + - if reject_code and reject_reason are set: assert that the correct reject message is received.""" + + with mininode_lock: + self.reject_code_received = None + self.reject_reason_received = None + + for tx in txs: + self.tx_store[tx.sha256] = tx + + for tx in txs: + self.send_message(msg_tx(tx)) + + self.sync_with_ping() + + raw_mempool = rpc.getrawmempool() + if success: + # Check that all txs are now in the mempool + for tx in txs: + assert tx.hash in raw_mempool, "{} not found in mempool".format(tx.hash) + else: + # 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) + + if reject_code is not None: + wait_until(lambda: self.reject_code_received == reject_code, lock=mininode_lock) + if reject_reason is not None: + wait_until(lambda: self.reject_reason_received == reject_reason, lock=mininode_lock) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 7fdc171332..8bf75ca1ae 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -292,6 +292,7 @@ def initialize_datadir(dirname, n): f.write("port=" + str(p2p_port(n)) + "\n") f.write("rpcport=" + str(rpc_port(n)) + "\n") f.write("listenonion=0\n") + f.write("bind=127.0.0.1\n") return datadir def get_datadir_path(dirname, n): diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 0cf3424c71..945f645eac 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -342,7 +342,7 @@ def run_tests(test_list, src_dir, build_dir, exeext, tmpdir, jobs=1, enable_cove print('\n============') print('{}Combined log for {}:{}'.format(BOLD[1], testdir, BOLD[0])) print('============\n') - combined_logs, _ = subprocess.Popen([sys.executble, os.path.join(tests_dir, 'combine_logs.py'), '-c', testdir], universal_newlines=True, stdout=subprocess.PIPE).communicate() + combined_logs, _ = subprocess.Popen([sys.executable, os.path.join(tests_dir, 'combine_logs.py'), '-c', testdir], universal_newlines=True, stdout=subprocess.PIPE).communicate() print("\n".join(deque(combined_logs.splitlines(), combined_logs_len))) print_results(test_results, max_len_name, (int(time.time() - time0))) |