Skip to content

feat(key-wallet): owner-tagged reservations to close the broadcast-release TOCTOU (platform#4185)#916

Open
bfoss765 wants to merge 2 commits into
dashpay:devfrom
bfoss765:feat/owner-tagged-reservations
Open

feat(key-wallet): owner-tagged reservations to close the broadcast-release TOCTOU (platform#4185)#916
bfoss765 wants to merge 2 commits into
dashpay:devfrom
bfoss765:feat/owner-tagged-reservations

Conversation

@bfoss765

@bfoss765 bfoss765 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Why this PR exists

This adds an owner-tagged reservation capability to key-wallet to close a real time-of-check/time-of-use double-spend window that the platform layer (dashpay/platform#4185) cannot fix on its own.

The bug it fixes: ReservationSet currently maps OutPoint → reserved-at-height with no per-reservation owner, and release removes by outpoint unconditionally. Platform's deferred-broadcast / asset-lock path reserves a transaction's inputs, .awaits the network broadcast, and on a Rejected result releases those inputs by outpoint. If key-wallet's TTL sweep reclaims the reservation during that await and a different concurrent build re-reserves the same outpoint (same wallet generation, so any Arc::ptr_eq generation guard still passes), the rejected build's cleanup frees the other build's reservation — opening a double-spend window. Platform cannot detect this, because the sweep happens invisibly inside key-wallet; the "release only if it's still mine" check must be atomic under the ReservationSet mutex. That's what this PR provides.

What changed

New public type ReservationToken (re-exported from crate root + managed_account): opaque Copy newtype over a private u64, with no public constructor — a token can only be obtained from reserve(), so it is forge-proof and cheap to hold across an await. Height cannot serve as the identity (it collides across same-height reserves).

ReservationSet (still pub(crate)):

  • reserve(&[OutPoint], height) -> ReservationToken — now returns a fresh per-call token stamping all outpoints; re-reserving transfers ownership to the new token.
  • release_if_owner(&[OutPoint], ReservationToken) (new) — atomically removes only outpoints whose stored owner == the token; a no-op for swept / re-reserved / unreserved outpoints.
  • release(...) (kept, unconditional) — doc now scopes it to the known-spent path only.
  • Storage: HashMap<OutPoint, Reservation { reserved_at_height, owner }> + a next_token counter; TTL sweep preserved.

Public seam so consumers can adopt the fix (all additive):

  • TransactionBuilder::build_unsigned_reserved() / build_signed_reserved() → return the extra Option<ReservationToken>.
  • ManagedCoreFundsAccount::release_reservation_if_owner(&Transaction, ReservationToken) — the owner-guarded release a Rejected path should call instead of the unconditional release_reservation.
  • AssetLockResult.reservation_token: Option<ReservationToken> — carries the token out of the asset-lock build.

The sign-failure path in build_signed (which also awaits) was switched to release_if_owner. The processed-spend release stays unconditional (the coin is genuinely spent).

Back-compat

  • build_unsigned / build_signed and release_reservation keep their existing signatures (they thin-delegate); the ~20 in-repo callers are untouched.
  • ReservationSet::reserve's changed return is pub(crate), zero external impact; all in-repo sites updated.
  • key-wallet-manager, dash-spv, key-wallet-ffi don't reference reservations; they still compile.

Validation

cargo build -p key-wallet -p key-wallet-manager -p dash-spv -p key-wallet-ffi clean; cargo test -p key-wallet --lib = 552 passed, 0 failed (13/13 reservation tests incl. the TOCTOU regression: reserve X→token A, simulate sweep + re-reserve X→token B, assert release_if_owner([X], A) does NOT free X); clippy + doc clean.

Scope note

The asset-lock flow (the one platform#4185 cites) is wired end-to-end. The general manager send wrappers (build_and_sign_transaction*) still return (Transaction, u64) and drop the token — platform's #4185 adoption can call build_signed_reserved directly; happy to thread the token through those wrappers too if preferred.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added reservation ownership tracking for funding inputs.
    • Asset-lock results now provide an optional reservation token.
    • Reservation-aware transaction building returns tokens for callers to manage.
  • Bug Fixes

    • Prevented stale or failed transaction builds from releasing inputs reserved by another concurrent build.
    • Improved safety when reservations expire and are subsequently reused.

…se the broadcast-release TOCTOU (platform#4185)

ReservationSet previously mapped OutPoint -> reserved-at-height with no
per-reservation owner, and released unconditionally by outpoint. The platform
broadcast path (dashpay/platform#4185) reserves an asset-lock/deferred send's
inputs, awaits the broadcast, and on a Rejected result releases them by
outpoint. If key-wallet's TTL sweep reclaims that reservation mid-await and a
different concurrent build re-reserves the same outpoint (same wallet
generation, so any Arc::ptr_eq generation guard still passes), the rejected
build's cleanup frees the OTHER build's reservation -> a double-spend window.
The platform layer cannot detect this because the sweep is invisible inside
key-wallet; the "release only if still mine" check must be atomic under the
ReservationSet mutex.

Stamp every reservation with a ReservationToken (a Copy newtype over a
monotonic per-set counter, unique even across same-height reserves). reserve()
returns the token it stamped; release_if_owner(outpoints, token) removes only
outpoints still owned by that token, atomically under the mutex, and is a no-op
for any swept-and-re-reserved outpoint. The unconditional release() is kept for
the processed-spend path, where the coin is genuinely spent and must leave the
set regardless of owner.

Wiring:
- assemble_unsigned returns the stamped token; build_signed's sign-failure
  path now releases owner-guarded (it too awaits, so it shares the hazard).
- Additive token-returning builds: build_unsigned_reserved /
  build_signed_reserved. build_unsigned / build_signed keep their signatures.
- Public seam for platform: ManagedCoreFundsAccount::release_reservation_if_owner
  and AssetLockResult::reservation_token; ReservationToken re-exported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@bfoss765, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b4b2a263-1fb2-480d-a9d7-339108e8b935

📥 Commits

Reviewing files that changed from the base of the PR and between e99959c and 9d9af46.

📒 Files selected for processing (1)
  • key-wallet/src/managed_account/managed_core_funds_account.rs
📝 Walkthrough

Walkthrough

Reservation ownership tokens are introduced for UTXO reservations, propagated through reserved transaction and asset-lock builders, and exposed through managed account and crate APIs. Signing-failure cleanup now releases only reservations still owned by the originating build.

Changes

Reservation ownership flow

Layer / File(s) Summary
Reservation ownership model
key-wallet/src/managed_account/reservation.rs
Reservations now store a per-build ReservationToken, support owner-guarded release, and test token uniqueness, ownership checks, and sweep/re-reserve behavior.
Reservation-aware transaction building
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Reserved unsigned and signed builds return optional reservation tokens and use owner-guarded cleanup after signing failures.
Asset-lock and account integration
key-wallet/src/managed_account/..., key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs, key-wallet/src/lib.rs
Asset-lock results expose reservation tokens, managed accounts add guarded release, and ReservationToken is re-exported publicly.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • dashpay/rust-dashcore#812: Both changes modify key-wallet reservation handling across reservation, transaction-builder, and managed-account code.
  • dashpay/rust-dashcore#905: Both changes integrate managed account APIs with the key-wallet reservation ledger.

Suggested labels: ready-for-review

Suggested reviewers: xdustinface, quantumexplorer

🚥 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 accurately summarizes the main change: owner-tagged reservations fixing the broadcast-release TOCTOU.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs (1)

219-252: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reservation tokens are captured but never used to guard cleanup on downstream (non-signing) failures. Both asset-lock builders take a reservation via build_signed_reserved and get back reservation_token, but a failure in the code that runs after the successful signed build (credit-key derivation, or the signer round-trip/bookkeeping) returns Err without releasing it — stranding the already-signed transaction's reserved inputs until the 24-block TTL sweep, since the caller never sees the token to release it itself.

  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs#L219-L252: in build_asset_lock, clone the funding account's ReservationSet before the credit-key derivation loop and call release_if_owner on the transaction's outpoints when resolve_funding_account/next_private_key fails, before returning Err.
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs#L293-L357: in build_asset_lock_with_signer, apply the same guarded-release pattern around the Phase 1–3 bookkeeping loop (resolve_funding_account, peek_next_path, signer.public_key(&path).await, mark_first_pool_index_used).
🤖 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/wallet/managed_wallet_info/asset_lock_builder.rs` around lines
219 - 252, Add guarded reservation cleanup in
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs:219-252 within
build_asset_lock by cloning the funding account’s ReservationSet before
credit-key derivation and calling release_if_owner on the signed transaction’s
outpoints whenever resolve_funding_account or next_private_key fails. Apply the
same pattern in
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs:293-357 within
build_asset_lock_with_signer for failures during the Phase 1–3 bookkeeping
operations, including resolve_funding_account, peek_next_path,
signer.public_key, and mark_first_pool_index_used; preserve successful returns
and only release reservations owned by this build.
🤖 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.

Outside diff comments:
In `@key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs`:
- Around line 219-252: Add guarded reservation cleanup in
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs:219-252 within
build_asset_lock by cloning the funding account’s ReservationSet before
credit-key derivation and calling release_if_owner on the signed transaction’s
outpoints whenever resolve_funding_account or next_private_key fails. Apply the
same pattern in
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs:293-357 within
build_asset_lock_with_signer for failures during the Phase 1–3 bookkeeping
operations, including resolve_funding_account, peek_next_path,
signer.public_key, and mark_first_pool_index_used; preserve successful returns
and only release reservations owned by this build.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 392d56f0-13b2-454e-aea8-f0de66b4c248

📥 Commits

Reviewing files that changed from the base of the PR and between 70d4bf8 and e99959c.

📒 Files selected for processing (6)
  • key-wallet/src/lib.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet/src/managed_account/reservation.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.39640% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.53%. Comparing base (70d4bf8) to head (9d9af46).

Files with missing lines Patch % Lines
.../src/managed_account/managed_core_funds_account.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #916      +/-   ##
==========================================
- Coverage   74.56%   74.53%   -0.04%     
==========================================
  Files         328      328              
  Lines       75711    75798      +87     
==========================================
+ Hits        56451    56493      +42     
- Misses      19260    19305      +45     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 50.53% <ø> (-0.01%) ⬇️
rpc 20.00% <ø> (ø)
spv 90.95% <ø> (-0.23%) ⬇️
wallet 74.49% <96.39%> (+0.08%) ⬆️
Files with missing lines Coverage Δ
key-wallet/src/managed_account/reservation.rs 100.00% <100.00%> (ø)
...c/wallet/managed_wallet_info/asset_lock_builder.rs 88.19% <100.00%> (+0.05%) ⬆️
.../wallet/managed_wallet_info/transaction_builder.rs 87.93% <100.00%> (+0.34%) ⬆️
.../src/managed_account/managed_core_funds_account.rs 77.10% <0.00%> (-0.76%) ⬇️

... and 10 files with indirect coverage changes

bfoss765 added a commit to bfoss765/platform that referenced this pull request Jul 23, 2026
Point all rust-dashcore crates at bfoss765/rust-dashcore
e99959ced0062159d629930f488374e29f63c42b (PR dashpay/rust-dashcore#916),
which is v4.1-dev's rust-dashcore tip 70d4bf8 plus the additive
owner-tagged reservation API: key_wallet::ReservationToken,
ReservationSet::reserve/release_if_owner,
TransactionBuilder::build_{unsigned,signed}_reserved,
ManagedCoreFundsAccount::release_reservation_if_owner, and
AssetLockResult.reservation_token. Additive over 70d4bf8, so it stays
compatible with the rest of the v4.1-dev workspace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant