Skip to content

fix(tx_pool): dedupe key-image conflict list; make remove_tx idempotent#199

Open
raw391 wants to merge 1 commit into
Beldex-Coin:devfrom
raw391:fix/tx-pool-dedupe-flash-conflicts
Open

fix(tx_pool): dedupe key-image conflict list; make remove_tx idempotent#199
raw391 wants to merge 1 commit into
Beldex-Coin:devfrom
raw391:fix/tx-pool-dedupe-flash-conflicts

Conversation

@raw391

@raw391 raw391 commented Jun 5, 2026

Copy link
Copy Markdown

tx_memory_pool::have_tx_keyimges_as_spent at src/cryptonote_core/tx_pool.cpp:1447-1465 appends every conflicting txid into the conflicting vector per spent key image, with no dedupe. If one mempool tx shares two or more key images with the incoming tx, its txid is appended multiple times.

The approved-flash path in tx_pool::add_tx (tx_pool.cpp:361-365) calls remove_flash_conflicts (which starts at tx_pool.cpp:674). remove_flash_conflicts iterates that vector and calls remove_tx per entry. The first call succeeds; the second call on the same txid fails at the sorted-container check at line 760. remove_flash_conflicts treats the failure as a hard error and returns early, so the LockedTXN destructor aborts the DB batch. The LMDB row is restored, but the in-memory mutations from the first call (m_txpool_weight decrement at line 787, remove_transaction_keyimages at line 788, sorted-container erase at line 789) are not rolled back. m_spent_key_images then no longer contains entries for that txs inputs, so a subsequent tx spending the same outputs is not rejected by have_tx_keyimges_as_spent until the orphaned row is aged out or the daemon restarts.

This is reachable when a single signer produces two transactions sharing 2+ key images (a normal mempool tx and an approved flash tx for the same inputs).

Patch dedupes the conflict list at source via unordered_set, and makes remove_tx idempotent for ad-hoc callers that pass stc_it = nullptr (with an MWARNING log so the path stays observable):

   bool tx_memory_pool::have_tx_keyimges_as_spent(const transaction& tx, std::vector<crypto::hash> *conflicting) const
   {
     auto locks = tools::unique_locks(m_transactions_lock, m_blockchain);

     bool ret = false;
+    std::unordered_set<crypto::hash> seen;
     for(const auto& in: tx.vin)
     {
       CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, tokey_in, true);
       auto it = m_spent_key_images.find(tokey_in.k_image);
       if (it != m_spent_key_images.end())
       {
         if (!conflicting)
           return true;
         ret = true;
-        conflicting->insert(conflicting->end(), it->second.begin(), it->second.end());
+        for (const auto &h : it->second)
+          if (seen.insert(h).second)
+            conflicting->push_back(h);
       }
     }
     return ret;
   }
     const auto it = stc_it ? *stc_it : find_tx_in_sorted_container(txid);
     if (it == m_txs_by_fee_and_receive_time.end())
     {
+      if (!stc_it)
+      {
+        MWARNING("remove_tx: tx " << txid << " not in sorted container, treating as already-removed");
+        return true;
+      }
       MERROR("Failed to find tx in txpool sorted list");
       return false;
     }

The bool-only fast path of have_tx_keyimges_as_spent (conflicting == nullptr) is unchanged. The pruner at line 794 passes a known-good stc_it and keeps the loud-fail semantics.

have_tx_keyimges_as_spent walks tx.vin and appends every conflicting
txid per spent key image. If one mempool tx shares 2+ key images with
the incoming tx, its txid is appended multiple times.

remove_flash_conflicts iterates that vector and calls remove_tx per
entry. The first call succeeds; the second on the same txid fails at
the sorted-container check. remove_flash_conflicts treats failure as
fatal and returns early, so the LockedTXN destructor aborts the DB
batch. The LMDB row is restored, but in-memory mutations from the
first call (m_txpool_weight decrement, remove_transaction_keyimages,
sorted-container erase) are not rolled back. m_spent_key_images then
no longer contains entries for that txs inputs, so subsequent txs
spending the same outputs are not rejected by have_tx_keyimges_as_spent
until the orphaned row is aged out or the daemon restarts.

Triggered by a single signer producing two txs sharing 2+ inputs.

Dedupes the conflict list at source in have_tx_keyimges_as_spent via
unordered_set. Makes remove_tx idempotent when called without stc_it:
a missing sorted-container entry is treated as already-removed with
an MWARNING log so the path stays observable.
@raw391 raw391 force-pushed the fix/tx-pool-dedupe-flash-conflicts branch from 75a6616 to 1769d5a Compare June 5, 2026 16:01
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved transaction removal handling so missing entries are treated as already removed instead of causing an error.
    • Reduced duplicate conflict results when checking spent key images, making conflict reporting cleaner and more consistent.

Walkthrough

Two defensive fixes in tx_pool.cpp: remove_tx now logs a warning and returns true when a transaction is absent from the sorted container (instead of failing silently), and have_tx_keyimges_as_spent deduplicates hashes in the conflicting output using a local seen set.

Changes

tx_pool defensive fixes

Layer / File(s) Summary
remove_tx robustness + conflict deduplication
src/cryptonote_core/tx_pool.cpp
remove_tx treats a missing sorted-container entry as non-fatal, logging a warning and returning true. have_tx_keyimges_as_spent adds a seen set so each conflicting hash is appended at most once.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Poem

🐇 A tx gone missing? No panic, no fright,
Just log it and carry on into the night.
Duplicates vanish with one little seen,
The pool hops along, tidy and clean.
Bouncing through blocks with a wiggle of ear! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: deduping conflicts and making remove_tx idempotent.
Description check ✅ Passed The description directly explains the tx_pool bug, fix, and impact, so it's clearly relevant.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)

❌ Error creating Unit Test PR.

  • Create PR with unit tests
  • Commit unit tests in branch fix/tx-pool-dedupe-flash-conflicts

Comment @coderabbitai help to get the list of available commands.

@victor-tucci

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cryptonote_core/tx_pool.cpp`:
- Around line 759-763: The early return in remove_tx treats a missing
m_txs_by_fee_and_receive_time entry as proof the tx is already gone, which can
skip the remaining txpool cleanup. Update remove_tx so that when the
sorted-container lookup fails it still falls back to the DB/meta/blob cleanup
path, and only returns idempotently after confirming the tx is absent there too;
keep the logging in place but do not let the miss short-circuit the removal
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 41f71ab8-424d-4d71-bdd2-91aec41ed18b

📥 Commits

Reviewing files that changed from the base of the PR and between c8e54f6 and 1769d5a.

📒 Files selected for processing (1)
  • src/cryptonote_core/tx_pool.cpp

Comment on lines +759 to +763
if (!stc_it)
{
MWARNING("remove_tx: tx " << txid << " not in sorted container, treating as already-removed");
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't treat a sorted-container miss as proof the tx is gone.

remove_stuck_transactions() (Lines 1016-1024 and 1039-1052) and validate() (Lines 1919-1938) already have paths where a tx can disappear from m_txs_by_fee_and_receive_time before its txpool DB row and key-image state are actually cleared. This early return true now masks that inconsistency and skips the remaining cleanup, so remove_flash_conflicts() can believe a conflicting tx was removed when it is still live in the txpool backend. Fall back to the DB/meta/blob cleanup path when the sorted entry is missing, and only make this idempotent when the tx is absent there as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cryptonote_core/tx_pool.cpp` around lines 759 - 763, The early return in
remove_tx treats a missing m_txs_by_fee_and_receive_time entry as proof the tx
is already gone, which can skip the remaining txpool cleanup. Update remove_tx
so that when the sorted-container lookup fails it still falls back to the
DB/meta/blob cleanup path, and only returns idempotently after confirming the tx
is absent there too; keep the logging in place but do not let the miss
short-circuit the removal flow.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Note

Unit test generation is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

❌ Failed to create PR with unit tests: AGENT_CHAT: Failed to open pull request

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.

3 participants