Skip to content

fix(sequencer): deduplicate forced transactions in pool - #3807

Open
nadeemb53 wants to merge 5 commits into
mainfrom
bugfix/3776-deduplicate-forced-transactions
Open

fix(sequencer): deduplicate forced transactions in pool#3807
nadeemb53 wants to merge 5 commits into
mainfrom
bugfix/3776-deduplicate-forced-transactions

Conversation

@nadeemb53

@nadeemb53 nadeemb53 commented Aug 17, 2026

Copy link
Copy Markdown
Member

Fixes #3776.
Deduplicates pending and completed forced transactions so coordinator redelivery cannot leave stale queue entries that overwrite inclusion status.

Signed-off-by: nadeemb53 <nadeemb53@gmail.com>

This comment was marked as low quality.

This comment was marked as low quality.

This comment was marked as low quality.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

linea-besu/plugins/linea-sequencer/sequencer/src/main/java/lineth/sequencer/forced/LineaForcedTransactionPool.java:379

  • recordStatus mutates statusCache and pendingTransactionNumbers, and runs concurrently with addForcedTransactions (RPC-driven) and processForBlock/onBlockAdded (Besu event-driven). Without a shared lock, addForcedTransactions can observe statusCache empty and enqueue a tx while recordStatus is in-flight, reintroducing duplicates.

To make the deduplication robust, guard status recording with the same lock as addForcedTransactions (for example by synchronizing this method) so the cache+set updates cannot interleave with enqueueing.

    statusCache.put(ftx.forcedTransactionNumber(), status);
    pendingTransactionNumbers.remove(ftx.forcedTransactionNumber());
    inclusionResultCounters.get(result).incrementAndGet();

linea-besu/plugins/linea-sequencer/sequencer/src/main/java/lineth/sequencer/forced/LineaForcedTransactionPool.java:156

  • The deduplication relies on pendingTransactionNumbers and statusCache, but statusCache can be updated concurrently by onBlockAdded/recordStatus while this loop is running. That creates an interleaving where statusCache becomes non-empty after the getIfPresent check but before pendingQueue.addLast(tx), allowing a forcedTxNumber that already has a final status recorded to be enqueued again.

Consider guarding the add/check/enqueue sequence with the same lock used when recording statuses (e.g., synchronized on the instance or a dedicated lock) so the pending-set and status-cache checks are atomic w.r.t. final status recording.

This issue also appears on line 377 of the same file.

      final long forcedTransactionNumber = tx.forcedTransactionNumber();
      if (!pendingTransactionNumbers.add(forcedTransactionNumber)) {
        continue;
      }
      if (statusCache.getIfPresent(forcedTransactionNumber) != null) {

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Signed-off-by: nadeemb53 <nadeemb53@gmail.com>
@nadeemb53
nadeemb53 force-pushed the bugfix/3776-deduplicate-forced-transactions branch from 7f9e939 to 22dae13 Compare August 17, 2026 17:42
@nadeemb53
nadeemb53 requested a review from Filter94 August 17, 2026 18:03
7_200;

private final Deque<ForcedTransaction> pendingQueue = new ConcurrentLinkedDeque<>();
private final Set<Long> pendingTransactionNumbers = ConcurrentHashMap.newKeySet();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like the this becomes bloated. What if we use an ConcurrentSkipListMap here instead of the pendingQueue?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

my understanding is that the set keeps the transaction number reserved while the transaction moves from the queue into the status cache. With only a map, removing the entry before recording the status creates a window where a retry can add it again, so we’d need extra locking. That doesn’t end up being simpler.

@Filter94 Filter94 Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What if we avoid this window by reordering the operations? Same as you've done with the pendingTransactionNumbers, removal from the queue after the status is updated

Also, a validation whether a transaction is within the statusCache already with INCLUDED status could also help with the duplicates. And I think we don't care about the resubmission with other, initially rejected statuses

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm let me try

@nadeemb53
nadeemb53 force-pushed the bugfix/3776-deduplicate-forced-transactions branch from 8c6d8ae to 22dae13 Compare August 19, 2026 14:40
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.

Sequencer includes FTX in non-monotonic order, halting coordinator finalization

3 participants