Merge pull request #4887 from knst/bc-bp-4

Bitcoin backports #13756 #15770 #17730 #17750 #17752 #17826 #17857 #15886
This commit is contained in:
PastaPastaPasta 2022-06-18 20:23:31 -07:00 committed by GitHub
commit aa97e52f26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 590 additions and 112 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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
{

View File

@ -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;

View File

@ -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);
}

View File

@ -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()

View File

@ -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;
}

View File

@ -53,17 +53,23 @@
#include <QFont>
#include <QFontDatabase>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QList>
#include <QMouseEvent>
#include <QPointer>
#include <QProgressDialog>
#include <QScreen>
#include <QSettings>
#include <QSize>
#include <QString>
#include <QTextDocument> // for Qt::mightBeRichText
#include <QThread>
#include <QTimer>
#include <QUrlQuery>
#include <QVBoxLayout>
#include <QtGlobal>
#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

View File

@ -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

View File

@ -319,23 +319,21 @@ void SendCoinsDialog::send(QList<SendCoinsRecipient> recipients)
QStringList formatted;
for (const SendCoinsRecipient &rcp : currentTransaction.getRecipients())
{
// generate bold amount string with wallet name in case of multiwallet
QString amount = "<b>" + 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(" <u>"+tr("from wallet %1").arg(GUIUtil::HtmlEscape(model->getWalletName()))+"</u> ");
amount.append(tr(" from wallet '%1'").arg(model->getWalletName()));
}
amount.append("</b> ");
// generate monospace address string
QString address = "<span style='font-family: monospace;'>" + rcp.address;
address.append("</span>");
// generate address string
QString address = rcp.address;
QString recipientElement;
recipientElement = "<br />";
{
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<SendCoinsRecipient> 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("<br /><br />");
questionString.append(formatted.join("<br />"));
questionString.append(formatted_short.join("<br />"));
questionString.append("<br />");
QString strCoinJoinName = QString::fromStdString(gCoinJoinName);
@ -372,6 +363,9 @@ void SendCoinsDialog::send(QList<SendCoinsRecipient> recipients)
questionString.append(tr("using") + " <b>" + tr("any available funds") + "</b>");
}
int messageEntries = formatted.size();
int displayedEntries = formatted_short.size();
if (displayedEntries < messageEntries) {
questionString.append("<br />");
questionString.append("<span style='" + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR) + "'>");
@ -437,9 +431,14 @@ void SendCoinsDialog::send(QList<SendCoinsRecipient> recipients)
questionString.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
.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<QMessageBox::StandardButton>(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();

View File

@ -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:

View File

@ -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;
}

View File

@ -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" },

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -46,6 +46,8 @@ public:
Optional<unsigned int> 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

View File

@ -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=<n>", 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

View File

@ -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 */

View File

@ -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<CWallet> 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<CWallet> 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<CWallet> 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<CWallet> 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<CWallet> 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<CWallet> 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<CWallet> 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<CWallet> 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<COutput> 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"} },

View File

@ -53,6 +53,14 @@
#include <boost/algorithm/string/replace.hpp>
const std::map<uint64_t,std::string> 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<std::map<uint256, CWalletTx>::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<const CWalletTx*, WalletTxHasher> 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<COutput> &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<COutput> &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> 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");

View File

@ -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<std::string,WalletFlags> 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<uint64_t,std::string> 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<OutputGroup> GroupOutputs(const std::vector<COutput>& 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 */

View File

@ -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 = "<hr />";
std::string warnings_concise;
std::string warnings_verbose;
const std::string warning_separator = "<hr />";
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;
}

View File

@ -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 <hr />
* @returns the warning string selected by strFor
* @param[in] verbose bool
* - if true, get all warnings, translated (where possible), separated by <hr />
* - 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

View File

@ -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__':

View File

@ -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',

View File

@ -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()

View File

@ -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)