-
Notifications
You must be signed in to change notification settings - Fork 6
Open
Description
Description
The idempotency check and UTXO selection are not atomic operations. Two concurrent requests can:
- Both pass the idempotency check
- Both select the same UTXOs as "unspent"
- Both attempt to lock those UTXOs
- Result in double-spending or transaction failures
Vulnerable Code
// src/transactions/fund_locker.rs:144-156
pub async fn lock(
&self,
account_id: i64,
amount: MicroMinotari,
// ... other params
) -> Result<LockFundsResult, anyhow::Error> {
// Step 1: Check idempotency (NOT in transaction)
let mut conn = self.db_pool.acquire().await?;
if let Some(idempotency_key_str) = &idempotency_key
&& let Some(response) = db::find_pending_transaction_locked_funds_by_idempotency_key(
&mut conn, idempotency_key_str, account_id).await? {
return Ok(response);
}
// ⚠️ GAP HERE - Another request can run!
// Step 2: Select UTXOs (sees same unspent outputs)
let input_selector = InputSelector::new(account_id, confirmation_window);
let utxo_selection = input_selector
.fetch_unspent_outputs(&mut conn, amount, num_outputs, fee_per_gram, estimated_output_size)
.await?;
// Step 3: Begin transaction (too late!)
let mut transaction = self.db_pool.begin().await?;
// Step 4: Create pending transaction
let pending_tx_id = db::create_pending_transaction(
&mut transaction,
&idempotency_key,
// ...
).await?;
// Step 5: Lock outputs
for utxo in &utxo_selection.utxos {
db::lock_output(&mut transaction, utxo.id, &pending_tx_id, expires_at).await?;
}
transaction.commit().await?;
Ok(result)
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels