-
Notifications
You must be signed in to change notification settings - Fork 53
fix: case-insensitive .dash suffix and UTXO double-spend prevention (backport from DET) #3466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/platform-wallet
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,9 @@ use std::sync::Arc; | |
|
|
||
| use super::balance::WalletBalance; | ||
|
|
||
| use dashcore::Address as DashAddress; | ||
| use dashcore::secp256k1::{Message, Secp256k1}; | ||
| use dashcore::sighash::SighashCache; | ||
| use dashcore::Address as DashAddress; | ||
| use dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; | ||
| use key_wallet::Utxo; | ||
| use tokio::sync::RwLock; | ||
|
|
@@ -316,6 +316,38 @@ impl CoreWallet { | |
| self.sign_transaction_inputs(&secp, &mut tx, &selected_utxos) | ||
| .await?; | ||
|
|
||
| // 5b. Validate-and-mark under write lock: verify that the UTXOs we | ||
| // selected (under a read lock earlier) haven't been claimed by a | ||
| // concurrent caller. If they have, fail fast with a clear error | ||
| // instead of broadcasting a transaction the network will reject. | ||
| { | ||
| use crate::wallet::platform_wallet_traits::WalletTransactionChecker; | ||
| use key_wallet::transaction_checking::TransactionContext; | ||
| let mut state = self.state.write().await; | ||
|
|
||
| // Re-check that our selected outpoints are still spendable. | ||
| let current_spendable: std::collections::BTreeSet<OutPoint> = state | ||
| .managed_state | ||
| .wallet_info() | ||
| .get_spendable_utxos() | ||
| .iter() | ||
| .map(|u| u.outpoint) | ||
| .collect(); | ||
|
|
||
| for (outpoint, _, _) in &selected_utxos { | ||
| if !current_spendable.contains(outpoint) { | ||
| return Err(PlatformWalletError::TransactionBuild( | ||
| "Selected UTXOs are no longer available (concurrent transaction). Please retry.".to_string(), | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| // 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?; | ||
|
Comment on lines
+345
to
352
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Blocking: send_transaction mutates wallet spend state before a fallible broadcast This new write-locked path calls source: ['codex'] 🤖 Fix this with AI agents |
||
|
|
||
|
|
@@ -479,12 +511,16 @@ impl CoreWallet { | |
| // Derive private keys and sign. | ||
| for (i, (input, sighash)) in tx.input.iter_mut().zip(sighashes).enumerate() { | ||
| let path = &derivation_paths[i]; | ||
| let extended_key = info.managed_state.wallet().derive_extended_private_key(path).map_err(|e| { | ||
| PlatformWalletError::TransactionBuild(format!( | ||
| "Failed to derive key for input {}: {}", | ||
| i, e | ||
| )) | ||
| })?; | ||
| let extended_key = info | ||
| .managed_state | ||
| .wallet() | ||
| .derive_extended_private_key(path) | ||
| .map_err(|e| { | ||
| PlatformWalletError::TransactionBuild(format!( | ||
| "Failed to derive key for input {}: {}", | ||
| i, e | ||
| )) | ||
| })?; | ||
| let input_private_key = extended_key.to_priv(); | ||
|
|
||
| let message = Message::from_digest(sighash.into()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Pre-broadcast state mutation leaves UTXOs locally spent when broadcast fails
send_transaction()now callscheck_core_transaction(&tx, TransactionContext::Mempool, true, true)beforebroadcast_transaction(). In this crate, that call delegates intomanaged_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. Ifbroadcast_transaction()then returnsTransactionBroadcast,SpvNotRunning, orSpvError, the function exits withErrbut never restores the consumed UTXOs or reverted balance, so a transient broadcast failure can make those outpoints disappear from futureget_spendable_utxos()results until an external sync repairs state.source: ['codex-general', 'codex-rust-quality']
🤖 Fix this with AI agents