Bug/1621 tx dropped silently when ctq full#2475
Merged
Merged
Conversation
|
SUGGESTIONS BEFORE MERGE:
|
Contributor
There was a problem hiding this comment.
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-providedstateNonceand an explicitallowFutureQueueflag, and addsImportResult::QueueIsFull. - Updates
Clientto pass the committed sender nonce and convertQueueIsFullinto a newTransactionQueueIsFullexception; 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. |
…om:skalenetwork/skaled into bug/1621-tx-dropped-silently-when-CTQ-full
kladkogex
previously approved these changes
May 20, 2026
kladkogex
approved these changes
Jun 4, 2026
badrogger
approved these changes
Jun 4, 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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
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:
That meant the caller could receive
Successeven though either: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:
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:
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:
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
QueueIsFullis now propagated to the caller instead of being hidden behindSuccess.On the client side,
QueueIsFullis converted into aTransactionQueueIsFullexception. 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:
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:
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 whiledropGood()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:
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:
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), andBITE2TransactionQueue::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.
The queue now routes transactions more explicitly:
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.
Fixes #1621