Skip to content

feat: add -dustprotectionthreshold CLI option and unify GUI settings#7265

Open
UdjinM6 wants to merge 4 commits intodashpay:developfrom
UdjinM6:feat/dust-protection-cli
Open

feat: add -dustprotectionthreshold CLI option and unify GUI settings#7265
UdjinM6 wants to merge 4 commits intodashpay:developfrom
UdjinM6:feat/dust-protection-cli

Conversation

@UdjinM6
Copy link
Copy Markdown

@UdjinM6 UdjinM6 commented Apr 3, 2026

Issue being fixed or feature implemented

Add a command-line option -dustprotectionthreshold=<n> to automatically lock UTXOs from external transactions at or below <n> duffs, protecting against dust attacks.

Also add a functional test which covers external dust locking, self-send exclusion, above-threshold exclusion, disabled threshold, restart persistence, multi-wallet dust protection, and invalid argument rejection.

What was done?

Move dust detection logic into CWallet (IsDustProtectionTarget, CheckAndLockDustOutputs, LockExistingDustOutputs) so it works for both CLI (dashd) and GUI (dash-qt) without duplication.

Convert GUI dust protection from standalone QSettings to the CLI-shared settings framework (Prune pattern), so CLI flags take precedence and the Options dialog shows an override label. The -prev suffix remembers the user's threshold when disabling, restoring it on re-enable. Includes QSettings migration for existing users upgrading from the old GUI-only implementation, with clamping to the 1,000,000 duff maximum.

How Has This Been Tested?

Run tests, run qt and verify it behaves as expected

Breaking Changes

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

UdjinM6 and others added 2 commits April 2, 2026 23:10
Add a command-line option (-dustprotectionthreshold=<n>) to automatically
lock UTXOs from external transactions at or below <n> duffs, protecting
against dust attacks. Move dust detection logic into CWallet (IsDustProtectionTarget,
CheckAndLockDustOutputs, LockExistingDustOutputs) so it works for both
CLI (dashd) and GUI (dash-qt) without duplication.

Convert GUI dust protection from standalone QSettings to the CLI-shared
settings framework (Prune pattern), so CLI flags take precedence and the
Options dialog shows an override label. The -prev suffix remembers the
user's threshold when disabling, restoring it on re-enable. Includes
QSettings migration for existing users upgrading from the old GUI-only
implementation, with clamping to the 1,000,000 duff maximum.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover external dust locking, self-send exclusion, above-threshold
exclusion, disabled threshold, restart persistence, multi-wallet
dust protection, and invalid argument rejection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@UdjinM6 UdjinM6 added this to the 24 milestone Apr 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

Please coordinate with the authors of these PRs to avoid merge conflicts.

@thepastaclaw
Copy link
Copy Markdown

thepastaclaw commented Apr 3, 2026

✅ Review complete (commit 30b132e)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5fd0e8879e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f742bde6-61bc-44e2-bafb-3cf379cd7171

📥 Commits

Reviewing files that changed from the base of the PR and between 03f4ed0 and 30b132e.

📒 Files selected for processing (4)
  • src/qt/optionsmodel.cpp
  • src/qt/optionsmodel.h
  • src/wallet/interfaces.cpp
  • test/functional/wallet_dust_protection.py
✅ Files skipped from review due to trivial changes (1)
  • test/functional/wallet_dust_protection.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/wallet/interfaces.cpp

Walkthrough

Adds a dust-protection feature that auto-locks wallet UTXOs at-or-below a configurable threshold for incoming external transactions. Introduces CLI option -dustprotectionthreshold, wallet state (m_dust_protection_threshold) and core methods to classify and lock dust outputs on insertion and at startup. Extends interfaces::Wallet with setDustProtectionThreshold and lockExistingDustOutputs, updates GUI options wiring and migration, simplifies WalletModel locking logic to delegate to core, and adds functional tests validating behavior and argument validation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "CLI / Node Init"
    participant GUI as "Options Dialog"
    participant Options as "OptionsModel"
    participant WalletModel as "WalletModel"
    participant Interfaces as "interfaces::Wallet"
    participant WalletCore as "CWallet (core)"

    User->>CLI: start node with -dustprotectionthreshold=500
    CLI->>WalletCore: parse & validate threshold
    WalletCore->>WalletCore: set m_dust_protection_threshold
    WalletCore->>WalletCore: LockExistingDustOutputs() on startup

    Note over WalletCore: Incoming external tx (value ≤ threshold)
    WalletCore->>WalletCore: IsDustProtectionTarget() -> true
    WalletCore->>WalletCore: CheckAndLockDustOutputs() -> lock UTXO(s)

    User->>GUI: open Options Dialog
    GUI->>Options: load settings
    Options->>Options: detect -dustprotectionthreshold override
    alt CLI override present
        Options->>GUI: disable dust controls
    else
        Options->>GUI: enable dust controls
    end

    User->>GUI: change threshold
    GUI->>Options: setOption(DustProtectionThreshold)
    Options->>WalletModel: notify threshold change
    WalletModel->>Interfaces: setDustProtectionThreshold(threshold)
    Interfaces->>WalletCore: set m_dust_protection_threshold
    WalletModel->>Interfaces: lockExistingDustOutputs()
    Interfaces->>WalletCore: LockExistingDustOutputs()
    WalletCore->>WalletCore: re-classify & lock matching UTXO(s)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a CLI option and unifying GUI settings for dust protection, which is the primary objective of the PR.
Description check ✅ Passed The description is well-related to the changeset, clearly explaining the issue, implementation approach, and testing strategy for the dust protection feature across files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/qt/optionsmodel.cpp`:
- Around line 381-385: Init() currently only validates DustProtection but not
the remembered DustProtectionThreshold, so a malformed saved value (used by
getOption(DustProtectionThreshold) via dustprotectionthreshold-prev fallback)
can bypass startup checks; update Init() in optionsmodel.cpp to also load and
validate the remembered threshold by calling getOption(DustProtectionThreshold)
(or explicitly reading the stored dustprotectionthreshold-prev key via
SettingName(DustProtectionThreshold)), parse/validate it the same way as the
live value, and if invalid reset it to a safe default and persist that corrected
value (and/or clear the prev entry) so subsequent queries cannot throw; ensure
you reference the DustProtectionThreshold symbol, the Init() function, and
SettingName(...) when locating the code to change.

In `@src/wallet/wallet.cpp`:
- Around line 2818-2819: The current check uses IsMine(txout) == ISMINE_NO which
still allows watch-only outputs; change the gating to require spendability
instead. Replace the IsMine(txout) check so that only outputs where
IsMine(txout) == ISMINE_SPENDABLE are processed (return false otherwise),
ensuring only spendable dust outputs (txout) are auto-locked; reference the
IsMine function and the ISMINE_SPENDABLE constant when making this change.
- Around line 3381-3382: The format string in the strprintf call for the dust
protection threshold uses %d but MAX_DUST_PROTECTION_THRESHOLD is a
CAmount/int64_t; update the format to a 64-bit-safe specifier (e.g. use "%"
PRId64 from <inttypes.h>) and ensure the argument is cast to int64_t
(referencing walletInstance->m_dust_protection_threshold and
MAX_DUST_PROTECTION_THRESHOLD in the strprintf call) so the printed maximum
value is correctly formatted across platforms.

In `@test/functional/wallet_dust_protection.py`:
- Around line 158-173: Instead of using a count from node3.listunspent() to
infer dust-only UTXOs, capture the exact dust outpoints before restarting: call
node3.listunspent() and filter by the dust criterion (e.g., amounts <
dustprotectionthreshold/relay logic) to build a set of txid:vout strings, then
after restart compare node3.listlockunspent() against that expected set (using
node3.listlockunspent() and matching txid/vout) to assert the same outpoints are
locked; update the test around restart_node(3, ["-dustrelayfee=0",
"-dustprotectionthreshold=10000"]) and restart_node(3, ["-dustrelayfee=0"]) to
use this explicit expected_outpoints set rather than num_dust.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 521b4d83-2ccb-4197-a8f4-60f34dedbd0a

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca9da3 and 5fd0e88.

📒 Files selected for processing (12)
  • src/interfaces/wallet.h
  • src/qt/optionsdialog.cpp
  • src/qt/optionsmodel.cpp
  • src/qt/optionsmodel.h
  • src/qt/walletmodel.cpp
  • src/qt/walletmodel.h
  • src/wallet/init.cpp
  • src/wallet/interfaces.cpp
  • src/wallet/wallet.cpp
  • src/wallet/wallet.h
  • test/functional/test_runner.py
  • test/functional/wallet_dust_protection.py
💤 Files with no reviewable changes (1)
  • src/qt/walletmodel.h

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/functional/wallet_dust_protection.py`:
- Line 173: Rename the ambiguous loop variable 'l' used in the set
comprehensions to a clear name (e.g., 'entry' or 'locked_item') to satisfy lint
E741; specifically update the comprehension that builds locked_outpoints =
{(l['txid'], l['vout']) for l in locked} and the similar comprehension at the
later occurrence (line ~181) so they use the new identifier and adjust any
internal indexing (['txid'], ['vout']) to match the new name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab819c2a-2033-4bb6-a49b-866d6c745c5a

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd0e88 and 8aa5b12.

📒 Files selected for processing (1)
  • test/functional/wallet_dust_protection.py

Compare the exact set of (txid, vout) pairs instead of just a count,
making the assertion more precise and resilient to test-order changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@UdjinM6 UdjinM6 force-pushed the feat/dust-protection-cli branch from 8aa5b12 to 03f4ed0 Compare April 3, 2026 14:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/functional/wallet_dust_protection.py (1)

35-35: Consider centralizing threshold literals to reduce drift.

10000 (and max 1000000) appears in several places. A class-level constant (or two) would make future threshold-boundary updates safer.

Also applies to: 161-161, 169-169, 224-224, 248-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/functional/wallet_dust_protection.py` at line 35, Introduce module- or
test-class-level constants (e.g., DUST_PROTECTION_THRESHOLD = 10000 and
DUST_PROTECTION_THRESHOLD_MAX = 1000000) in
test/functional/wallet_dust_protection.py and replace all hardcoded occurrences
of 10000 and 1000000 (including the array entry ["-dustrelayfee=0",
"-dustprotectionthreshold=10000"] and the other noted locations) with those
constants; update any string arguments or comparisons to format or reference the
constants so threshold changes are centralized and avoid literal drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/functional/wallet_dust_protection.py`:
- Line 35: Introduce module- or test-class-level constants (e.g.,
DUST_PROTECTION_THRESHOLD = 10000 and DUST_PROTECTION_THRESHOLD_MAX = 1000000)
in test/functional/wallet_dust_protection.py and replace all hardcoded
occurrences of 10000 and 1000000 (including the array entry ["-dustrelayfee=0",
"-dustprotectionthreshold=10000"] and the other noted locations) with those
constants; update any string arguments or comparisons to format or reference the
constants so threshold changes are centralized and avoid literal drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 53b4330b-56e7-4e0b-b139-9d7520604476

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa5b12 and 03f4ed0.

📒 Files selected for processing (1)
  • test/functional/wallet_dust_protection.py

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Verified 7 findings from claude-general against source code. 1 blocking downgraded to suggestion (code duplication, not a bug), 1 finding invalidated (settings bypass doesn't exist), 1 dropped as duplicate. 5 findings confirmed with adjusted severities.

Reviewed commit: 03f4ed0

🟡 4 suggestion(s) | 💬 1 nitpick(s)

4 additional findings

🟡 suggestion: Duplicate InputIsMine: static copy in wallet.cpp duplicates public function in receive.cpp

src/wallet/wallet.cpp (line 1)

An identical static InputIsMine was added at src/wallet/wallet.cpp:59 that duplicates the public version at src/wallet/receive.cpp:13. Both have the exact same implementation (AssertLockHeld, GetWalletTx lookup, IsMine check). The static copy is used only at wallet.cpp:2823 in the dust protection check. Since wallet.cpp does not include receive.h, including it and removing the static copy would eliminate the duplication.

🟡 suggestion: Same constant name DEFAULT_DUST_PROTECTION_THRESHOLD used for two different values (0 vs 10000)

src/qt/optionsmodel.h (line 1)

src/wallet/wallet.h:115 defines DEFAULT_DUST_PROTECTION_THRESHOLD = 0 (CLI: disabled by default). src/qt/optionsmodel.h:28 defines DEFAULT_DUST_PROTECTION_THRESHOLD = 10000 (GUI: enabled with sensible default). Different scopes prevent ODR issues, and the design is intentional (CLI opt-in vs GUI protected-by-default). However, reusing the same identifier for different values is confusing. Consider renaming the GUI constant to DEFAULT_GUI_DUST_PROTECTION_THRESHOLD to match the existing MAX_GUI_DUST_PROTECTION_THRESHOLD pattern at optionsmodel.h:30.

🟡 suggestion: dustProtectionChanged signal emitted twice when toggling DustProtection off

src/qt/optionsmodel.cpp (line 1)

At src/qt/optionsmodel.cpp:1043-1053: when toggling DustProtection OFF, line 1045 recursively calls setOption(DustProtection, true, "-prev") to save the previous state. If the -prev value changed (typical after toggling ON which clears it at line 1051), the recursive call emits dustProtectionChanged() at line 1052, then the outer call emits it again at line 1052. Same double-emission pattern exists for DustProtectionThreshold (lines 1055-1063) when protection is disabled. Connected slots will be invoked twice per state change.

🟡 suggestion: setDustProtectionThreshold in wallet interface has no range validation

src/wallet/interfaces.cpp (line 1)

src/wallet/interfaces.cpp:347-351 directly assigns the threshold to m_wallet->m_dust_protection_threshold with no bounds checking. The CLI path (wallet.cpp:3376-3384) validates both minimum and maximum. The GUI optionsmodel (line 1060) enforces minimum via std::max<int64_t>(..., 1) but has no max check — it relies on the UI spinbox cap. A corrupted settings file or programmatic caller could set a value exceeding MAX_DUST_PROTECTION_THRESHOLD (1,000,000).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/wallet/wallet.cpp`:
- [SUGGESTION] line 1: Duplicate InputIsMine: static copy in wallet.cpp duplicates public function in receive.cpp
  An identical static `InputIsMine` was added at `src/wallet/wallet.cpp:59` that duplicates the public version at `src/wallet/receive.cpp:13`. Both have the exact same implementation (AssertLockHeld, GetWalletTx lookup, IsMine check). The static copy is used only at wallet.cpp:2823 in the dust protection check. Since wallet.cpp does not include `receive.h`, including it and removing the static copy would eliminate the duplication.

In `src/qt/optionsmodel.h`:
- [SUGGESTION] line 1: Same constant name DEFAULT_DUST_PROTECTION_THRESHOLD used for two different values (0 vs 10000)
  `src/wallet/wallet.h:115` defines `DEFAULT_DUST_PROTECTION_THRESHOLD = 0` (CLI: disabled by default). `src/qt/optionsmodel.h:28` defines `DEFAULT_DUST_PROTECTION_THRESHOLD = 10000` (GUI: enabled with sensible default). Different scopes prevent ODR issues, and the design is intentional (CLI opt-in vs GUI protected-by-default). However, reusing the same identifier for different values is confusing. Consider renaming the GUI constant to `DEFAULT_GUI_DUST_PROTECTION_THRESHOLD` to match the existing `MAX_GUI_DUST_PROTECTION_THRESHOLD` pattern at optionsmodel.h:30.

In `src/qt/optionsmodel.cpp`:
- [SUGGESTION] line 1: dustProtectionChanged signal emitted twice when toggling DustProtection off
  At `src/qt/optionsmodel.cpp:1043-1053`: when toggling DustProtection OFF, line 1045 recursively calls `setOption(DustProtection, true, "-prev")` to save the previous state. If the `-prev` value changed (typical after toggling ON which clears it at line 1051), the recursive call emits `dustProtectionChanged()` at line 1052, then the outer call emits it again at line 1052. Same double-emission pattern exists for DustProtectionThreshold (lines 1055-1063) when protection is disabled. Connected slots will be invoked twice per state change.

In `src/wallet/interfaces.cpp`:
- [SUGGESTION] line 1: setDustProtectionThreshold in wallet interface has no range validation
  `src/wallet/interfaces.cpp:347-351` directly assigns the threshold to `m_wallet->m_dust_protection_threshold` with no bounds checking. The CLI path (wallet.cpp:3376-3384) validates both minimum and maximum. The GUI optionsmodel (line 1060) enforces minimum via `std::max<int64_t>(..., 1)` but has no max check — it relies on the UI spinbox cap. A corrupted settings file or programmatic caller could set a value exceeding MAX_DUST_PROTECTION_THRESHOLD (1,000,000).

- Rename GUI constant to DEFAULT_GUI_DUST_PROTECTION_THRESHOLD to avoid
  confusion with the CLI default (0) in wallet.h
- Add range validation (std::clamp) in wallet interface setter to guard
  against corrupted settings or programmatic callers
- Fix copyright year to 2026

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@UdjinM6
Copy link
Copy Markdown
Author

UdjinM6 commented Apr 3, 2026

Duplicate InputIsMine: intentional, avoids circular dependency
Double signal emission: matches existing Prune/PruneSize pattern, not an issue imo
Everything else should be fixed via 30b132e

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30b132ecd6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Clean fix commit addressing 3 of 4 prior review suggestions. The dual-default confusion is resolved by renaming the GUI constant to DEFAULT_GUI_DUST_PROTECTION_THRESHOLD. Range validation added via std::clamp. Copyright year fixed. The duplicate InputIsMine finding from the prior review is not addressed — likely intentional (the static helper in wallet.cpp avoids a header include dependency).

Reviewed commit: 30b132e

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Validated PR #7265 at HEAD30b132ecd62afc5d1eacf8b3a8b9df0ca2104e44 against the candidate reviews and CodeRabbit comments. I did not find any concrete, user-impacting issues still present on this head. The remaining live CodeRabbit concern about validating the remembered dust threshold does not reproduce: getOption(DustProtectionThreshold) already routes malformed or non-positive persisted values through SettingToInt(...) and safely falls back to the remembered/default GUI threshold without throwing.

Reviewed commit: 30b132e

Copy link
Copy Markdown
Member

@PastaPastaPasta PastaPastaPasta left a comment

Choose a reason for hiding this comment

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

utACK 30b132e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants