feat(key-wallet): reserve receive addresses on hand-out#818
feat(key-wallet): reserve receive addresses on hand-out#818xdustinface wants to merge 2 commits into
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughIntroduces ChangesAddress Reservation Lifecycle
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 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
|
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
key-wallet-ffi/src/address_pool.rskey-wallet-manager/src/events.rskey-wallet/src/managed_account/address_pool.rskey-wallet/src/managed_account/managed_core_funds_account.rskey-wallet/src/tests/address_pool_tests.rskey-wallet/src/tests/address_reservation_tests.rskey-wallet/src/tests/mod.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)
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.
AddressStateenum (Available,Reserved { at },Used { at }) onAddressInfo, replacing the separateused/used_atfields. The states are mutually exclusive by construction, so the invariant "a used address is never reserved" holds structurally instead of being maintained by hand.next_unused_and_reserve,release_reservation, andsweep_expired_reservations(TTL backstop) onAddressPool; reserved addresses are excluded from hand-out, count against the gap limit, and are never pruned or aged out when clockless.next_receive_address_and_reserve,release_receive_reservation, andsweep_expired_receive_reservationsthroughManagedCoreFundsAccount, bumping the monitor revision on change.reserved_counttoPoolStats.Summary by CodeRabbit
Release Notes
New Features
Error Handling
Tests