Skip to content

feat(key-wallet): reserve receive addresses on hand-out#818

Open
xdustinface wants to merge 2 commits into
devfrom
feat/address-reservation
Open

feat(key-wallet): reserve receive addresses on hand-out#818
xdustinface wants to merge 2 commits into
devfrom
feat/address-reservation

Conversation

@xdustinface

@xdustinface xdustinface commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Adds a reservation lifecycle to addresses so a receive address handed out to a caller is not re-issued before it is funded or explicitly released. This closes the hand-out race where two sequential requests returned the same address.

  • Model address lifecycle as an AddressState enum (Available, Reserved { at }, Used { at }) on AddressInfo, replacing the separate used/used_at fields. The states are mutually exclusive by construction, so the invariant "a used address is never reserved" holds structurally instead of being maintained by hand.
  • Add next_unused_and_reserve, release_reservation, and sweep_expired_reservations (TTL backstop) on AddressPool; reserved addresses are excluded from hand-out, count against the gap limit, and are never pruned or aged out when clockless.
  • Wire next_receive_address_and_reserve, release_receive_reservation, and sweep_expired_receive_reservations through ManagedCoreFundsAccount, bumping the monitor revision on change.
  • Add reserved_count to PoolStats.
  • Cover reserve/release/sweep, serde and bincode round-trips, gap-limit and prune interaction, and end-to-end promotion on funding.

Summary by CodeRabbit

Release Notes

New Features

  • Added reservation-based receive-address flow for Standard accounts, allowing temporary holds on receive addresses with TTL-based expiry.
  • You can now release a receive address reservation back to the available pool.
  • Address lifecycle tracking is now modeled with distinct states: available, reserved, and used.

Error Handling

  • Introduced an explicit “Invalid state” error for internal invariant violations and exposed it through the FFI error mapping.

Tests

  • Expanded coverage for reservation hand-off, release idempotency, sweeping behavior, and pool selection behavior.

Adds a reservation lifecycle to addresses so a receive address handed out to a caller is not re-issued before it is funded or explicitly released. This closes the hand-out race where two sequential requests returned the same address.

- Model address lifecycle as an `AddressState` enum (`Available`, `Reserved { at }`, `Used { at }`) on `AddressInfo`, replacing the separate `used`/`used_at` fields. The states are mutually exclusive by construction, so the invariant "a used address is never reserved" holds structurally instead of being maintained by hand.
- Add `next_unused_and_reserve`, `release_reservation`, and `sweep_expired_reservations` (TTL backstop) on `AddressPool`; reserved addresses are excluded from hand-out, count against the gap limit, and are never pruned or aged out when clockless.
- Wire `next_receive_address_and_reserve`, `release_receive_reservation`, and `sweep_expired_receive_reservations` through `ManagedCoreFundsAccount`, bumping the monitor revision on change.
- Add `reserved_count` to `PoolStats`.
- Cover reserve/release/sweep, serde and bincode round-trips, gap-limit and prune interaction, and end-to-end promotion on funding.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b19100d-b3b7-4a49-ba98-591b7e775bbd

📥 Commits

Reviewing files that changed from the base of the PR and between b4a2439 and fc0668f.

📒 Files selected for processing (3)
  • key-wallet-ffi/src/error.rs
  • key-wallet/src/error.rs
  • key-wallet/src/managed_account/address_pool.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-wallet/src/managed_account/address_pool.rs

📝 Walkthrough

Walkthrough

Introduces AddressState (Available, Reserved, Used) to replace the flat used/used_at fields on AddressInfo. AddressPool gains next_unused_and_reserve, release_reservation, and sweep_expired_reservations. ManagedCoreFundsAccount exposes matching Standard-only wrappers. FFI adapters and downstream test fixtures are updated throughout.

Changes

Address Reservation Lifecycle

Layer / File(s) Summary
AddressState enum and AddressInfo contract
key-wallet/src/managed_account/address_pool.rs
Defines AddressState (Available, Reserved { at }, Used { at }) with serde/bincode support. Refactors AddressInfo to carry state instead of used/used_at. Updates both constructors and rewrites all lifecycle predicates (is_available, is_reserved, is_used, used_at, reserved_at, mark_used).
AddressPool selection, marking, and stats
key-wallet/src/managed_account/address_pool.rs
Updates next_unused*, next_unused_multiple*, unused_addresses_count, unused_addresses, mark_used, mark_index_used, scan_for_usage, needs_more_addresses, stats (adds reserved_count), reset_usage, prune_unused, PoolStats, and Display to use is_available()/is_reserved()/is_used().
InvalidState error and maintain_gap_limit safety
key-wallet/src/error.rs, key-wallet/src/managed_account/address_pool.rs
Adds Error::InvalidState(String) variant with Display formatting. Replaces panic in maintain_gap_limit with returning Error::InvalidState.
New reservation methods on AddressPool
key-wallet/src/managed_account/address_pool.rs
Implements next_unused_and_reserve, release_reservation, and sweep_expired_reservations with defined edge-case semantics for now==0, ttl==0, and at==0.
ManagedCoreFundsAccount reservation wrappers
key-wallet/src/managed_account/managed_core_funds_account.rs
Adds next_receive_address_and_reserve, release_receive_reservation, and sweep_expired_receive_reservations as Standard-only methods, each bumping the monitor revision on observable state changes.
FFI adapter and downstream fixture updates
key-wallet-ffi/src/address_pool.rs, key-wallet-ffi/src/error.rs, key-wallet-manager/src/events.rs
Updates address_info_to_ffi to call is_used()/used_at(). Adds Error::InvalidState to FFI error mapping. Fixes AddressInfo construction in FFI and manager test fixtures to use state: AddressState::Available.
Tests: pool units, batch selection, and end-to-end reservation
key-wallet/src/managed_account/address_pool.rs, key-wallet/src/tests/address_pool_tests.rs, key-wallet/src/tests/address_reservation_tests.rs, key-wallet/src/tests/mod.rs
Adds/reworks pool unit tests for reservation handout, release, idempotency, serde round-trip, sweep, reset, prune, and headroom. Updates batch-selection tests to assert reserved addresses are skipped. Adds address_reservation_tests module with async end-to-end, failure-mode, and TTL sweep boundary tests.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant ManagedCoreFundsAccount
    participant AddressPool
    participant AddressInfo

    Caller->>ManagedCoreFundsAccount: next_receive_address_and_reserve(xpub, now)
    ManagedCoreFundsAccount->>AddressPool: next_unused_and_reserve(key_source, now)
    AddressPool->>AddressInfo: find Available entry or derive new
    AddressInfo-->>AddressPool: state = Reserved { at: now }
    AddressPool-->>ManagedCoreFundsAccount: Address
    ManagedCoreFundsAccount-->>Caller: Ok(Address) + bump monitor_revision

    Caller->>ManagedCoreFundsAccount: release_receive_reservation(&address)
    ManagedCoreFundsAccount->>AddressPool: release_reservation(index)
    AddressPool->>AddressInfo: if Reserved → state = Available
    AddressPool-->>ManagedCoreFundsAccount: bool (was_reserved)
    ManagedCoreFundsAccount-->>Caller: bool + bump monitor_revision if true

    Caller->>ManagedCoreFundsAccount: sweep_expired_receive_reservations(now, ttl)
    ManagedCoreFundsAccount->>AddressPool: sweep_expired_reservations(now, ttl)
    AddressPool->>AddressInfo: Reserved { at } where (now - at) > ttl → Available
    AddressPool-->>ManagedCoreFundsAccount: usize (reclaimed)
    ManagedCoreFundsAccount-->>Caller: usize + bump monitor_revision if > 0
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested labels

ready-for-review

Suggested reviewers

  • ZocoLini

Poem

🐇 Hop, hop — no more address race!
Three states now keep each coin in place:
Available, Reserved, or Used with care,
The bunny stamps a timestamp there.
Sweep the stale ones, free the rest —
A tidy pool is always best! 🌿

🚥 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 'feat(key-wallet): reserve receive addresses on hand-out' accurately and concisely summarizes the main change: introducing address reservation functionality for receive address hand-out to prevent race conditions.
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 feat/address-reservation

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.

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.73256% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.20%. Comparing base (0b056c2) to head (fc0668f).
⚠️ Report is 3 commits behind head on dev.

Files with missing lines Patch % Lines
key-wallet/src/managed_account/address_pool.rs 92.56% 22 Missing ⚠️
.../src/managed_account/managed_core_funds_account.rs 95.23% 2 Missing ⚠️
key-wallet/src/error.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #818      +/-   ##
==========================================
- Coverage   73.38%   73.20%   -0.18%     
==========================================
  Files         323      323              
  Lines       72288    72333      +45     
==========================================
- Hits        53048    52953      -95     
- Misses      19240    19380     +140     
Flag Coverage Δ
core 76.74% <ø> (ø)
ffi 47.35% <100.00%> (-1.53%) ⬇️
rpc 20.00% <ø> (-13.05%) ⬇️
spv 90.30% <ø> (+0.04%) ⬆️
wallet 72.45% <92.64%> (+0.80%) ⬆️
Files with missing lines Coverage Δ
key-wallet-ffi/src/address_pool.rs 36.72% <100.00%> (-3.11%) ⬇️
key-wallet-ffi/src/error.rs 67.02% <100.00%> (ø)
key-wallet-manager/src/events.rs 67.98% <100.00%> (-0.16%) ⬇️
key-wallet/src/error.rs 10.52% <0.00%> (-0.29%) ⬇️
.../src/managed_account/managed_core_funds_account.rs 78.52% <95.23%> (+1.79%) ⬆️
key-wallet/src/managed_account/address_pool.rs 78.42% <92.56%> (+12.57%) ⬆️

... and 19 files with indirect coverage changes

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 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/address_pool.rs`:
- Around line 1053-1058: The needs_more_addresses() method now evaluates
available addresses using the is_available() predicate to determine if more
addresses are needed, but the maintain_gap_limit() function only checks against
highest_used when deciding whether to generate new addresses. This creates a
mismatch where needs_more_addresses() can return true for reserved-but-unused
pools while maintain_gap_limit() generates no new addresses. Update the
maintain_gap_limit() function to use the same availability predicate that counts
is_available() addresses when deciding whether to replenish the pool, ensuring
both methods use consistent logic for determining when the gap limit has been
breached.
- Around line 676-679: Replace the expect() call on the get_mut(&next_index)
operation with proper error handling using ok_or() to convert the Option into a
Result, then propagate this error through the return type of the containing
function instead of panicking. This ensures that missing pool entries result in
a returned error rather than a process crash, which is appropriate for library
code.
- Around line 250-251: The state field on AddressState lacks backward
compatibility support for deserialization of existing wallet payloads. Since the
legacy used and used_at fields have been removed, older serialized wallets will
fail to deserialize. Add the #[serde(default)] attribute to the state field
declaration to allow missing values during deserialization, or implement a
custom Deserialize handler or deserialize_with function that maps the old used
and used_at fields to the appropriate AddressState enum variant. Verify the fix
works by adding a test that attempts to deserialize a wallet payload with the
legacy field structure.
🪄 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: 53d08b96-0bb9-4ded-afe8-c70aa70862c6

📥 Commits

Reviewing files that changed from the base of the PR and between 95a3c8f and b4a2439.

📒 Files selected for processing (7)
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-manager/src/events.rs
  • key-wallet/src/managed_account/address_pool.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/tests/address_pool_tests.rs
  • key-wallet/src/tests/address_reservation_tests.rs
  • key-wallet/src/tests/mod.rs

Comment thread key-wallet/src/managed_account/address_pool.rs
Comment thread key-wallet/src/managed_account/address_pool.rs Outdated
Comment thread key-wallet/src/managed_account/address_pool.rs
…nvariant miss

`next_unused_and_reserve` and `maintain_gap_limit` guarded the "entry must exist after `generate_address_at_index(add_to_state=true)`" invariant with `expect()` and `panic!()`. This crate's error-handling philosophy is to never panic in library code, so both sites now propagate a new `Error::InvalidState` variant through their existing `Result` return type. The FFI maps it to `FFIErrorCode::InvalidState`.

Addresses CodeRabbit review comment on PR #818
#818 (comment)
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Jun 23, 2026
@xdustinface xdustinface requested a review from ZocoLini June 23, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant