Skip to content

fix(dash-spv): wallet reload pool-state loss past gap limit#759

Open
ZocoLini wants to merge 1 commit into
v0.42-devfrom
fix/wallet-reload-pool-gap-limit
Open

fix(dash-spv): wallet reload pool-state loss past gap limit#759
ZocoLini wants to merge 1 commit into
v0.42-devfrom
fix/wallet-reload-pool-gap-limit

Conversation

@ZocoLini
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini commented May 12, 2026

I has a little issue in iOS where, after restarting the app, the address pool didnt know enough addresswes to cover the UTXO that the transaction builder chose, with thi quick fix idea I ensure that, every time we find a UTXO and it gets inserted into the account, the address pool knows the addresses derivation path, so it is able to resolve it in a future

  • Privatize ManagedCoreFundsAccount::utxos and route all insertions through a new insert_utxo(utxo, key_source) that reconciles the owning address into the account's address pool — fixing an issue where when trying to sign as trasaction using utxo which address past the initial gap limit lost its derivation path and made signing fail.
  • Add dashd_sync regression test covering wallet rebuild with a UTXO at index 50 can find the derivation path for the address used.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Wallets now correctly track and resolve UTXOs associated with addresses at high derivation indices beyond the initial gap limit.
  • Refactor

    • UTXO insertion now uses an internal accessor-based mechanism with automatic address pool reconciliation to ensure proper tracking of all UTXOs with their corresponding addresses.

Review Change Stack

@ZocoLini ZocoLini requested a review from xdustinface May 12, 2026 22:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

This PR encapsulates the public utxos field of ManagedCoreFundsAccount as private and provides controlled access through accessor and insertion methods. All code paths—transaction building, wallet interface, and test suites—are updated to use the new API. A regression test validates persisted UTXO behavior across wallet reconstruction.

Changes

UTXO Field Encapsulation

Layer / File(s) Summary
UTXO Encapsulation Core API
key-wallet/src/managed_account/managed_core_funds_account.rs
ManagedCoreFundsAccount.utxos is made private. New utxos() accessor, insert_utxo() with optional address reconciliation via ensure_address_indexed(), and test-only clear_utxos() are introduced. Docs updated to enforce insertion discipline.
Account and Core Integration
key-wallet-ffi/src/managed_account.rs, key-wallet/src/transaction_checking/account_checker.rs, key-wallet/src/test_utils/wallet.rs
FFI UTXO count, transaction input matching, and test helper UTXO retrieval updated to use the utxos() accessor method.
Transaction and Wallet Integration
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs, key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs
Transaction builder funding source and wallet interface UTXO/immature-transaction iteration updated to call utxos() instead of accessing field.
Test Suite Adaptation
key-wallet-ffi/src/utxo_tests.rs, key-wallet/src/tests/balance_tests.rs, key-wallet/src/transaction_checking/wallet_checker.rs, key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs, key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
All test setups updated to insert UTXOs via insert_utxo(..., &KeySource::NoKeySource).unwrap() and assertions updated to read via utxos() accessor and clear_utxos() method.
Regression Test for Persisted UTXO Resolution
dash-spv/tests/dashd_sync/tests_restart.rs
New test test_persisted_utxo_outside_initial_gap_limit_resolves_after_reload extends a wallet's address pool beyond the initial gap limit, funds and syncs a high-index address, snapshots xpub/UTXO state, rebuilds wallet from xpubs only, and validates the rebuilt wallet still resolves derivation and tracks the persisted UTXO.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A wallet's keeper locked the chest,
Made UTXOs rest in privacy blessed,
Through reconciled gates and methods new,
The scattered tests now pass on through,
And wallets reborn still know their way!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(dash-spv): wallet reload pool-state loss past gap limit' directly addresses the main objective of the PR: fixing an issue where wallet reload loses pool state for addresses past the gap limit.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ 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 fix/wallet-reload-pool-gap-limit

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
Contributor

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@key-wallet/src/managed_account/managed_core_funds_account.rs`:
- Around line 155-175: ensure_address_indexed currently mutates address pools
during probing because it calls generate_address_at_index(..., true) on
address_pools_mut(), which can grow pools even when the address is not
derivable; change to a two‑phase approach: probe using non‑mutating reads (use
address_pools() and call generate_address_at_index with the non‑mutating flag or
an equivalent no‑grow path) across MAX_SCAN and pool_count to find a match, and
only if a match is found perform the actual mutation/commit (calling
generate_address_at_index with grow=true or otherwise extending
address_pools_mut()) to update state atomically; refer to
ensure_address_indexed, generate_address_at_index, address_pools(),
address_pools_mut(), and MAX_SCAN to locate the code to modify.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f05f0042-6e45-48cd-b870-e507e569311f

📥 Commits

Reviewing files that changed from the base of the PR and between 5297d61 and 0834e95.

📒 Files selected for processing (12)
  • dash-spv/tests/dashd_sync/tests_restart.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/utxo_tests.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/test_utils/wallet.rs
  • key-wallet/src/tests/balance_tests.rs
  • key-wallet/src/transaction_checking/account_checker.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
  • key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs

Comment on lines +155 to +175
fn ensure_address_indexed(
&mut self,
address: &Address,
key_source: &KeySource,
) -> crate::error::Result<()> {
const MAX_SCAN: u32 = 10_000;
let pool_count = self.keys.managed_account_type().address_pools().len();
for index in 0..MAX_SCAN {
for pool_idx in 0..pool_count {
let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
let derived = pool.generate_address_at_index(index, key_source, true)?;
if &derived == address {
return Ok(());
}
}
}
Err(crate::error::Error::InvalidAddress(format!(
"address {} not derivable within scan window for this account",
address
)))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid mutating address pools while probing for a match.

ensure_address_indexed currently extends pools during scan (generate_address_at_index(..., true)), so a non-derivable address can leave partial pool growth before returning Err. That breaks atomicity and makes failure paths stateful.

💡 Proposed fix (two-phase probe/commit)
 fn ensure_address_indexed(
     &mut self,
     address: &Address,
     key_source: &KeySource,
 ) -> crate::error::Result<()> {
     const MAX_SCAN: u32 = 10_000;
     let pool_count = self.keys.managed_account_type().address_pools().len();
     for index in 0..MAX_SCAN {
         for pool_idx in 0..pool_count {
-            let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
-            let derived = pool.generate_address_at_index(index, key_source, true)?;
-            if &derived == address {
+            let is_match = {
+                let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
+                let derived = pool.generate_address_at_index(index, key_source, false)?;
+                &derived == address
+            };
+            if is_match {
+                let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
+                let _ = pool.generate_address_at_index(index, key_source, true)?;
                 return Ok(());
             }
         }
     }
     Err(crate::error::Error::InvalidAddress(format!(
         "address {} not derivable within scan window for this account",
         address
     )))
 }

As per coding guidelines: "Implement atomic state updates: when modifying wallet state (transactions, UTXOs, balance, used addresses), update all related state atomically in Rust code".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn ensure_address_indexed(
&mut self,
address: &Address,
key_source: &KeySource,
) -> crate::error::Result<()> {
const MAX_SCAN: u32 = 10_000;
let pool_count = self.keys.managed_account_type().address_pools().len();
for index in 0..MAX_SCAN {
for pool_idx in 0..pool_count {
let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
let derived = pool.generate_address_at_index(index, key_source, true)?;
if &derived == address {
return Ok(());
}
}
}
Err(crate::error::Error::InvalidAddress(format!(
"address {} not derivable within scan window for this account",
address
)))
}
fn ensure_address_indexed(
&mut self,
address: &Address,
key_source: &KeySource,
) -> crate::error::Result<()> {
const MAX_SCAN: u32 = 10_000;
let pool_count = self.keys.managed_account_type().address_pools().len();
for index in 0..MAX_SCAN {
for pool_idx in 0..pool_count {
let is_match = {
let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
let derived = pool.generate_address_at_index(index, key_source, false)?;
&derived == address
};
if is_match {
let pool = &mut self.keys.managed_account_type_mut().address_pools_mut()[pool_idx];
let _ = pool.generate_address_at_index(index, key_source, true)?;
return Ok(());
}
}
}
Err(crate::error::Error::InvalidAddress(format!(
"address {} not derivable within scan window for this account",
address
)))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@key-wallet/src/managed_account/managed_core_funds_account.rs` around lines
155 - 175, ensure_address_indexed currently mutates address pools during probing
because it calls generate_address_at_index(..., true) on address_pools_mut(),
which can grow pools even when the address is not derivable; change to a
two‑phase approach: probe using non‑mutating reads (use address_pools() and call
generate_address_at_index with the non‑mutating flag or an equivalent no‑grow
path) across MAX_SCAN and pool_count to find a match, and only if a match is
found perform the actual mutation/commit (calling generate_address_at_index with
grow=true or otherwise extending address_pools_mut()) to update state
atomically; refer to ensure_address_indexed, generate_address_at_index,
address_pools(), address_pools_mut(), and MAX_SCAN to locate the code to modify.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 72.05882% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.27%. Comparing base (5297d61) to head (0834e95).

Files with missing lines Patch % Lines
.../src/managed_account/managed_core_funds_account.rs 53.65% 19 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #759      +/-   ##
=============================================
+ Coverage      72.26%   72.27%   +0.01%     
=============================================
  Files            320      320              
  Lines          70275    70316      +41     
=============================================
+ Hits           50785    50823      +38     
- Misses         19490    19493       +3     
Flag Coverage Δ
core 76.30% <ø> (ø)
ffi 46.10% <100.00%> (+0.01%) ⬆️
rpc 20.00% <ø> (ø)
spv 89.85% <ø> (+0.09%) ⬆️
wallet 71.22% <71.64%> (-0.04%) ⬇️
Files with missing lines Coverage Δ
key-wallet-ffi/src/managed_account.rs 59.68% <100.00%> (ø)
...wallet/src/transaction_checking/account_checker.rs 54.02% <100.00%> (ø)
...-wallet/src/transaction_checking/wallet_checker.rs 99.21% <100.00%> (ø)
...c/wallet/managed_wallet_info/asset_lock_builder.rs 82.77% <100.00%> (ø)
.../wallet/managed_wallet_info/transaction_builder.rs 84.90% <100.00%> (ø)
...wallet/managed_wallet_info/transaction_building.rs 88.59% <100.00%> (ø)
...allet/managed_wallet_info/wallet_info_interface.rs 83.15% <100.00%> (ø)
.../src/managed_account/managed_core_funds_account.rs 74.53% <53.65%> (-2.19%) ⬇️

... and 4 files with indirect coverage changes

Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface left a comment

Choose a reason for hiding this comment

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

This basically breaks the idea of the gap limit and seems more to be a workaround for an issue/misuse of the platform wallet.

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.

2 participants