Skip to content

fix(toolkit): [PM-20016] propagate DustWallet spend state update#877

Merged
m2ux merged 12 commits into
mainfrom
fix/PM-20016-dustwallet-spend-state-update
Apr 14, 2026
Merged

fix(toolkit): [PM-20016] propagate DustWallet spend state update#877
m2ux merged 12 commits into
mainfrom
fix/PM-20016-dustwallet-spend-state-update

Conversation

@m2ux

@m2ux m2ux commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix DustWallet::speculative_spend to return the updated DustLocalState alongside spends, and extend mark_spent to commit the state atomically with nullifier recording. Prevents stale-state bugs where consecutive spends select already-spent outputs. This is Least Authority audit finding Issue AO (Medium impact / Low severity, pp. 56-57).

🎫 Ticket 📐 Engineering


Motivation

DustWallet::speculative_spend clones dust_local_state, applies DustLocalState::spend iteratively (which sets pending_until on spent UTXOs to exclude them from future utxos() calls), then discards the updated state. The companion mark_spent method records nullifiers in spent_utxos but does not apply the state changes from DustLocalState::spend.

This means consecutive transaction constructions operate on stale state — spent UTXOs remain selectable, balances are overreported, and the ledger may reject transactions due to nullifier reuse. The issue persists until event replay rebuilds state from on-chain data.


Changes

  • wallet/dust.rsspeculative_spend — Return type changed from Result<Vec<DustSpend>> to Result<(Vec<DustSpend>, Sp<DustLocalState>)>, returning the accumulated state alongside spends
  • wallet/dust.rsmark_spent — Extended to accept updated_state: Sp<DustLocalState> and assign it to self.dust_local_state before recording nullifiers
  • transaction.rsgather_dust_spends — Collects per-wallet updated states in HashMap<WalletSeed, Sp<DustLocalState>> alongside spends
  • transaction.rsconfirm_dust_spends — Consumes per-wallet updated states via .remove() and passes each to mark_spent

📌 Submission Checklist

  • Changes are backward-compatible (or flagged if breaking)
  • Pull request description explains why the change is needed
  • Self-reviewed the diff
  • I have included a change file, or skipped for this reason: changeset added
  • If the changes introduce a new feature, I have bumped the node minor version
  • Update documentation (if relevant)
  • No new todos introduced

🔱 Fork Strategy

  • Node Runtime Update
  • Node Client Update
  • Other
  • N/A

🗹 TODO before merging

  • Ready for review

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

kics-logo

KICS version: v2.1.19

Category Results
CRITICAL CRITICAL 0
HIGH HIGH 2
MEDIUM MEDIUM 52
LOW LOW 3
INFO INFO 64
TRACE TRACE 0
TOTAL TOTAL 121
Metric Values
Files scanned placeholder 27
Files parsed placeholder 27
Files failed to scan placeholder 0
Total executed queries placeholder 73
Queries failed to execute placeholder 0
Execution time placeholder 11

@m2ux m2ux self-assigned this Mar 6, 2026
@m2ux m2ux force-pushed the fix/PM-20016-dustwallet-spend-state-update branch from be637f8 to fb44f53 Compare March 6, 2026 13:50
@m2ux m2ux marked this pull request as ready for review March 6, 2026 16:52
@m2ux m2ux requested a review from a team as a code owner March 6, 2026 16:52
Comment thread changes/changed/dustwallet-spend-state-update.md Outdated
@m2ux m2ux enabled auto-merge March 10, 2026 15:12
@m2ux m2ux requested a review from ozgb March 10, 2026 17:47
m2ux added 10 commits March 12, 2026 09:38
Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor
Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor
Modify speculative_spend to return the updated DustLocalState alongside
spends, and extend mark_spent to accept and assign the updated state to
self.dust_local_state atomically with nullifier recording.

Update gather_dust_spends and confirm_dust_spends in transaction.rs to
thread per-wallet updated states from speculation through to commit.

This ensures DustLocalState::spend's pending_until flags are propagated,
preventing utxos() from returning already-spent outputs in consecutive
spend operations.

Addresses Least Authority audit finding Issue AO.

Made-with: Cursor
Made-with: Cursor
@m2ux m2ux force-pushed the fix/PM-20016-dustwallet-spend-state-update branch from 95b390d to 489bc6e Compare March 12, 2026 09:39
@m2ux m2ux requested a review from justinfrevert March 12, 2026 14:59
@m2ux m2ux changed the title fix(toolkit): propagate DustWallet spend state update [PM-20016] fix(toolkit): [PM-20016] propagate DustWallet spend state update Mar 30, 2026
gilescope pushed a commit that referenced this pull request Apr 8, 2026
@m2ux m2ux added this pull request to the merge queue Apr 14, 2026
Merged via the queue into main with commit 9732ea0 Apr 14, 2026
33 checks passed
@m2ux m2ux deleted the fix/PM-20016-dustwallet-spend-state-update branch April 14, 2026 10:51
m2ux added a commit that referenced this pull request Apr 23, 2026
…uthorities (#877)

Signed-off-by: Mike Clay <mike.clay@shielded.io>
m2ux added a commit that referenced this pull request Apr 23, 2026
* chore: add changeset for DustWallet spend state fix [PM-20016]

Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor

* chore: add changeset for DustWallet spend state fix [PM-20016]

Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor

* fix(toolkit): propagate DustWallet spend state update [PM-20016]

Modify speculative_spend to return the updated DustLocalState alongside
spends, and extend mark_spent to accept and assign the updated state to
self.dust_local_state atomically with nullifier recording.

Update gather_dust_spends and confirm_dust_spends in transaction.rs to
thread per-wallet updated states from speculation through to commit.

This ensures DustLocalState::spend's pending_until flags are propagated,
preventing utxos() from returning already-spent outputs in consecutive
spend operations.

Addresses Least Authority audit finding Issue AO.

Made-with: Cursor

* chore: update changeset with PR and JIRA links [PM-20016]

Made-with: Cursor

* chore: remove stale changeset from pre-rebase

Made-with: Cursor

* refactor: consume updated_states in confirm_dust_spends to avoid clone

Made-with: Cursor

* style: cargo fmt

Made-with: Cursor

* fix: resolve clippy type_complexity lint by adding type aliases

Made-with: Cursor

* fix: add GatheredDustSpends type alias to resolve remaining type_complexity lint

Made-with: Cursor

* chore: fix change file format per review feedback

Made-with: Cursor
Signed-off-by: Mike Clay <mike.clay@shielded.io>
m2ux added a commit that referenced this pull request Apr 23, 2026
…uthorities (#877)

Signed-off-by: Mike Clay <mike.clay@shielded.io>
m2ux added a commit that referenced this pull request Apr 23, 2026
* chore: add changeset for DustWallet spend state fix [PM-20016]

Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor

* chore: add changeset for DustWallet spend state fix [PM-20016]

Initial changeset for fix/PM-20016. Implementation to follow after
planning phase completion.

Made-with: Cursor

* fix(toolkit): propagate DustWallet spend state update [PM-20016]

Modify speculative_spend to return the updated DustLocalState alongside
spends, and extend mark_spent to accept and assign the updated state to
self.dust_local_state atomically with nullifier recording.

Update gather_dust_spends and confirm_dust_spends in transaction.rs to
thread per-wallet updated states from speculation through to commit.

This ensures DustLocalState::spend's pending_until flags are propagated,
preventing utxos() from returning already-spent outputs in consecutive
spend operations.

Addresses Least Authority audit finding Issue AO.

Made-with: Cursor

* chore: update changeset with PR and JIRA links [PM-20016]

Made-with: Cursor

* chore: remove stale changeset from pre-rebase

Made-with: Cursor

* refactor: consume updated_states in confirm_dust_spends to avoid clone

Made-with: Cursor

* style: cargo fmt

Made-with: Cursor

* fix: resolve clippy type_complexity lint by adding type aliases

Made-with: Cursor

* fix: add GatheredDustSpends type alias to resolve remaining type_complexity lint

Made-with: Cursor

* chore: fix change file format per review feedback

Made-with: Cursor
Signed-off-by: Mike Clay <mike.clay@shielded.io>
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.

5 participants