Skip to content

fix(utxo): mempool atomicity BUG-1 + BUG-4 (PR 6146 review)#1

Open
crowniteto wants to merge 5 commits into
mainfrom
fix/utxo-mempool-atomicity-bug1-bug4
Open

fix(utxo): mempool atomicity BUG-1 + BUG-4 (PR 6146 review)#1
crowniteto wants to merge 5 commits into
mainfrom
fix/utxo-mempool-atomicity-bug1-bug4

Conversation

@crowniteto
Copy link
Copy Markdown
Owner

Fixes UTXO mempool atomicity issues identified in PR 6146 review.

BUG-1: mempool_remove() must atomically remove from both utxo_mempool and utxo_mempool_inputs within a single BEGIN IMMEDIATE transaction. Without this, a crash between the two operations could leave orphan input-claim rows that block future spends of the same box.

BUG-4: apply_transaction() must proactively evict mempool transactions whose inputs (including data_inputs) reference boxes that were just spent on-chain. Without this, stale mempool txs hold their normal inputs reserved in utxo_mempool_inputs until candidate selection catches them.

Changes:

  • node/utxo_db.py: After COMMIT in apply_transaction(), compute spent_ids (union of input_box_ids and data_inputs), then call _evict_stale_data_input_txs() in a best-effort try/except. Failure does not affect the committed transaction.
  • tests/test_utxo_mempool_atomicity_bug1_bug4.py: 7 regression tests covering:
    1. mempool_remove atomically removes from both tables
    2. mempool_remove on nonexistent tx is safe
    3. _evict_stale_data_input_txs removes stale txs
    4. Eviction with no stale txs is a no-op
    5. Eviction with empty list returns 0
    6. apply_transaction evicts stale mempool tx via input spend (integration test)
    7. apply_transaction preserves unrelated mempool txs (specificity test)

All 46 UTXO-related tests pass.

…on (BUG-4)

BUG-1: mempool_remove() now uses BEGIN IMMEDIATE to ensure both DELETE
operations are atomic. Previously, a crash between the two deletes could
leave orphan utxo_mempool_inputs rows, causing a persistent UTXO lock /
mempool DoS. Impact clarified per @MolhamHamwi review: this is not a
double-spend vector (box_id PK + mempool_check_double_spend still prevent
that), but an availability issue.

BUG-4: New _evict_stale_data_input_txs() method proactively removes
mempool txs whose data_inputs were just spent by apply_transaction().
Previously, stale txs held their normal inputs reserved in
utxo_mempool_inputs until candidate selection or expiry caught them.

5 regression tests passing.

Addresses review feedback on PR Scottcjn#6146.
Code review fixes:
- Move _spent_ids computation before COMMIT (consistent with original flow)
- Wrap eviction in try/except inside the try block (after COMMIT)
- If eviction fails, the already-committed transaction is unaffected
- Swallow exceptions to prevent rollbacks on committed data

Regression test improvements:
- Fix all output values to respect DUST_THRESHOLD (1000 nRTC)
- Fix conservation law: output + fee = input for all test transactions
- Use real minting + mempool_add flow (not direct DB inserts) for
  integration-level BUG-4 regression tests
- Add test_apply_transaction_evicts_stale_mempool_tx_via_input:
  verifies mempool tx is evicted when its input box is spent on-chain
- Add test_apply_transaction_preserves_unrelated_mempool_txs:
  verifies that spending box_a does NOT evict mempool tx claiming box_b
- Fix method signature typo (fulg,obj -> self)
- Fix fetchone/fetchall typos
- 7/7 tests passing, 46/46 UTXO tests passing
…tx_data_json scan

Reviewer (ayskobtw-lil) found that _evict_stale_data_input_txs only
searched utxo_mempool_inputs, but mempool_add() does NOT record
data_inputs there — only regular inputs. This means spending a box
used as a data_input by a mempool tx would NOT evict that tx.

Changes:
- _evict_stale_data_input_txs now uses a two-phase search:
  1. Check utxo_mempool_inputs for txs claiming spent boxes as
     regular inputs (fast path)
  2. Scan utxo_mempool.tx_data_json for txs whose data_inputs
     reference any spent box (covers the gap)
- Added try/except wrapper in apply_transaction() for best-effort
  eviction after COMMIT — failure does not affect committed tx
- New regression tests:
  - test_evict_finds_tx_via_data_input_in_tx_data_json:
    unit test verifying tx_data_json scanning path works
  - test_apply_transaction_evicts_mempool_tx_with_data_input:
    integration test using real mempool_add() with data_inputs +
    apply_transaction() spending the data_input box
  - test_apply_transaction_evicts_stale_mempool_tx_via_input:
    integration test for regular input eviction
  - test_apply_transaction_preserves_unrelated_mempool_txs:
    verifies spending box_a does not evict tx claiming box_b

All 48 UTXO tests passing (9 regression + 39 existing).
…eview v3)

The stale data-input eviction helper opened its own SQLite connection,
which fails silently when the outer caller holds BEGIN IMMEDIATE (the
utxo_endpoints.py transfer pattern). The DELETE is blocked by SQLite's
single-writer lock, the exception is swallowed, and stale mempool txs
survive after the outer COMMIT.

Fix:
- _evict_stale_data_input_txs() now accepts optional conn= parameter
- When manage_tx=False (external connection), eviction runs on the
  caller's connection inside their transaction (before their COMMIT)
- When manage_tx=True (own transaction), eviction runs after COMMIT
  on a separate connection (same as before, failure is safe)
- own_conn flag controls commit/rollback/close in the helper
- 3 new regression tests using real external-connection shape

Addresses: ayskobtw-lil review on PR Scottcjn#6306
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant