Skip to content

Bug/1621 tx dropped silently when ctq full#2475

Merged
PropzSaladaz merged 22 commits into
v5.1.0from
bug/1621-tx-dropped-silently-when-CTQ-full
Jun 5, 2026
Merged

Bug/1621 tx dropped silently when ctq full#2475
PropzSaladaz merged 22 commits into
v5.1.0from
bug/1621-tx-dropped-silently-when-CTQ-full

Conversation

@PropzSaladaz
Copy link
Copy Markdown
Collaborator

@PropzSaladaz PropzSaladaz commented Apr 28, 2026

Description

This PR fixes a transaction queue issue where a transaction could be silently dropped when the current transaction queue (CTQ) was full. See #1621.

Problem

In v5.1.0, CTQ admission was not explicit.

When a new transaction was imported, the queue inserted it into CTQ and then trimmed the queue if the configured CTQ limit was exceeded:

insert transaction into CTQ
if CTQ is over limit:
    remove lowest-priority transaction
return Success

That meant the caller could receive Success even though either:

  • the newly submitted transaction was not actually retained, or
  • another pending transaction was evicted as a side effect.

The client/RPC layer had no reliable way to know that queue capacity caused a transaction to be dropped.

This was especially problematic because CTQ is the queue used for transactions that are executable now. Silently evicting from CTQ changes local pending state without surfacing a rejection to the transaction submitter.

New Behavior

Queue admission is now explicit.

A transaction is routed based on its nonce and the current chain state:

nonce-compatible:
    try CTQ

future nonce:
    try FTQ only if future queueing is allowed

old nonce:
    reject as already in chain

CTQ

CTQ accepts only transactions whose nonce is currently executable or contiguous with already queued current transactions for the same sender.

If CTQ does not have capacity, the transaction is rejected immediately:

CTQ full -> QueueIsFull

The transaction is not inserted and no existing CTQ transaction is evicted to make room.

There is intentionally no fallback from CTQ to FTQ for a CTQ-eligible transaction. FTQ is for future-nonce transactions, not overflow storage for current transactions.

As a result, this is valid behavior:

CTQ full
FTQ has free space
tx is CTQ-eligible

result: QueueIsFull

FTQ

FTQ accepts transactions whose nonce is greater than the next expected nonce, but only when future queueing is enabled.

If future queueing is disabled, or FTQ is at capacity, the transaction is rejected with QueueIsFull.

Client / RPC Impact

QueueIsFull is now propagated to the caller instead of being hidden behind Success.

On the client side, QueueIsFull is converted into a TransactionQueueIsFull exception. The RPC layer can then return a clear error to the submitter instead of silently accepting a transaction that was not retained.

Related Queue Semantics

This change makes the distinction between CTQ and FTQ stricter:

CTQ:
    current, nonce-compatible transactions

FTQ:
    future-nonce transactions

However, CTQ capacity introduces one important edge case: a transaction may be nonce-compatible but still remain in FTQ if promotion from FTQ to CTQ was blocked by CTQ capacity.

This is because a tx in FTQ was already accepted, and client informed of its acceptance - meaning we should not drop this tx. Instead, we keep it in FTQ until CTQ has space for it to keep the new invariant that if a tx is accepted, then it will eventually be executed.

For example:

CTQ is full
sender A has nonce 0 already consumed
sender A nonce 1 exists in FTQ
promotion of nonce 1 is blocked by CTQ capacity

That transaction is not logically a “future nonce” anymore, but it is still physically stored in FTQ until CTQ capacity opens.

Because of this, queue cleanup must handle consumed transactions in both CTQ and FTQ.

Block Import Follow-up

Before this PR, dropGood() was called while parsing block payload transactions. That was safe only while dropGood() did not remove regular transactions from FTQ.

After CTQ capacity changes, dropGood() must be able to remove from FTQ, because a consumed transaction may be physically stored there due to blocked CTQ promotion.

That exposed a corner case:

state nonce for A = 0
block payload contains tx A nonce 1 (stored in FTQ)
local execution returns WouldNotBeInBlock

In that case, the transaction appeared in the block payload but was not actually consumed. It must remain pending.

So block-import cleanup was adjusted:

Do not remove a transaction merely because it appeared in a block payload.
Remove it only after execution/recovery determines it was consumed.

This preserves future-nonce transactions while still removing CTQ-capacity-blocked transactions from FTQ when they are genuinely consumed by a block.

BITE2 / CTX Validation

Moving queue cleanup out of block parsing also affected BITE2 CTXs.

Previously, CTX parsing called dropGood(ctx), and BITE2TransactionQueue::dropGood() implicitly validated that the CTX matched the front of the local BITE2 queue.

After cleanup moved to the consumed-transaction path, that validation needed to be explicit.

This PR adds a non-mutating validation step:

validateNextExpectedBITE2CTXsAndGetOrigins(...)

It verifies that CTXs in the proposed block match the next expected pending BITE2 CTXs in FIFO order and returns their origins from the same queue snapshot.

This keeps the old safety property without mutating the queue during parsing.

Summary

The main behavior change is explicit CTQ/FTQ admission.

Before:
    CTQ could exceed capacity, evict internally, and still return Success.

After:
    CTQ capacity is checked before insertion.
    If CTQ is full, insertion fails with QueueIsFull.
    No transaction is silently evicted.

The queue now routes transactions more explicitly:

CTQ:
    nonce-compatible transactions that can be proposed now.
    If CTQ is full, insertion fails with QueueIsFull.

FTQ:
    future-nonce transactions, when future queueing is allowed.
    If FTQ is disabled or full, insertion fails with QueueIsFull.

This PR intentionally keeps the existing two-queue model. A transaction that becomes nonce-compatible while CTQ is full can still remain physically stored in FTQ, with blocked promotion tracked separately. A dedicated blocked-current queue could make that distinction more explicit in the future, but queue changes were kept minimal here because broader transaction-queue refactoring is planned separately.

Block-import cleanup was also tightened. The node no longer removes transactions from the queue merely because they appear in a block payload that is about to be executed. Instead, queue cleanup happens only after execution or recovery determines that the transaction was actually consumed.

Before:
    tx appears in proposed block payload
    -> remove from queue during parsing

After:
    tx is executed/recovered and determined consumed
    -> remove from queue

Fixes #1621

@PropzSaladaz PropzSaladaz self-assigned this Apr 28, 2026
Copilot AI review requested due to automatic review settings April 28, 2026 14:51
@PropzSaladaz PropzSaladaz requested a review from kladkogex as a code owner April 28, 2026 14:51
@github-actions
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates SKALED’s transaction queue admission logic to avoid “successful” imports that later result in silent transaction loss when the current transaction queue (CTQ) is at capacity. It makes queue insertion outcomes explicit, propagates QueueIsFull up to the client/RPC layer, and expands unit tests around CTQ/FTQ behavior.

Changes:

  • Refactors TransactionQueue::import() to require caller-provided stateNonce and an explicit allowFutureQueue flag, and adds ImportResult::QueueIsFull.
  • Updates Client to pass the committed sender nonce and convert QueueIsFull into a new TransactionQueueIsFull exception; maps that exception to an RPC error message.
  • Updates/extends unit tests to validate CTQ/FTQ admission and full-queue rejection behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
libethereum/TransactionQueue.h Changes import API to require allowFutureQueue + stateNonce; adds new internal helpers.
libethereum/TransactionQueue.cpp Implements explicit CTQ/FTQ insertion paths and QueueIsFull handling; adjusts drop/import semantics.
libethcore/Common.h Adds ImportResult::QueueIsFull.
libethcore/Exceptions.h Introduces TransactionQueueIsFull exception.
libethereum/Client.cpp Passes stateNonce/allowFutureQueue into TQ import; throws TransactionQueueIsFull on QueueIsFull.
libweb3jsonrpc/Eth.cpp Maps TransactionQueueIsFull to a user-facing RPC error string.
test/unittests/libethereum/TransactionQueue.cpp Updates existing tests and adds new cases for CTQ-full fallback / FTQ limits / explicit rejection.
test/unittests/libethereum/SkaleHost.cpp Updates test imports to new TQ import signature.
test/tools/libtesteth/BlockChainHelper.cpp Updates helper queue imports to new TQ import signature.

Comment thread libethereum/TransactionQueue.cpp Outdated
Comment thread libethereum/TransactionQueue.cpp Outdated
Comment thread libethereum/TransactionQueue.cpp Outdated
Comment thread libethereum/TransactionQueue.cpp Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 9 out of 9 changed files in this pull request and generated 4 comments.

Comment thread libethereum/TransactionQueue.h Outdated
Comment thread libethereum/TransactionQueue.h Outdated
Comment thread libethereum/TransactionQueue.cpp
Comment thread test/unittests/libethereum/TransactionQueue.cpp
kladkogex
kladkogex previously approved these changes May 20, 2026
@PropzSaladaz PropzSaladaz marked this pull request as draft May 20, 2026 15:07
@PropzSaladaz PropzSaladaz marked this pull request as ready for review May 27, 2026 17:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 16 out of 16 changed files in this pull request and generated no new comments.

@kladkogex kladkogex self-requested a review June 4, 2026 10:49
@PropzSaladaz PropzSaladaz merged commit 7210c45 into v5.1.0 Jun 5, 2026
11 checks passed
@PropzSaladaz PropzSaladaz deleted the bug/1621-tx-dropped-silently-when-CTQ-full branch June 5, 2026 09:58
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

To investigate: Skaled not dropped out transaction from future transaction queue

4 participants