diff options
46 files changed, 658 insertions, 193 deletions
diff --git a/.travis.yml b/.travis.yml index cd691e49cf..dec517f0a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,13 +26,13 @@ env: - WINEDEBUG=fixme-all - DOCKER_PACKAGES="build-essential libtool autotools-dev automake pkg-config bsdmainutils curl git ca-certificates ccache" before_install: - - set -o errexit; source .travis/test_03_before_install.sh + - set -o errexit; if ! source .travis/test_03_before_install.sh; then set +o errexit; false; fi install: - - set -o errexit; source .travis/test_04_install.sh + - set -o errexit; if ! source .travis/test_04_install.sh; then set +o errexit; false; fi before_script: - - set -o errexit; source .travis/test_05_before_script.sh + - set -o errexit; if ! source .travis/test_05_before_script.sh; then set +o errexit; false; fi script: - - if [ $SECONDS -gt 1200 ]; then set +o errexit; echo "Travis early exit to cache current state"; false; else set -o errexit; source .travis/test_06_script.sh; fi + - if [ $SECONDS -gt 1200 ]; then set +o errexit; echo "Travis early exit to cache current state"; false; else set -o errexit; if ! source .travis/test_06_script.sh; then set +o errexit; false; fi; fi after_script: - echo $TRAVIS_COMMIT_RANGE - echo $TRAVIS_COMMIT_LOG @@ -92,7 +92,7 @@ jobs: DEP_OPTS="NO_QT=1 NO_UPNP=1 DEBUG=1 ALLOW_HOST_PACKAGES=1" GOAL="install" BITCOIN_CONFIG="--enable-zmq --with-gui=qt5 --enable-glibc-back-compat --enable-reduce-exports --enable-debug CXXFLAGS=\"-g0 -O2\"" -# x86_64 Linux (xenial, no depends, only system libs, sanitizers: thread (TSAN)) +# x86_64 Linux (xenial, no depends, only system libs, sanitizers: thread (TSan)) - stage: test env: >- HOST=x86_64-unknown-linux-gnu @@ -102,14 +102,15 @@ jobs: RUN_FUNCTIONAL_TESTS=false # Disabled for now. TODO identify suppressions or exclude specific tests GOAL="install" BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=thread --disable-hardening --disable-asm CC=clang CXX=clang++" -# x86_64 Linux (no depends, only system libs, sanitizers: undefined (UBSAN) + integer) +# x86_64 Linux (no depends, only system libs, sanitizers: address/leak (ASan + LSan) + undefined (UBSan) + integer) - stage: test env: >- HOST=x86_64-unknown-linux-gnu PACKAGES="clang llvm python3-zmq qtbase5-dev qttools5-dev-tools libssl1.0-dev libevent-dev bsdmainutils libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev libdb5.3++-dev libminiupnpc-dev libzmq3-dev libprotobuf-dev protobuf-compiler libqrencode-dev" NO_DEPENDS=1 + FUNCTIONAL_TESTS_CONFIG="--exclude wallet_multiwallet.py" # Temporarily suppress ASan heap-use-after-free (see issue #14163) GOAL="install" - BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=integer,undefined CC=clang CXX=clang++" + BITCOIN_CONFIG="--enable-zmq --with-incompatible-bdb --with-gui=qt5 CPPFLAGS=-DDEBUG_LOCKORDER --with-sanitizers=address,integer,undefined CC=clang CXX=clang++" # x86_64 Linux, No wallet - stage: test env: >- diff --git a/.travis/test_04_install.sh b/.travis/test_04_install.sh index 75de71c583..03a61ea9f8 100755 --- a/.travis/test_04_install.sh +++ b/.travis/test_04_install.sh @@ -7,9 +7,11 @@ export LC_ALL=C.UTF-8 travis_retry docker pull "$DOCKER_NAME_TAG" +export ASAN_OPTIONS="" +export LSAN_OPTIONS="suppressions=${TRAVIS_BUILD_DIR}/test/sanitizer_suppressions/lsan" export TSAN_OPTIONS="suppressions=${TRAVIS_BUILD_DIR}/test/sanitizer_suppressions/tsan" export UBSAN_OPTIONS="suppressions=${TRAVIS_BUILD_DIR}/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1" -env | grep -E '^(CCACHE_|WINEDEBUG|LC_ALL|BOOST_TEST_RANDOM|CONFIG_SHELL|(TSAN|UBSAN)_OPTIONS)' | tee /tmp/env +env | grep -E '^(BITCOIN_CONFIG|CCACHE_|WINEDEBUG|LC_ALL|BOOST_TEST_RANDOM|CONFIG_SHELL|(ASAN|LSAN|TSAN|UBSAN)_OPTIONS)' | tee /tmp/env if [[ $HOST = *-mingw32 ]]; then DOCKER_ADMIN="--cap-add SYS_ADMIN" fi diff --git a/.travis/test_06_script.sh b/.travis/test_06_script.sh index 62d58ecf4d..506d2b518c 100755 --- a/.travis/test_06_script.sh +++ b/.travis/test_06_script.sh @@ -56,6 +56,6 @@ fi if [ "$RUN_FUNCTIONAL_TESTS" = "true" ]; then BEGIN_FOLD functional-tests - DOCKER_EXEC test/functional/test_runner.py --ci --combinedlogslen=4000 --coverage --quiet --failfast ${extended} + DOCKER_EXEC test/functional/test_runner.py --ci --combinedlogslen=4000 --coverage --quiet --failfast ${extended} ${FUNCTIONAL_TESTS_CONFIG} END_FOLD fi diff --git a/doc/release-notes-14477.md b/doc/release-notes-14477.md new file mode 100644 index 0000000000..bb8c0a623e --- /dev/null +++ b/doc/release-notes-14477.md @@ -0,0 +1,5 @@ +Miscellaneous RPC changes +------------ + +- `getaddressinfo` now reports `solvable`, a boolean indicating whether all information necessary for signing is present in the wallet (ignoring private keys). +- `getaddressinfo`, `listunspent`, and `scantxoutset` have a new output field `desc`, an output descriptor that encapsulates all signing information and key paths for the address (only available when `solvable` is true for `getaddressinfo` and `listunspent`). diff --git a/src/addrman.cpp b/src/addrman.cpp index 093b263ab3..44328c3056 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -11,22 +11,22 @@ int CAddrInfo::GetTriedBucket(const uint256& nKey) const { - uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetHash().GetCheapHash(); - uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)).GetHash().GetCheapHash(); + uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetCheapHash(); + uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)).GetCheapHash(); return hash2 % ADDRMAN_TRIED_BUCKET_COUNT; } int CAddrInfo::GetNewBucket(const uint256& nKey, const CNetAddr& src) const { std::vector<unsigned char> vchSourceGroupKey = src.GetGroup(); - uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << vchSourceGroupKey).GetHash().GetCheapHash(); - uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)).GetHash().GetCheapHash(); + uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << vchSourceGroupKey).GetCheapHash(); + uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)).GetCheapHash(); return hash2 % ADDRMAN_NEW_BUCKET_COUNT; } int CAddrInfo::GetBucketPosition(const uint256 &nKey, bool fNew, int nBucket) const { - uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << (fNew ? 'N' : 'K') << nBucket << GetKey()).GetHash().GetCheapHash(); + uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << (fNew ? 'N' : 'K') << nBucket << GetKey()).GetCheapHash(); return hash1 % ADDRMAN_BUCKET_SIZE; } diff --git a/src/bench/coin_selection.cpp b/src/bench/coin_selection.cpp index 8552ed34fd..74641191a1 100644 --- a/src/bench/coin_selection.cpp +++ b/src/bench/coin_selection.cpp @@ -4,25 +4,19 @@ #include <bench/bench.h> #include <interfaces/chain.h> -#include <wallet/wallet.h> #include <wallet/coinselection.h> +#include <wallet/wallet.h> #include <set> -static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<OutputGroup>& groups) +static void addCoin(const CAmount& nValue, const CWallet& wallet, std::vector<std::unique_ptr<CWalletTx>>& wtxs) { - int nInput = 0; - static int nextLockTime = 0; CMutableTransaction tx; tx.nLockTime = nextLockTime++; // so all transactions get different hashes - tx.vout.resize(nInput + 1); - tx.vout[nInput].nValue = nValue; - CWalletTx* wtx = new CWalletTx(&wallet, MakeTransactionRef(std::move(tx))); - - int nAge = 6 * 24; - COutput output(wtx, nInput, nAge, true /* spendable */, true /* solvable */, true /* safe */); - groups.emplace_back(output.GetInputCoin(), 6, false, 0, 0); + tx.vout.resize(1); + tx.vout[0].nValue = nValue; + wtxs.push_back(MakeUnique<CWalletTx>(&wallet, MakeTransactionRef(std::move(tx)))); } // Simple benchmark for wallet coin selection. Note that it maybe be necessary @@ -36,14 +30,21 @@ static void CoinSelection(benchmark::State& state) { auto chain = interfaces::MakeChain(); const CWallet wallet(*chain, WalletLocation(), WalletDatabase::CreateDummy()); + std::vector<std::unique_ptr<CWalletTx>> wtxs; LOCK(wallet.cs_wallet); // Add coins. - std::vector<OutputGroup> groups; for (int i = 0; i < 1000; ++i) { - addCoin(1000 * COIN, wallet, groups); + addCoin(1000 * COIN, wallet, wtxs); + } + addCoin(3 * COIN, wallet, wtxs); + + // Create groups + std::vector<OutputGroup> groups; + for (const auto& wtx : wtxs) { + COutput output(wtx.get(), 0 /* iIn */, 6 * 24 /* nDepthIn */, true /* spendable */, true /* solvable */, true /* safe */); + groups.emplace_back(output.GetInputCoin(), 6, false, 0, 0); } - addCoin(3 * COIN, wallet, groups); const CoinEligibilityFilter filter_standard(1, 6, 0); const CoinSelectionParams coin_selection_params(true, 34, 148, CFeeRate(0), 0); diff --git a/src/hash.h b/src/hash.h index 6acab0b161..c295568a3e 100644 --- a/src/hash.h +++ b/src/hash.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_HASH_H #define BITCOIN_HASH_H +#include <crypto/common.h> #include <crypto/ripemd160.h> #include <crypto/sha256.h> #include <prevector.h> @@ -138,6 +139,15 @@ public: return result; } + /** + * Returns the first 64 bits from the resulting hash. + */ + inline uint64_t GetCheapHash() { + unsigned char result[CHash256::OUTPUT_SIZE]; + ctx.Finalize(result); + return ReadLE64(result); + } + template<typename T> CHashWriter& operator<<(const T& obj) { // Serialize to this stream diff --git a/src/net.cpp b/src/net.cpp index 65a308780a..b85a8c2c1d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -82,8 +82,8 @@ bool fDiscover = true; bool fListen = true; bool fRelayTxes = true; CCriticalSection cs_mapLocalHost; -std::map<CNetAddr, LocalServiceInfo> mapLocalHost; -static bool vfLimited[NET_MAX] = {}; +std::map<CNetAddr, LocalServiceInfo> mapLocalHost GUARDED_BY(cs_mapLocalHost); +static bool vfLimited[NET_MAX] GUARDED_BY(cs_mapLocalHost) = {}; std::string strSubVersion; limitedmap<uint256, int64_t> mapAlreadyAskedFor(MAX_INV_SZ); @@ -715,7 +715,10 @@ void CNode::copyStats(CNodeStats &stats) X(nRecvBytes); } X(fWhitelisted); - X(minFeeFilter); + { + LOCK(cs_feeFilter); + X(minFeeFilter); + } // It is common for nodes with good ping times to suddenly become lagged, // due to a new block arriving or other large transfer. @@ -874,16 +877,7 @@ const uint256& CNetMessage::GetMessageHash() const return data_hash; } - - - - - - - - -// requires LOCK(cs_vSend) -size_t CConnman::SocketSendData(CNode *pnode) const +size_t CConnman::SocketSendData(CNode *pnode) const EXCLUSIVE_LOCKS_REQUIRED(pnode->cs_vSend) { auto it = pnode->vSendMsg.begin(); size_t nSentSize = 0; @@ -1011,6 +1005,7 @@ bool CConnman::AttemptToEvictConnection() continue; if (node->fDisconnect) continue; + LOCK(node->cs_filter); NodeEvictionCandidate candidate = {node->GetId(), node->nTimeConnected, node->nMinPingUsecTime, node->nLastBlockTime, node->nLastTXTime, HasAllDesirableServiceFlags(node->nServices), @@ -400,12 +400,12 @@ private: std::vector<ListenSocket> vhListenSocket; std::atomic<bool> fNetworkActive; - banmap_t setBanned; + banmap_t setBanned GUARDED_BY(cs_setBanned); CCriticalSection cs_setBanned; - bool setBannedIsDirty; + bool setBannedIsDirty GUARDED_BY(cs_setBanned); bool fAddressesInitialized; CAddrMan addrman; - std::deque<std::string> vOneShots; + std::deque<std::string> vOneShots GUARDED_BY(cs_vOneShots); CCriticalSection cs_vOneShots; std::vector<std::string> vAddedNodes GUARDED_BY(cs_vAddedNodes); CCriticalSection cs_vAddedNodes; @@ -540,7 +540,7 @@ struct LocalServiceInfo { }; extern CCriticalSection cs_mapLocalHost; -extern std::map<CNetAddr, LocalServiceInfo> mapLocalHost; +extern std::map<CNetAddr, LocalServiceInfo> mapLocalHost GUARDED_BY(cs_mapLocalHost); typedef std::map<std::string, uint64_t> mapMsgCmdSize; //command, total bytes class CNodeStats @@ -630,23 +630,23 @@ class CNode public: // socket std::atomic<ServiceFlags> nServices; - SOCKET hSocket; + SOCKET hSocket GUARDED_BY(cs_hSocket); size_t nSendSize; // total size of all vSendMsg entries size_t nSendOffset; // offset inside the first vSendMsg already sent - uint64_t nSendBytes; - std::deque<std::vector<unsigned char>> vSendMsg; + uint64_t nSendBytes GUARDED_BY(cs_vSend); + std::deque<std::vector<unsigned char>> vSendMsg GUARDED_BY(cs_vSend); CCriticalSection cs_vSend; CCriticalSection cs_hSocket; CCriticalSection cs_vRecv; CCriticalSection cs_vProcessMsg; - std::list<CNetMessage> vProcessMsg; + std::list<CNetMessage> vProcessMsg GUARDED_BY(cs_vProcessMsg); size_t nProcessQueueSize; CCriticalSection cs_sendProcessing; std::deque<CInv> vRecvGetData; - uint64_t nRecvBytes; + uint64_t nRecvBytes GUARDED_BY(cs_vRecv); std::atomic<int> nRecvVersion; std::atomic<int64_t> nLastSend; @@ -662,7 +662,7 @@ public: // to be printed out, displayed to humans in various forms and so on. So we sanitize it and // store the sanitized version in cleanSubVer. The original should be used when dealing with // the network or wire types and the cleaned string used when displayed or logged. - std::string strSubVer, cleanSubVer; + std::string strSubVer GUARDED_BY(cs_SubVer), cleanSubVer GUARDED_BY(cs_SubVer); CCriticalSection cs_SubVer; // used for both cleanSubVer and strSubVer bool fWhitelisted; // This peer can bypass DoS banning. bool fFeeler; // If true this node is being used as a short lived feeler. @@ -677,11 +677,11 @@ public: // a) it allows us to not relay tx invs before receiving the peer's version message // b) the peer may tell us in its version message that we should not relay tx invs // unless it loads a bloom filter. - bool fRelayTxes; //protected by cs_filter + bool fRelayTxes GUARDED_BY(cs_filter); bool fSentAddr; CSemaphoreGrant grantOutbound; - CCriticalSection cs_filter; - std::unique_ptr<CBloomFilter> pfilter; + mutable CCriticalSection cs_filter; + std::unique_ptr<CBloomFilter> pfilter PT_GUARDED_BY(cs_filter); std::atomic<int> nRefCount; const uint64_t nKeyedNetGroup; @@ -690,7 +690,7 @@ public: protected: mapMsgCmdSize mapSendBytesPerMsgCmd; - mapMsgCmdSize mapRecvBytesPerMsgCmd; + mapMsgCmdSize mapRecvBytesPerMsgCmd GUARDED_BY(cs_vRecv); public: uint256 hashContinue; @@ -701,27 +701,26 @@ public: CRollingBloomFilter addrKnown; bool fGetAddr; std::set<uint256> setKnown; - int64_t nNextAddrSend; - int64_t nNextLocalAddrSend; + int64_t nNextAddrSend GUARDED_BY(cs_sendProcessing); + int64_t nNextLocalAddrSend GUARDED_BY(cs_sendProcessing); // inventory based relay - CRollingBloomFilter filterInventoryKnown; + CRollingBloomFilter filterInventoryKnown GUARDED_BY(cs_inventory); // Set of transaction ids we still have to announce. // They are sorted by the mempool before relay, so the order is not important. std::set<uint256> setInventoryTxToSend; // List of block ids we still have announce. // There is no final sorting before sending, as they are always sent immediately // and in the order requested. - std::vector<uint256> vInventoryBlockToSend; + std::vector<uint256> vInventoryBlockToSend GUARDED_BY(cs_inventory); CCriticalSection cs_inventory; std::set<uint256> setAskFor; std::multimap<int64_t, CInv> mapAskFor; int64_t nNextInvSend; // Used for headers announcements - unfiltered blocks to relay - // Also protected by cs_inventory - std::vector<uint256> vBlockHashesToAnnounce; - // Used for BIP35 mempool sending, also protected by cs_inventory - bool fSendMempool; + std::vector<uint256> vBlockHashesToAnnounce GUARDED_BY(cs_inventory); + // Used for BIP35 mempool sending + bool fSendMempool GUARDED_BY(cs_inventory); // Last time a "MEMPOOL" request was serviced. std::atomic<int64_t> timeLastMempoolReq; @@ -742,7 +741,7 @@ public: // Whether a ping is requested. std::atomic<bool> fPingQueued; // Minimum fee rate with which to filter inv's to this node - CAmount minFeeFilter; + CAmount minFeeFilter GUARDED_BY(cs_feeFilter); CCriticalSection cs_feeFilter; CAmount lastSentFeeFilter; int64_t nextSendTimeFeeFilter; @@ -762,10 +761,10 @@ private: std::list<CNetMessage> vRecvMsg; // Used only by SocketHandler thread mutable CCriticalSection cs_addrName; - std::string addrName; + std::string addrName GUARDED_BY(cs_addrName); // Our address, as reported by the peer - CService addrLocal; + CService addrLocal GUARDED_BY(cs_addrLocal); mutable CCriticalSection cs_addrLocal; public: diff --git a/src/netbase.cpp b/src/netbase.cpp index 6a750d5141..1c043fc981 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -26,9 +26,9 @@ #endif // Settings -static proxyType proxyInfo[NET_MAX]; -static proxyType nameProxy; static CCriticalSection cs_proxyInfos; +static proxyType proxyInfo[NET_MAX] GUARDED_BY(cs_proxyInfos); +static proxyType nameProxy GUARDED_BY(cs_proxyInfos); int nConnectTimeout = DEFAULT_CONNECT_TIMEOUT; bool fNameLookup = DEFAULT_NAME_LOOKUP; diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index ef82351551..ed705d6ba8 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -1089,10 +1089,10 @@ bool BitcoinGUI::handlePaymentRequest(const SendCoinsRecipient& recipient) return false; } -void BitcoinGUI::setHDStatus(int hdEnabled) +void BitcoinGUI::setHDStatus(bool privkeyDisabled, int hdEnabled) { - labelWalletHDStatusIcon->setPixmap(platformStyle->SingleColorIcon(hdEnabled ? ":/icons/hd_enabled" : ":/icons/hd_disabled").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); - labelWalletHDStatusIcon->setToolTip(hdEnabled ? tr("HD key generation is <b>enabled</b>") : tr("HD key generation is <b>disabled</b>")); + labelWalletHDStatusIcon->setPixmap(platformStyle->SingleColorIcon(privkeyDisabled ? ":/icons/eye" : hdEnabled ? ":/icons/hd_enabled" : ":/icons/hd_disabled").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); + labelWalletHDStatusIcon->setToolTip(privkeyDisabled ? tr("Private key <b>disabled</b>") : hdEnabled ? tr("HD key generation is <b>enabled</b>") : tr("HD key generation is <b>disabled</b>")); // eventually disable the QLabel to set its opacity to 50% labelWalletHDStatusIcon->setEnabled(hdEnabled); @@ -1138,7 +1138,7 @@ void BitcoinGUI::updateWalletStatus() } WalletModel * const walletModel = walletView->getWalletModel(); setEncryptionStatus(walletModel->getEncryptionStatus()); - setHDStatus(walletModel->wallet().hdEnabled()); + setHDStatus(walletModel->privateKeysDisabled(), walletModel->wallet().hdEnabled()); } #endif // ENABLE_WALLET diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index e8b857c17c..aeff5dae30 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -223,7 +223,7 @@ private: @param[in] hdEnabled current hd enabled status @see WalletModel::EncryptionStatus */ - void setHDStatus(int hdEnabled); + void setHDStatus(bool privkeyDisabled, int hdEnabled); public Q_SLOTS: bool handlePaymentRequest(const SendCoinsRecipient& recipient); diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index ea970c0bc9..77f8bcf901 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -129,8 +129,6 @@ CoinControlDialog::CoinControlDialog(const PlatformStyle *_platformStyle, QWidge ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320); ui->treeWidget->setColumnWidth(COLUMN_DATE, 130); ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 110); - ui->treeWidget->setColumnHidden(COLUMN_TXHASH, true); // store transaction hash in this column, but don't show it - ui->treeWidget->setColumnHidden(COLUMN_VOUT_INDEX, true); // store vout index in this column, but don't show it // default view is sorted by amount desc sortView(COLUMN_AMOUNT, Qt::DescendingOrder); @@ -203,10 +201,10 @@ void CoinControlDialog::showMenu(const QPoint &point) contextMenuItem = item; // disable some items (like Copy Transaction ID, lock, unlock) for tree roots in context menu - if (item->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means it is a child node, so it is not a parent node in tree mode) + if (item->data(COLUMN_ADDRESS, TxHashRole).toString().length() == 64) // transaction hash is 64 characters (this means it is a child node, so it is not a parent node in tree mode) { copyTransactionHashAction->setEnabled(true); - if (model->wallet().isLockedCoin(COutPoint(uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()))) + if (model->wallet().isLockedCoin(COutPoint(uint256S(item->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), item->data(COLUMN_ADDRESS, VOutRole).toUInt()))) { lockAction->setEnabled(false); unlockAction->setEnabled(true); @@ -256,7 +254,7 @@ void CoinControlDialog::copyAddress() // context menu action: copy transaction id void CoinControlDialog::copyTransactionHash() { - GUIUtil::setClipboard(contextMenuItem->text(COLUMN_TXHASH)); + GUIUtil::setClipboard(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString()); } // context menu action: lock coin @@ -265,7 +263,7 @@ void CoinControlDialog::lockCoin() if (contextMenuItem->checkState(COLUMN_CHECKBOX) == Qt::Checked) contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); - COutPoint outpt(uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt()); + COutPoint outpt(uint256S(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), contextMenuItem->data(COLUMN_ADDRESS, VOutRole).toUInt()); model->wallet().lockCoin(outpt); contextMenuItem->setDisabled(true); contextMenuItem->setIcon(COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed")); @@ -275,7 +273,7 @@ void CoinControlDialog::lockCoin() // context menu action: unlock coin void CoinControlDialog::unlockCoin() { - COutPoint outpt(uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt()); + COutPoint outpt(uint256S(contextMenuItem->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), contextMenuItem->data(COLUMN_ADDRESS, VOutRole).toUInt()); model->wallet().unlockCoin(outpt); contextMenuItem->setDisabled(false); contextMenuItem->setIcon(COLUMN_CHECKBOX, QIcon()); @@ -371,9 +369,9 @@ void CoinControlDialog::radioListMode(bool checked) // checkbox clicked by user void CoinControlDialog::viewItemChanged(QTreeWidgetItem* item, int column) { - if (column == COLUMN_CHECKBOX && item->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means it is a child node, so it is not a parent node in tree mode) + if (column == COLUMN_CHECKBOX && item->data(COLUMN_ADDRESS, TxHashRole).toString().length() == 64) // transaction hash is 64 characters (this means it is a child node, so it is not a parent node in tree mode) { - COutPoint outpt(uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()); + COutPoint outpt(uint256S(item->data(COLUMN_ADDRESS, TxHashRole).toString().toStdString()), item->data(COLUMN_ADDRESS, VOutRole).toUInt()); if (item->checkState(COLUMN_CHECKBOX) == Qt::Unchecked) coinControl()->UnSelect(outpt); @@ -693,10 +691,10 @@ void CoinControlDialog::updateView() itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.depth_in_main_chain)); // transaction hash - itemOutput->setText(COLUMN_TXHASH, QString::fromStdString(output.hash.GetHex())); + itemOutput->setData(COLUMN_ADDRESS, TxHashRole, QString::fromStdString(output.hash.GetHex())); // vout index - itemOutput->setText(COLUMN_VOUT_INDEX, QString::number(output.n)); + itemOutput->setData(COLUMN_ADDRESS, VOutRole, output.n); // disable locked coins if (model->wallet().isLockedCoin(output)) diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index 9c3f6a46a2..8f15ae4b20 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -80,9 +80,14 @@ private: COLUMN_ADDRESS, COLUMN_DATE, COLUMN_CONFIRMATIONS, - COLUMN_TXHASH, - COLUMN_VOUT_INDEX, }; + + enum + { + TxHashRole = Qt::UserRole, + VOutRole + }; + friend class CCoinControlWidgetItem; private Q_SLOTS: diff --git a/src/qt/forms/coincontroldialog.ui b/src/qt/forms/coincontroldialog.ui index d1237ad283..bd7f3c5f56 100644 --- a/src/qt/forms/coincontroldialog.ui +++ b/src/qt/forms/coincontroldialog.ui @@ -402,7 +402,7 @@ <bool>false</bool> </property> <property name="columnCount"> - <number>10</number> + <number>6</number> </property> <attribute name="headerShowSortIndicator" stdset="0"> <bool>true</bool> diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 1db9609979..bec79335e7 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -161,15 +161,21 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances) { int unit = walletModel->getOptionsModel()->getDisplayUnit(); m_balances = balances; - ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways)); - ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways)); - ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways)); - ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); - + if (walletModel->privateKeysDisabled()) { + ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + } else { + ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balances.balance, false, BitcoinUnits::separatorAlways)); + ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_balance, false, BitcoinUnits::separatorAlways)); + ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_balance, false, BitcoinUnits::separatorAlways)); + ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, balances.unconfirmed_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, false, BitcoinUnits::separatorAlways)); + } // only show immature (newly mined) balance if it's non-zero, so as not to complicate things // for the non-mining users bool showImmature = balances.immature_balance != 0; @@ -178,7 +184,7 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances) // for symmetry reasons also show immature label when the watch-only one is shown ui->labelImmature->setVisible(showImmature || showWatchOnlyImmature); ui->labelImmatureText->setVisible(showImmature || showWatchOnlyImmature); - ui->labelWatchImmature->setVisible(showWatchOnlyImmature); // show watch-only immature balance + ui->labelWatchImmature->setVisible(!walletModel->privateKeysDisabled() && showWatchOnlyImmature); // show watch-only immature balance } // show/hide watch-only labels @@ -231,8 +237,10 @@ void OverviewPage::setWalletModel(WalletModel *model) connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &OverviewPage::updateDisplayUnit); - updateWatchOnlyLabels(wallet.haveWatchOnly()); - connect(model, &WalletModel::notifyWatchonlyChanged, this, &OverviewPage::updateWatchOnlyLabels); + updateWatchOnlyLabels(wallet.haveWatchOnly() && !model->privateKeysDisabled()); + connect(model, &WalletModel::notifyWatchonlyChanged, [this](bool showWatchOnly) { + updateWatchOnlyLabels(showWatchOnly && !walletModel->privateKeysDisabled()); + }); } // update the display unit, to not use the default ("BTC") diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index e3d9357358..403e3e397c 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2187,6 +2187,7 @@ UniValue scantxoutset(const JSONRPCRequest& request) " \"txid\" : \"transactionid\", (string) The transaction id\n" " \"vout\": n, (numeric) the vout value\n" " \"scriptPubKey\" : \"script\", (string) the script key\n" + " \"desc\" : \"descriptor\", (string) A specialized descriptor for the matched scriptPubKey\n" " \"amount\" : x.xxx, (numeric) The total amount in " + CURRENCY_UNIT + " of the unspent output\n" " \"height\" : n, (numeric) Height of the unspent transaction output\n" " }\n" @@ -2221,6 +2222,7 @@ UniValue scantxoutset(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\""); } std::set<CScript> needles; + std::map<CScript, std::string> descriptors; CAmount total_in = 0; // loop through the scan objects @@ -2253,7 +2255,11 @@ UniValue scantxoutset(const JSONRPCRequest& request) if (!desc->Expand(i, provider, scripts, provider)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str)); } - needles.insert(scripts.begin(), scripts.end()); + for (const auto& script : scripts) { + std::string inferred = InferDescriptor(script, provider)->ToString(); + needles.emplace(script); + descriptors.emplace(std::move(script), std::move(inferred)); + } } } @@ -2286,6 +2292,7 @@ UniValue scantxoutset(const JSONRPCRequest& request) unspent.pushKV("txid", outpoint.hash.GetHex()); unspent.pushKV("vout", (int32_t)outpoint.n); unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(), txo.scriptPubKey.end())); + unspent.pushKV("desc", descriptors[txo.scriptPubKey]); unspent.pushKV("amount", ValueFromAmount(txo.nValue)); unspent.pushKV("height", (int32_t)coin.nHeight); diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index a3ed4e86d9..41f3ac6b4a 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -939,7 +939,7 @@ static UniValue signrawtransactionwithkey(const JSONRPCRequest& request) "this transaction depends on but may not yet be in the block chain.\n", { {"hexstring", RPCArg::Type::STR, false}, - {"privkyes", RPCArg::Type::ARR, + {"privkeys", RPCArg::Type::ARR, { {"privatekey", RPCArg::Type::STR_HEX, false}, }, @@ -1142,7 +1142,7 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request) throw std::runtime_error( // clang-format off "testmempoolaccept [\"rawtxs\"] ( allowhighfees )\n" - "\nReturns if raw transaction (serialized, hex-encoded) would be accepted by mempool.\n" + "\nReturns result of mempool acceptance tests indicating if raw transaction (serialized, hex-encoded) would be accepted by mempool.\n" "\nThis checks if the transaction violates the consensus or policy rules.\n" "\nSee sendrawtransaction call.\n" "\nArguments:\n" diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index d343972c40..ca80d3451f 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -211,6 +211,7 @@ public: AddressDescriptor(CTxDestination destination) : m_destination(std::move(destination)) {} bool IsRange() const override { return false; } + bool IsSolvable() const override { return false; } std::string ToString() const override { return "addr(" + EncodeDestination(m_destination) + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; } bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override @@ -229,6 +230,7 @@ public: RawDescriptor(CScript script) : m_script(std::move(script)) {} bool IsRange() const override { return false; } + bool IsSolvable() const override { return false; } std::string ToString() const override { return "raw(" + HexStr(m_script.begin(), m_script.end()) + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; } bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override @@ -249,6 +251,7 @@ public: SingleKeyDescriptor(std::unique_ptr<PubkeyProvider> prov, const std::function<CScript(const CPubKey&)>& fn, const std::string& name) : m_script_fn(fn), m_fn_name(name), m_provider(std::move(prov)) {} bool IsRange() const override { return m_provider->IsRange(); } + bool IsSolvable() const override { return true; } std::string ToString() const override { return m_fn_name + "(" + m_provider->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { @@ -290,6 +293,8 @@ public: return false; } + bool IsSolvable() const override { return true; } + std::string ToString() const override { std::string ret = strprintf("multi(%i", m_threshold); @@ -343,6 +348,7 @@ public: ConvertorDescriptor(std::unique_ptr<Descriptor> descriptor, const std::function<CScript(const CScript&)>& fn, const std::string& name) : m_convert_fn(fn), m_fn_name(name), m_descriptor(std::move(descriptor)) {} bool IsRange() const override { return m_descriptor->IsRange(); } + bool IsSolvable() const override { return m_descriptor->IsSolvable(); } std::string ToString() const override { return m_fn_name + "(" + m_descriptor->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { @@ -377,6 +383,7 @@ public: ComboDescriptor(std::unique_ptr<PubkeyProvider> provider) : m_provider(std::move(provider)) {} bool IsRange() const override { return m_provider->IsRange(); } + bool IsSolvable() const override { return true; } std::string ToString() const override { return "combo(" + m_provider->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { @@ -625,6 +632,80 @@ std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext return nullptr; } +std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider) +{ + std::unique_ptr<PubkeyProvider> key_provider = MakeUnique<ConstPubkeyProvider>(pubkey); + KeyOriginInfo info; + if (provider.GetKeyOrigin(pubkey.GetID(), info)) { + return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(key_provider)); + } + return key_provider; +} + +std::unique_ptr<Descriptor> InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider) +{ + std::vector<std::vector<unsigned char>> data; + txnouttype txntype = Solver(script, data); + + if (txntype == TX_PUBKEY) { + CPubKey pubkey(data[0].begin(), data[0].end()); + if (pubkey.IsValid()) { + return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2PKGetScript, "pk"); + } + } + if (txntype == TX_PUBKEYHASH) { + uint160 hash(data[0]); + CKeyID keyid(hash); + CPubKey pubkey; + if (provider.GetPubKey(keyid, pubkey)) { + return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2PKHGetScript, "pkh"); + } + } + if (txntype == TX_WITNESS_V0_KEYHASH && ctx != ParseScriptContext::P2WSH) { + uint160 hash(data[0]); + CKeyID keyid(hash); + CPubKey pubkey; + if (provider.GetPubKey(keyid, pubkey)) { + return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2WPKHGetScript, "wpkh"); + } + } + if (txntype == TX_MULTISIG) { + std::vector<std::unique_ptr<PubkeyProvider>> providers; + for (size_t i = 1; i + 1 < data.size(); ++i) { + CPubKey pubkey(data[i].begin(), data[i].end()); + providers.push_back(InferPubkey(pubkey, ctx, provider)); + } + return MakeUnique<MultisigDescriptor>((int)data[0][0], std::move(providers)); + } + if (txntype == TX_SCRIPTHASH && ctx == ParseScriptContext::TOP) { + uint160 hash(data[0]); + CScriptID scriptid(hash); + CScript subscript; + if (provider.GetCScript(scriptid, subscript)) { + auto sub = InferScript(subscript, ParseScriptContext::P2SH, provider); + if (sub) return MakeUnique<ConvertorDescriptor>(std::move(sub), ConvertP2SH, "sh"); + } + } + if (txntype == TX_WITNESS_V0_SCRIPTHASH && ctx != ParseScriptContext::P2WSH) { + CScriptID scriptid; + CRIPEMD160().Write(data[0].data(), data[0].size()).Finalize(scriptid.begin()); + CScript subscript; + if (provider.GetCScript(scriptid, subscript)) { + auto sub = InferScript(subscript, ParseScriptContext::P2WSH, provider); + if (sub) return MakeUnique<ConvertorDescriptor>(std::move(sub), ConvertP2WSH, "wsh"); + } + } + + CTxDestination dest; + if (ExtractDestination(script, dest)) { + if (GetScriptForDestination(dest) == script) { + return MakeUnique<AddressDescriptor>(std::move(dest)); + } + } + + return MakeUnique<RawDescriptor>(script); +} + } // namespace std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out) @@ -634,3 +715,8 @@ std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProv if (sp.size() == 0 && ret) return ret; return nullptr; } + +std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider) +{ + return InferScript(script, ParseScriptContext::TOP, provider); +} diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 87e07369c7..0111972f85 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -32,6 +32,10 @@ struct Descriptor { /** Whether the expansion of this descriptor depends on the position. */ virtual bool IsRange() const = 0; + /** Whether this descriptor has all information about signing ignoring lack of private keys. + * This is true for all descriptors except ones that use `raw` or `addr` constructions. */ + virtual bool IsSolvable() const = 0; + /** Convert the descriptor back to a string, undoing parsing. */ virtual std::string ToString() const = 0; @@ -51,5 +55,20 @@ struct Descriptor { /** Parse a descriptor string. Included private keys are put in out. Returns nullptr if parsing fails. */ std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out); -#endif // BITCOIN_SCRIPT_DESCRIPTOR_H +/** Find a descriptor for the specified script, using information from provider where possible. + * + * A non-ranged descriptor which only generates the specified script will be returned in all + * circumstances. + * + * For public keys with key origin information, this information will be preserved in the returned + * descriptor. + * + * - If all information for solving `script` is present in `provider`, a descriptor will be returned + * which is `IsSolvable()` and encapsulates said information. + * - Failing that, if `script` corresponds to a known address type, an "addr()" descriptor will be + * returned (which is not `IsSolvable()`). + * - Failing that, a "raw()" descriptor is returned. + */ +std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider); +#endif // BITCOIN_SCRIPT_DESCRIPTOR_H diff --git a/src/script/sign.h b/src/script/sign.h index a478f49789..20c7203b26 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -24,6 +24,11 @@ struct KeyOriginInfo { unsigned char fingerprint[4]; std::vector<uint32_t> path; + + friend bool operator==(const KeyOriginInfo& a, const KeyOriginInfo& b) + { + return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path; + } }; /** An interface to be implemented by keystores that support signing. */ diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index 8c2873d916..55fe19cebe 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -34,7 +34,7 @@ public: int RandomInt(int nMax) override { - state = (CHashWriter(SER_GETHASH, 0) << state).GetHash().GetCheapHash(); + state = (CHashWriter(SER_GETHASH, 0) << state).GetCheapHash(); return (unsigned int)(state % nMax); } diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 57e4b067c0..0e98f5a826 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -62,7 +62,7 @@ void Check(const std::string& prv, const std::string& pub, int flags, const std: // Check that both versions serialize back to the public version. std::string pub1 = parse_priv->ToString(); - std::string pub2 = parse_priv->ToString(); + std::string pub2 = parse_pub->ToString(); BOOST_CHECK_EQUAL(pub, pub1); BOOST_CHECK_EQUAL(pub, pub2); @@ -102,7 +102,19 @@ void Check(const std::string& prv, const std::string& pub, int flags, const std: spend.vout.resize(1); BOOST_CHECK_MESSAGE(SignSignature(Merge(keys_priv, script_provider), spks[n], spend, 0, 1, SIGHASH_ALL), prv); } + + /* Infer a descriptor from the generated script, and verify its solvability and that it roundtrips. */ + auto inferred = InferDescriptor(spks[n], script_provider); + BOOST_CHECK_EQUAL(inferred->IsSolvable(), !(flags & UNSOLVABLE)); + std::vector<CScript> spks_inferred; + FlatSigningProvider provider_inferred; + BOOST_CHECK(inferred->Expand(0, provider_inferred, spks_inferred, provider_inferred)); + BOOST_CHECK_EQUAL(spks_inferred.size(), 1); + BOOST_CHECK(spks_inferred[0] == spks[n]); + BOOST_CHECK_EQUAL(IsSolvable(provider_inferred, spks_inferred[0]), !(flags & UNSOLVABLE)); + BOOST_CHECK(provider_inferred.origins == script_provider.origins); } + // Test whether the observed key path is present in the 'paths' variable (which contains expected, unobserved paths), // and then remove it from that set. for (const auto& origin : script_provider.origins) { diff --git a/src/uint256.h b/src/uint256.h index 26a3331d92..97e0cfa015 100644 --- a/src/uint256.h +++ b/src/uint256.h @@ -12,7 +12,6 @@ #include <stdint.h> #include <string> #include <vector> -#include <crypto/common.h> /** Template base class for fixed-sized opaque blobs. */ template<unsigned int BITS> @@ -123,16 +122,6 @@ class uint256 : public base_blob<256> { public: uint256() {} explicit uint256(const std::vector<unsigned char>& vch) : base_blob<256>(vch) {} - - /** A cheap hash function that just returns 64 bits from the result, it can be - * used when the contents are considered uniformly random. It is not appropriate - * when the value can easily be influenced from outside as e.g. a network adversary could - * provide values to trigger worst-case behavior. - */ - uint64_t GetCheapHash() const - { - return ReadLE64(data); - } }; /* uint256 from const char *. diff --git a/src/validation.cpp b/src/validation.cpp index 241957878e..512a3619ca 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1680,8 +1680,7 @@ void ThreadScriptCheck() { scriptcheckqueue.Thread(); } -// Protected by cs_main -VersionBitsCache versionbitscache; +VersionBitsCache versionbitscache GUARDED_BY(cs_main); int32_t ComputeBlockVersion(const CBlockIndex* pindexPrev, const Consensus::Params& params) { @@ -1722,8 +1721,7 @@ public: } }; -// Protected by cs_main -static ThresholdConditionCache warningcache[VERSIONBITS_NUM_BITS]; +static ThresholdConditionCache warningcache[VERSIONBITS_NUM_BITS] GUARDED_BY(cs_main); // 0.13.0 was shipped with a segwit deployment defined for testnet, but not for // mainnet. We no longer need to support disabling the segwit deployment @@ -3532,12 +3530,14 @@ bool ProcessNewBlock(const CChainParams& chainparams, const std::shared_ptr<cons CBlockIndex *pindex = nullptr; if (fNewBlock) *fNewBlock = false; CValidationState state; - // Ensure that CheckBlock() passes before calling AcceptBlock, as - // belt-and-suspenders. - bool ret = CheckBlock(*pblock, state, chainparams.GetConsensus()); + // CheckBlock() does not support multi-threaded block validation because CBlock::fChecked can cause data race. + // Therefore, the following critical section must include the CheckBlock() call as well. LOCK(cs_main); + // Ensure that CheckBlock() passes before calling AcceptBlock, as + // belt-and-suspenders. + bool ret = CheckBlock(*pblock, state, chainparams.GetConsensus()); if (ret) { // Store to disk ret = g_chainstate.AcceptBlock(pblock, state, chainparams, &pindex, fForceProcessing, nullptr, fNewBlock); diff --git a/src/validation.h b/src/validation.h index 3e98ebc866..b5548a9293 100644 --- a/src/validation.h +++ b/src/validation.h @@ -12,6 +12,7 @@ #include <amount.h> #include <coins.h> +#include <crypto/common.h> // for ReadLE64 #include <fs.h> #include <protocol.h> // For CMessageHeader::MessageStartChars #include <policy/feerate.h> @@ -138,7 +139,10 @@ static const int DEFAULT_STOPATHEIGHT = 0; struct BlockHasher { - size_t operator()(const uint256& hash) const { return hash.GetCheapHash(); } + // this used to call `GetCheapHash()` in uint256, which was later moved; the + // cheap hash function simply calls ReadLE64() however, so the end result is + // identical + size_t operator()(const uint256& hash) const { return ReadLE64(hash.begin()); } }; extern CScript COINBASE_FLAGS; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index ecc8fa2643..b4c21631ab 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -22,6 +22,7 @@ #include <rpc/rawtransaction.h> #include <rpc/server.h> #include <rpc/util.h> +#include <script/descriptor.h> #include <script/sign.h> #include <shutdown.h> #include <timedata.h> @@ -2009,21 +2010,13 @@ static UniValue walletpassphrase(const JSONRPCRequest& request) nSleepTime = MAX_SLEEP_TIME; } - if (strWalletPass.length() > 0) - { - if (!pwallet->Unlock(strWalletPass)) { - throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); - } + if (strWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } + + if (!pwallet->Unlock(strWalletPass)) { + throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); } - else - throw std::runtime_error( - RPCHelpMan{"walletpassphrase", - "Stores the wallet decryption key in memory for <timeout> seconds.", - { - {"passphrase", RPCArg::Type::STR, false}, - {"timeout", RPCArg::Type::NUM, false}, - }} - .ToString()); pwallet->TopUpKeyPool(); @@ -2089,15 +2082,9 @@ static UniValue walletpassphrasechange(const JSONRPCRequest& request) strNewWalletPass.reserve(100); strNewWalletPass = request.params[1].get_str().c_str(); - if (strOldWalletPass.length() < 1 || strNewWalletPass.length() < 1) - throw std::runtime_error( - RPCHelpMan{"walletpassphrasechange", - "Changes the wallet passphrase from <oldpassphrase> to <newpassphrase>.", - { - {"oldpassphrase", RPCArg::Type::STR, false}, - {"newpassphrase", RPCArg::Type::STR, false}, - }} - .ToString()); + if (strOldWalletPass.empty() || strNewWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } if (!pwallet->ChangeWalletPassphrase(strOldWalletPass, strNewWalletPass)) { throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "Error: The wallet passphrase entered was incorrect."); @@ -2200,14 +2187,9 @@ static UniValue encryptwallet(const JSONRPCRequest& request) strWalletPass.reserve(100); strWalletPass = request.params[0].get_str().c_str(); - if (strWalletPass.length() < 1) - throw std::runtime_error( - RPCHelpMan{"encryptwallet", - "Encrypts the wallet with <passphrase>.", - { - {"passphrase", RPCArg::Type::STR, false}, - }} - .ToString()); + if (strWalletPass.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "passphrase can not be empty"); + } if (!pwallet->EncryptWallet(strWalletPass)) { throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: Failed to encrypt the wallet."); @@ -2864,6 +2846,7 @@ static UniValue listunspent(const JSONRPCRequest& request) " \"redeemScript\" : n (string) The redeemScript if scriptPubKey is P2SH\n" " \"spendable\" : xxx, (bool) Whether we have the private keys to spend this output\n" " \"solvable\" : xxx, (bool) Whether we know how to spend this output, ignoring the lack of keys\n" + " \"desc\" : xxx, (string, only when solvable) A descriptor for spending this output\n" " \"safe\" : xxx (bool) Whether this output is considered safe to spend. Unconfirmed transactions\n" " from outside keys and unconfirmed replacement transactions are considered unsafe\n" " and are not eligible for spending by fundrawtransaction and sendtoaddress.\n" @@ -2982,6 +2965,10 @@ static UniValue listunspent(const JSONRPCRequest& request) entry.pushKV("confirmations", out.nDepth); entry.pushKV("spendable", out.fSpendable); entry.pushKV("solvable", out.fSolvable); + if (out.fSolvable) { + auto descriptor = InferDescriptor(scriptPubKey, *pwallet); + entry.pushKV("desc", descriptor->ToString()); + } entry.pushKV("safe", out.fSafe); results.push_back(entry); } @@ -3768,6 +3755,8 @@ UniValue getaddressinfo(const JSONRPCRequest& request) " \"ismine\" : true|false, (boolean) If the address is yours or not\n" " \"solvable\" : true|false, (boolean) If the address is solvable by the wallet\n" " \"iswatchonly\" : true|false, (boolean) If the address is watchonly\n" + " \"solvable\" : true|false, (boolean) Whether we know how to spend coins sent to this address, ignoring the possible lack of private keys\n" + " \"desc\" : \"desc\", (string, optional) A descriptor for spending coins sent to this address (only when solvable)\n" " \"isscript\" : true|false, (boolean) If the key is a script\n" " \"ischange\" : true|false, (boolean) If the address was used for change output\n" " \"iswitness\" : true|false, (boolean) If the address is a witness address\n" @@ -3821,6 +3810,11 @@ UniValue getaddressinfo(const JSONRPCRequest& request) isminetype mine = IsMine(*pwallet, dest); ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE)); + bool solvable = IsSolvable(*pwallet, scriptPubKey); + ret.pushKV("solvable", solvable); + if (solvable) { + ret.pushKV("desc", InferDescriptor(scriptPubKey, *pwallet)->ToString()); + } ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY)); ret.pushKV("solvable", IsSolvable(*pwallet, scriptPubKey)); UniValue detail = DescribeWalletAddress(pwallet, dest); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index c6aac8aad5..623c5c39a2 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -17,6 +17,7 @@ #include <validation.h> #include <wallet/coincontrol.h> #include <wallet/test/wallet_test_fixture.h> +#include <policy/policy.h> #include <boost/test/unit_test.hpp> #include <univalue.h> @@ -394,4 +395,47 @@ BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) BOOST_CHECK(!wallet->GetKeyFromPool(pubkey, false)); } +// Explicit calculation which is used to test the wallet constant +// We get the same virtual size due to rounding(weight/4) for both use_max_sig values +static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) +{ + // Generate ephemeral valid pubkey + CKey key; + key.MakeNewKey(true); + CPubKey pubkey = key.GetPubKey(); + + // Generate pubkey hash + uint160 key_hash(Hash160(pubkey.begin(), pubkey.end())); + + // Create inner-script to enter into keystore. Key hash can't be 0... + CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end()); + + // Create outer P2SH script for the output + uint160 script_id(Hash160(inner_script.begin(), inner_script.end())); + CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL; + + // Add inner-script to key store and key to watchonly + CBasicKeyStore keystore; + keystore.AddCScript(inner_script); + keystore.AddKeyPubKey(key, pubkey); + + // Fill in dummy signatures for fee calculation. + SignatureData sig_data; + + if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { + // We're hand-feeding it correct arguments; shouldn't happen + assert(false); + } + + CTxIn tx_in; + UpdateInput(tx_in, sig_data); + return (size_t)GetVirtualTransactionInputSize(tx_in); +} + +BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) +{ + BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE); + BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 360d0f177c..d7798e005f 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1530,8 +1530,6 @@ int CalculateMaximumSignedInputSize(const CTxOut& txout, const CWallet* wallet, CMutableTransaction txn; txn.vin.push_back(CTxIn(COutPoint())); if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) { - // This should never happen, because IsAllFromMe(ISMINE_SPENDABLE) - // implies that we can sign for every input. return -1; } return GetVirtualTransactionInputSize(txn.vin[0]); @@ -2755,7 +2753,14 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std if (pick_new_inputs) { nValueIn = 0; setCoins.clear(); - coin_selection_params.change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); + int change_spend_size = CalculateMaximumSignedInputSize(change_prototype_txout, this); + // If the wallet doesn't know how to sign change output, assume p2sh-p2wpkh + // as lower-bound to allow BnB to do it's thing + if (change_spend_size == -1) { + coin_selection_params.change_spend_size = DUMMY_NESTED_P2WPKH_INPUT_SIZE; + } else { + coin_selection_params.change_spend_size = (size_t)change_spend_size; + } coin_selection_params.effective_fee = nFeeRateNeeded; if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used)) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index f96798201f..4291163bea 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -85,6 +85,9 @@ static const bool DEFAULT_WALLET_RBF = false; static const bool DEFAULT_WALLETBROADCAST = true; static const bool DEFAULT_DISABLE_WALLET = false; +//! Pre-calculated constants for input size estimation in *virtual size* +static constexpr size_t DUMMY_NESTED_P2WPKH_INPUT_SIZE = 91; + class CBlockIndex; class CCoinControl; class COutput; diff --git a/test/functional/combine_logs.py b/test/functional/combine_logs.py index 3230d5cb6b..5bb3b5c094 100755 --- a/test/functional/combine_logs.py +++ b/test/functional/combine_logs.py @@ -2,7 +2,9 @@ """Combine logs from multiple bitcoin nodes as well as the test_framework log. This streams the combined log output to stdout. Use combine_logs.py > outputfile -to write to an outputfile.""" +to write to an outputfile. + +If no argument is provided, the most recent test directory will be used.""" import argparse from collections import defaultdict, namedtuple @@ -11,6 +13,13 @@ import itertools import os import re import sys +import tempfile + +# N.B.: don't import any local modules here - this script must remain executable +# without the parent module installed. + +# Should match same symbol in `test_framework.test_framework`. +TMPDIR_PREFIX = "bitcoin_func_test_" # Matches on the date format at the start of the log event TIMESTAMP_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{6})?Z") @@ -19,22 +28,30 @@ LogEvent = namedtuple('LogEvent', ['timestamp', 'source', 'event']) def main(): """Main function. Parses args, reads the log files and renders them as text or html.""" - - parser = argparse.ArgumentParser(usage='%(prog)s [options] <test temporary directory>', description=__doc__) + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument( + 'testdir', nargs='?', default='', + help=('temporary test directory to combine logs from. ' + 'Defaults to the most recent')) parser.add_argument('-c', '--color', dest='color', action='store_true', help='outputs the combined log with events colored by source (requires posix terminal colors. Use less -r for viewing)') parser.add_argument('--html', dest='html', action='store_true', help='outputs the combined log as html. Requires jinja2. pip install jinja2') - args, unknown_args = parser.parse_known_args() + args = parser.parse_args() if args.html and args.color: print("Only one out of --color or --html should be specified") sys.exit(1) - # There should only be one unknown argument - the path of the temporary test directory - if len(unknown_args) != 1: - print("Unexpected arguments" + str(unknown_args)) + testdir = args.testdir or find_latest_test_dir() + + if not testdir: + print("No test directories found") sys.exit(1) - log_events = read_logs(unknown_args[0]) + if not args.testdir: + print("Opening latest test directory: {}".format(testdir), file=sys.stderr) + + log_events = read_logs(testdir) print_logs(log_events, color=args.color, html=args.html) @@ -53,6 +70,29 @@ def read_logs(tmp_dir): return heapq.merge(*[get_log_events(source, f) for source, f in files]) + +def find_latest_test_dir(): + """Returns the latest tmpfile test directory prefix.""" + tmpdir = tempfile.gettempdir() + + def join_tmp(basename): + return os.path.join(tmpdir, basename) + + def is_valid_test_tmpdir(basename): + fullpath = join_tmp(basename) + return ( + os.path.isdir(fullpath) + and basename.startswith(TMPDIR_PREFIX) + and os.access(fullpath, os.R_OK) + ) + + testdir_paths = [ + join_tmp(name) for name in os.listdir(tmpdir) if is_valid_test_tmpdir(name) + ] + + return max(testdir_paths, key=os.path.getmtime) if testdir_paths else None + + def get_log_events(source, logfile): """Generator function that returns individual log events. diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py index 8847777ba7..bec6a0050a 100755 --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -5,6 +5,7 @@ """Test mempool acceptance of raw transactions.""" from io import BytesIO +import math from test_framework.test_framework import BitcoinTestFramework from test_framework.messages import ( BIP125_SEQUENCE_NUMBER, @@ -181,7 +182,7 @@ class MempoolAcceptanceTest(BitcoinTestFramework): self.log.info('A really large transaction') tx.deserialize(BytesIO(hex_str_to_bytes(raw_tx_reference))) - tx.vin = [tx.vin[0]] * (MAX_BLOCK_BASE_SIZE // len(tx.vin[0].serialize())) + tx.vin = [tx.vin[0]] * math.ceil(MAX_BLOCK_BASE_SIZE / len(tx.vin[0].serialize())) self.check_mempool_result( result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': '16: bad-txns-oversize'}], rawtxs=[bytes_to_hex_str(tx.serialize())], diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 9f01be0646..6e74731349 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -25,7 +25,7 @@ from test_framework.util import ( assert_raises_rpc_error, bytes_to_hex_str as b2x, ) - +from test_framework.script import CScriptNum def assert_template(node, block, expect, rehash=True): if rehash: @@ -65,11 +65,19 @@ class MiningTest(BitcoinTestFramework): assert 'proposal' in tmpl['capabilities'] assert 'coinbasetxn' not in tmpl - coinbase_tx = create_coinbase(height=int(tmpl["height"]) + 1) + next_height = int(tmpl["height"]) + coinbase_tx = create_coinbase(height=next_height) # sequence numbers must not be max for nLockTime to have effect coinbase_tx.vin[0].nSequence = 2 ** 32 - 2 coinbase_tx.rehash() + # round-trip the encoded bip34 block height commitment + assert_equal(CScriptNum.decode(coinbase_tx.vin[0].scriptSig), next_height) + # round-trip negative and multi-byte CScriptNums to catch python regression + assert_equal(CScriptNum.decode(CScriptNum.encode(CScriptNum(1500))), 1500) + assert_equal(CScriptNum.decode(CScriptNum.encode(CScriptNum(-1500))), -1500) + assert_equal(CScriptNum.decode(CScriptNum.encode(CScriptNum(-1))), -1) + block = CBlock() block.nVersion = tmpl["version"] block.hashPrevBlock = int(tmpl["previousblockhash"], 16) diff --git a/test/functional/p2p_invalid_messages.py b/test/functional/p2p_invalid_messages.py index a2d40fab1a..65997a5f9d 100755 --- a/test/functional/p2p_invalid_messages.py +++ b/test/functional/p2p_invalid_messages.py @@ -3,6 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test node responses to invalid network messages.""" +import os import struct from test_framework import messages @@ -66,7 +67,10 @@ class InvalidMessagesTest(BitcoinTestFramework): msg_at_size = msg_unrecognized("b" * valid_data_limit) assert len(msg_at_size.serialize()) == msg_limit - with node.assert_memory_usage_stable(perc_increase_allowed=0.03): + increase_allowed = 0.5 + if [s for s in os.environ.get("BITCOIN_CONFIG", "").split(" ") if "--with-sanitizers" in s and "address" in s]: + increase_allowed = 3.5 + with node.assert_memory_usage_stable(increase_allowed=increase_allowed): self.log.info( "Sending a bunch of large, junk messages to test " "memory exhaustion. May take a bit...") diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 04d9bb65a6..272ebe65cb 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -210,6 +210,10 @@ class PSBTTest(BitcoinTestFramework): assert tx_in["sequence"] > MAX_BIP125_RBF_SEQUENCE assert_equal(decoded_psbt["tx"]["locktime"], 0) + # Make sure change address wallet does not have P2SH innerscript access to results in success + # when attempting BnB coin selection + self.nodes[0].walletcreatefundedpsbt([], [{self.nodes[2].getnewaddress():unspent["amount"]+1}], block_height+2, {"changeAddress":self.nodes[1].getnewaddress()}, False) + # Regression test for 14473 (mishandling of already-signed witness transaction): psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}]) complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"]) diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 881b839a4e..11b4db6ec5 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -10,6 +10,9 @@ from decimal import Decimal import shutil import os +def descriptors(out): + return sorted(u['desc'] for u in out['unspents']) + class ScantxoutsetTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -93,5 +96,10 @@ class ScantxoutsetTest(BitcoinTestFramework): assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) + # Test the reported descriptors for a few matches + assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])), ["pkh([0c5f9a1e/0'/0'/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)", "pkh([0c5f9a1e/0'/0'/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)"]) + assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)"]) + assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])), ['pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)', 'pkh([0c5f9a1e/1/1/1500]03832901c250025da2aebae2bfb38d5c703a57ab66ad477f9c578bfbcd78abca6f)', 'pkh([0c5f9a1e/1/1/1]030d820fc9e8211c4169be8530efbc632775d8286167afd178caaf1089b77daba7)']) + if __name__ == '__main__': ScantxoutsetTest().main() diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index 2fe44010ba..2c5ba24a6a 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -385,6 +385,22 @@ class CScriptNum: r[-1] |= 0x80 return bytes([len(r)]) + r + @staticmethod + def decode(vch): + result = 0 + # We assume valid push_size and minimal encoding + value = vch[1:] + if len(value) == 0: + return result + for i, byte in enumerate(value): + result |= int(byte) << 8*i + if value[-1] >= 0x80: + # Mask for all but the highest result bit + num_mask = (2**(len(value)*8) - 1) >> 1 + result &= num_mask + result *= -1 + return result + class CScript(bytes): """Serialized script diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 44fc185e6d..0dfa9e0d24 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -43,6 +43,8 @@ TEST_EXIT_PASSED = 0 TEST_EXIT_FAILED = 1 TEST_EXIT_SKIPPED = 77 +TMPDIR_PREFIX = "bitcoin_func_test_" + class SkipTest(Exception): """This exception is raised to skip a test""" @@ -151,7 +153,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): self.options.tmpdir = os.path.abspath(self.options.tmpdir) os.makedirs(self.options.tmpdir, exist_ok=False) else: - self.options.tmpdir = tempfile.mkdtemp(prefix="test") + self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) self._start_logging() self.log.debug('Setting up network thread') diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 9dcc0e6d0e..27f99c259c 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -115,7 +115,7 @@ class TestNode(): ] return PRIV_KEYS[self.index] - def get_mem_rss(self): + def get_mem_rss_kilobytes(self): """Get the memory usage (RSS) per `ps`. Returns None if `ps` is unavailable. @@ -291,15 +291,19 @@ class TestNode(): self._raise_assertion_error('Expected message "{}" does not partially match log:\n\n{}\n\n'.format(expected_msg, print_log)) @contextlib.contextmanager - def assert_memory_usage_stable(self, perc_increase_allowed=0.03): + def assert_memory_usage_stable(self, *, increase_allowed=0.03): """Context manager that allows the user to assert that a node's memory usage (RSS) hasn't increased beyond some threshold percentage. + + Args: + increase_allowed (float): the fractional increase in memory allowed until failure; + e.g. `0.12` for up to 12% increase allowed. """ - before_memory_usage = self.get_mem_rss() + before_memory_usage = self.get_mem_rss_kilobytes() yield - after_memory_usage = self.get_mem_rss() + after_memory_usage = self.get_mem_rss_kilobytes() if not (before_memory_usage and after_memory_usage): self.log.warning("Unable to detect memory usage (RSS) - skipping memory check.") @@ -307,10 +311,10 @@ class TestNode(): perc_increase_memory_usage = (after_memory_usage / before_memory_usage) - 1 - if perc_increase_memory_usage > perc_increase_allowed: + if perc_increase_memory_usage > increase_allowed: self._raise_assertion_error( "Memory usage increased over threshold of {:.3f}% from {} to {} ({:.3f}%)".format( - perc_increase_allowed * 100, before_memory_usage, after_memory_usage, + increase_allowed * 100, before_memory_usage, after_memory_usage, perc_increase_memory_usage * 100)) def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs): diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index da55a3a156..1167b4bba2 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -153,6 +153,7 @@ BASE_SCRIPTS = [ 'wallet_importprunedfunds.py', 'p2p_leak_tx.py', 'rpc_signmessage.py', + 'wallet_balance.py', 'feature_nulldummy.py', 'mempool_accept.py', 'wallet_import_rescan.py', diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py index 0f75045c9d..bafa556aad 100755 --- a/test/functional/wallet_address_types.py +++ b/test/functional/wallet_address_types.py @@ -99,6 +99,8 @@ class AddressTypeTest(BitcoinTestFramework): """Run sanity checks on an address.""" info = self.nodes[node].getaddressinfo(address) assert(self.nodes[node].validateaddress(address)['isvalid']) + assert_equal(info.get('solvable'), True) + if not multisig and typ == 'legacy': # P2PKH assert(not info['isscript']) @@ -146,6 +148,47 @@ class AddressTypeTest(BitcoinTestFramework): # Unknown type assert(False) + def test_desc(self, node, address, multisig, typ, utxo): + """Run sanity checks on a descriptor reported by getaddressinfo.""" + info = self.nodes[node].getaddressinfo(address) + assert('desc' in info) + assert_equal(info['desc'], utxo['desc']) + assert(self.nodes[node].validateaddress(address)['isvalid']) + + # Use a ridiculously roundabout way to find the key origin info through + # the PSBT logic. However, this does test consistency between the PSBT reported + # fingerprints/paths and the descriptor logic. + psbt = self.nodes[node].createpsbt([{'txid':utxo['txid'], 'vout':utxo['vout']}],[{address:0.00010000}]) + psbt = self.nodes[node].walletprocesspsbt(psbt, False, "ALL", True) + decode = self.nodes[node].decodepsbt(psbt['psbt']) + key_descs = {} + for deriv in decode['inputs'][0]['bip32_derivs']: + assert_equal(len(deriv['master_fingerprint']), 8) + assert_equal(deriv['path'][0], 'm') + key_descs[deriv['pubkey']] = '[' + deriv['master_fingerprint'] + deriv['path'][1:] + ']' + deriv['pubkey'] + + if not multisig and typ == 'legacy': + # P2PKH + assert_equal(info['desc'], "pkh(%s)" % key_descs[info['pubkey']]) + elif not multisig and typ == 'p2sh-segwit': + # P2SH-P2WPKH + assert_equal(info['desc'], "sh(wpkh(%s))" % key_descs[info['pubkey']]) + elif not multisig and typ == 'bech32': + # P2WPKH + assert_equal(info['desc'], "wpkh(%s)" % key_descs[info['pubkey']]) + elif typ == 'legacy': + # P2SH-multisig + assert_equal(info['desc'], "sh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]])) + elif typ == 'p2sh-segwit': + # P2SH-P2WSH-multisig + assert_equal(info['desc'], "sh(wsh(multi(2,%s,%s)))" % (key_descs[info['embedded']['pubkeys'][0]], key_descs[info['embedded']['pubkeys'][1]])) + elif typ == 'bech32': + # P2WSH-multisig + assert_equal(info['desc'], "wsh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]])) + else: + # Unknown type + assert(False) + def test_change_output_type(self, node_sender, destinations, expected_type): txid = self.nodes[node_sender].sendmany(dummy="", amounts=dict.fromkeys(destinations, 0.001)) raw_tx = self.nodes[node_sender].getrawtransaction(txid) @@ -198,6 +241,7 @@ class AddressTypeTest(BitcoinTestFramework): self.log.debug("Old balances are {}".format(old_balances)) to_send = (old_balances[from_node] / 101).quantize(Decimal("0.00000001")) sends = {} + addresses = {} self.log.debug("Prepare sends") for n, to_node in enumerate(range(from_node, from_node + 4)): @@ -228,6 +272,7 @@ class AddressTypeTest(BitcoinTestFramework): # Output entry sends[address] = to_send * 10 * (1 + n) + addresses[to_node] = (address, typ) self.log.debug("Sending: {}".format(sends)) self.nodes[from_node].sendmany("", sends) @@ -244,6 +289,17 @@ class AddressTypeTest(BitcoinTestFramework): self.nodes[5].generate(1) sync_blocks(self.nodes) + # Verify that the receiving wallet contains a UTXO with the expected address, and expected descriptor + for n, to_node in enumerate(range(from_node, from_node + 4)): + to_node %= 4 + found = False + for utxo in self.nodes[to_node].listunspent(): + if utxo['address'] == addresses[to_node][0]: + found = True + self.test_desc(to_node, addresses[to_node][0], multisig, addresses[to_node][1], utxo) + break + assert found + new_balances = self.get_balances() self.log.debug("Check new balances: {}".format(new_balances)) # We don't know what fee was set, so we can only check bounds on the balance of the sending node diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py new file mode 100755 index 0000000000..05c97e0340 --- /dev/null +++ b/test/functional/wallet_balance.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 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 the wallet balance RPC methods.""" +from decimal import Decimal + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +RANDOM_COINBASE_ADDRESS = 'mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ' + +def create_transactions(node, address, amt, fees): + # Create and sign raw transactions from node to address for amt. + # Creates a transaction for each fee and returns an array + # of the raw transactions. + utxos = node.listunspent(0) + + # Create transactions + inputs = [] + ins_total = 0 + for utxo in utxos: + inputs.append({"txid": utxo["txid"], "vout": utxo["vout"]}) + ins_total += utxo['amount'] + if ins_total > amt: + break + + txs = [] + for fee in fees: + outputs = {address: amt, node.getrawchangeaddress(): ins_total - amt - fee} + raw_tx = node.createrawtransaction(inputs, outputs, 0, True) + raw_tx = node.signrawtransactionwithwallet(raw_tx) + txs.append(raw_tx) + + return txs + +class WalletTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + # Check that nodes don't own any UTXOs + assert_equal(len(self.nodes[0].listunspent()), 0) + assert_equal(len(self.nodes[1].listunspent()), 0) + + self.log.info("Mining one block for each node") + + self.nodes[0].generate(1) + self.sync_all() + self.nodes[1].generate(1) + self.nodes[1].generatetoaddress(100, RANDOM_COINBASE_ADDRESS) + self.sync_all() + + assert_equal(self.nodes[0].getbalance(), 50) + assert_equal(self.nodes[1].getbalance(), 50) + + self.log.info("Test getbalance with different arguments") + assert_equal(self.nodes[0].getbalance("*"), 50) + assert_equal(self.nodes[0].getbalance("*", 1), 50) + assert_equal(self.nodes[0].getbalance("*", 1, True), 50) + assert_equal(self.nodes[0].getbalance(minconf=1), 50) + + # Send 40 BTC from 0 to 1 and 60 BTC from 1 to 0. + txs = create_transactions(self.nodes[0], self.nodes[1].getnewaddress(), 40, [Decimal('0.01')]) + self.nodes[0].sendrawtransaction(txs[0]['hex']) + self.nodes[1].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation + + self.sync_all() + txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), 60, [Decimal('0.01'), Decimal('0.02')]) + self.nodes[1].sendrawtransaction(txs[0]['hex']) + self.nodes[0].sendrawtransaction(txs[0]['hex']) # sending on both nodes is faster than waiting for propagation + self.sync_all() + + # First argument of getbalance must be set to "*" + assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[1].getbalance, "") + + self.log.info("Test getbalance and getunconfirmedbalance with unconfirmed inputs") + + # getbalance without any arguments includes unconfirmed transactions, but not untrusted transactions + assert_equal(self.nodes[0].getbalance(), Decimal('9.99')) # change from node 0's send + assert_equal(self.nodes[1].getbalance(), Decimal('29.99')) # change from node 1's send + # Same with minconf=0 + assert_equal(self.nodes[0].getbalance(minconf=0), Decimal('9.99')) + assert_equal(self.nodes[1].getbalance(minconf=0), Decimal('29.99')) + # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago + # TODO: fix getbalance tracking of coin spentness depth + assert_equal(self.nodes[0].getbalance(minconf=1), Decimal('0')) + assert_equal(self.nodes[1].getbalance(minconf=1), Decimal('0')) + # getunconfirmedbalance + assert_equal(self.nodes[0].getunconfirmedbalance(), Decimal('60')) # output of node 1's spend + assert_equal(self.nodes[1].getunconfirmedbalance(), Decimal('0')) # Doesn't include output of node 0's send since it was spent + + # Node 1 bumps the transaction fee and resends + self.nodes[1].sendrawtransaction(txs[1]['hex']) + self.sync_all() + + self.log.info("Test getbalance and getunconfirmedbalance with conflicted unconfirmed inputs") + + assert_equal(self.nodes[0].getwalletinfo()["unconfirmed_balance"], Decimal('60')) # output of node 1's send + assert_equal(self.nodes[0].getunconfirmedbalance(), Decimal('60')) + assert_equal(self.nodes[1].getwalletinfo()["unconfirmed_balance"], Decimal('0')) # Doesn't include output of node 0's send since it was spent + assert_equal(self.nodes[1].getunconfirmedbalance(), Decimal('0')) + + self.nodes[1].generatetoaddress(1, RANDOM_COINBASE_ADDRESS) + self.sync_all() + + # balances are correct after the transactions are confirmed + assert_equal(self.nodes[0].getbalance(), Decimal('69.99')) # node 1's send plus change from node 0's send + assert_equal(self.nodes[1].getbalance(), Decimal('29.98')) # change from node 0's send + + # Send total balance away from node 1 + txs = create_transactions(self.nodes[1], self.nodes[0].getnewaddress(), Decimal('29.97'), [Decimal('0.01')]) + self.nodes[1].sendrawtransaction(txs[0]['hex']) + self.nodes[1].generatetoaddress(2, RANDOM_COINBASE_ADDRESS) + self.sync_all() + + # getbalance with a minconf incorrectly excludes coins that have been spent more recently than the minconf blocks ago + # TODO: fix getbalance tracking of coin spentness depth + # getbalance with minconf=3 should still show the old balance + assert_equal(self.nodes[1].getbalance(minconf=3), Decimal('0')) + + # getbalance with minconf=2 will show the new balance. + assert_equal(self.nodes[1].getbalance(minconf=2), Decimal('0')) + +if __name__ == '__main__': + WalletTest().main() diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index c9b40905f0..7184bb8cb6 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -67,15 +67,6 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[1].getbalance(), 50) assert_equal(self.nodes[2].getbalance(), 0) - # Check getbalance with different arguments - assert_equal(self.nodes[0].getbalance("*"), 50) - assert_equal(self.nodes[0].getbalance("*", 1), 50) - assert_equal(self.nodes[0].getbalance("*", 1, True), 50) - assert_equal(self.nodes[0].getbalance(minconf=1), 50) - - # first argument of getbalance must be excluded or set to "*" - assert_raises_rpc_error(-32, "dummy first argument must be excluded or set to \"*\"", self.nodes[0].getbalance, "") - # Check that only first and second nodes have UTXOs utxos = self.nodes[0].listunspent() assert_equal(len(utxos), 1) @@ -248,10 +239,6 @@ class WalletTest(BitcoinTestFramework): assert(txid1 in self.nodes[3].getrawmempool()) - # Exercise balance rpcs - assert_equal(self.nodes[0].getwalletinfo()["unconfirmed_balance"], 1) - assert_equal(self.nodes[0].getunconfirmedbalance(), 1) - # check if we can list zero value tx as available coins # 1. create raw_tx # 2. hex-changed one output to 0.0 diff --git a/test/functional/wallet_encryption.py b/test/functional/wallet_encryption.py index ab9ebed8d4..c514b7e0b4 100755 --- a/test/functional/wallet_encryption.py +++ b/test/functional/wallet_encryption.py @@ -31,12 +31,18 @@ class WalletEncryptionTest(BitcoinTestFramework): privkey = self.nodes[0].dumpprivkey(address) assert_equal(privkey[:1], "c") assert_equal(len(privkey), 52) + assert_raises_rpc_error(-15, "Error: running with an unencrypted wallet, but walletpassphrase was called", self.nodes[0].walletpassphrase, 'ff', 1) + assert_raises_rpc_error(-15, "Error: running with an unencrypted wallet, but walletpassphrasechange was called.", self.nodes[0].walletpassphrasechange, 'ff', 'ff') # Encrypt the wallet + assert_raises_rpc_error(-8, "passphrase can not be empty", self.nodes[0].encryptwallet, '') self.nodes[0].encryptwallet(passphrase) # Test that the wallet is encrypted assert_raises_rpc_error(-13, "Please enter the wallet passphrase with walletpassphrase first", self.nodes[0].dumpprivkey, address) + assert_raises_rpc_error(-15, "Error: running with an encrypted wallet, but encryptwallet was called.", self.nodes[0].encryptwallet, 'ff') + assert_raises_rpc_error(-8, "passphrase can not be empty", self.nodes[0].walletpassphrase, '', 1) + assert_raises_rpc_error(-8, "passphrase can not be empty", self.nodes[0].walletpassphrasechange, '', 'ff') # Check that walletpassphrase works self.nodes[0].walletpassphrase(passphrase, 2) diff --git a/test/sanitizer_suppressions/lsan b/test/sanitizer_suppressions/lsan new file mode 100644 index 0000000000..6f15c0f1d4 --- /dev/null +++ b/test/sanitizer_suppressions/lsan @@ -0,0 +1,6 @@ +# Suppress warnings triggered in dependencies +leak:libcrypto +leak:libqminimal +leak:libQt5Core +leak:libQt5Gui +leak:libQt5Widgets diff --git a/test/sanitizer_suppressions/tsan b/test/sanitizer_suppressions/tsan index 209c46f096..996f342eb9 100644 --- a/test/sanitizer_suppressions/tsan +++ b/test/sanitizer_suppressions/tsan @@ -1,9 +1,6 @@ # ThreadSanitizer suppressions # ============================ -# fChecked is theoretically racy, practically only in unit tests -race:CheckBlock - # WalletBatch (unidentified deadlock) deadlock:WalletBatch |