Skip to content

Address-reservation lifecycle is half-wired: no release/sweep, no FFI representation #4188

Description

@Claudius-Maginificent

TL;DR: Address reservations handed out by the wallet are never released, never swept, and can't even be seen over FFI — the feature is only half-wired end to end.

User story

As a wallet user, I want an address reserved for a payment that never completes to eventually become available again, so my address pool doesn't slowly fill up with permanently stuck addresses.

As a mobile/desktop app developer consuming the FFI (SwiftExampleApp, dash-evo-tool), I want to see whether an address is available, reserved, or used, so I don't generate duplicate addresses or misreport wallet state to the user.

Scenario

Base flow

rs-platform-wallet's next_unused_receive_address hands out a fresh receive address via AddressPool::next_unused_and_reserve (from rust-dashcore PR #818), marking it AddressState::Reserved { at }.

Actual behavior

Nothing in rs-platform-wallet ever calls a matching release or sweep method. Grepped the whole crate — zero call sites for release_reservation, release_receive_reservation, or sweep_expired_reservations. A reservation is never explicitly released (e.g. when the payment flow that requested the address is abandoned, or its broadcast fails) and never reclaimed by a TTL backstop for long-running sessions.

Separately, rs-platform-wallet-ffi's wire format for a pool entry only carries is_used: bool — there's no field for the Reserved state at all. Even after the state is correctly tracked in-process, Swift/JNI consumers can't see it; it silently reads as "not used."

Expected behavior

A reservation that's abandoned or fails should eventually free up — via an explicit release call on broadcast failure and/or a periodic TTL sweep — and the FFI layer should be able to represent and surface Reserved to native consumers.

Detailed discussion

Source: found during review of PR #3968, which forward-ports address-reservation support pinned to a combined dash-evo-tool integration branch merging two sibling rust-dashcore PRs (neither is a superset of the other, hence the combined branch):

rust-dashcore already provides everything needed — nothing missing upstream. Confirmed by reading the pinned key_wallet source directly:

Level Release Sweep
AddressPool (used directly by our current hand-out call site) release_reservation(index) -> bool sweep_expired_reservations(now, ttl) -> usize
ManagedCoreFundsAccount (per-account wrapper, idempotent, bumps monitor revision) release_receive_reservation(address) -> bool sweep_expired_receive_reservations(now, ttl) -> usize
ManagedWalletInfo (trait, wallet-wide) sweep_expired_reservations(now, ttl) -> usize — sums across every funding account in one call

The wallet-wide sweep's doc comment is explicit about intent: "Backstop for reservations handed out but never funded and never explicitly released, which would otherwise pin gap-limit headroom forever." The design is deliberately caller-driven — "the wallet keeps no clock," now/ttl are always caller-supplied, and there is no baked-in default TTL anywhere in key_wallet. (The pre-existing, unrelated UTXO-level reservation system has its own RESERVATION_TTL_BLOCKS = 24 block-height-based constant — a different mechanism with different semantics; not a usable numeric precedent here.)

Needed work (all inside rs-platform-wallet / rs-platform-wallet-ffi, deliberately out of scope for PR #3968, which touches rs-platform-wallet-storage only):

  1. Call release_reservation / release_receive_reservation from the existing broadcast-failure reconciliation path (wallet/reservations.rs, broadcaster.rs) alongside the existing UTXO-release call, when a transaction funding a reserved address fails or is abandoned.
  2. Wire a periodic call to sweep_expired_reservations (wallet-wide trait method, or the per-account variant) into one of the existing periodic managers (platform_address_sync, identity_sync, dashpay_sync all follow the same interval_secs pattern) as a TTL backstop for sessions that never restart.
  3. Widen rs-platform-wallet-ffi's pool-entry wire struct (currently is_used: bool only) to represent AddressState::Reserved { at }, and update the Swift/JNI bindings and consumers accordingly.
  4. Widen platform_wallet::wallet::provider_key_at_index::insert_platform_node_pool_entry's signature (currently used: bool) to accept the full tri-state, so rs-platform-wallet-storage — which starts persisting reserved_at in PR feat(platform-wallet-storage): embeddable SQLite persistence backend with seedless rehydration #3968 — can actually thread the restored value back into the rebuilt AddressPool on load. Today the read/restore path is deliberately left bool-only (see the TODO in core_pool.rs/util/wallet.rs), so a restored wallet still shows every previously-Reserved address as Available in memory even though the value is now safely stored in SQLite.

Accepted trade-off: until item 4 lands, no self-healing/restore-time reconstruction of Reserved state exists — this matches pre-#3968 behavior exactly (no regression), since #3968 only makes the SQLite column correct, it doesn't yet feed it back into the live wallet. Once item 4 does land, items 1–2 become load-bearing: a restored Reserved address that nothing ever releases or sweeps would otherwise pin gap-limit headroom forever, so this issue's items should land together rather than piecemeal.

Prior work

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions