Skip to content

fix(spv): atomically extend BIP44 receive pool on wallet import#830

Draft
lklimek wants to merge 1 commit into
v1.0-devfrom
fix/spv-wallet-address-sync
Draft

fix(spv): atomically extend BIP44 receive pool on wallet import#830
lklimek wants to merge 1 commit into
v1.0-devfrom
fix/spv-wallet-address-sync

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Apr 16, 2026

Summary

Fixes the second of the four layers behind the zero-balance-on-import bug tracked in #829: addresses generated via the UI beyond the WalletManager's default BIP44 gap limit (30) are not re-registered into the live monitored set on startup, so SPV filter matching misses any UTXO at those higher indices.

This PR is now narrowly focused on the bug-2 fix only. The platform pin bump and clear_data_dir fix have been extracted into #834 and must land first.

Reproduction

  1. In an existing wallet, use the UI to generate 35+ receive addresses (indices 0–34).
  2. Restart DET.
  3. Send a testnet transaction to the address at an index ≥ 30 (beyond the WalletManager default gap limit).
  4. Wait for the transaction to confirm, then observe the wallet balance in the UI.

Expected: The balance at the receiving address appears, because the WalletManager's monitored address set should include every index that exists in DET's local database.

Actual: The address shows zero balance. On restart, the WalletManager is reconstructed with WalletAccountCreationOptions::Default which pre-generates BIP44 indices 0–29 only. Any indices DET previously stored in wallet_addresses beyond 29 are never re-registered into the WalletManager, so the live monitored address set is narrower than DET's on-disk state. SPV filter matching misses any UTXO at indices ≥ 30.

Observed with a reference testnet wallet that has a multi-output transaction at BIP44 indices 0 and 32–40 (motivating tx: 56bdab0ac5dfefc47e136eedeede07a83ff0f2cb753683578fd41677975f8b32 in block 1,459,352). Only index 0 shows balance; 32–40 do not.

Root cause

Upstream's BIP44 gap-limit reactive extension works as designed: if at least one address within the current gap is observed, the pool extends. But DET stores more addresses in its local DB than it restores to the live monitored set on startup, and there's no code path that reconciles the two.

There's also a second, subtler race: earlier drafts extended the pool after load_wallet_from_seed returned, meaning wm.wallet_count() could cross the threshold that run_spv_loop waits on while the pool was still at its default size — SPV's filter pipeline then started matching against an incomplete set.

Fix

extend_bip44_receive_pool_locked runs inside the WalletManager write lock held by load_wallet_from_seed. wm.wallet_count() does not increment until the pool is final, closing the race. The extension target is max_db_idx (the highest BIP44 receive index present in the wallet_addresses DB table), plus a small slack (LOOKAHEAD_SLACK = 100).

Supporting helpers

  • Database::max_bip44_receive_index() — reads the DB's wallet_addresses table for the highest receive index known for a given wallet.
  • SpvManager::extend_bip44_receive_pool_locked — called under the existing write lock; iterates get_receive_address(wallet_id, i, BIP44, mark_used=true) up to max_db_idx + LOOKAHEAD_SLACK. Uses upstream's existing gap-limit reactive extension without adding new API surface.

Files

File Lines Purpose
src/context/wallet_lifecycle.rs +35 Pool extension plumbing inside load_wallet_from_seed's write lock
src/database/wallet.rs +125 Database::max_bip44_receive_index helper + 3 unit tests
src/spv/manager.rs +95 SpvManager::extend_bip44_receive_pool_locked

Total: +253 / -2 lines. Single commit.

Test plan

Automated (all green)

  • cargo build --all-features — clean
  • cargo clippy --all-features --all-targets -- -D warnings — clean
  • cargo +nightly fmt --all -- --check — clean
  • cargo test --all-features --workspace --lib — 455 / 455 pass

Manual on testnet

  • Cold start with existing wallet containing indices ≥ 30. Launch DET against a DB that already has addresses generated up to e.g. index 40. Expected: on first sync, balance on indices ≥ 30 appears without manual clear-and-resync. Log contains Extended SPV BIP44 receive pool wallet=… generated=<N> target_index=<max_db_idx>.
  • Fresh install regression. rm -rf ~/.config/dash-evo-tool/data.db ~/.config/dash-evo-tool/spv/ → launch → fresh wallet with no historical UTXOs. Expected: no crash, standard checkpoint sync, balance = 0.

What this PR does not carry

Merge order

  1. #834 (platform bump + clear_data_dir fix)
  2. #833 (bug 1 startup race — independent, can land first or alongside)
  3. This PR (bug 2 pool extension)

Related

🤖 Co-authored by Claudius the Magnificent AI Agent

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 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: 59e0ad86-a470-42fa-934c-1c4d140be8bf

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 fix/spv-wallet-address-sync

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.

@lklimek lklimek changed the title fix(spv): prevent zero balance on imported wallets after restart fix(spv): prevent zero balance on wallet import (startup + mid-session) Apr 17, 2026
@lklimek lklimek changed the title fix(spv): prevent zero balance on wallet import (startup + mid-session) fix(spv): BIP44 pool extension on wallet import + platform pin bump Apr 20, 2026
@lklimek lklimek force-pushed the fix/spv-wallet-address-sync branch from 3a2b32c to a3f6814 Compare April 20, 2026 14:09
On restart, WalletManager is initialised with the default gap limit
(30 addresses), but the database may contain indices 30+ from prior
sessions. SpvManager::load_wallet_from_seed now calls
extend_bip44_receive_pool_locked while still holding the WalletManager
write lock, so the monitored address set is fully populated before any
observer (notably run_spv_loop's wait-for-wallets loop) sees
wallet_count increment.

The extension target is max_db_bip44_receive_index + LOOKAHEAD_SLACK
(slack=100). A fresh import therefore covers ~100 addresses before the
first scan, discovering historical UTXOs without a restart. A restart
with DB-known indices at, say, 32-40 also works in a single scan.

Database::max_bip44_receive_index queries the high-water BIP44 receive
index (m/44'/.../0/N) directly from wallet_addresses; change-chain
rows (m/.../1/*) are excluded by the LIKE pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lklimek lklimek force-pushed the fix/spv-wallet-address-sync branch from a3f6814 to 16a685c Compare April 20, 2026 14:30
@lklimek lklimek changed the title fix(spv): BIP44 pool extension on wallet import + platform pin bump fix(spv): atomically extend BIP44 receive pool on wallet import Apr 20, 2026
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