Skip to content

perf(crowdnode): persist the fruitless-restore memo per wallet - #975

Merged
QuantumExplorer merged 1 commit into
developfrom
claude/youthful-heyrovsky-85daf2
Aug 10, 2026
Merged

perf(crowdnode): persist the fruitless-restore memo per wallet#975
QuantumExplorer merged 1 commit into
developfrom
claude/youthful-heyrovsky-85daf2

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

CrowdNode.restoreState() runs a full post-2022 history scan when looking for a CrowdNode account. The scan is memoized in-process when it finds nothing (fruitlessRestoreTxCount), but that memo died with the process — so every cold launch of a wallet that never used CrowdNode still paid one ~7s main-thread scan at sync-done (measured ~1.1 ms/row materialization on a 7k-tx store).

What was done?

Persisted the memo per wallet in CrowdNodeDefaults (already scoped by active walletId), so a launch skips the scan when a previous launch already scanned the exact same history fruitlessly:

  • New per-wallet key crowdNodeFruitlessRestoreTxCountKey, cached like the other CrowdNodeDefaults properties. restoreState() reads/writes it in place of the old in-memory var (the prefs _-cache plays the old in-memory role).
  • Keyed to the persisted-row count read before the scans — the same value the pass actually saw, never re-read after (a save landing mid-restore must not mask unscanned rows).
  • Written only by the account-not-found branch. A nil row count (SDK container not up yet) writes nothing — the scans were vacuous, and a prior launch's valid memo must not be clobbered.
  • Cleared everywhere a rescan must be guaranteed: resetUserDefaults() / reset() (network change, alien-address teardown), resetForWipe() and clearPerWalletKeys(forWalletIdHex:) via membership in kPerWalletKeys (full wipe, single-wallet deletion), and handleActiveWalletChanged() — the row count is store-global while the scan is wallet-scoped, so a memo can't be trusted across a wallet switch.
  • A restored seed whose history syncs in later rescans naturally: new rows → count differs → memo miss.
  • Key resolution deliberately bypasses resolvedKey's seed-from-legacy step: this key is post-multi-wallet (no legacy install base), and a bare-key value can only come from a no-active-wallet pass whose wallet-scoped scan was vacuous — seeding it would let a wallet skip a scan it never ran.

Orthogonal to the CrowdNode release posture: the state machine stays app/SDK-owned, no DashSync surface is touched, and the change is inert while CrowdNode is hidden.

How Has This Been Tested?

  • Clean dashpay scheme build (arm64 iOS simulator).
  • QA-iPhone16 simulator smoke against the synthetic 7k-tx mainnet store, three launches:
    • Launch A (no memo): one restore pass, full scan (5944 post-2022 rows, ~7s fetch), "account not found", memo persisted as exactly the store's row count (7007) under the active wallet's key.
    • Launch B (memo == count): reached the same checkCrowdNodeState trigger, zero "restoring CrowdNode state" / "CrowdNode scan:" log lines — cold-launch scan skipped.
    • Launch C (memo forced stale, 6900 ≠ 7007): full rescan ran — exercises the restored-seed path.
  • Simulator restored to testnet + pre-synthetic store afterwards.

Unit-test target is currently broken repo-wide (pre-existing); verification standard per CLAUDE.md is the clean dashpay build + testnet/sim smoke above.

Breaking Changes

None.

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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Restore progress is now saved separately for each wallet.
    • Reopening the app or switching wallets preserves the relevant restore state.
    • Restores can avoid unnecessary rescanning when transaction data has not changed.
    • Restore state is cleared appropriately when wallets are reset or removed, ensuring future restores use fresh data.
  • Bug Fixes
    • Prevented valid restore information from being overwritten when transaction data is temporarily unavailable.

The in-memory memo that lets restoreState() skip the full post-2022
history scan after a fruitless pass died with the process, so every cold
launch of a wallet that never used CrowdNode still paid one ~7s
main-thread scan at sync-done. Persist it per wallet in CrowdNodeDefaults
(keyed to the row count read before the scans) so a later launch skips
the scan when the exact same history was already scanned fruitlessly.

- Written only by the account-not-found branch; a nil row count (SDK
  container not up) writes nothing and preserves a prior launch's memo.
- Cleared by resetUserDefaults()/reset() (network change, alien address),
  resetForWipe()/clearPerWalletKeys (via kPerWalletKeys), and on active-
  wallet change - the row count is store-global while the scan is
  wallet-scoped, so a switch always rescans.
- A restored seed whose history syncs in later rescans: count changes,
  memo misses.
- Key resolution bypasses resolvedKey's seed-from-legacy step: a bare-key
  memo can only come from a no-wallet (vacuous) pass and must not be
  promoted into a wallet's key.

Verified on the QA sim with the synthetic 7k mainnet store: launch A
scans and persists the memo (7007 == row count), launch B skips the scan
entirely, launch C with a stale memo (6900) rescans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 9dceb85 into develop Aug 10, 2026
1 of 2 checks passed
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2bd0c9e2-ab1d-45d5-a934-f921e1edf642

📥 Commits

Reviewing files that changed from the base of the PR and between 19c48d5 and 6ee68b0.

📒 Files selected for processing (2)
  • DashWallet/Sources/Models/CrowdNode/CrowdNode+UserDefaults.swift
  • DashWallet/Sources/Models/CrowdNode/CrowdNode.swift

📝 Walkthrough

Walkthrough

CrowdNode now persists the fruitless restore transaction count per wallet. Restore skips scans when the stored count matches the current count. Reset and active-wallet changes clear the stored memo.

Changes

Per-wallet fruitless restore persistence

Layer / File(s) Summary
Memo storage and lifecycle
DashWallet/Sources/Models/CrowdNode/CrowdNode+UserDefaults.swift
Adds wallet-scoped storage for the fruitless restore count. Cache invalidation, wallet deletion, full wipes, and active-wallet resets clear the value.
Restore flow integration
DashWallet/Sources/Models/CrowdNode/CrowdNode.swift
Uses the persisted count to skip unchanged restores. Records the pre-scan count only when available and clears the memo during resets and active-wallet changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: llbartekll, jeanpierreroma

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/youthful-heyrovsky-85daf2

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

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.

1 participant