Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/qt/forms/optionsdialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,66 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="dustProtection">
<property name="toolTip">
<string>Automatically lock small incoming transactions from external sources that may be dust attacks. Locked UTXOs will be excluded from coin selection.</string>
</property>
<property name="text">
<string>Enable &amp;dust attack protection</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutDustThreshold">
<item>
<widget class="QLabel" name="dustThresholdLabel">
<property name="text">
<string>Dust threshold:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="dustProtectionThreshold">
<property name="toolTip">
<string>Transactions with outputs at or below this amount will be considered dust when received from external sources.</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
<property name="value">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dustProtectionThresholdUnitLabel">
<property name="text">
<string>duffs</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
Comment on lines +446 to +453
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -nC3 'dustProtectionThresholdUnitLabel' --type=cpp

Repository: dashpay/dash

Length of output: 551


Fix hardcoded unit label to be network-aware.

The unit label is hardcoded to "duffs" in both the UI file (line 446-453) and at runtime in src/qt/optionsdialog.cpp:78 via BitcoinUnits::name(BitcoinUnits::Unit::duffs). This conflicts with the PR objective to use a network-aware label (duffs/tduffs). On testnet, the label should display "tduffs" instead of "duffs".

🤖 Prompt for AI Agents
In @src/qt/forms/optionsdialog.ui around lines 446 - 453, Remove the hardcoded
"duffs" string from the optionsdialog.ui widget
(dustProtectionThresholdUnitLabel) and instead set that label at runtime in
src/qt/optionsdialog.cpp using the network-aware unit name; replace the current
call that uses BitcoinUnits::name(BitcoinUnits::Unit::duffs) with the
network-aware variant (use the BitcoinUnits::name overload that derives the unit
name from the active network or obtain the unit string from chain params), and
assign it to dustProtectionThresholdUnitLabel->setText(...) in the OptionsDialog
initialization so the label shows "duffs" or "tduffs" depending on the active
network.

</item>
<item>
<spacer name="horizontalSpacerDustThreshold">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
Expand Down
7 changes: 7 additions & 0 deletions src/qt/optionsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ OptionsDialog::OptionsDialog(QWidget *parent, bool enableWallet) :
ui->pruneSize->setEnabled(false);
connect(ui->prune, &QPushButton::toggled, ui->pruneSize, &QWidget::setEnabled);

/* Dust protection */
ui->dustProtectionThreshold->setEnabled(false);
ui->dustProtectionThresholdUnitLabel->setText(BitcoinUnits::name(BitcoinUnits::Unit::duffs));
connect(ui->dustProtection, &QCheckBox::toggled, ui->dustProtectionThreshold, &QWidget::setEnabled);

/* Wallet */
ui->coinJoinEnabled->setText(tr("Enable %1 features").arg(QString::fromStdString(gCoinJoinName)));

Expand Down Expand Up @@ -344,6 +349,8 @@ void OptionsDialog::setMapper()
mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls);
mapper->addMapping(ui->keepChangeAddress, OptionsModel::KeepChangeAddress);
mapper->addMapping(ui->dustProtection, OptionsModel::DustProtection);
mapper->addMapping(ui->dustProtectionThreshold, OptionsModel::DustProtectionThreshold);
mapper->addMapping(ui->showMasternodesTab, OptionsModel::ShowMasternodesTab);
mapper->addMapping(ui->showGovernanceTab, OptionsModel::ShowGovernanceTab);
mapper->addMapping(ui->showAdvancedCJUI, OptionsModel::ShowAdvancedCJUI);
Expand Down
23 changes: 23 additions & 0 deletions src/qt/optionsmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,15 @@ bool OptionsModel::Init(bilingual_str& error)

if (!settings.contains("fLowKeysWarning"))
settings.setValue("fLowKeysWarning", true);

// Dust protection
if (!settings.contains("fDustProtection"))
settings.setValue("fDustProtection", false);
fDustProtection = settings.value("fDustProtection", false).toBool();

if (!settings.contains("nDustProtectionThreshold"))
settings.setValue("nDustProtectionThreshold", (qlonglong)DEFAULT_DUST_PROTECTION_THRESHOLD);
nDustProtectionThreshold = settings.value("nDustProtectionThreshold", (qlonglong)DEFAULT_DUST_PROTECTION_THRESHOLD).toLongLong();
#endif // ENABLE_WALLET

// These are shared with the core or have a command-line parameter
Expand Down Expand Up @@ -705,6 +714,10 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con
return settings.value("enable_psbt_controls");
case KeepChangeAddress:
return fKeepChangeAddress;
case DustProtection:
return fDustProtection;
case DustProtectionThreshold:
return qlonglong(nDustProtectionThreshold);
#endif // ENABLE_WALLET
case Prune:
return PruneEnabled(setting());
Expand Down Expand Up @@ -987,6 +1000,16 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value, const std::
settings.setValue("fKeepChangeAddress", fKeepChangeAddress);
Q_EMIT keepChangeAddressChanged(fKeepChangeAddress);
break;
case DustProtection:
fDustProtection = value.toBool();
settings.setValue("fDustProtection", fDustProtection);
Q_EMIT dustProtectionChanged();
break;
case DustProtectionThreshold:
nDustProtectionThreshold = value.toLongLong();
settings.setValue("nDustProtectionThreshold", qlonglong(nDustProtectionThreshold));
Q_EMIT dustProtectionChanged();
break;
#endif // ENABLE_WALLET
case Prune:
if (changed()) {
Expand Down
10 changes: 10 additions & 0 deletions src/qt/optionsmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class Node;
extern const char *DEFAULT_GUI_PROXY_HOST;
static constexpr uint16_t DEFAULT_GUI_PROXY_PORT = 9050;

/** Default threshold for dust attack protection (in duffs) */
static constexpr qint64 DEFAULT_DUST_PROTECTION_THRESHOLD = 10000;

/**
* Convert configured prune target MiB to displayed GB. Round up to avoid underestimating max disk usage.
*/
Expand Down Expand Up @@ -95,6 +98,8 @@ class OptionsModel : public QAbstractListModel
Server, // bool
EnablePSBTControls, // bool
MaskValues, // bool
DustProtection, // bool
DustProtectionThreshold, // CAmount (in duffs)
Comment on lines +101 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider DustProtection to be CAmount;

Suggested change
DustProtection, // bool
DustProtectionThreshold, // CAmount (in duffs)
DustProtection, // CAmount (in duffs, disabled if equals to 0)

OptionIDRowCount,
};

Expand Down Expand Up @@ -130,6 +135,8 @@ class OptionsModel : public QAbstractListModel
bool getEnablePSBTControls() const { return m_enable_psbt_controls; }
bool getKeepChangeAddress() const { return fKeepChangeAddress; }
bool getShowAdvancedCJUI() { return fShowAdvancedCJUI; }
bool getDustProtection() const { return fDustProtection; }
qint64 getDustProtectionThreshold() const { return nDustProtectionThreshold; }
const QString& getOverriddenByCommandLine() { return strOverriddenByCommandLine; }
bool isOptionOverridden(const QString& option) const { return strOverriddenByCommandLine.contains(option); }
void emitCoinJoinEnabledChanged();
Expand Down Expand Up @@ -160,6 +167,8 @@ class OptionsModel : public QAbstractListModel
bool m_mask_values;
bool fKeepChangeAddress;
bool fShowAdvancedCJUI;
bool fDustProtection{false};
qint64 nDustProtectionThreshold{DEFAULT_DUST_PROTECTION_THRESHOLD};

/* settings that were overridden by command-line */
QString strOverriddenByCommandLine;
Expand All @@ -183,6 +192,7 @@ class OptionsModel : public QAbstractListModel
void keepChangeAddressChanged(bool);
void showTrayIconChanged(bool);
void fontForMoneyChanged(const QFont&);
void dustProtectionChanged();
};

Q_DECLARE_METATYPE(OptionsModel::FontChoice)
Expand Down
3 changes: 3 additions & 0 deletions src/qt/overviewpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <qt/optionsmodel.h>
#include <qt/transactionfilterproxy.h>
#include <qt/transactionoverviewwidget.h>
#include <qt/transactionrecord.h>
#include <qt/transactiontablemodel.h>
#include <qt/utilitydialog.h>
#include <qt/walletmodel.h>
Expand Down Expand Up @@ -759,6 +760,8 @@ void OverviewPage::SetupTransactionList(int nNumItems)
filter->setDynamicSortFilter(true);
filter->setSortRole(Qt::EditRole);
filter->setShowInactive(false);
// Exclude dust receive transactions from overview
filter->setTypeFilter(TransactionFilterProxy::ALL_TYPES & ~TransactionFilterProxy::TYPE(TransactionRecord::DustReceive));
filter->sort(TransactionTableModel::Date, Qt::DescendingOrder);
ui->listTransactions->setModel(filter.get());
}
Expand Down
21 changes: 20 additions & 1 deletion src/qt/transactionrecord.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ bool TransactionRecord::showTransaction()
/*
* Decompose CWallet transaction to model transaction records.
*/
QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx)
QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx,
bool dustProtectionEnabled, CAmount dustThreshold)
{
QList<TransactionRecord> parts;
int64_t nTime = wtx.time;
Expand All @@ -40,6 +41,15 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod
std::map<std::string, std::string> mapValue = wtx.value_map;
auto& coinJoinOptions = node.coinJoinOptions();

// Check if any inputs belong to this wallet (for dust detection)
bool isFromMe = false;
for (const isminetype mine : wtx.txin_is_mine) {
if (mine) {
isFromMe = true;
break;
}
}

if (nNet > 0 || wtx.is_coinbase || wtx.is_platform_transfer)
{
//
Expand Down Expand Up @@ -81,6 +91,15 @@ QList<TransactionRecord> TransactionRecord::decomposeTransaction(interfaces::Nod
sub.type = TransactionRecord::PlatformTransfer;
}

// Check for dust attack: external receive with small amount
// Only override if not already a special type (coinbase, platform transfer)
if (dustProtectionEnabled && !isFromMe && !wtx.is_coinbase && !wtx.is_platform_transfer &&
sub.credit > 0 && sub.credit <= dustThreshold &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider refactoring: move sub.credit > 0 && sub.credit <= dustThreshold && and !isFromMe to the helper and use it here in TransactionRecord::decomposeTransaction and in checkAndLockDustOutputs

(sub.type == TransactionRecord::RecvWithAddress || sub.type == TransactionRecord::RecvFromOther))
{
sub.type = TransactionRecord::DustReceive;
}

parts.append(sub);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/qt/transactionrecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class TransactionRecord
CoinJoinCreateDenominations,
CoinJoinSend,
PlatformTransfer,
DustReceive,
};

/** Number of confirmation recommended for accepting a transaction */
Expand Down Expand Up @@ -116,7 +117,8 @@ class TransactionRecord
/** Decompose CWallet transaction to model transaction records.
*/
static bool showTransaction();
static QList<TransactionRecord> decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx);
static QList<TransactionRecord> decomposeTransaction(interfaces::Node& node, interfaces::Wallet& wallet, const interfaces::WalletTx& wtx,
bool dustProtectionEnabled = false, CAmount dustThreshold = 0);

/** @name Immutable transaction attributes
@{*/
Expand Down
17 changes: 15 additions & 2 deletions src/qt/transactiontablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ class TransactionTablePriv
assert(!m_loaded || force);
cachedWallet.clear();
try {
bool dustProtection = parent->walletModel->getOptionsModel()->getDustProtection();
qint64 dustThreshold = parent->walletModel->getOptionsModel()->getDustProtectionThreshold();
for (const auto& wtx : wallet.getWalletTxs()) {
if (TransactionRecord::showTransaction()) {
cachedWallet.append(TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx));
cachedWallet.append(TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx, dustProtection, dustThreshold));
}
}
} catch(const std::exception& e) {
Expand Down Expand Up @@ -174,8 +176,10 @@ class TransactionTablePriv
break;
}
// Added -- insert at the right position
bool dustProtection = parent->walletModel->getOptionsModel()->getDustProtection();
qint64 dustThreshold = parent->walletModel->getOptionsModel()->getDustProtectionThreshold();
QList<TransactionRecord> toInsert =
TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx);
TransactionRecord::decomposeTransaction(parent->walletModel->node(), wallet, wtx, dustProtection, dustThreshold);
if(!toInsert.isEmpty()) /* only if something to insert */
{
parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
Expand Down Expand Up @@ -278,6 +282,8 @@ TransactionTableModel::TransactionTableModel(WalletModel *parent):
priv->refreshWallet(walletModel->wallet());

connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit);
// Refresh wallet when dust protection settings change to re-evaluate transaction types
connect(walletModel->getOptionsModel(), &OptionsModel::dustProtectionChanged, this, [this]() { refreshWallet(true); });
}

TransactionTableModel::~TransactionTableModel()
Expand Down Expand Up @@ -429,6 +435,8 @@ QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
return tr("Mined");
case TransactionRecord::PlatformTransfer:
return tr("Platform Transfer");
case TransactionRecord::DustReceive:
return tr("Dust Receive");

case TransactionRecord::CoinJoinMixing:
return tr("%1 Mixing").arg(QString::fromStdString(gCoinJoinName));
Expand Down Expand Up @@ -473,6 +481,7 @@ QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, b
case TransactionRecord::Generated:
case TransactionRecord::CoinJoinSend:
case TransactionRecord::PlatformTransfer:
case TransactionRecord::DustReceive:
return formatAddressLabel(wtx->strAddress, wtx->label, tooltip) + watchAddress;
case TransactionRecord::SendToOther:
return QString::fromStdString(wtx->strAddress) + watchAddress;
Expand All @@ -499,6 +508,7 @@ QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
case TransactionRecord::PlatformTransfer:
case TransactionRecord::CoinJoinSend:
case TransactionRecord::RecvWithCoinJoin:
case TransactionRecord::DustReceive:
{
if (wtx->label.isEmpty()) {
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BAREADDRESS);
Expand Down Expand Up @@ -550,6 +560,7 @@ QVariant TransactionTableModel::amountColor(const TransactionRecord *rec) const
case TransactionRecord::CoinJoinCollateralPayment:
case TransactionRecord::CoinJoinMakeCollaterals:
case TransactionRecord::CoinJoinCreateDenominations:
case TransactionRecord::DustReceive:
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::ORANGE);
}
return GUIUtil::getThemedQColor(GUIUtil::ThemedColor::DEFAULT);
Expand Down Expand Up @@ -737,6 +748,8 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
return formatTxAmount(rec, false, BitcoinUnits::SeparatorStyle::NEVER);
case StatusRole:
return rec->status.status;
case OutputIndexRole:
return rec->idx;
}
return QVariant();
}
Expand Down
2 changes: 2 additions & 0 deletions src/qt/transactiontablemodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class TransactionTableModel : public QAbstractTableModel
StatusRole,
/** Unprocessed icon */
RawDecorationRole,
/** Output index within transaction (for UTXO identification) */
OutputIndexRole,
};

int rowCount(const QModelIndex &parent) const override;
Expand Down
Loading
Loading