diff --git a/depends/packages/qt.mk b/depends/packages/qt.mk index 5a4b70b07b..2874e52869 100644 --- a/depends/packages/qt.mk +++ b/depends/packages/qt.mk @@ -47,11 +47,15 @@ $(package)_config_opts += -no-ico $(package)_config_opts += -no-iconv $(package)_config_opts += -no-kms $(package)_config_opts += -no-linuxfb +$(package)_config_opts += -no-libjpeg +$(package)_config_opts += -no-libproxy $(package)_config_opts += -no-libudev $(package)_config_opts += -no-mtdev $(package)_config_opts += -no-openssl $(package)_config_opts += -no-openvg $(package)_config_opts += -no-reduce-relocations +$(package)_config_opts += -no-sctp +$(package)_config_opts += -no-securetransport $(package)_config_opts += -no-sql-db2 $(package)_config_opts += -no-sql-ibase $(package)_config_opts += -no-sql-oci @@ -61,6 +65,7 @@ $(package)_config_opts += -no-sql-odbc $(package)_config_opts += -no-sql-psql $(package)_config_opts += -no-sql-sqlite $(package)_config_opts += -no-sql-sqlite2 +$(package)_config_opts += -no-system-proxies $(package)_config_opts += -no-use-gold-linker $(package)_config_opts += -nomake examples $(package)_config_opts += -nomake tests @@ -69,7 +74,6 @@ $(package)_config_opts += -opensource $(package)_config_opts += -pkg-config $(package)_config_opts += -prefix $(host_prefix) $(package)_config_opts += -qt-libpng -$(package)_config_opts += -qt-libjpeg $(package)_config_opts += -qt-pcre $(package)_config_opts += -qt-harfbuzz $(package)_config_opts += -qt-zlib @@ -82,15 +86,19 @@ $(package)_config_opts += -no-feature-concurrent $(package)_config_opts += -no-feature-dial $(package)_config_opts += -no-feature-fontcombobox $(package)_config_opts += -no-feature-ftp +$(package)_config_opts += -no-feature-http $(package)_config_opts += -no-feature-image_heuristic_mask $(package)_config_opts += -no-feature-keysequenceedit $(package)_config_opts += -no-feature-lcdnumber +$(package)_config_opts += -no-feature-networkdiskcache +$(package)_config_opts += -no-feature-networkproxy $(package)_config_opts += -no-feature-pdf $(package)_config_opts += -no-feature-printdialog $(package)_config_opts += -no-feature-printer $(package)_config_opts += -no-feature-printpreviewdialog $(package)_config_opts += -no-feature-printpreviewwidget $(package)_config_opts += -no-feature-sessionmanager +$(package)_config_opts += -no-feature-socks5 $(package)_config_opts += -no-feature-sql $(package)_config_opts += -no-feature-sqlmodel $(package)_config_opts += -no-feature-statemachine diff --git a/doc/developer-notes.md b/doc/developer-notes.md index 28cf00dbd5..47deb02ab1 100644 --- a/doc/developer-notes.md +++ b/doc/developer-notes.md @@ -924,7 +924,7 @@ Current subtrees include: - Used by leveldb for hardware acceleration of CRC32C checksums for data integrity. - Upstream at https://github.com/google/crc32c ; Maintained by Google. -- src/libsecp256k1 +- src/secp256k1 - Upstream at https://github.com/bitcoin-core/secp256k1/ ; actively maintained by Core contributors. - src/crypto/ctaes diff --git a/doc/release-notes-13756.md b/doc/release-notes-13756.md new file mode 100644 index 0000000000..21006f46a0 --- /dev/null +++ b/doc/release-notes-13756.md @@ -0,0 +1,39 @@ +Coin selection +-------------- + +### Reuse Avoidance + +A new wallet flag `avoid_reuse` has been added (default off). When enabled, +a wallet will distinguish between used and unused addresses, and default to not +use the former in coin selection. + +(Note: rescanning the blockchain is required, to correctly mark previously +used destinations.) + +Together with "avoid partial spends" (present as of Bitcoin v0.17), this +addresses a serious privacy issue where a malicious user can track spends by +peppering a previously paid to address with near-dust outputs, which would then +be inadvertently included in future payments. + +New RPCs +-------- + +- A new `setwalletflag` RPC sets/unsets flags for an existing wallet. + + +Updated RPCs +------------ + +Several RPCs have been updated to include an "avoid_reuse" flag, used to control +whether already used addresses should be left out or included in the operation. +These include: + +- createwallet +- getbalance +- sendtoaddress + +In addition, `sendtoaddress` has been changed to enable `-avoidpartialspends` when +`avoid_reuse` is enabled. + +The listunspent RPC has also been updated to now include a "reused" bool, for nodes +with "avoid_reuse" enabled. diff --git a/src/Makefile.am b/src/Makefile.am index 8101638eec..ac2086039b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -905,13 +905,13 @@ clean-local: check-symbols: $(bin_PROGRAMS) if GLIBC_BACK_COMPAT @echo "Checking glibc back compat..." - $(AM_V_at) READELF=$(READELF) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py < $(bin_PROGRAMS) + $(AM_V_at) READELF=$(READELF) CPPFILT=$(CPPFILT) $(PYTHON) $(top_srcdir)/contrib/devtools/symbol-check.py $(bin_PROGRAMS) endif check-security: $(bin_PROGRAMS) if HARDEN @echo "Checking binary security..." - $(AM_V_at) READELF=$(READELF) OBJDUMP=$(OBJDUMP) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/security-check.py < $(bin_PROGRAMS) + $(AM_V_at) READELF=$(READELF) OBJDUMP=$(OBJDUMP) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/security-check.py $(bin_PROGRAMS) endif diff --git a/src/interfaces/node.cpp b/src/interfaces/node.cpp index 7a797e6ac0..b1db4d297f 100644 --- a/src/interfaces/node.cpp +++ b/src/interfaces/node.cpp @@ -192,7 +192,7 @@ public: std::string getNetwork() override { return Params().NetworkIDString(); } void initLogging() override { InitLogging(gArgs); } void initParameterInteraction() override { InitParameterInteraction(gArgs); } - std::string getWarnings(const std::string& type) override { return GetWarnings(type); } + std::string getWarnings() override { return GetWarnings(true); } uint64_t getLogCategories() override { return LogInstance().GetCategoryMask(); } bool baseInitialize() override { diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 9ec6f2dd38..1e928bbbc1 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -152,7 +152,7 @@ public: virtual void initParameterInteraction() = 0; //! Get warnings. - virtual std::string getWarnings(const std::string& type) = 0; + virtual std::string getWarnings() = 0; // Get log flags. virtual uint64_t getLogCategories() = 0; diff --git a/src/interfaces/wallet.cpp b/src/interfaces/wallet.cpp index c78a6eb9bb..facf25cf59 100644 --- a/src/interfaces/wallet.cpp +++ b/src/interfaces/wallet.cpp @@ -430,7 +430,7 @@ public: CAmount getAvailableBalance(const CCoinControl& coin_control) override { if (coin_control.IsUsingCoinJoin()) { - return m_wallet->GetBalance(0, false, &coin_control).m_anonymized; + return m_wallet->GetBalance(0, coin_control.m_avoid_address_reuse, false, &coin_control).m_anonymized; } else { return m_wallet->GetAvailableBalance(&coin_control); } diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 7b44457790..3767c5108c 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -166,7 +166,7 @@ enum BlockSource ClientModel::getBlockSource() const QString ClientModel::getStatusBarWarnings() const { - return QString::fromStdString(m_node.getWarnings("gui")); + return QString::fromStdString(m_node.getWarnings()); } OptionsModel *ClientModel::getOptionsModel() diff --git a/src/qt/dash.cpp b/src/qt/dash.cpp index 7b0a97c75f..441746adc0 100644 --- a/src/qt/dash.cpp +++ b/src/qt/dash.cpp @@ -141,7 +141,7 @@ BitcoinCore::BitcoinCore(interfaces::Node& node) : void BitcoinCore::handleRunawayException(const std::exception_ptr e) { PrintExceptionContinue(e, "Runaway exception"); - Q_EMIT runawayException(QString::fromStdString(m_node.getWarnings("gui"))); + Q_EMIT runawayException(QString::fromStdString(m_node.getWarnings())); } void BitcoinCore::initialize() @@ -587,6 +587,7 @@ int GuiMain(int argc, char* argv[]) qInstallMessageHandler(DebugMessageHandler); // Allow parameter interaction before we create the options model app.parameterSetup(); + GUIUtil::LogQtInfo(); // Load custom application fonts and setup font management if (!GUIUtil::loadFonts()) { QMessageBox::critical(0, PACKAGE_NAME, @@ -705,7 +706,7 @@ int GuiMain(int argc, char* argv[]) } } catch (...) { PrintExceptionContinue(std::current_exception(), "Runaway exception"); - app.handleRunawayException(QString::fromStdString(node->getWarnings("gui"))); + app.handleRunawayException(QString::fromStdString(node->getWarnings())); } return rv; } diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index 3369336da6..119d4ba18a 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -53,17 +53,23 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include +#include +#include #include // for Qt::mightBeRichText #include #include #include #include +#include #if defined(Q_OS_MAC) @@ -1876,4 +1882,23 @@ int TextWidth(const QFontMetrics& fm, const QString& text) #endif } +void LogQtInfo() +{ +#ifdef QT_STATIC + const std::string qt_link{"static"}; +#else + const std::string qt_link{"dynamic"}; +#endif +#ifdef QT_STATICPLUGIN + const std::string plugin_link{"static"}; +#else + const std::string plugin_link{"dynamic"}; +#endif + LogPrintf("Qt %s (%s), plugin=%s (%s)\n", qVersion(), qt_link, QGuiApplication::platformName().toStdString(), plugin_link); + LogPrintf("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(), QSysInfo::buildAbi().toStdString()); + for (const QScreen* s : QGuiApplication::screens()) { + LogPrintf("Screen: %s %dx%d, pixel ratio=%.1f\n", s->name().toStdString(), s->size().width(), s->size().height(), s->devicePixelRatio()); + } +} + } // namespace GUIUtil diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 9a214d6b5d..fcfcdfc6d9 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -475,6 +475,11 @@ namespace GUIUtil * In Qt 5.11 the QFontMetrics::horizontalAdvance() was introduced. */ int TextWidth(const QFontMetrics& fm, const QString& text); + + /** + * Writes to debug.log short info about the used Qt and the host system. + */ + void LogQtInfo(); } // namespace GUIUtil #endif // BITCOIN_QT_GUIUTIL_H diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index d6ffda002e..a08e9a1097 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -319,23 +319,21 @@ void SendCoinsDialog::send(QList recipients) QStringList formatted; for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients()) { - // generate bold amount string with wallet name in case of multiwallet - QString amount = "" + BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount); + // generate amount string with wallet name in case of multiwallet + QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount); if (model->isMultiwallet()) { - amount.append(" "+tr("from wallet %1").arg(GUIUtil::HtmlEscape(model->getWalletName()))+" "); + amount.append(tr(" from wallet '%1'").arg(model->getWalletName())); } - amount.append(" "); - // generate monospace address string - QString address = "" + rcp.address; - address.append(""); + + // generate address string + QString address = rcp.address; QString recipientElement; - recipientElement = "
"; { if(rcp.label.length() > 0) // label with address { - recipientElement.append(tr("%1 to %2").arg(amount, GUIUtil::HtmlEscape(rcp.label))); + recipientElement.append(tr("%1 to '%2'").arg(amount, rcp.label)); recipientElement.append(QString(" (%1)").arg(address)); } else // just address @@ -347,21 +345,14 @@ void SendCoinsDialog::send(QList recipients) } // Limit number of displayed entries - int messageEntries = formatted.size(); - int displayedEntries = 0; - for(int i = 0; i < formatted.size(); i++){ - if(i >= MAX_SEND_POPUP_ENTRIES){ - formatted.removeLast(); - i--; - } - else{ - displayedEntries = i+1; - } + QStringList formatted_short(formatted); + if (formatted_short.size() > MAX_SEND_POPUP_ENTRIES) { + formatted_short.erase(formatted_short.begin() + MAX_SEND_POPUP_ENTRIES, formatted_short.end()); } QString questionString = tr("Are you sure you want to send?"); questionString.append("

"); - questionString.append(formatted.join("
")); + questionString.append(formatted_short.join("
")); questionString.append("
"); QString strCoinJoinName = QString::fromStdString(gCoinJoinName); @@ -372,6 +363,9 @@ void SendCoinsDialog::send(QList recipients) questionString.append(tr("using") + " " + tr("any available funds") + ""); } + int messageEntries = formatted.size(); + int displayedEntries = formatted_short.size(); + if (displayedEntries < messageEntries) { questionString.append("
"); questionString.append(""); @@ -437,9 +431,14 @@ void SendCoinsDialog::send(QList recipients) questionString.append(QString("
(=%1)") .arg(alternativeUnits.join(" " + tr("or") + " "))); - // Display message box - SendConfirmationDialog confirmationDialog(tr("Confirm send coins"), - questionString, SEND_CONFIRM_DELAY, this); + QString informative_text; + QString detailed_text; + if (formatted.size() > 1) { + informative_text = tr("To review recipient list click \"Show Details...\""); + detailed_text = formatted.join("\n\n"); + } + + SendConfirmationDialog confirmationDialog(tr("Confirm send coins"), questionString, informative_text, detailed_text, SEND_CONFIRM_DELAY, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = static_cast(confirmationDialog.result()); @@ -954,11 +953,16 @@ void SendCoinsDialog::keepChangeAddressChanged(bool checked) fKeepChangeAddress = checked; } -SendConfirmationDialog::SendConfirmationDialog(const QString &title, const QString &text, int _secDelay, - QWidget *parent) : - QMessageBox(QMessageBox::Question, title, text, QMessageBox::Yes | QMessageBox::Cancel, parent), secDelay(_secDelay) +SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, QWidget* parent) + : QMessageBox(parent), secDelay(_secDelay) { GUIUtil::updateFonts(); + setIcon(QMessageBox::Question); + setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines). + setText(text); + setInformativeText(informative_text); + setDetailedText(detailed_text); + setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); setDefaultButton(QMessageBox::Cancel); yesButton = button(QMessageBox::Yes); updateYesButton(); diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 22e92a3561..7ceb344d0a 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -114,6 +114,7 @@ class SendConfirmationDialog : public QMessageBox public: SendConfirmationDialog(const QString &title, const QString &text, int secDelay = 0, QWidget *parent = nullptr); + SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = 0, QWidget* parent = nullptr); int exec() override; private Q_SLOTS: diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 3fdada4cd0..cac5900f79 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1564,7 +1564,7 @@ UniValue getblockchaininfo(const JSONRPCRequest& request) obj.pushKV("softforks", softforks); obj.pushKV("bip9_softforks", bip9_softforks); - obj.pushKV("warnings", GetWarnings("statusbar")); + obj.pushKV("warnings", GetWarnings(false)); return obj; } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 3f15c40ee0..a383d1c7dd 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -42,6 +42,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtoaddress", 5, "use_is" }, { "sendtoaddress", 6, "use_cj" }, { "sendtoaddress", 7, "conf_target" }, + { "sendtoaddress", 9, "avoid_reuse" }, { "settxfee", 0, "amount" }, { "getreceivedbyaddress", 1, "minconf" }, { "getreceivedbyaddress", 2, "addlocked" }, @@ -59,6 +60,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getbalance", 1, "minconf" }, { "getbalance", 2, "addlocked" }, { "getbalance", 3, "include_watchonly" }, + { "getbalance", 4, "avoid_reuse" }, { "getchaintips", 0, "count" }, { "getchaintips", 1, "branchlen" }, { "getblockhash", 0, "height" }, @@ -160,6 +162,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setnetworkactive", 0, "state" }, { "setcoinjoinrounds", 0, "rounds" }, { "setcoinjoinamount", 0, "amount" }, + { "setwalletflag", 1, "value" }, { "getmempoolancestors", 1, "verbose" }, { "getmempooldescendants", 1, "verbose" }, { "logging", 0, "include" }, @@ -195,6 +198,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "rescanblockchain", 1, "stop_height"}, { "createwallet", 1, "disable_private_keys"}, { "createwallet", 2, "blank"}, + { "createwallet", 4, "avoid_reuse"}, { "upgradetohd", 3, "rescan"}, { "getnodeaddresses", 0, "count"}, { "stop", 0, "wait" }, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 38a2e2bcd5..09e1114215 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -443,7 +443,7 @@ static UniValue getmininginfo(const JSONRPCRequest& request) obj.pushKV("networkhashps", getnetworkhashps(request)); obj.pushKV("pooledtx", (uint64_t)mempool.size()); obj.pushKV("chain", Params().NetworkIDString()); - obj.pushKV("warnings", GetWarnings("statusbar")); + obj.pushKV("warnings", GetWarnings(false)); return obj; } diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 2bdaa6de23..4567ae401f 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -576,7 +576,7 @@ static UniValue getnetworkinfo(const JSONRPCRequest& request) } } obj.pushKV("localaddresses", localAddresses); - obj.pushKV("warnings", GetWarnings("statusbar")); + obj.pushKV("warnings", GetWarnings(false)); return obj; } diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index e83b228caa..66b1dfd0a9 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -826,11 +826,9 @@ UniValue sendrawtransaction(const JSONRPCRequest& request) // TODO: temporary migration code for old clients. Remove in v0.20 if (request.params[1].isBool()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Second argument must be numeric (maxfeerate) and no longer supports a boolean. To allow a transaction with high fees, set maxfeerate to 0."); - } else if (request.params[1].isNum()) { + } else if (!request.params[1].isNull()) { CFeeRate fr(AmountFromValue(request.params[1])); max_raw_tx_fee = fr.GetFee(GetVirtualTransactionSize(*tx)); - } else if (!request.params[1].isNull()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "second argument (maxfeerate) must be numeric"); } bool bypass_limits = false; @@ -904,11 +902,9 @@ static UniValue testmempoolaccept(const JSONRPCRequest& request) // TODO: temporary migration code for old clients. Remove in v0.20 if (request.params[1].isBool()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Second argument must be numeric (maxfeerate) and no longer supports a boolean. To allow a transaction with high fees, set maxfeerate to 0."); - } else if (request.params[1].isNum()) { + } else if (!request.params[1].isNull()) { CFeeRate fr(AmountFromValue(request.params[1])); max_raw_tx_fee = fr.GetFee(GetVirtualTransactionSize(*tx)); - } else if (!request.params[1].isNull()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "second argument (maxfeerate) must be numeric"); } CTxMemPool& mempool = EnsureMemPool(request.context); diff --git a/src/test/timedata_tests.cpp b/src/test/timedata_tests.cpp index a4b4394fa1..f421fd0e99 100644 --- a/src/test/timedata_tests.cpp +++ b/src/test/timedata_tests.cpp @@ -65,7 +65,7 @@ BOOST_AUTO_TEST_CASE(addtimedata) MultiAddTimeData(1, DEFAULT_MAX_TIME_ADJUSTMENT + 1); //filter size 5 } - BOOST_CHECK(GetWarnings("gui").find("clock is wrong") != std::string::npos); + BOOST_CHECK(GetWarnings(true).find("clock is wrong") != std::string::npos); // nTimeOffset is not changed if the median of offsets exceeds DEFAULT_MAX_TIME_ADJUSTMENT BOOST_CHECK_EQUAL(GetTimeOffset(), 0); diff --git a/src/wallet/coincontrol.cpp b/src/wallet/coincontrol.cpp index 0ac275d358..7431f1fcab 100644 --- a/src/wallet/coincontrol.cpp +++ b/src/wallet/coincontrol.cpp @@ -13,6 +13,7 @@ void CCoinControl::SetNull(bool fResetCoinType) fAllowOtherInputs = false; fAllowWatchOnly = false; m_avoid_partial_spends = gArgs.GetBoolArg("-avoidpartialspends", DEFAULT_AVOIDPARTIALSPENDS); + m_avoid_address_reuse = false; setSelected.clear(); m_feerate.reset(); fOverrideFeeRate = false; diff --git a/src/wallet/coincontrol.h b/src/wallet/coincontrol.h index 4db9712c4c..b226feecc9 100644 --- a/src/wallet/coincontrol.h +++ b/src/wallet/coincontrol.h @@ -46,6 +46,8 @@ public: Optional m_confirm_target; //! Avoid partial use of funds sent to a given address bool m_avoid_partial_spends; + //! Forbids inclusion of dirty (previously used) addresses + bool m_avoid_address_reuse; //! Fee estimation mode to control arguments to estimateSmartFee FeeEstimateMode m_fee_mode; //! Minimum chain depth value for coin availability diff --git a/src/wallet/init.cpp b/src/wallet/init.cpp index 39c32a2bf0..35f009d341 100644 --- a/src/wallet/init.cpp +++ b/src/wallet/init.cpp @@ -44,7 +44,7 @@ const WalletInitInterface& g_wallet_init_interface = WalletInit(); void WalletInit::AddWalletOptions(ArgsManager& argsman) const { - argsman.AddArg("-avoidpartialspends", strprintf("Group outputs by address, selecting all or none, instead of selecting on a per-output basis. Privacy is improved as an address is only used once (unless someone sends to it after spending from it), but may result in slightly higher fees as suboptimal coin selection may result due to the added limitation (default: %u)", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); + argsman.AddArg("-avoidpartialspends", strprintf("Group outputs by address, selecting all or none, instead of selecting on a per-output basis. Privacy is improved as an address is only used once (unless someone sends to it after spending from it), but may result in slightly higher fees as suboptimal coin selection may result due to the added limitation (default: %u (always enabled for wallets with \"avoid_reuse\" enabled))", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); argsman.AddArg("-createwalletbackups=", strprintf("Number of automatic wallet backups (default: %u)", nWalletBackups), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET); #if HAVE_SYSTEM diff --git a/src/wallet/ismine.h b/src/wallet/ismine.h index 56e6e24b39..7e0af24d2f 100644 --- a/src/wallet/ismine.h +++ b/src/wallet/ismine.h @@ -20,7 +20,9 @@ enum isminetype : unsigned int ISMINE_NO = 0, ISMINE_WATCH_ONLY = 1 << 0, ISMINE_SPENDABLE = 1 << 1, + ISMINE_USED = 1 << 2, ISMINE_ALL = ISMINE_WATCH_ONLY | ISMINE_SPENDABLE, + ISMINE_ALL_USED = ISMINE_ALL | ISMINE_USED, ISMINE_ENUM_ELEMENTS, }; /** used for bitflags of isminetype */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index dbcca95e73..cbae9e2d30 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -49,6 +49,17 @@ static const std::string WALLET_ENDPOINT_BASE = "/wallet/"; +static inline bool GetAvoidReuseFlag(CWallet * const pwallet, const UniValue& param) { + bool can_avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); + bool avoid_reuse = param.isNull() ? can_avoid_reuse : param.get_bool(); + + if (avoid_reuse && !can_avoid_reuse) { + throw JSONRPCError(RPC_WALLET_ERROR, "wallet does not have the \"avoid reuse\" feature enabled"); + } + + return avoid_reuse; +} + bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name) { if (request.URI.substr(0, WALLET_ENDPOINT_BASE.size()) == WALLET_ENDPOINT_BASE) { @@ -280,7 +291,7 @@ static UniValue setlabel(const JSONRPCRequest& request) static CTransactionRef SendMoney(CWallet* const pwallet, const CTxDestination& address, CAmount nValue, bool fSubtractFeeFromAmount, const CCoinControl& coin_control, mapValue_t mapValue) { - CAmount curBalance = pwallet->GetBalance().m_mine_trusted; + CAmount curBalance = pwallet->GetBalance(0, coin_control.m_avoid_address_reuse).m_mine_trusted; // Check amount if (nValue <= 0) @@ -319,6 +330,10 @@ static CTransactionRef SendMoney(CWallet* const pwallet, const CTxDestination& a static UniValue sendtoaddress(const JSONRPCRequest& request) { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + RPCHelpMan{"sendtoaddress", "\nSend an amount to a given address." + HelpRequiringPassphrase() + "\n", @@ -339,6 +354,8 @@ static UniValue sendtoaddress(const JSONRPCRequest& request) " \"UNSET\"\n" " \"ECONOMICAL\"\n" " \"CONSERVATIVE\""}, + {"avoid_reuse", RPCArg::Type::BOOL, /* default */ pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) ? "true" : "unavailable", "Avoid spending from dirty addresses; addresses are considered\n" + " dirty if they have previously been used in a transaction."}, }, RPCResult{ RPCResult::Type::STR_HEX, "txid", "The transaction id." @@ -351,10 +368,6 @@ static UniValue sendtoaddress(const JSONRPCRequest& request) }, }.Check(request); - std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); - if (!wallet) return NullUniValue; - CWallet* const pwallet = wallet.get(); - // Make sure the results are valid at least up to the most recent block // the user could have gotten from another RPC command prior to now pwallet->BlockUntilSyncedToCurrentChain(); @@ -399,6 +412,10 @@ static UniValue sendtoaddress(const JSONRPCRequest& request) } } + coin_control.m_avoid_address_reuse = GetAvoidReuseFlag(pwallet, request.params[9]); + // We also enable partial spend avoidance if reuse avoidance is set. + coin_control.m_avoid_partial_spends |= coin_control.m_avoid_address_reuse; + EnsureWalletIsUnlocked(pwallet); CTransactionRef tx = SendMoney(pwallet, dest, nAmount, fSubtractFeeFromAmount, coin_control, std::move(mapValue)); @@ -711,6 +728,10 @@ static UniValue getreceivedbylabel(const JSONRPCRequest& request) static UniValue getbalance(const JSONRPCRequest& request) { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + RPCHelpMan{"getbalance", "\nReturns the total available balance.\n" "The available balance is what the wallet considers currently spendable, and is\n" @@ -720,6 +741,7 @@ static UniValue getbalance(const JSONRPCRequest& request) {"minconf", RPCArg::Type::NUM, /* default */ "0", "Only include transactions confirmed at least this many times."}, {"addlocked", RPCArg::Type::BOOL, /* default */ "false", "Whether to include transactions locked via InstantSend in the wallet's balance."}, {"include_watchonly", RPCArg::Type::BOOL, /* default */ "false", "Also include balance in watch-only addresses (see 'importaddress')"}, + {"avoid_reuse", RPCArg::Type::BOOL, /* default */ pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) ? "true" : "unavailable", "Do not include balance in dirty outputs; addresses are considered dirty if they have previously been used in a transaction."}, }, RPCResult{ RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received for this wallet." @@ -734,10 +756,6 @@ static UniValue getbalance(const JSONRPCRequest& request) }, }.Check(request); - std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); - if (!wallet) return NullUniValue; - CWallet* const pwallet = wallet.get(); - // Make sure the results are valid at least up to the most recent block // the user could have gotten from another RPC command prior to now pwallet->BlockUntilSyncedToCurrentChain(); @@ -765,7 +783,8 @@ static UniValue getbalance(const JSONRPCRequest& request) include_watchonly = true; } - const auto bal = pwallet->GetBalance(min_depth, fAddLocked); + bool avoid_reuse = GetAvoidReuseFlag(pwallet, request.params[4]); + const auto bal = pwallet->GetBalance(min_depth, avoid_reuse, fAddLocked); return ValueFromAmount(bal.m_mine_trusted + (include_watchonly ? bal.m_watchonly_trusted : 0)); } @@ -2414,6 +2433,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) {RPCResult::Type::NUM, "hdinternalkeyindex", "current internal childkey index"}, }}, }}, + {RPCResult::Type::BOOL, "avoid_reuse", "whether this wallet tracks clean/dirty coins in terms of reuse"}, {RPCResult::Type::OBJ, "scanning", "current scanning details, or false if no scan is in progress", { {RPCResult::Type::NUM, "duration", "elapsed seconds since scan start"}, @@ -2477,6 +2497,7 @@ static UniValue getwalletinfo(const JSONRPCRequest& request) } obj.pushKV("hdaccounts", accounts); } + obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)); if (pwallet->IsScanning()) { UniValue scanning(UniValue::VOBJ); scanning.pushKV("duration", pwallet->ScanningDuration() / 1000); @@ -2703,6 +2724,72 @@ static UniValue loadwallet(const JSONRPCRequest& request) return obj; } +static UniValue setwalletflag(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + std::string flags = ""; + for (auto& it : WALLET_FLAG_MAP) + if (it.second & MUTABLE_WALLET_FLAGS) + flags += (flags == "" ? "" : ", ") + it.first; + + RPCHelpMan{"setwalletflag", + "\nChange the state of the given wallet flag for a wallet.\n", + { + {"flag", RPCArg::Type::STR, RPCArg::Optional::NO, "The name of the flag to change. Current available flags: " + flags}, + {"value", RPCArg::Type::BOOL, /* default */ "true", "The new state."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "flag_name", "The name of the flag that was modified"}, + {RPCResult::Type::BOOL, "flag_state", "The new state of the flag"}, + {RPCResult::Type::STR, "warnings", "Any warnings associated with the change"}, + } + }, + RPCExamples{ + HelpExampleCli("setwalletflag", "avoid_reuse") + + HelpExampleRpc("setwalletflag", "\"avoid_reuse\"") + }, + }.Check(request); + + std::string flag_str = request.params[0].get_str(); + bool value = request.params[1].isNull() || request.params[1].get_bool(); + + if (!WALLET_FLAG_MAP.count(flag_str)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unknown wallet flag: %s", flag_str)); + } + + auto flag = WALLET_FLAG_MAP.at(flag_str); + + if (!(flag & MUTABLE_WALLET_FLAGS)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is immutable: %s", flag_str)); + } + + UniValue res(UniValue::VOBJ); + + if (pwallet->IsWalletFlagSet(flag) == value) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Wallet flag is already set to %s: %s", value ? "true" : "false", flag_str)); + } + + res.pushKV("flag_name", flag_str); + res.pushKV("flag_state", value); + + if (value) { + pwallet->SetWalletFlag(flag); + } else { + pwallet->UnsetWalletFlag(flag); + } + + if (flag && value && WALLET_FLAG_CAVEATS.count(flag)) { + res.pushKV("warnings", WALLET_FLAG_CAVEATS.at(flag)); + } + + return res; +} + static UniValue createwallet(const JSONRPCRequest& request) { RPCHelpMan{ @@ -2713,6 +2800,7 @@ static UniValue createwallet(const JSONRPCRequest& request) {"disable_private_keys", RPCArg::Type::BOOL, /* default */ "false", "Disable the possibility of private keys (only watchonlys are possible in this mode)."}, {"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."}, {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, + {"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -2747,6 +2835,10 @@ static UniValue createwallet(const JSONRPCRequest& request) } } + if (!request.params[4].isNull() && request.params[4].get_bool()) { + flags |= WALLET_FLAG_AVOID_REUSE; + } + bilingual_str error; std::shared_ptr wallet; WalletCreationStatus status = CreateWallet(*context.chain, passphrase, flags, request.params[0].get_str(), error, warnings, wallet); @@ -2810,6 +2902,11 @@ static UniValue unloadwallet(const JSONRPCRequest& request) static UniValue listunspent(const JSONRPCRequest& request) { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + bool avoid_reuse = pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); RPCHelpMan{"listunspent", "\nReturns array of unspent transaction outputs\n" "with between minconf and maxconf (inclusive) confirmations.\n" @@ -2852,6 +2949,7 @@ static UniValue listunspent(const JSONRPCRequest& request) {RPCResult::Type::BOOL, "spendable", "Whether we have the private keys to spend this output"}, {RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"}, {RPCResult::Type::STR, "desc", "(only when solvable) A descriptor for spending this output"}, + {RPCResult::Type::BOOL, "reused", /* optional*/ true, "Whether this output is reused/dirty (sent to an address that was previously spent from)"}, {RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions" " from outside keys and unconfirmed replacement transactions are considered unsafe\n" "and are not eligible for spending by fundrawtransaction and sendtoaddress."}, @@ -2867,10 +2965,6 @@ static UniValue listunspent(const JSONRPCRequest& request) }, }.Check(request); - std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); - if (!wallet) return NullUniValue; - CWallet* const pwallet = wallet.get(); - int nMinDepth = 1; if (!request.params[0].isNull()) { RPCTypeCheckArgument(request.params[0], UniValue::VNUM); @@ -2960,6 +3054,8 @@ static UniValue listunspent(const JSONRPCRequest& request) UniValue results(UniValue::VARR); std::vector vecOutputs; { + coinControl.m_avoid_address_reuse = false; + LOCK(pwallet->cs_wallet); pwallet->AvailableCoins(vecOutputs, !include_unsafe, &coinControl, nMinimumAmount, nMaximumAmount, nMinimumSumAmount, nMaximumCount, nMinDepth, nMaxDepth); } @@ -2970,6 +3066,7 @@ static UniValue listunspent(const JSONRPCRequest& request) CTxDestination address; const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey; bool fValidAddress = ExtractDestination(scriptPubKey, address); + bool reused = avoid_reuse && pwallet->IsUsedDestination(address); if (destinations.size() && (!fValidAddress || !destinations.count(address))) continue; @@ -3004,6 +3101,7 @@ static UniValue listunspent(const JSONRPCRequest& request) auto descriptor = InferDescriptor(scriptPubKey, *pwallet); entry.pushKV("desc", descriptor->ToString()); } + if (avoid_reuse) entry.pushKV("reused", reused); entry.pushKV("safe", out.fSafe); entry.pushKV("coinjoin_rounds", pwallet->GetRealOutpointCoinJoinRounds(COutPoint(out.tx->GetHash(), out.i))); results.push_back(entry); @@ -3872,14 +3970,14 @@ static const CRPCCommand commands[] = { "wallet", "abortrescan", &abortrescan, {} }, { "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label"} }, { "wallet", "backupwallet", &backupwallet, {"destination"} }, - { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase"} }, + { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} }, { "wallet", "dumphdinfo", &dumphdinfo, {} }, { "wallet", "dumpprivkey", &dumpprivkey, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, {"passphrase"} }, { "wallet", "getaddressesbylabel", &getaddressesbylabel, {"label"} }, { "wallet", "getaddressinfo", &getaddressinfo, {"address"} }, - { "wallet", "getbalance", &getbalance, {"dummy","minconf","addlocked","include_watchonly"} }, + { "wallet", "getbalance", &getbalance, {"dummy","minconf","addlocked","include_watchonly", "avoid_reuse"} }, { "wallet", "getnewaddress", &getnewaddress, {"label"} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, {} }, { "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf","addlocked"} }, @@ -3911,11 +4009,12 @@ static const CRPCCommand commands[] = { "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} }, { "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} }, { "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","addlocked","comment","subtractfeefrom","use_is","use_cj","conf_target","estimate_mode"} }, - { "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode"} }, + { "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode", "avoid_reuse"} }, { "wallet", "setcoinjoinrounds", &setcoinjoinrounds, {"rounds"} }, { "wallet", "setcoinjoinamount", &setcoinjoinamount, {"amount"} }, { "wallet", "setlabel", &setlabel, {"address","label"} }, { "wallet", "settxfee", &settxfee, {"amount"} }, + { "wallet", "setwalletflag", &setwalletflag, {"flag","value"} }, { "wallet", "signmessage", &signmessage, {"address","message"} }, { "wallet", "signrawtransactionwithwallet", &signrawtransactionwithwallet, {"hexstring","prevtxs","sighashtype"} }, { "wallet", "unloadwallet", &unloadwallet, {"wallet_name"} }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index bff3987702..4a0af87c6d 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -53,6 +53,14 @@ #include +const std::map WALLET_FLAG_CAVEATS{ + {WALLET_FLAG_AVOID_REUSE, + "You need to rescan the blockchain in order to correctly mark used " + "destinations in the past. Until this is done, some destinations may " + "be considered unused, even if the opposite is the case." + }, +}; + static const size_t OUTPUT_GROUP_MAX_ENTRIES = 10; static CCriticalSection cs_wallets; @@ -1178,6 +1186,37 @@ void CWallet::MarkDirty() fAnonymizableTallyCachedNonDenom = false; } +void CWallet::SetUsedDestinationState(const uint256& hash, unsigned int n, bool used) +{ + const CWalletTx* srctx = GetWalletTx(hash); + if (!srctx) return; + + CTxDestination dst; + if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) { + if (::IsMine(*this, dst)) { + LOCK(cs_wallet); + if (used && !GetDestData(dst, "used", nullptr)) { + AddDestData(dst, "used", "p"); // p for "present", opposite of absent (null) + } else if (!used && GetDestData(dst, "used", nullptr)) { + EraseDestData(dst, "used"); + } + } + } +} + +bool CWallet::IsUsedDestination(const CTxDestination& dst) const +{ + LOCK(cs_wallet); + return ::IsMine(*this, dst) && GetDestData(dst, "used", nullptr); +} + +bool CWallet::IsUsedDestination(const uint256& hash, unsigned int n) const +{ + CTxDestination dst; + const CWalletTx* srctx = GetWalletTx(hash); + return srctx && ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst) && IsUsedDestination(dst); +} + bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose) { LOCK(cs_wallet); @@ -1186,6 +1225,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose) uint256 hash = wtxIn.GetHash(); + if (IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) { + // Mark used destinations + for (const CTxIn& txin : wtxIn.tx->vin) { + const COutPoint& op = txin.prevout; + SetUsedDestinationState(op.hash, op.n, true); + } + } + // Inserts only if not already there, returns tx inserted or tx found std::pair::iterator, bool> ret = mapWallet.insert(std::make_pair(hash, wtxIn)); CWalletTx& wtx = (*ret.first).second; @@ -2122,7 +2169,7 @@ void CWallet::UnsetWalletFlag(WalletBatch& batch, uint64_t flag) throw std::runtime_error(std::string(__func__) + ": writing wallet flags failed"); } -bool CWallet::IsWalletFlagSet(uint64_t flag) +bool CWallet::IsWalletFlagSet(uint64_t flag) const { return (m_wallet_flags & flag); } @@ -2131,7 +2178,7 @@ bool CWallet::SetWalletFlags(uint64_t overwriteFlags, bool memonly) { LOCK(cs_wallet); m_wallet_flags = overwriteFlags; - if (((overwriteFlags & g_known_wallet_flags) >> 32) ^ (overwriteFlags >> 32)) { + if (((overwriteFlags & KNOWN_WALLET_FLAGS) >> 32) ^ (overwriteFlags >> 32)) { // contains unknown non-tolerable wallet flags return false; } @@ -2631,7 +2678,7 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter return 0; // Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future). - bool allow_cache = filter == ISMINE_SPENDABLE || filter == ISMINE_WATCH_ONLY; + bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL; // Must wait until coinbase is safely deep enough in the chain before valuing it if (IsImmatureCoinBase()) @@ -2641,11 +2688,12 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter return m_amounts[AVAILABLE_CREDIT].m_value[filter]; } + bool allow_used_addresses = (filter & ISMINE_USED) || !pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); CAmount nCredit = 0; uint256 hashTx = GetHash(); for (unsigned int i = 0; i < tx->vout.size(); i++) { - if (!pwallet->IsSpent(hashTx, i)) + if (!pwallet->IsSpent(hashTx, i) && (allow_used_addresses || !pwallet->IsUsedDestination(hashTx, i))) { const CTxOut &txout = tx->vout[i]; nCredit += pwallet->GetCredit(txout, filter); @@ -2899,16 +2947,17 @@ std::unordered_set CWallet::GetSpendableTXs() return ret; } -CWallet::Balance CWallet::GetBalance(const int min_depth, const bool fAddLocked, const CCoinControl* coinControl) const +CWallet::Balance CWallet::GetBalance(const int min_depth, const bool avoid_reuse, const bool fAddLocked, const CCoinControl* coinControl) const { Balance ret; + isminefilter reuse_filter = avoid_reuse ? 0 : ISMINE_USED; { LOCK(cs_wallet); for (auto pcoin : GetSpendableTXs()) { const bool is_trusted{pcoin->IsTrusted()}; const int tx_depth{pcoin->GetDepthInMainChain()}; - const CAmount tx_credit_mine{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE)}; - const CAmount tx_credit_watchonly{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY)}; + const CAmount tx_credit_mine{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_SPENDABLE | reuse_filter)}; + const CAmount tx_credit_watchonly{pcoin->GetAvailableCredit(/* fUseCache */ true, ISMINE_WATCH_ONLY | reuse_filter)}; if (is_trusted && ((tx_depth >= min_depth) || (fAddLocked && pcoin->IsLockedByInstantSend()))) { ret.m_mine_trusted += tx_credit_mine; ret.m_watchonly_trusted += tx_credit_watchonly; @@ -3020,6 +3069,9 @@ void CWallet::AvailableCoins(std::vector &vCoins, bool fOnlySafe, const CoinType nCoinType = coinControl ? coinControl->nCoinType : CoinType::ALL_COINS; CAmount nTotal = 0; + // Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where + // a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses + bool allow_used_addresses = !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || (coinControl && !coinControl->m_avoid_address_reuse); for (auto pcoin : GetSpendableTXs()) { const uint256& wtxid = pcoin->GetHash(); @@ -3084,6 +3136,9 @@ void CWallet::AvailableCoins(std::vector &vCoins, bool fOnlySafe, const continue; } + if (!allow_used_addresses && IsUsedDestination(wtxid, i)) { + continue; + } bool solvable = IsSolvable(*this, pcoin->tx->vout[i].scriptPubKey); bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); @@ -5071,12 +5126,8 @@ std::shared_ptr CWallet::CreateWalletFromFile(interfaces::Chain& chain, if (fFirstRun) { - if ((wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - //selective allow to set flags - walletInstance->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); - } else if (wallet_creation_flags & WALLET_FLAG_BLANK_WALLET) { - walletInstance->SetWalletFlag(WALLET_FLAG_BLANK_WALLET); - } else { + walletInstance->SetWalletFlags(wallet_creation_flags, false); + if (!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) { // Create new HD chain if (gArgs.GetBoolArg("-usehd", DEFAULT_USE_HD_WALLET) && !walletInstance->IsHDEnabled()) { std::string strSeed = gArgs.GetArg("-hdseed", "not hex"); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index c9c89833c5..77ecf85aed 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -132,6 +132,10 @@ enum WalletFlags : uint64_t { // wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown // unknown wallet flags in the lower section <= (1 << 31) will be tolerated + // will categorize coins as clean (not reused) and dirty (reused), and handle + // them with privacy considerations in mind + WALLET_FLAG_AVOID_REUSE = (1ULL << 0), + // Indicates that the metadata has already been upgraded to contain key origins WALLET_FLAG_KEY_ORIGIN_METADATA = (1ULL << 1), @@ -151,7 +155,23 @@ enum WalletFlags : uint64_t { WALLET_FLAG_BLANK_WALLET = (1ULL << 33), }; -static constexpr uint64_t g_known_wallet_flags = WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET | WALLET_FLAG_KEY_ORIGIN_METADATA; +static constexpr uint64_t KNOWN_WALLET_FLAGS = + WALLET_FLAG_AVOID_REUSE + | WALLET_FLAG_BLANK_WALLET + | WALLET_FLAG_KEY_ORIGIN_METADATA + | WALLET_FLAG_DISABLE_PRIVATE_KEYS; + +static constexpr uint64_t MUTABLE_WALLET_FLAGS = + WALLET_FLAG_AVOID_REUSE; + +static const std::map WALLET_FLAG_MAP{ + {"avoid_reuse", WALLET_FLAG_AVOID_REUSE}, + {"blank", WALLET_FLAG_BLANK_WALLET}, + {"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA}, + {"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS}, +}; + +extern const std::map WALLET_FLAG_CAVEATS; /** A key from a CWallet's keypool * @@ -1005,6 +1025,12 @@ public: bool IsFullyMixed(const COutPoint& outpoint) const; bool IsSpent(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + + // Whether this or any UTXO with the same CTxDestination has been spent. + bool IsUsedDestination(const CTxDestination& dst) const; + bool IsUsedDestination(const uint256& hash, unsigned int n) const; + void SetUsedDestinationState(const uint256& hash, unsigned int n, bool used); + std::vector GroupOutputs(const std::vector& outputs, bool single_coin) const; bool IsLockedCoin(uint256 hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -1144,7 +1170,7 @@ public: CAmount m_denominated_trusted{0}; CAmount m_denominated_untrusted_pending{0}; }; - Balance GetBalance(int min_depth = 0, const bool fAddLocked = false, const CCoinControl* coinControl = nullptr) const; + Balance GetBalance(const int min_depth = 0, const bool avoid_reuse = true, const bool fAddLocked = false, const CCoinControl* coinControl = nullptr) const; CAmount GetAnonymizableBalance(bool fSkipDenominated = false, bool fSkipUnconfirmed = true) const; float GetAverageAnonymizedRounds() const; @@ -1420,7 +1446,7 @@ public: void UnsetWalletFlag(WalletBatch& batch, uint64_t flag); /** check if a certain wallet flag is set */ - bool IsWalletFlagSet(uint64_t flag); + bool IsWalletFlagSet(uint64_t flag) const; /** overwrite all flags by the given uint64_t returns false if unknown, non-tolerable flags are present */ diff --git a/src/warnings.cpp b/src/warnings.cpp index 63a595341a..f50a997b2c 100644 --- a/src/warnings.cpp +++ b/src/warnings.cpp @@ -39,41 +39,34 @@ void SetfLargeWorkInvalidChainFound(bool flag) fLargeWorkInvalidChainFound = flag; } -std::string GetWarnings(const std::string& strFor) +std::string GetWarnings(bool verbose) { - std::string strStatusBar; - std::string strGUI; - const std::string uiAlertSeparator = "
"; + std::string warnings_concise; + std::string warnings_verbose; + const std::string warning_separator = "
"; LOCK(g_warnings_mutex); + // Pre-release build warning if (!CLIENT_VERSION_IS_RELEASE) { - strStatusBar = "This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"; - strGUI = _("This is a pre-release test build - use at your own risk - do not use for mining or merchant applications").translated; + warnings_concise = "This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"; + warnings_verbose = _("This is a pre-release test build - use at your own risk - do not use for mining or merchant applications").translated; } // Misc warnings like out of disk space and clock is wrong - if (strMiscWarning != "") - { - strStatusBar = strMiscWarning; - strGUI += (strGUI.empty() ? "" : uiAlertSeparator) + strMiscWarning; + if (strMiscWarning != "") { + warnings_concise = strMiscWarning; + warnings_verbose += (warnings_verbose.empty() ? "" : warning_separator) + strMiscWarning; } - if (fLargeWorkForkFound) - { - strStatusBar = "Warning: The network does not appear to fully agree! Some miners appear to be experiencing issues."; - strGUI += (strGUI.empty() ? "" : uiAlertSeparator) + _("Warning: The network does not appear to fully agree! Some miners appear to be experiencing issues.").translated; - } - else if (fLargeWorkInvalidChainFound) - { - strStatusBar = "Warning: We do not appear to fully agree with our peers! You may need to upgrade, or other nodes may need to upgrade."; - strGUI += (strGUI.empty() ? "" : uiAlertSeparator) + _("Warning: We do not appear to fully agree with our peers! You may need to upgrade, or other nodes may need to upgrade.").translated; + if (fLargeWorkForkFound) { + warnings_concise = "Warning: The network does not appear to fully agree! Some miners appear to be experiencing issues."; + warnings_verbose += (warnings_verbose.empty() ? "" : warning_separator) + _("Warning: The network does not appear to fully agree! Some miners appear to be experiencing issues.").translated; + } else if (fLargeWorkInvalidChainFound) { + warnings_concise = "Warning: We do not appear to fully agree with our peers! You may need to upgrade, or other nodes may need to upgrade."; + warnings_verbose += (warnings_verbose.empty() ? "" : warning_separator) + _("Warning: We do not appear to fully agree with our peers! You may need to upgrade, or other nodes may need to upgrade.").translated; } - if (strFor == "gui") - return strGUI; - else if (strFor == "statusbar") - return strStatusBar; - assert(!"GetWarnings(): invalid parameter"); - return "error"; + if (verbose) return warnings_verbose; + else return warnings_concise; } diff --git a/src/warnings.h b/src/warnings.h index 5b2a18ee47..4bf7af5368 100644 --- a/src/warnings.h +++ b/src/warnings.h @@ -13,11 +13,11 @@ void SetfLargeWorkForkFound(bool flag); bool GetfLargeWorkForkFound(); void SetfLargeWorkInvalidChainFound(bool flag); /** Format a string that describes several potential problems detected by the core. - * @param[in] strFor can have the following values: - * - "statusbar": get the most important warning - * - "gui": get all warnings, translated (where possible) for GUI, separated by
- * @returns the warning string selected by strFor + * @param[in] verbose bool + * - if true, get all warnings, translated (where possible), separated by
+ * - if false, get the most important warning + * @returns the warning string */ -std::string GetWarnings(const std::string& strFor); +std::string GetWarnings(bool verbose); #endif // BITCOIN_WARNINGS_H diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index e7df1ddfe0..730ddb92ef 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -389,9 +389,9 @@ class RawTransactionsTest(BitcoinTestFramework): # and sendrawtransaction should throw assert_raises_rpc_error(-26, "absurdly-high-fee", self.nodes[2].sendrawtransaction, rawTxSigned['hex'], 0.00001000) # And below calls should both succeed - testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']], maxfeerate=0.00007000)[0] + testres = self.nodes[2].testmempoolaccept(rawtxs=[rawTxSigned['hex']], maxfeerate='0.00007000')[0] assert_equal(testres['allowed'], True) - self.nodes[2].sendrawtransaction(hexstring=rawTxSigned['hex'], maxfeerate=0.00007000) + self.nodes[2].sendrawtransaction(hexstring=rawTxSigned['hex'], maxfeerate='0.00007000') if __name__ == '__main__': diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d647ae774c..0cea9d58d0 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -141,6 +141,7 @@ BASE_SCRIPTS = [ 'rpc_misc.py', 'interface_rest.py', 'mempool_spend_coinbase.py', + 'wallet_avoidreuse.py', 'mempool_reorg.py', 'mempool_persist.py', 'wallet_multiwallet.py', diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py new file mode 100755 index 0000000000..8f75da9569 --- /dev/null +++ b/test/functional/wallet_avoidreuse.py @@ -0,0 +1,217 @@ +#!/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 avoid_reuse and setwalletflag features.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + connect_nodes, +) + +# TODO: Copied from wallet_groups.py -- should perhaps move into util.py +def assert_approx(v, vexp, vspan=0.00001): + if v < vexp - vspan: + raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) + if v > vexp + vspan: + raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) + +def reset_balance(node, discardaddr): + '''Throw away all owned coins by the node so it gets a balance of 0.''' + balance = node.getbalance(avoid_reuse=False) + if balance > 0.5: + node.sendtoaddress(address=discardaddr, amount=balance, subtractfeefromamount=True, avoid_reuse=False) + +def count_unspent(node): + '''Count the unspent outputs for the given node and return various statistics''' + r = { + "total": { + "count": 0, + "sum": 0, + }, + "reused": { + "count": 0, + "sum": 0, + }, + } + supports_reused = True + for utxo in node.listunspent(minconf=0): + r["total"]["count"] += 1 + r["total"]["sum"] += utxo["amount"] + if supports_reused and "reused" in utxo: + if utxo["reused"]: + r["reused"]["count"] += 1 + r["reused"]["sum"] += utxo["amount"] + else: + supports_reused = False + r["reused"]["supported"] = supports_reused + return r + +def assert_unspent(node, total_count=None, total_sum=None, reused_supported=None, reused_count=None, reused_sum=None): + '''Make assertions about a node's unspent output statistics''' + stats = count_unspent(node) + if total_count is not None: + assert_equal(stats["total"]["count"], total_count) + if total_sum is not None: + assert_approx(stats["total"]["sum"], total_sum, 0.001) + if reused_supported is not None: + assert_equal(stats["reused"]["supported"], reused_supported) + if reused_count is not None: + assert_equal(stats["reused"]["count"], reused_count) + if reused_sum is not None: + assert_approx(stats["reused"]["sum"], reused_sum, 0.001) + +class AvoidReuseTest(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = False + self.num_nodes = 2 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + '''Set up initial chain and run tests defined below''' + + self.test_persistence() + self.test_immutable() + + self.nodes[0].generate(110) + self.sync_all() + reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) + self.test_fund_send_fund_senddirty() + reset_balance(self.nodes[1], self.nodes[0].getnewaddress()) + self.test_fund_send_fund_send() + + def test_persistence(self): + '''Test that wallet files persist the avoid_reuse flag.''' + # Configure node 1 to use avoid_reuse + self.nodes[1].setwalletflag('avoid_reuse') + + # Flags should be node1.avoid_reuse=false, node2.avoid_reuse=true + assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) + assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) + + # Stop and restart node 1 + self.stop_node(1) + self.start_node(1) + connect_nodes(self.nodes[0], 1) + + # Flags should still be node1.avoid_reuse=false, node2.avoid_reuse=true + assert_equal(self.nodes[0].getwalletinfo()["avoid_reuse"], False) + assert_equal(self.nodes[1].getwalletinfo()["avoid_reuse"], True) + + # Attempting to set flag to its current state should throw + assert_raises_rpc_error(-8, "Wallet flag is already set to false", self.nodes[0].setwalletflag, 'avoid_reuse', False) + assert_raises_rpc_error(-8, "Wallet flag is already set to true", self.nodes[1].setwalletflag, 'avoid_reuse', True) + + def test_immutable(self): + '''Test immutable wallet flags''' + # Attempt to set the disable_private_keys flag; this should not work + assert_raises_rpc_error(-8, "Wallet flag is immutable", self.nodes[1].setwalletflag, 'disable_private_keys') + + tempwallet = ".wallet_avoidreuse.py_test_immutable_wallet.dat" + + # Create a wallet with disable_private_keys set; this should work + self.nodes[1].createwallet(tempwallet, True) + w = self.nodes[1].get_wallet_rpc(tempwallet) + + # Attempt to unset the disable_private_keys flag; this should not work + assert_raises_rpc_error(-8, "Wallet flag is immutable", w.setwalletflag, 'disable_private_keys', False) + + # Unload temp wallet + self.nodes[1].unloadwallet(tempwallet) + + def test_fund_send_fund_senddirty(self): + ''' + Test the same as test_fund_send_fund_send, except send the 10 BTC with + the avoid_reuse flag set to false. This means the 10 BTC send should succeed, + where it fails in test_fund_send_fund_send. + ''' + + fundaddr = self.nodes[1].getnewaddress() + retaddr = self.nodes[0].getnewaddress() + + self.nodes[0].sendtoaddress(fundaddr, 10) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 1 single, unused 10 btc output + assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) + + self.nodes[1].sendtoaddress(retaddr, 5) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 1 single, unused 5 btc output + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) + + self.nodes[0].sendtoaddress(fundaddr, 10) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) + assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) + + self.nodes[1].sendtoaddress(address=retaddr, amount=10, avoid_reuse=False) + + # listunspent should show 1 total outputs (5 btc), unused + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_count=0) + + # node 1 should now have about 5 btc left (for both cases) + assert_approx(self.nodes[1].getbalance(), 5, 0.001) + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 5, 0.001) + + def test_fund_send_fund_send(self): + ''' + Test the simple case where [1] generates a new address A, then + [0] sends 10 BTC to A. + [1] spends 5 BTC from A. (leaving roughly 5 BTC useable) + [0] sends 10 BTC to A again. + [1] tries to spend 10 BTC (fails; dirty). + [1] tries to spend 4 BTC (succeeds; change address sufficient) + ''' + + fundaddr = self.nodes[1].getnewaddress() + retaddr = self.nodes[0].getnewaddress() + + self.nodes[0].sendtoaddress(fundaddr, 10) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 1 single, unused 10 btc output + assert_unspent(self.nodes[1], total_count=1, total_sum=10, reused_supported=True, reused_count=0) + + self.nodes[1].sendtoaddress(retaddr, 5) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 1 single, unused 5 btc output + assert_unspent(self.nodes[1], total_count=1, total_sum=5, reused_supported=True, reused_count=0) + + self.nodes[0].sendtoaddress(fundaddr, 10) + self.nodes[0].generate(1) + self.sync_all() + + # listunspent should show 2 total outputs (5, 10 btc), one unused (5), one reused (10) + assert_unspent(self.nodes[1], total_count=2, total_sum=15, reused_count=1, reused_sum=10) + + # node 1 should now have a balance of 5 (no dirty) or 15 (including dirty) + assert_approx(self.nodes[1].getbalance(), 5, 0.001) + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 15, 0.001) + + assert_raises_rpc_error(-6, "Insufficient funds", self.nodes[1].sendtoaddress, retaddr, 10) + + self.nodes[1].sendtoaddress(retaddr, 4) + + # listunspent should show 2 total outputs (1, 10 btc), one unused (1), one reused (10) + assert_unspent(self.nodes[1], total_count=2, total_sum=11, reused_count=1, reused_sum=10) + + # node 1 should now have about 1 btc left (no dirty) and 11 (including dirty) + assert_approx(self.nodes[1].getbalance(), 1, 0.001) + assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001) + +if __name__ == '__main__': + AvoidReuseTest().main() diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 63072e2c61..2a9f21e643 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -79,6 +79,9 @@ class WalletTest(BitcoinTestFramework): assert_equal(self.nodes[0].getbalance("*"), 500) assert_equal(self.nodes[0].getbalance("*", 1), 500) assert_equal(self.nodes[0].getbalance("*", 1, True), 500) + assert_equal(self.nodes[0].getbalance("*", 1, True, False), 500) + assert_equal(self.nodes[0].getbalance(minconf=1, addlocked=True), 500) + assert_equal(self.nodes[0].getbalance(minconf=1, avoid_reuse=False), 500) assert_equal(self.nodes[0].getbalance(minconf=1), 500) assert_equal(self.nodes[0].getbalance(minconf=0, include_watchonly=True), 1000) assert_equal(self.nodes[1].getbalance(minconf=0, include_watchonly=True), 500)