fix(platform-wallet): reserve receive address via AddressReservations (Found-026) [backport]#3658
fix(platform-wallet): reserve receive address via AddressReservations (Found-026) [backport]#3658lklimek wants to merge 8 commits into
Conversation
… (Found-026) [backport] Semantic backport of the #3549-proven Found-026 reserve-on-hand-out fix. Region diverged on v3.1-dev so re-applied by hand, not cherry-picked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughnext_unused_receive_address now retrieves address metadata (including index) via next_unused_with_info, marks that index as used in-memory with mark_index_used, and then returns the corresponding PlatformAddress. A test verifies two immediate handouts are distinct without a sync. ChangesPlatform Address Reservation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 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✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## fix/dpns-case-and-utxo-race-v3.1-dev #3658 +/- ##
========================================================================
- Coverage 87.16% 87.15% -0.01%
========================================================================
Files 2607 2606 -1
Lines 319420 319221 -199
========================================================================
- Hits 278413 278219 -194
+ Misses 41007 41002 -5
🚀 New features to boost your workflow:
|
|
✅ DashSDKFFI.xcframework built for this PR.
SwiftPM (host the zip at a stable URL, then use): .binaryTarget(
name: "DashSDKFFI",
url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
checksum: "ddc2e0db0d5263b55e16d1e50fa8bb79d8e69f8a0d64220e4522cef293f1c2e3"
)Xcode manual integration:
|
There was a problem hiding this comment.
Code Review
Surgical Found-026 backport that correctly closes the in-process duplicate-address race by reserving the index under the wallet_manager write guard. However, the reservation is in-memory only — no changeset is emitted to the persister — so the same Found-026 invariant can still be violated across a process restart. Several style nitpicks (variable shadowing, unnecessary clone) and a PR-description audit-trail drift are also worth correcting.
Inline comments could not be attached, so I posted the verified findings as a top-level review body.
Reviewed commit: 4acd3c2
🔴 1 blocking | 🟡 1 suggestion(s) | 💬 3 nitpick(s)
5 additional finding(s)
blocking: Reservation is in-memory only — restart re-opens the same Found-026 window
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs (line 268)
mark_index_used(info.index) flips the used flag inside the live AddressPool but no changeset is emitted: there is no self.persister.store(...) call on this hand-out path, and the changeset docs (changeset.rs:919-922) explicitly state that pool snapshots fire on "used-flag flip". The only account_address_pools emission in this branch is at wallet registration (manager/wallet_lifecycle.rs:279). On restart, PlatformPaymentAddressProvider::from_persisted and PerAccountPlatformAddressState::from_persisted (provider.rs:95, 279) reconstruct state from BiBTreeMap<AddressIndex, PlatformP2PKHAddress> + a found balance map; neither carries the used flag. So if the app hands out address A, crashes/restarts before a sync funds A, then calls next_unused_receive_address again, the rebuilt pool still considers index A unused and re-hands it. The PR description scopes this to the in-process race, but Found-026 is the duplicate-address invariant — closing one of two paths to it still reproduces the same user-visible failure. Either (a) emit a pool snapshot from this hand-out path via self.persister.store(...), (b) document the restart gap explicitly in the PR body and confirm it is also unfixed on #3549, or (c) confirm a sync is unconditionally invoked before the address is returned.
nitpick: `info` shadows the outer wallet-info binding
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs (line 268)
The outer let (wallet, info) = wm.get_wallet_mut_and_info_mut(...) on line 228 binds info to the wallet info. The new let info = managed_account.addresses.next_unused_with_info(...) shadows that binding while the mutable borrow rooted in the outer info is still alive (via managed_account). Rust allows this and it compiles, but reusing the same identifier for two semantically distinct values in a concurrency-fix block hurts readability. Rename to address_info to make the lock-scoped reservation logic obvious.
// Reserve the address on hand-out (Found-026): platform-payment
// `used` only flips on a positive synced balance, so without
// marking it here a concurrent caller's `next_unused` would
// re-hand the same index before the sync pass. `mark_index_used`
// is idempotent — a later real sync hit on this index is a
// no-op, so gap-limit/`highest_used` accounting isn't doubled.
let address_info = managed_account
.addresses
.next_unused_with_info(&key_source, true)
.map_err(|e| PlatformWalletError::AddressSync(e.to_string()))?;
managed_account.addresses.mark_index_used(address_info.index);
let address = address_info.address;
nitpick: Unnecessary clone of `info.address`
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs (line 272)
info.address.clone() allocates a fresh Address, but the only other use of info is info.index (which is Copy). Flip the order — mark_index_used(info.index) first, then let address = info.address; — and the clone disappears. The clone exists only because of statement ordering, not borrowing constraints.
let info = managed_account
.addresses
.next_unused_with_info(&key_source, true)
.map_err(|e| PlatformWalletError::AddressSync(e.to_string()))?;
managed_account.addresses.mark_index_used(info.index);
let address = info.address;
suggestion: No regression test guarding the duplicate-address race on v3.1-dev
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs (line 223)
The PR description acknowledges that v3.1-dev lacks the e2e harness and found_026 guards (those live only on #3549). For a fix whose entire purpose is concurrent correctness, a focused unit test asserting that two consecutive next_unused_receive_address calls return distinct indices without an intervening sync would prevent regression on this branch. Without it, a future refactor here has no in-tree signal that it has reintroduced the bug.
nitpick: PR description cites a key-wallet pin that does not match Cargo.toml
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs (line 262)
The PR body states it audited the v3.1-dev key-wallet pin 53130869e5b... and confirmed byte-identical API surface for AddressPool / next_unused_with_info / mark_index_used / AddressInfo. The actual root Cargo.toml on this branch pins key-wallet at a different revision. Since cargo check passes, the API does exist at the real pin — but the cited audit trail no longer matches the source of truth. Either update the PR body to cite the current pin or note that the pin moved post-audit and re-state the API parity check against it.
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
- [BLOCKING] In `packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`:268-273: Reservation is in-memory only — restart re-opens the same Found-026 window
`mark_index_used(info.index)` flips the used flag inside the live `AddressPool` but no changeset is emitted: there is no `self.persister.store(...)` call on this hand-out path, and the changeset docs (changeset.rs:919-922) explicitly state that pool snapshots fire on "used-flag flip". The only `account_address_pools` emission in this branch is at wallet registration (manager/wallet_lifecycle.rs:279). On restart, `PlatformPaymentAddressProvider::from_persisted` and `PerAccountPlatformAddressState::from_persisted` (provider.rs:95, 279) reconstruct state from `BiBTreeMap<AddressIndex, PlatformP2PKHAddress>` + a `found` balance map; neither carries the used flag. So if the app hands out address A, crashes/restarts before a sync funds A, then calls `next_unused_receive_address` again, the rebuilt pool still considers index A unused and re-hands it. The PR description scopes this to the in-process race, but Found-026 is the duplicate-address invariant — closing one of two paths to it still reproduces the same user-visible failure. Either (a) emit a pool snapshot from this hand-out path via `self.persister.store(...)`, (b) document the restart gap explicitly in the PR body and confirm it is also unfixed on #3549, or (c) confirm a sync is unconditionally invoked before the address is returned.
- [NITPICK] In `packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`:268-273: `info` shadows the outer wallet-info binding
The outer `let (wallet, info) = wm.get_wallet_mut_and_info_mut(...)` on line 228 binds `info` to the wallet info. The new `let info = managed_account.addresses.next_unused_with_info(...)` shadows that binding while the mutable borrow rooted in the outer `info` is still alive (via `managed_account`). Rust allows this and it compiles, but reusing the same identifier for two semantically distinct values in a concurrency-fix block hurts readability. Rename to `address_info` to make the lock-scoped reservation logic obvious.
- [NITPICK] In `packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`:272-273: Unnecessary clone of `info.address`
`info.address.clone()` allocates a fresh Address, but the only other use of `info` is `info.index` (which is `Copy`). Flip the order — `mark_index_used(info.index)` first, then `let address = info.address;` — and the clone disappears. The clone exists only because of statement ordering, not borrowing constraints.
- [SUGGESTION] In `packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`:223-278: No regression test guarding the duplicate-address race on v3.1-dev
The PR description acknowledges that v3.1-dev lacks the e2e harness and found_026 guards (those live only on #3549). For a fix whose entire purpose is concurrent correctness, a focused unit test asserting that two consecutive `next_unused_receive_address` calls return distinct indices without an intervening sync would prevent regression on this branch. Without it, a future refactor here has no in-tree signal that it has reintroduced the bug.
- [NITPICK] In `packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs`:262-267: PR description cites a key-wallet pin that does not match Cargo.toml
The PR body states it audited the v3.1-dev key-wallet pin `53130869e5b...` and confirmed byte-identical API surface for `AddressPool` / `next_unused_with_info` / `mark_index_used` / `AddressInfo`. The actual root `Cargo.toml` on this branch pins key-wallet at a different revision. Since `cargo check` passes, the API does exist at the real pin — but the cited audit trail no longer matches the source of truth. Either update the PR body to cite the current pin or note that the pin moved post-audit and re-state the API parity check against it.
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Four carried-forward findings are still valid on eecd81e24d46a45b6fe9a320c4c372f543f961b7: the duplicate-address fix remains volatile across restart, there is still no focused regression test for that path, and the two wallet.rs nitpicks are unchanged. The prior key-wallet pin mismatch finding is outdated on the current head because the workspace dependency pin changed from the old revision; I found no additional source-backed defect in the latest delta beyond the unresolved carried-forward items.
Reviewed commit: eecd81e
🔴 1 blocking | 🟡 1 suggestion(s) | 💬 2 nitpick(s)
Inline posting hit GitHub validation for this overlapping carried-forward review, so all verified findings are included in the review body.
1. blocking: Receive-address reservation is still volatile, so a restart can hand out the same address again
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs:274-279
next_unused_receive_address() reserves the returned index only inside the live AddressPool by calling mark_index_used(info.index), and this path still does not emit any PlatformWalletChangeSet or persistence callback afterward. On restart, the platform-address restore path rebuilds PlatformAddressSyncStartState from persisted balance rows only (packages/rs-platform-wallet-ffi/src/persistence.rs:2728-2796), while PlatformPaymentAddressProvider::prepare_for_sync() repopulates pending addresses from that restored balance state plus whatever is currently in the live pool (packages/rs-platform-wallet/src/wallet/platform_addresses/provider.rs:368-418). A reserved-but-unfunded address is therefore forgotten after process exit and becomes eligible for next_unused_with_info() again. The in-memory race is fixed only until the app restarts; a crash, OS eviction, or relaunch before the next funded sync reopens the same duplicate-address/privacy window this fix is meant to close.
2. suggestion: There is still no focused regression test for repeated address hand-out before sync or after restart
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs:229-284
The repository still has no test that calls next_unused_receive_address() twice before a sync pass and asserts that two distinct addresses are returned. rg finds only the implementation site, and the only wallet integration test in packages/rs-platform-wallet/tests/spv_sync.rs is an ignored SPV smoke test that does not exercise this path. That leaves the exact Found-026 failure mode unguarded, including the restart case where the reservation should survive persistence if the blocker above is fixed.
3. nitpick: `info` still shadows the outer wallet-info binding
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs:274-279
This function binds info from get_wallet_mut_and_info_mut() and then immediately reuses info for the result of next_unused_with_info(). Both bindings are live in the same short block and refer to unrelated concepts, which makes the ownership and mutation flow harder to audit in a function already mutating both wallet-manager and address-pool state.
let address_info = managed_account
.addresses
.next_unused_with_info(&key_source, true)
.map_err(|e| PlatformWalletError::AddressSync(e.to_string()))?;
let address = address_info.address.clone();
managed_account.addresses.mark_index_used(address_info.index);
4. nitpick: The returned address is still cloned even though the owned value can be moved
packages/rs-platform-wallet/src/wallet/platform_addresses/wallet.rs:278-281
next_unused_with_info() returns an owned AddressInfo, and after reserving the index the function does not need that struct again. Cloning info.address adds an unnecessary copy on a hot path whose only job is to reserve and return the next address.
let address_info = managed_account
.addresses
.next_unused_with_info(&key_source, true)
.map_err(|e| PlatformWalletError::AddressSync(e.to_string()))?;
managed_account.addresses.mark_index_used(address_info.index);
PlatformAddress::try_from(address_info.address).map_err(|e| {
PlatformWalletError::AddressSync(format!("Failed to convert to PlatformAddress: {e}"))
})
Prior finding reconciliation
{
"resolved_findings": [
{
"finding_hash": "prior-key-wallet-pin-mismatch",
"original_title": "PR description cites a key-wallet pin that does not match Cargo.toml",
"resolution": "Outdated on the current head. `Cargo.toml` now pins `key-wallet` to `f569e7b7b99dfe589c41f9ba7d36fbbe6805acdc` at lines 55-57, so the prior line-referenced mismatch against the older workspace revision no longer exists in source."
}
],
"dropped_findings": [
{
"original_title": "Receive-address reservation is still volatile, so a restart reintroduces duplicate-address reuse",
"source": [
"codex"
],
"reason": "Merged into the single blocking restart-persistence finding."
},
{
"original_title": "Receive-address reservation is still in-memory only, so a restart reopens the duplicate-address window",
"source": [
"codex"
],
"reason": "Merged into the single blocking restart-persistence finding."
},
{
"original_title": "The Found-026 fix still has no regression test for repeated hand-out across in-flight or restart boundaries",
"source": [
"codex"
],
"reason": "Merged into the single test-coverage finding."
},
{
"original_title": "Inner `info` binding still shadows the outer wallet-info binding",
"source": [
"codex"
],
"reason": "Merged into the single naming finding."
},
{
"original_title": "The handed-out address is still cloned even though it can be moved after reservation",
"source": [
"codex"
],
"reason": "Merged into the single performance finding."
},
{
"original_title": "The PR description's claimed key-wallet pin still does not match the actual dependency revision",
"source": [
"codex"
],
"reason": "Outdated on the current head. The workspace pin changed to `f569e7b7b99dfe589c41f9ba7d36fbbe6805acdc`, so the prior code-side mismatch no longer exists at the referenced lines, and the repository contents do not include the PR description needed to prove a remaining docs mismatch."
},
{
"original_title": "The review narrative's claimed key-wallet baseline does not match the code now under review",
"source": [
"codex"
],
"reason": "Outdated on the current head for the same reason: the old pin mismatch is gone in source, and the claimed PR narrative cannot be verified from the worktree."
}
]
}Address review feedback on PR #3658: - Rename inner `info` to `address_info` in next_unused_receive_address to disambiguate from the outer wallet-info binding. - Elide `info.address.clone()` by reordering mark_index_used to use the Copy `index` before moving `address` out. - Document the in-memory-only reservation design via an INTENTIONAL comment block (accept-risk decision on persistence-gap finding). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address PR #3658 reviewer (thepastaclaw) request for a regression test on the v3.1-dev backport. Ports the `found_026_back_to_back_handout_returns_distinct_addresses` unit test from the e2e campaign tree (PR #3549) along with the `wallet_with_platform_account` helper. Asserts two consecutive next_unused_receive_address calls (no intervening sync) return distinct addresses — the exact Found-026 race the fix in 8d0f0fb closes. The companion test (`found_026_repeated_handouts_advance_gap_limit_exactly_k`) is intentionally NOT ported — it reaches into platform_payment_managed_account pool internals, which is on the deprecation path per QuantumExplorer's review on #3648. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@thepastaclaw Thanks for the review — all five findings addressed in 1. Persistence gap ( 2. Variable shadowing. Renamed inner 3. Unnecessary clone. Reordered to call 4. Missing regression test. Ported 5. PR body audit-trail drift. Updated the PR description to cite the current key-wallet pin
🤖 Co-authored by Claudius the Magnificent AI Agent |
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The latest delta correctly fixes the immediate duplicate-handout regression in next_unused_receive_address() by reserving the selected pool entry before releasing the wallet-manager write lock, and the new focused unit test covers two back-to-back hand-outs with no intervening sync. Revalidation of the prior review items on head 480375c8be10256d4528b84d42706faabda97c2f: the shadowing nit is fixed, the unnecessary clone is fixed, and the missing pre-sync regression test is fixed. The only surviving old concern is the restart-time reservation gap, but current head explicitly documents that behavior as an intentional in-memory-only limitation and next_unused_receive_address() still has no production call sites in this branch, so there is no remaining actionable finding for this PR; I could not complete the targeted cargo test because the build script attempted to download Tenderdash over the network in the sandbox.
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-checked the latest head (480375c8be10256d4528b84d42706faabda97c2f). The follow-up commits address the review items for this backport:
- accepted the persistence/restart gap as an explicit design trade-off and documented the remaining window + follow-up-before-FFI requirement in both source and PR body
- removed the
infoshadowing and unnecessary address clone - added the focused Found-026 back-to-back hand-out regression
- updated the key-wallet audit trail to the current
f569e7b7...pin
I also verified there are still no non-test call sites for next_unused_receive_address, inspected the delta from eecd81e2 to 480375c8, and ran cargo test -p platform-wallet --lib -- found_026 locally (passed). Good from my side now.
QuantumExplorer
left a comment
There was a problem hiding this comment.
No this is wrong. We should not mark an address as used unless it is indeed used. This could have other side effects.
|
@QuantumExplorer agreed — I prepared a narrow fix at
Validation: I could not push directly to |
|
@QuantumExplorer — agreed. The premature Plan: refactor along the same pattern we shipped in #3585 for UTXO same-input race protection (
Result: no premature gap-limit / Sequencing: this stacks naturally on #3585. The mechanism there will be generalized to Dropping "ready for final review" while this lands. Will ping when the refactored version is up for re-review. 🤖 Co-authored by Claudius the Magnificent AI Agent |
|
Status update — retarget + Reservations refactor Per @QuantumExplorer's concern that Approach:
Mechanics:
🤖 Co-authored by Claudius the Magnificent AI Agent |
… and address races Generalizes the OutpointReservations RAII-guard mechanism into Reservations<K: Eq + Hash + Clone + Debug> + ReservationGuard<K>, exposing AddressReservations (K = (u32, u32)) as a type alias for the platform-address hand-out path. OutpointReservations is preserved as a domain wrapper composing Reservations<OutPoint> + Reservations<Address> so the change-address peek/commit logic stays bound to the broadcast caller. release_after_commit and leak_until_sync semantics from #3585 flow through to both aliases unchanged. Tests cover the address alias (distinct keys, leak_until_sync hold, snapshot reflection) alongside the existing outpoint suite. Also fixes pre-existing clippy::manual_dangling_ptr findings in rs-platform-wallet-ffi/src/core_wallet/broadcast.rs tests, surfaced once the workspace -D warnings gate ran on this branch. Addresses @QuantumExplorer review feedback on #3658: marking address index used at hand-out time advanced gap-limit / highest_used prematurely. Reservation guard holds the slot until the chain-sync positive-balance flip commits highest_used, mirroring leak_until_sync semantics from #3585. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…essReservations Replaces the eager `managed_account.addresses.mark_index_used(idx)` call on the platform-payment hand-out path with an in-memory AddressReservations slot. Hand-out picks the first `!used && !reserved` index in `0..=highest_generated`, falling back to minting at `highest_generated + 1` when the generated range is exhausted. The upstream pool's `highest_used` is left untouched until a positive-balance sync hit flips `used`, addressing @QuantumExplorer's review feedback on #3658. Reservation guards are leaked via `leak_until_sync` so concurrent / back-to-back hand-outs see the slot as taken; the in-memory set is cleared on process restart, matching the documented "addresses requested but never paid to are freed for reuse on the next session" contract. Regression tests added: - next_unused_receive_address_does_not_advance_highest_used - handout_skips_reserved_but_not_used_indices (three back-to-back hand-outs yield three distinct indices) The original Found-026 back-to-back distinct-address invariant still holds — guarded by the unchanged `found_026_back_to_back_handout_*` test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Refactor landed — Three commits on top of the previous tip:
Summary of changes vs. earlier state:
PR title + body rewritten to reflect the new scope. Base is still Ready for re-review. 🤖 Co-authored by Claudius the Magnificent AI Agent |
Semantic backport to
v3.1-devof the Found-026 reserve-on-hand-out fix. Iterated through reviewer feedback to land on a reservation-based design shared with #3585's UTXO race protection.Problem
key-wallet
AddressPool::next_unusedreturns the firstused==falseindex WITHOUT marking it;usedonly flips on a positive synced balance. Two concurrent callers ofPlatformAddressWallet::next_unused_receive_addresstherefore receive the SAME address before the sync pass — duplicate-address race (Found-026).Fix
This PR builds on #3585's generic
Reservations<K>RAII guard. Two consumers now share the abstraction:OutpointReservations(from fix: case-insensitive .dash + atomic state on broadcast failure #3585) — UTXO same-input race protection.AddressReservations(this PR) — keyed by(account_index, address_index), holds platform-receive-address slots until chain sync confirms or the broadcast abandons.next_unused_receive_addressno longer eagerly callsmark_index_usedon hand-out (per @QuantumExplorer feedback — advanceshighest_used/ gap-limit prematurely). Instead it snapshotsAddressReservations, picks the first!used && !reservedindex, and returns aleak_until_sync-flavored guard. The slot stays reserved until the matching positive-balance sync flipsused(then the in-memory entry is cleared) or until process restart (clears all in-memory reservations; the documented "unpaid addresses freed next session" contract).Generic
Reservations<K>Refactored #3585's
OutpointReservationstoReservations<K: Eq + Hash + Clone + Debug>withReservationGuard<K>:release_after_commit()— drop guard normally.leak_until_sync()—std::mem::forget(self)so a successful broadcast holds the slot through the event-loop tick.OutpointReservationsis retained as a domain wrapper composingReservations<OutPoint>+Reservations<Address>because the change-address-tracking surface (pending_change_snapshot()etc.) doesn't fit a plainKalias.AddressReservations = Reservations<(u32, u32)>IS a clean alias.Validation
cargo test -p platform-wallet --lib: 153 passed, 0 failed.cargo test -p platform-wallet-ffi --lib: 80 passed, 0 failed.cargo fmt --all+cargo clippy --workspace --all-targets -- -D warnings: clean.New tests:
address_reservation_distinct_keys_independentaddress_reservation_leak_until_sync_holds_slotaddress_reservation_same_key_twice_is_redundant_releaseaddress_reservation_snapshot_reflects_statenext_unused_receive_address_does_not_advance_highest_usedhandout_skips_reserved_but_not_used_indicesBase branch
This PR is currently retargeted at
fix/dpns-case-and-utxo-race-v3.1-dev(#3585's branch) since the genericReservations<K>lives there. When #3585 merges tov3.1-dev, this PR's base will be retargeted back tov3.1-dev.Drive-by
Fixed three pre-existing
clippy::manual_dangling_ptrlints (Rust 1.92) inplatform-wallet-ffithat surfaced under workspace-D warnings.Backport sibling of #3549 / Found-026.
🤖 Co-authored by Claudius the Magnificent AI Agent