Skip to content

feat(dash-spv): add notify_wallet_addresses_changed() API#567

Draft
lklimek wants to merge 7 commits intofeat/mempool-supportfrom
feat/mempool-filter-rebuild
Draft

feat(dash-spv): add notify_wallet_addresses_changed() API#567
lklimek wants to merge 7 commits intofeat/mempool-supportfrom
feat/mempool-filter-rebuild

Conversation

@lklimek
Copy link
Contributor

@lklimek lklimek commented Mar 19, 2026

Summary

Add a public API for triggering mempool bloom filter rebuild when wallet addresses change externally.

  • New SyncEvent::WalletAddressesChanged variant
  • New DashSpvClient::notify_wallet_addresses_changed() public method
  • MempoolManager handles the event by rebuilding the bloom filter on all active peers

Problem

The MempoolManager builds its bloom filter when peers are first activated (after sync completes). When new wallets are added or addresses registered after initial connection, the bloom filter becomes stale — it doesn't include the new addresses. This means incoming transactions and InstantSend locks for those addresses are not relayed by peers.

Concrete failure in dash-evo-tool E2E tests

The send_funds test does a two-hop payment chain:

Framework wallet  →(5M duffs)→  Test wallet A  →(2M duffs)→  Test wallet B
     hop 1                           hop 2

Hop 1 works (~25s): Framework wallet was loaded before SPV started, so its addresses are in the initial bloom filter.

Hop 2 fails (120s timeout): Test wallet B is created after SPV started. Its addresses are added to WalletManager but the mempool bloom filter is never rebuilt. The IS lock message for hop 2 passes through the stale bloom filter, doesn't match, and the peer never relays it.

Result: Timed out waiting for spendable balance >= 5000000 duffs (confirmed: 0, total: 5000000) — funds visible but never become spendable because the IS lock is never received.

Affected code paths in dash-evo-tool

  1. New wallet creationqueue_spv_wallet_load() adds a wallet to WalletManager after SPV is running
  2. DashPay contact addressesregister_dashpay_addresses_for_identity() adds contact payment addresses to the wallet's watched_addresses map
  3. BlockchainIdentities addresses — Identity System addresses (m/9'/coinType'/5'/subfeature'/index) bootstrapped by DET directly

All three paths add addresses to the wallet after the bloom filter was built. Without a rebuild trigger, these addresses aren't monitored until the next block confirmation with gap limit changes or SPV reconnect.

What we need from rust-dashcore

Requirement: A public API on DashSpvClient that triggers a mempool bloom filter rebuild using the wallet's current monitored_addresses() and watched_outpoints().

This PR implements one possible approach (SyncEvent::WalletAddressesChanged dispatched through the coordinator's broadcast channel). Alternative approaches that satisfy the same requirement are welcome — the key contract is:

  1. Consumer calls a method after modifying the wallet's address set
  2. The mempool bloom filter is rebuilt on all active peers with the updated addresses
  3. No peer disconnection or other side effects

Test plan

  • cargo check --workspace passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test -p dash-spv --lib — 358 tests pass
  • Integration test with dash-evo-tool E2E send_funds (in progress — verifying IS lock detection for dynamically created wallets)

🤖 Co-authored by Claudius the Magnificent AI Agent

xdustinface and others added 5 commits March 19, 2026 14:56
- Add `TransactionStatusChanged` variant to `WalletEvent` and a `status` field to `TransactionReceived`.
- Wire up event emission in `check_transaction_in_all_wallets`, `process_instant_send_lock`, and `update_synced_height`.
- Add FFI callback for status changes.
…send_lock`

Addresses CodeRabbit review comment on PR #552
File: key-wallet-manager/src/test_utils/wallet.rs
`process_instant_send_lock` did not record txids in `instant_send_locks`,
so a subsequent `check_transaction_in_all_wallets(..., InstantSend)` for
the same txid would emit a duplicate `TransactionStatusChanged` event.

Record the txid in `instant_send_locks` inside `mark_instant_send_utxos`
so both paths share the same deduplication set. Add tests covering both
orderings of the mixed entry points.

Addresses CodeRabbit review comment on PR #552
Files: key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs,
       key-wallet-manager/src/wallet_manager/event_tests.rs
See the changes in `ARCHITECTURE.md` for a detailed description.

- Add `MempoolManager` that activates after initial sync to monitor unconfirmed transactions via BIP37 bloom filters or local address matching. Includes peer relay management, dedup tracking, IS lock handling, and auto-rebuilding filters on address pool changes.
- Extend `WalletInterface` with `MempoolTransactionResult` return type and `watched_outpoints()` for bloom filter construction. Wire mempool manager into `SyncCoordinator` and propagate `confirmed_txids` through `BlockProcessed` events for mempool eviction.
- Add FFI bindings, dashd integration tests, and wallet unit tests.
Add a public method on DashSpvClient to trigger a mempool bloom filter
rebuild when wallet addresses change externally (new wallet added,
DashPay contact addresses registered, etc.).

Introduces SyncEvent::WalletAddressesChanged handled by MempoolManager
to rebuild the bloom filter on all active peers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7aaca7f8-0254-4b8c-bb03-bfd5525c9994

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mempool-filter-rebuild
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@codecov
Copy link

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 91.60457% with 169 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.29%. Comparing base (1de2479) to head (8e6e267).
⚠️ Report is 8 commits behind head on feat/mempool-support.

Files with missing lines Patch % Lines
dash-spv/src/sync/mempool/sync_manager.rs 87.21% 51 Missing ⚠️
dash-spv/src/sync/mempool/manager.rs 97.55% 25 Missing ⚠️
dash-spv/src/sync/progress.rs 36.11% 23 Missing ⚠️
dash-spv-ffi/src/bin/ffi_cli.rs 0.00% 17 Missing ⚠️
dash-spv-ffi/src/callbacks.rs 29.16% 17 Missing ⚠️
key-wallet-manager/src/wallet_manager/mod.rs 81.08% 7 Missing ⚠️
dash-spv/src/sync/sync_coordinator.rs 45.45% 6 Missing ⚠️
key-wallet-manager/src/events.rs 0.00% 6 Missing ⚠️
dash-spv/src/client/events.rs 0.00% 4 Missing ⚠️
...wallet-manager/src/wallet_manager/process_block.rs 96.66% 4 Missing ⚠️
... and 7 more
Additional details and impacted files
@@                   Coverage Diff                    @@
##           feat/mempool-support     #567      +/-   ##
========================================================
+ Coverage                 66.37%   67.29%   +0.92%     
========================================================
  Files                       311      316       +5     
  Lines                     64979    66972    +1993     
========================================================
+ Hits                      43130    45070    +1940     
- Misses                    21849    21902      +53     
Flag Coverage Δ
core 75.13% <ø> (ø)
ffi 37.64% <47.69%> (+0.12%) ⬆️
rpc 28.28% <ø> (ø)
spv 83.21% <93.22%> (+2.00%) ⬆️
wallet 66.46% <92.03%> (+0.52%) ⬆️
Files with missing lines Coverage Δ
dash-spv-ffi/src/types.rs 92.85% <100.00%> (+0.70%) ⬆️
dash-spv/src/client/lifecycle.rs 64.81% <100.00%> (+1.35%) ⬆️
dash-spv/src/network/mod.rs 72.72% <100.00%> (+23.36%) ⬆️
dash-spv/src/sync/blocks/manager.rs 94.82% <100.00%> (+0.09%) ⬆️
dash-spv/src/sync/identifier.rs 100.00% <100.00%> (ø)
dash-spv/src/sync/mempool/progress.rs 100.00% <100.00%> (ø)
...wallet/src/transaction_checking/account_checker.rs 64.01% <100.00%> (+0.05%) ⬆️
...-wallet/src/transaction_checking/wallet_checker.rs 95.42% <ø> (+0.31%) ⬆️
...allet/managed_wallet_info/wallet_info_interface.rs 79.48% <100.00%> (+10.18%) ⬆️
dash-spv/src/network/manager.rs 63.19% <0.00%> (+1.69%) ⬆️
... and 16 more

... and 8 files with indirect coverage changes

lklimek and others added 2 commits March 19, 2026 16:03
Add three unit tests that prove the bug cascade observed on Dash testnet:

1. test_is_synced_blocked_by_masternode_failure (progress.rs):
   SyncProgress::is_synced() returns false when masternodes is not Synced,
   even though headers, filter_headers, filters, and blocks are all Synced.
   This prevents the coordinator from emitting SyncComplete.

2. test_mempool_not_activated_by_filters_sync_complete (sync_manager.rs):
   MempoolManager ignores FiltersSyncComplete -- it only activates on
   SyncComplete. This proves the coupling to the coordinator event.

3. test_mempool_stays_dead_without_sync_complete (sync_manager.rs):
   MempoolManager stays in WaitForEvents after receiving all other sync
   events (BlockHeaderSyncComplete, FilterHeadersSyncComplete,
   FiltersSyncComplete, BlockProcessed, ManagerError) but NOT SyncComplete.
   This reproduces the production scenario where masternode failure causes
   the wallet to never see unconfirmed transactions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three tests that FAIL until the underlying bugs are fixed, proving
the cascade observed on Dash testnet:

1. `test_is_synced_not_blocked_by_masternode_failure` (progress.rs):
   `is_synced()` should return true when chain sync is complete even if
   masternodes are still syncing/failed. Currently blocks SyncComplete.

2. `test_mempool_activates_on_filters_sync_complete` (sync_manager.rs):
   MempoolManager should activate on FiltersSyncComplete since mempool
   monitoring only needs the filter chain. Currently requires SyncComplete.

3. `test_mempool_activates_despite_masternode_failure` (sync_manager.rs):
   Replays exact testnet event sequence where masternodes fail. Mempool
   should still activate after FiltersSyncComplete.

All marked #[ignore] to avoid breaking CI — run with --ignored to
verify the bugs still exist.

Root cause: MasternodeManager fails on testnet with "Required rotated
chain lock sig at h - 0 not present" → never Synced → is_synced()
false → SyncComplete never emitted → MempoolManager never activates
→ wallet never sees unconfirmed txs as spendable via IS locks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek changed the base branch from v0.42-dev to feat/mempool-support March 19, 2026 15:13
@xdustinface xdustinface force-pushed the feat/mempool-support branch from 1af96dd to d0ced48 Compare March 20, 2026 09:12
@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Mar 20, 2026
@github-actions
Copy link

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@xdustinface xdustinface force-pushed the feat/mempool-support branch from d0ced48 to b03252a Compare March 20, 2026 09:40
lklimek added a commit to dashpay/dash-evo-tool that referenced this pull request Mar 20, 2026
* feat: integrate platform mempool support

Bump dash-sdk to platform commit 6d9efa97b which includes
rust-dashcore mempool support (MempoolManager with bloom filter
monitoring) and SDK RPITIT refactor.

Key changes:
- Configure SPV client with BloomFilter mempool strategy
- Update process_mempool_transaction() call signature
- Adapt to Network::Dash -> Network::Mainnet rename
- Add DB migration 29 to update stored "dash" network strings
- Add Send-asserting wrappers for backend task futures to work
  around RPITIT HRTB Send inference limitations in the SDK

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix formatting in settings.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: remove unsafe assert_send workaround

Rebase platform dependency onto aa86b74f (before PR #3376 which
paradoxically broke Send inference) so the compiler can natively
prove the backend task future is Send.

Changes:
- Update dash-sdk rev to 2414799b7 (mempool bump on clean base)
- Remove assert_send() transmute and all _send() wrapper methods
- Revert app.rs to call run_backend_task() directly

This commit is self-contained — revert it to restore the workaround
if a future platform update reintroduces the HRTB regression.

See: dashpay/platform#3376
See: rust-lang/rust#100013

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add trace-level timing instrumentation to E2E harness

Add structured tracing to wait helpers and create_funded_test_wallet()
for diagnosing IS lock timing issues. All new logs are trace-level
(enable with RUST_LOG=backend_e2e=trace) except the final setup
summary which logs at info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump platform rev for bloom filter rebuild API

Update dash-sdk to platform commit d9de0fd2 which includes
rust-dashcore c451a1c6 adding notify_wallet_addresses_changed()
for triggering mempool bloom filter rebuilds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: trigger bloom filter rebuild on wallet/address changes

Call notify_wallet_addresses_changed() after loading a new wallet into
SPV and after registering DashPay contact addresses. This ensures the
mempool bloom filter includes newly added addresses so incoming
transactions are detected immediately rather than waiting for the next
block or SPV reconnect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): let RUST_LOG override default trace level in E2E harness

The hardcoded .add_directive("backend_e2e=info") was overriding
RUST_LOG=backend_e2e=trace. Change to use RUST_LOG as primary source
with info as fallback when the env var is not set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: use original rust-dashcore 1af96dd without filter rebuild additions

Reverts bloom filter rebuild API additions (notify_wallet_addresses_changed)
that were added in commit eb948ae. The upstream MempoolManager has bugs
(masternode sync cascade blocking activation) that make these calls
ineffective. Failing tests proving these bugs are tracked in
dashpay/rust-dashcore#567.

Platform rev pinned to 2af8cac6 which uses rust-dashcore 1af96dd (PR #558).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update Cargo.lock for platform rev 2af8cac6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump platform rev to be2082b3c (rust-dashcore b03252af)

Updates platform to be2082b3c which pins rust-dashcore b03252af,
bringing bloom filter staleness detection via monitor_revision polling.

Adapts imports for key-wallet-manager → key-wallet crate merge
(upstream rust-dashcore #503).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): wait for SPV sync completion before broadcasting in E2E harness

The E2E harness was broadcasting funding transactions before the
MempoolManager's bloom filter was loaded to peers. Peers never relayed
IS locks for those txs because the filter didn't exist yet.

- Add wait_for_spv_running() that polls ConnectionStatus::spv_status()
  until SpvStatus::Running (fires after SyncComplete + bloom filter built)
- Call it in BackendTestContext::init() after spendable balance is confirmed
- Add 200ms sleep in create_funded_test_wallet() after wallet appears in
  SPV to let the mempool manager tick rebuild the bloom filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): use SpvManager status directly instead of ConnectionStatus

ConnectionStatus.spv_status is only updated from the UI frame loop,
which doesn't run in the E2E test harness. Read from SpvManager's
internal RwLock instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: bump platform rev to 93556aa4b (rust-dashcore 450f72f)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: push SPV status to ConnectionStatus from event handlers

Eliminate polling of SpvManager.status() from the UI frame loop.
SpvManager event handlers now push status, peer count, and error
updates directly to ConnectionStatus via new setter methods, making
SPV status visible to headless/test code without a UI polling cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: document ConnectionStatus as single source of truth for connection health

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): replace wallet_addresses with identity_token_balances in migration 29

wallet_addresses table has no `network` column, causing migration 29
(rename "dash" → "mainnet") to fail with "no such column: network".
Replace with identity_token_balances which does have the column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): include table name in migration 29 error messages

If a table update fails during the dash→mainnet rename, the error
now identifies which table caused the failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(spv): push Starting/Stopping status to ConnectionStatus immediately

write_status() updated the internal RwLock but did not push to
ConnectionStatus. The UI reads ConnectionStatus.spv_status() to
show the SPV Sync Status section, so it stayed hidden until
spawn_progress_watcher fired its first async tick.

Now start() and stop() push status to ConnectionStatus immediately,
matching the pattern used in all other status transitions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(spv): centralize ConnectionStatus push in write_status()

Move the ConnectionStatus push into write_status() so every status
transition automatically propagates to the UI. Remove redundant
manual pushes from start(), stop(), run_spv_loop(), and run_client().

Async event handlers (spawn_progress_watcher, spawn_sync_event_handler)
still push directly because they write to the RwLock without going
through write_status() (they hold Arc<RwLock>, not &self).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining PR review comments

- fix(ui): testnet MNListDiff fallback now uses Network::Testnet (was
  incorrectly Network::Mainnet in file-not-found path)
- fix(spv): progress watcher no longer clears last_error when a second
  error arrives with error_msg=None (preserves first error)
- fix(spv): clear spv_no_peers_since when SPV transitions to non-active
  state, preventing stale "no peers" tooltip warnings
- docs: clarify ConnectionStatus scope — health is push-based, detailed
  sync progress may still be read from SpvManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add key_wallet and mempool_filter to default tracing filter

These crates were renamed/added upstream in rust-dashcore 450f72f
and were missing from the default EnvFilter, hiding debug logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): show actionable SPV error details in banner and Settings

- Error banner now says "Go to Settings" and attaches the actual error
  message via with_details() for the collapsible details panel
- Settings SPV status label shows the error message instead of just "Error"
- Add public spv_last_error() getter on ConnectionStatus (DRY)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): deduplicate wallet transactions by txid before insert

With mempool support enabled, upstream wallet_transaction_history()
can return the same txid twice — as both a mempool entry (no height)
and a confirmed entry (with height). This caused a UNIQUE constraint
violation on (seed_hash, txid, network).

Deduplicate in replace_wallet_transactions(), preferring the confirmed
version over unconfirmed when both exist for the same txid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wallet): add TransactionStatus enum for transaction lifecycle tracking

Add TransactionStatus (Unconfirmed/InstantSendLocked/Confirmed/ChainLocked)
to WalletTransaction model and database schema.

- New TransactionStatus enum with from_height() heuristic for inferring
  status from block height (confirmed vs unconfirmed)
- Migration 30: adds `status` column to wallet_transactions table
  (default 2=Confirmed for existing rows)
- INSERT OR REPLACE handles duplicate txids from mempool + block
- UI shows status labels: "Pending", "⚡ InstantSend", "Confirmed @n",
  "🔒 ChainLocked @n"

InstantSendLocked and ChainLocked require upstream support
(rust-dashcore#569) — currently only Unconfirmed/Confirmed are inferred.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(db): consolidate migrations 29+30 into single migration 29

Both migrations were introduced in this PR — no need for users to run
them separately. Migration 29 now does both: rename network "dash" to
"mainnet" AND add the transaction status column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): throttle system theme detection to prevent white flash during sync

When using the "System" color theme, dark_light::detect() was called
every frame. During SPV sync (high repaint frequency), transient wrong
values caused brief theme flips visible as white flashes.

Changes:
- Cache the resolved theme in AppState and only re-poll the OS every 2s
- Guard font initialization with AtomicBool so set_fonts runs once
- Immediately resolve and apply theme on preference change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "fix(ui): throttle system theme detection to prevent white flash during sync"

This reverts commit 7f822bb.

* fix(db): default transaction status to Unconfirmed for new inserts

The table-creation DEFAULT was Confirmed (2), which is wrong — transactions
should be unconfirmed until proven otherwise. The migration ALTER TABLE
correctly keeps DEFAULT 2 for pre-existing rows (all from confirmed RPC).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review comments (migration idempotency, status field, poison handling)

- Guard ALTER TABLE in migration 30 with pragma_table_info check to
  prevent duplicate column crash on DBs upgrading from version <14
- Make is_confirmed() authoritative via TransactionStatus instead of height
- Consistent recover-from-poison in ConnectionStatus setters
- Add TODO for write_last_error() ConnectionStatus push consolidation
- Add maintenance comments for progress watcher write_status bypass
- Add tracing::debug for unknown Network variant fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflict The PR conflicts with the target branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants