fix(key-wallet): reserve selected UTXOs to prevent double-spend#812
fix(key-wallet): reserve selected UTXOs to prevent double-spend#812xdustinface wants to merge 6 commits into
Conversation
|
Warning Review limit reached
Next review available in: 59 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughIntroduces a ChangesUTXO Reservation System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
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 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 #812 +/- ##
==========================================
+ Coverage 73.16% 73.25% +0.08%
==========================================
Files 323 324 +1
Lines 72438 72622 +184
==========================================
+ Hits 52999 53197 +198
+ Misses 19439 19425 -14
|
461de8a to
fc51d06
Compare
ZocoLini
left a comment
There was a problem hiding this comment.
I find this a little confusing and overkill, why don't we use a static HashMap in the transaction_builder module and we only update that, also prevents a user from creating twice the same wallet and falling in the same issue when building transactions.
At the same time, I don't see how this would avoid reserving utxo used in a tx that the user decided not to broadcast, maybe I am missing it :/
ZocoLini
left a comment
There was a problem hiding this comment.
I find this a little confusing and overkill, why don't we use a static HashMap in the transaction_builder module and we only update that, also prevents a user from creating twice the same wallet and falling in the same issue when building transactions.
At the same time, I don't see how this would avoid reserving utxo used in a tx that the user decided not to broadcast, maybe I am missing it :/
I added a function the that consumers can release it manually. About the static HashMap. Im not sure if the hazard of global state with mixed networks is worth it to fight an edge case of using two of the same wallets in one process? |
|
I don't like static stuff either, but this feels hard to use, what about a empty Rc field in an UTXO that only increases when a Transaction holds it, something like that, Maybe a wrappers around UTXO that tracks if they are being hold in a transaction with a Rc field, something that drops atomically when the Transaction is freed |
Im not sure what issue you are trying to solve here and what is hard to use. And im also not quite sure how you envision this to be implemented to be honest. Feel free to come up with another PR for this if you have strong feelings about this. |
|
This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them. |
…spend A freshly built transaction is not reflected in the UTXO set until its broadcast is processed back into the wallet, so a second build in that window can select the same coins and produce a double-spend the network rejects. - Add an ephemeral `ReservationSet` on `ManagedCoreFundsAccount`: `set_funding` skips reserved outpoints during coin selection and `assemble_unsigned` reserves the inputs it selects. - Release the reservation when the spend is processed, and on a failed sign, so coin selection is unaffected by a build that never reaches the network. - Reclaim stale reservations via a roughly one-hour (24-block) TTL backstop, skipping the sweep at height 0 where elapsed time is unknown. - Share the set via `Arc<Mutex<_>>` so a reservation outlives the wallet lock, and never persist it: after a restart, chain and mempool sync are the source of truth. - Cover the read, write, release, TTL, and height-0 paths with unit tests, plus a `dashd` integration test asserting two pre-broadcast builds select disjoint inputs.
Drop the data-shape restatement from the `ReservationSet` struct doc and lead the `lock` method doc with its rationale rather than the method name. Make the height-0 sweep comment explicit that a height-0 entry relies on a later non-zero height to ever be reclaimed.
`processing_a_spend_releases_its_reservation` only exercised the `Mempool` context. Extend it so a second funded outpoint is reserved and then spent under `TransactionContext::InBlock`, asserting the reservation is released on the confirmed path as well.
`build_unsigned` reserved the selected inputs in `assemble_unsigned` and then encoded the transaction with `unwrap`, leaking the reservation until the TTL backstop if the encode ever failed. Route both builders through a fallible `encoded_size` helper that returns a `BuilderError` instead of panicking, and release the reserved inputs on that failure path so `build_unsigned` matches the existing release behavior in `build_signed`.
Strengthen the `set_funding` lock invariant to state that the builder must not be held across an `await` before `build_signed` or `assemble_unsigned`, since suspending there reopens the read-then-reserve window for a concurrent build.
Add `ManagedCoreFundsAccount::release_reservation` so a caller can release the reservations held for a built transaction's inputs when that transaction will not be broadcast (e.g. the user cancelled). Without this the inputs stay reserved until the TTL backstop reclaims them, making the funds appear unspendable for about an hour. Broadcast transactions still release on their own once the spend is processed back into the wallet, so this is only for abandoned builds.
e9ff904 to
97e8acd
Compare
A freshly built transaction is not reflected in the UTXO set until its broadcast is processed back into the wallet, so a second build in that window can select the same coins and produce a double-spend the network rejects.
ReservationSetonManagedCoreFundsAccount:set_fundingskips reserved outpoints during coin selection andassemble_unsignedreserves the inputs it selects.Arc<Mutex<_>>so a reservation outlives the wallet lock, and never persist it: after a restart, chain and mempool sync are the source of truth.dashdintegration test asserting two pre-broadcast builds select disjoint inputs.Summary by CodeRabbit
Bug Fixes
Tests