fix: case-insensitive .dash suffix and UTXO double-spend prevention (backport from DET)#3466
fix: case-insensitive .dash suffix and UTXO double-spend prevention (backport from DET)#3466lklimek wants to merge 3 commits intofeat/platform-walletfrom
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
|
✅ Review complete (commit d971326) |
1. DPNS name normalization: strip the `.dash` suffix case-insensitively before querying Platform, so `.DASH`, `.Dash`, etc. all resolve. 2. UTXO double-spend prevention via optimistic validation: after signing the transaction (optimistically, without locks), re-acquire the write lock and verify that every selected UTXO is still in the spendable set before marking them spent. If a concurrent caller already claimed an outpoint, return an error instead of broadcasting a transaction the network would reject. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1ef274a to
c9c81b9
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
I verified the non-empty agent findings against HEAD c9c81b9. The DPNS suffix change is fine, but the wallet change introduces a real state-consistency bug: send_transaction() now mutates wallet spendability and balance before the broadcast succeeds, and there is no rollback on broadcast failure.
Reviewed commit: c9c81b9
🔴 1 blocking
🤖 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.
In `packages/rs-platform-wallet/src/wallet/core/wallet.rs`:
- [BLOCKING] lines 345-352: Pre-broadcast state mutation leaves UTXOs locally spent when broadcast fails
`send_transaction()` now calls `check_core_transaction(&tx, TransactionContext::Mempool, true, true)` before `broadcast_transaction()`. In this crate, that call delegates into `managed_state.check_core_transaction(...)` and immediately refreshes the wallet balance atomics when the transaction is relevant, so the wallet state is committed before any network side effect is known to have succeeded. If `broadcast_transaction()` then returns `TransactionBroadcast`, `SpvNotRunning`, or `SpvError`, the function exits with `Err` but never restores the consumed UTXOs or reverted balance, so a transient broadcast failure can make those outpoints disappear from future `get_spendable_utxos()` results until an external sync repairs state.
| // All UTXOs still available — mark them as spent atomically. | ||
| state | ||
| .check_core_transaction(&tx, TransactionContext::Mempool, true, true) | ||
| .await; | ||
| } | ||
|
|
||
| // 6. Broadcast. | ||
| self.broadcast_transaction(&tx).await?; |
There was a problem hiding this comment.
🔴 Blocking: Pre-broadcast state mutation leaves UTXOs locally spent when broadcast fails
send_transaction() now calls check_core_transaction(&tx, TransactionContext::Mempool, true, true) before broadcast_transaction(). In this crate, that call delegates into managed_state.check_core_transaction(...) and immediately refreshes the wallet balance atomics when the transaction is relevant, so the wallet state is committed before any network side effect is known to have succeeded. If broadcast_transaction() then returns TransactionBroadcast, SpvNotRunning, or SpvError, the function exits with Err but never restores the consumed UTXOs or reverted balance, so a transient broadcast failure can make those outpoints disappear from future get_spendable_utxos() results until an external sync repairs state.
source: ['codex-general', 'codex-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-wallet/src/wallet/core/wallet.rs`:
- [BLOCKING] lines 345-352: Pre-broadcast state mutation leaves UTXOs locally spent when broadcast fails
`send_transaction()` now calls `check_core_transaction(&tx, TransactionContext::Mempool, true, true)` before `broadcast_transaction()`. In this crate, that call delegates into `managed_state.check_core_transaction(...)` and immediately refreshes the wallet balance atomics when the transaction is relevant, so the wallet state is committed before any network side effect is known to have succeeded. If `broadcast_transaction()` then returns `TransactionBroadcast`, `SpvNotRunning`, or `SpvError`, the function exits with `Err` but never restores the consumed UTXOs or reverted balance, so a transient broadcast failure can make those outpoints disappear from future `get_spendable_utxos()` results until an external sync repairs state.
Path dependencies on ../rust-dashcore break when this repo is used as a git dependency (e.g. from dash-evo-tool CI). Switch to git deps pointing to feat/blockchain-identities-account-type branch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This reverts commit fe8f263.
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The DPNS suffix change is fine. The wallet concurrency fix introduces a real state-consistency regression: send_transaction() now commits wallet-local spend state before the only fallible network step, so a transient broadcast failure can make the wallet report funds as spent even though the transaction was never accepted. That is a blocking correctness issue for wallet behavior on this head.
Reviewed commit: d971326
🔴 1 blocking
🤖 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.
In `packages/rs-platform-wallet/src/wallet/core/wallet.rs`:
- [BLOCKING] lines 345-352: send_transaction mutates wallet spend state before a fallible broadcast
This new write-locked path calls `check_core_transaction(&tx, TransactionContext::Mempool, true, true)` before broadcasting. In this crate that is not a dry run: `PlatformWalletInfo::check_core_transaction()` forwards `update_state=true` into `managed_state.check_core_transaction(...)` and then refreshes the shared balance cache when the transaction is relevant (`packages/rs-platform-wallet/src/wallet/platform_wallet_traits.rs:179`). The broadcaster call that follows can still fail with ordinary runtime errors such as `TransactionBroadcast`, `SpvNotRunning`, or `SpvError` (`packages/rs-platform-wallet/src/broadcaster.rs:41`, `packages/rs-platform-wallet/src/spv/runtime.rs:128`), and there is no compensating rollback on the error path. That leaves the live wallet in a self-contradictory state: `send_transaction()` returns `Err`, but the selected outpoints have already been removed from future spendable-UTXO selection and the cached balance may already be reduced until some later sync repairs it.
| // All UTXOs still available — mark them as spent atomically. | ||
| state | ||
| .check_core_transaction(&tx, TransactionContext::Mempool, true, true) | ||
| .await; | ||
| } | ||
|
|
||
| // 6. Broadcast. | ||
| self.broadcast_transaction(&tx).await?; |
There was a problem hiding this comment.
🔴 Blocking: send_transaction mutates wallet spend state before a fallible broadcast
This new write-locked path calls check_core_transaction(&tx, TransactionContext::Mempool, true, true) before broadcasting. In this crate that is not a dry run: PlatformWalletInfo::check_core_transaction() forwards update_state=true into managed_state.check_core_transaction(...) and then refreshes the shared balance cache when the transaction is relevant (packages/rs-platform-wallet/src/wallet/platform_wallet_traits.rs:179). The broadcaster call that follows can still fail with ordinary runtime errors such as TransactionBroadcast, SpvNotRunning, or SpvError (packages/rs-platform-wallet/src/broadcaster.rs:41, packages/rs-platform-wallet/src/spv/runtime.rs:128), and there is no compensating rollback on the error path. That leaves the live wallet in a self-contradictory state: send_transaction() returns Err, but the selected outpoints have already been removed from future spendable-UTXO selection and the cached balance may already be reduced until some later sync repairs it.
source: ['codex']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-wallet/src/wallet/core/wallet.rs`:
- [BLOCKING] lines 345-352: send_transaction mutates wallet spend state before a fallible broadcast
This new write-locked path calls `check_core_transaction(&tx, TransactionContext::Mempool, true, true)` before broadcasting. In this crate that is not a dry run: `PlatformWalletInfo::check_core_transaction()` forwards `update_state=true` into `managed_state.check_core_transaction(...)` and then refreshes the shared balance cache when the transaction is relevant (`packages/rs-platform-wallet/src/wallet/platform_wallet_traits.rs:179`). The broadcaster call that follows can still fail with ordinary runtime errors such as `TransactionBroadcast`, `SpvNotRunning`, or `SpvError` (`packages/rs-platform-wallet/src/broadcaster.rs:41`, `packages/rs-platform-wallet/src/spv/runtime.rs:128`), and there is no compensating rollback on the error path. That leaves the live wallet in a self-contradictory state: `send_transaction()` returns `Err`, but the selected outpoints have already been removed from future spendable-UTXO selection and the cached balance may already be reduced until some later sync repairs it.
Summary
Two targeted fixes backported from dash-evo-tool into the platform wallet codebase. No upstream merge — just the fixes on top of
feat/platform-wallet.Context: what moved from dash-evo-tool to platform
The
feat/platform-walletrefactor extracted wallet logic from dash-evo-tool intopackages/rs-platform-wallet/:src/spv/manager.rs— SPV lifecycle, broadcastrs-platform-wallet/src/spv/— rewritten aroundWalletManagersrc/backend_task/dashpay/dip14_derivation.rsrs-platform-wallet/src/wallet/dashpay/dip14.rssrc/model/wallet/asset_lock_transaction.rsrs-platform-wallet/src/wallet/asset_lock/rs-platform-wallet/src/wallet/core/wallet.rs— unifiedCoreWalletThese fixes address bugs discovered in dash-evo-tool (PRs #810 and #815) that also exist in the platform-side code after the split.
Fix 1: Case-insensitive
.dashsuffix (SDK)File:
packages/rs-sdk/src/platform/dpns_usernames/mod.rsresolve_dpns_name()stripped the.dashsuffix using exact match (suffix == ".dash"). Inputs like"Alice.DASH"or"alice.Dash"would not match — the full string including.dashwould be normalized as the label → DPNS lookup miss.Backported from dash-evo-tool PR #810 which fixed the equivalent bug in DET's DPNS helpers.
Fix 2: UTXO double-spend prevention (platform-wallet)
File:
packages/rs-platform-wallet/src/wallet/core/wallet.rsCoreWallet::send_transaction()had a TOCTOU race between reading spendable UTXOs and broadcasting:Concurrent callers could both select the same UTXOs between steps 1–3. The second broadcast would be rejected by the network (no IS lock issued for the conflicting tx), but the user sees a failed payment with no clear cause.
Fix — optimistic validation pattern:
After signing, acquire the write lock and re-validate that the selected outpoints are still in the spendable UTXO set. If a concurrent caller already claimed them via
check_core_transaction, return a clear error instead of broadcasting a doomed transaction. If all UTXOs are still available, mark them atomically before dropping the lock:This eliminates the race: the second concurrent caller gets
"Selected UTXOs are no longer available (concurrent transaction). Please retry."instead of a network rejection.Backported from dash-evo-tool v1.0-dev commit
f4d3b3b3which fixed the same race in the old SPV manager, adapted to the platform-wallet'sRwLock<PlatformWalletInfo>architecture.Companion PR
🤖 Co-authored by Claudius the Magnificent AI Agent