[UTXO-BUG] HIGH-1: TOCTOU race in spend_box() — silent double-spend#2069
Merged
Scottcjn merged 1 commit intoScottcjn:mainfrom Apr 5, 2026
Merged
Conversation
…le-spend spend_box() performs SELECT then UPDATE without an exclusive lock when called standalone (own=True). Two concurrent callers can both read spent_at IS NULL, then both UPDATE — the second gets rowcount=0 but the method never checks rowcount, so it returns the box dict as if the spend succeeded. The caller proceeds thinking the box was spent. Fixes: 1. Acquire BEGIN IMMEDIATE when own=True to serialize the operation 2. Check UPDATE rowcount == 1; raise ValueError if 0 (concurrent spend) 3. Proper ROLLBACK on all early-exit paths Tests added: - test_spend_box_double_spend_raises - test_spend_box_nonexistent_returns_none All 36 tests pass. Bounty: #2819 (High, 100 RTC)
|
Welcome to RustChain! Thanks for your first pull request. Before we review, please make sure:
Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150) A maintainer will review your PR soon. Thanks for contributing! |
|
Great security audit work! These fixes are critical for the protocol. |
Owner
|
✅ Merged. Excellent find — the TOCTOU race between SELECT and UPDATE in 75 RTC paid. Wallet: |
This was referenced Apr 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability Class
High — Race condition / TOCTOU (100 RTC bounty)
The Bug
spend_box()performs a SELECT then UPDATE without an exclusive lock when called standalone. Two concurrent callers can both readspent_at IS NULL, then both UPDATE — the second getsrowcount=0but the method never checks rowcount, so it returns the box dict as if the spend succeeded.Race Sequence
Thread B believes it successfully spent the box but the UPDATE was a no-op.
Fix
BEGIN IMMEDIATEwhenown=Trueto serialize the SELECT+UPDATEUPDATE rowcount == 1; raiseValueErrorif 0 (concurrent spend)ROLLBACKon all early-exit pathsTests Added
test_spend_box_double_spend_raises— verifies ValueError on double-spend via spend_box()test_spend_box_nonexistent_returns_none— verifies None return for missing boxAll 36 tests pass.
Files Changed
node/utxo_db.py— spend_box() rewritten with BEGIN IMMEDIATE + rowcount checknode/test_utxo_db.py— 2 test cases addedRef: Bounty #2819
MY WALLET IS aroky-x86-miner