SUMMARY
reth's transaction pool throttles an account's transaction inclusion based on stateless ECDSA recovery of EIP-7702 authorization signatures, without checking that those authorizations are execution-valid.
An attacker can harvest any EIP-7702 authorization a victim has ever signed. These authorizations are public because they travel in type-4 transactions through the mempool and on-chain. The attacker can then replay the authorization inside the attacker's own cheap type-4 transaction.
On insertion, the victim is registered as a delegated or pending-delegation account. The victim's own ordinary transactions, including legacy, EIP-1559, and other transaction types, are then throttled to max_inflight_delegated_slot_limit in-flight transactions. The default is 1. Nonce-gapped transactions are rejected, even though nothing about the victim changed on-chain and the replayed authorization is a guaranteed no-op at execution.
This is a liveness and targeted griefing vulnerability, not a consensus or state-corruption issue. It is live on Ethereum mainnet because EIP-7702 has been active since the Pectra upgrade on 2025-05-07. It is cheap, sustainable, repeatable, can target many victims per attacker transaction, and is amplified network-wide because the poisoning transaction propagates to every honest reth node.
SEVERITY ASSESSMENT
Class: Denial of service / liveness issue involving targeted transaction-inclusion throttling and mempool unfairness.
This is not a consensus issue, not state corruption, and not fund loss. On-chain execution is unaffected because the replayed authorization is correctly skipped per EIP-7702.
Realized effect on a victim: while one attacker type-4 transaction naming the victim is pool-resident, the victim is limited to 1 in-flight transaction at a time and cannot submit nonce-gapped, pipelined, or batched transactions. The victim can still land its immediate-next-nonce transaction one at a time, but loses the ability to batch, pipeline, or pre-submit. This is a material degradation for active users, dapps, relayers, MEV searchers, and anyone submitting more than one transaction before the previous one mines.
Exposure: affects all reth nodes; default configuration is the worst case.
AFFECTED SCOPE
Component: reth-transaction-pool
Version / commit verified: reth v2.2.0, commit b61b543, which was current main at the time of this report.
Default config is worst case: DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS = 1 in crates/transaction-pool/src/config.rs:35.
This is not fixed by PR #23406, "fix: properly validate authorities in the pool", merged 2026-04-09. That PR only made the AuthorityReserved displacement check honor the configurable slot limit instead of a hardcoded 1. It did not add authorization-validity gating. I found no other issue or PR that addresses this.
RELEVANT EIP-7702 BACKGROUND
An EIP-7702 authorization tuple is:
(chain_id, address, nonce, y_parity, r, s)
The tuple is signed by the authority, which is an EOA. At execution time, an authorization is applied only if, among other checks, authorization.nonce == authority.account.nonce and chain_id is either 0 or the current chain ID.
A failing authorization is skipped with zero state effect. The enclosing transaction remains valid, the authority's nonce is not incremented, and no delegation is set.
The relevant consequences are:
Authorizations are public because they are carried in type-4 transactions in the mempool and on-chain.
A stale-nonce authorization, where the authority's account nonce has since advanced, is a guaranteed execution no-op.
A chain_id == 0 authorization is replayable on any chain indefinitely.
Recovering the authority from the signature is independent of whether the authorization is currently execution-valid.
ROOT CAUSE AND VERIFIED DATA FLOW
All references below are to commit b61b543.
- The pool accepts type-4 transactions carrying arbitrary stale, replayed, or chain_id == 0 authorizations.
The only validation applied to a type-4 transaction's authorization list is that Prague is active and the list is non-empty.
Reference: crates/transaction-pool/src/validate/eth.rs:539-548
Snippet:
if transaction.is_eip7702() {
if !self.fork_tracker.is_prague_activated() {
return Err(InvalidTransactionError::TxTypeNotSupported.into())
}
if transaction.authorization_list().is_none_or(|l| l.is_empty()) {
return Err(Eip7702PoolTransactionError::MissingEip7702AuthorizationList.into())
}
}
There is no check of any authorization's nonce or chain_id against state. A repository-wide search confirms no nonce or chain-id authority filtering exists anywhere in the transaction-pool crate.
- Authorities are recovered statelessly and attached unconditionally.
Reference: crates/transaction-pool/src/validate/eth.rs:806-811
Snippet:
fn recover_authorities(&self, transaction: &Tx) -> Option<Vec<Address>> {
transaction
.authorization_list()
.map(|auths| auths.iter().flat_map(|auth| auth.recover_authority()).collect::<Vec<_>>())
}
recover_authority(), in alloy-eip7702-0.6.3/src/auth_list.rs:248, performs only ECDSA recovery plus an s-malleability check. It does not perform state, nonce, or chain-id validation. It recovers the victim's address from any authorization the victim ever signed.
The result is attached unconditionally to the validation outcome at eth.rs:646 and eth.rs:661 as TransactionValidationOutcome::Valid { authorities }.
- auths is populated unconditionally at pool insertion, including for queued transactions.
Reference: crates/transaction-pool/src/pool/txpool.rs:2090-2095, inside AllTransactions::insert_tx
Snippet:
if let Some(auths) = &transaction.authority_ids {
let tx_hash = transaction.hash();
for auth in auths {
self.auths.entry(*auth).or_default().insert(*tx_hash);
}
}
There is no execution-validity filter. This runs before subpool assignment, so even a nonce-gapped, never-mined, perpetually queued attacker transaction registers the victim in auths.
- The victim's own ordinary transactions are throttled.
add_transaction is the generic insertion path for all transaction types. There is no type-4 gate. It calls validate_auth, then check_delegation_limit, unconditionally.
References: crates/transaction-pool/src/pool/txpool.rs:764, :968, and check_delegation_limit at :897-950
Snippet:
// short-circuit: only skip the throttle if sender is NOT an authority and has no on-chain code
if (on_chain_code_hash.is_none() || on_chain_code_hash == Some(KECCAK_EMPTY)) &&
!self.all_transactions.auths.contains_key(&transaction.sender_id())
{
return Ok(())
}
// otherwise, with no pending txs:
let nonce_gap_distance = transaction.nonce().saturating_sub(on_chain_nonce);
if nonce_gap_distance >= self.config.max_inflight_delegated_slot_limit as u64 {
return Err(...)
}
// or with pending txs: count >= limit -> InflightTxLimitReached
Because the victim is in auths, the short-circuit does not fire for the victim's plain legacy or EIP-1559 transactions. They are subject to the slot-limit and no-nonce-gap rules and are rejected with Eip7702PoolTransactionError::OutOfOrderTxFromDelegated or Eip7702PoolTransactionError::InflightTxLimitReached, defined at crates/transaction-pool/src/error.rs:212 and :216.
- The on-chain victim is unchanged.
Per EIP-7702, the replayed authorization is skipped at execution. The victim's nonce and code do not change. The throttle is therefore fully decoupled from any real or impending delegation, confirming this is unintended behavior and not the intended sweep-protection.
- Network amplification.
The attacker transaction is propagated externally, with propagate: true at eth.rs:654-655. Every honest reth node independently recovers the victim and applies the throttle locally.
WHY THE EXISTING THROTTLE SAFETY CHECK DOES NOT PREVENT THIS
validate_auth includes an AuthorityReserved check at txpool.rs:970-986. It rejects the attacker transaction if the named authority already has more than max_inflight_delegated_slot_limit transactions pooled.
This does not protect an idle account. If the victim has less than or equal to the slot limit currently pooled, which is the common case for the vast majority of accounts at any given moment, the attacker transaction is accepted, the victim enters auths, and the victim's next transaction is throttled. The attacker can target idle accounts or wait for a victim's pool to drain.
ATTACK SCENARIO ON MAINNET WITH DEFAULT CONFIGURATION
- Harvest.
The attacker collects any EIP-7702 authorization tuple the victim V has ever signed from a past type-4 transaction in the public mempool or on-chain history. Preferably this is now a guaranteed execution no-op, such as a stale-nonce authorization where V's account nonce advanced past auth.nonce, or a chain_id == 0 authorization.
- Inject.
From a funded attacker account, the attacker submits one type-4 transaction whose authorization_list contains V's harvested tuple and optionally many other victims' tuples. The transaction is pool-valid because Prague is active and the list is non-empty.
The cost is approximately 21,000 gas base plus approximately 12,500 gas per named victim, using the EIP-7702 PER_AUTH_BASE_COST.
- Poison.
On insertion, V is included in auths. To sustain the attack at near-zero cost, the attacker can make the transaction perpetually queued, for example nonce-gapped from the attacker's own account so it never executes and never pays gas, then resubmit when evicted or replaced. auths is populated for queued transactions too.
- Grief.
Until the attacker transaction leaves the pool, V's own ordinary transactions are throttled to 1 in-flight transaction. Any nonce-gapped V transaction is rejected with OutOfOrderTxFromDelegated or InflightTxLimitReached across all honest reth nodes, while on-chain V has no delegation and an unchanged nonce.
A direct unit test at the TxPool layer can drive steps 2 through 4 via add_transaction with a hand-built authority_ids value to assert:
The attacker transaction is accepted and V is present in auths.
V cannot exceed max_inflight_delegated_slot_limit pooled transactions and a nonce-gapped V transaction is rejected.
The on-chain V account is unchanged after the attacker transaction executes.
IMPACT
This allows cheap, targeted, sustained throttling of arbitrary victims' transaction inclusion through honest reth nodes' public mempool, decoupled from any real on-chain delegation.
Important amplifiers:
Many victims can be targeted per attacker transaction.
chain_id == 0 and stale authorizations are indefinitely replayable.
The sustained cost can be near zero via a perpetually queued poisoning transaction.
The only victim prerequisite is having ever signed a 7702 authorization, which is a routine post-Pectra user action, and being momentarily idle.
Severity scales inversely with max_inflight_delegated_slot_limit. The default of 1 is the worst case. The design also self-griefs because a throttled victim cannot escape by sending faster.
SUGGESTED REMEDIATION
Gate authority tracking, and/or the check_delegation_limit short-circuit, on authorization execution-plausibility using state already available at validation time.
Possible fixes:
When recovering authorities, additionally require chain_id to be either 0 or spec.chain_id, and require authorization.nonce == state_nonce(authority), before treating an address as a tracked authority. The stateful account nonce is already available in the stateful validation path.
Alternatively, only apply the delegation throttle to an authority that is also the sender of a pooled transaction, meaning an account with a genuinely impending delegation, rather than to any signature-matched address.
Alternatively, expire auths entries aggressively and key them only on execution-plausible authorizations.
Any fix should be evaluated against the throttle's original goal, which appears to be protecting against an account being swept by an unrelated party once it is genuinely delegated, so the fix does not reopen that issue.
Code of Conduct
SUMMARY
reth's transaction pool throttles an account's transaction inclusion based on stateless ECDSA recovery of EIP-7702 authorization signatures, without checking that those authorizations are execution-valid.
An attacker can harvest any EIP-7702 authorization a victim has ever signed. These authorizations are public because they travel in type-4 transactions through the mempool and on-chain. The attacker can then replay the authorization inside the attacker's own cheap type-4 transaction.
On insertion, the victim is registered as a delegated or pending-delegation account. The victim's own ordinary transactions, including legacy, EIP-1559, and other transaction types, are then throttled to max_inflight_delegated_slot_limit in-flight transactions. The default is 1. Nonce-gapped transactions are rejected, even though nothing about the victim changed on-chain and the replayed authorization is a guaranteed no-op at execution.
This is a liveness and targeted griefing vulnerability, not a consensus or state-corruption issue. It is live on Ethereum mainnet because EIP-7702 has been active since the Pectra upgrade on 2025-05-07. It is cheap, sustainable, repeatable, can target many victims per attacker transaction, and is amplified network-wide because the poisoning transaction propagates to every honest reth node.
SEVERITY ASSESSMENT
Class: Denial of service / liveness issue involving targeted transaction-inclusion throttling and mempool unfairness.
This is not a consensus issue, not state corruption, and not fund loss. On-chain execution is unaffected because the replayed authorization is correctly skipped per EIP-7702.
Realized effect on a victim: while one attacker type-4 transaction naming the victim is pool-resident, the victim is limited to 1 in-flight transaction at a time and cannot submit nonce-gapped, pipelined, or batched transactions. The victim can still land its immediate-next-nonce transaction one at a time, but loses the ability to batch, pipeline, or pre-submit. This is a material degradation for active users, dapps, relayers, MEV searchers, and anyone submitting more than one transaction before the previous one mines.
Exposure: affects all reth nodes; default configuration is the worst case.
AFFECTED SCOPE
Component: reth-transaction-pool
Version / commit verified: reth v2.2.0, commit b61b543, which was current main at the time of this report.
Default config is worst case: DEFAULT_MAX_INFLIGHT_DELEGATED_SLOTS = 1 in crates/transaction-pool/src/config.rs:35.
This is not fixed by PR #23406, "fix: properly validate authorities in the pool", merged 2026-04-09. That PR only made the AuthorityReserved displacement check honor the configurable slot limit instead of a hardcoded 1. It did not add authorization-validity gating. I found no other issue or PR that addresses this.
RELEVANT EIP-7702 BACKGROUND
An EIP-7702 authorization tuple is:
(chain_id, address, nonce, y_parity, r, s)
The tuple is signed by the authority, which is an EOA. At execution time, an authorization is applied only if, among other checks, authorization.nonce == authority.account.nonce and chain_id is either 0 or the current chain ID.
A failing authorization is skipped with zero state effect. The enclosing transaction remains valid, the authority's nonce is not incremented, and no delegation is set.
The relevant consequences are:
Authorizations are public because they are carried in type-4 transactions in the mempool and on-chain.
A stale-nonce authorization, where the authority's account nonce has since advanced, is a guaranteed execution no-op.
A chain_id == 0 authorization is replayable on any chain indefinitely.
Recovering the authority from the signature is independent of whether the authorization is currently execution-valid.
ROOT CAUSE AND VERIFIED DATA FLOW
All references below are to commit b61b543.
The only validation applied to a type-4 transaction's authorization list is that Prague is active and the list is non-empty.
Reference: crates/transaction-pool/src/validate/eth.rs:539-548
Snippet:
There is no check of any authorization's nonce or chain_id against state. A repository-wide search confirms no nonce or chain-id authority filtering exists anywhere in the transaction-pool crate.
Reference: crates/transaction-pool/src/validate/eth.rs:806-811
Snippet:
recover_authority(), in alloy-eip7702-0.6.3/src/auth_list.rs:248, performs only ECDSA recovery plus an s-malleability check. It does not perform state, nonce, or chain-id validation. It recovers the victim's address from any authorization the victim ever signed.
The result is attached unconditionally to the validation outcome at eth.rs:646 and eth.rs:661 as TransactionValidationOutcome::Valid { authorities }.
Reference: crates/transaction-pool/src/pool/txpool.rs:2090-2095, inside AllTransactions::insert_tx
Snippet:
There is no execution-validity filter. This runs before subpool assignment, so even a nonce-gapped, never-mined, perpetually queued attacker transaction registers the victim in auths.
add_transaction is the generic insertion path for all transaction types. There is no type-4 gate. It calls validate_auth, then check_delegation_limit, unconditionally.
References: crates/transaction-pool/src/pool/txpool.rs:764, :968, and check_delegation_limit at :897-950
Snippet:
Because the victim is in auths, the short-circuit does not fire for the victim's plain legacy or EIP-1559 transactions. They are subject to the slot-limit and no-nonce-gap rules and are rejected with Eip7702PoolTransactionError::OutOfOrderTxFromDelegated or Eip7702PoolTransactionError::InflightTxLimitReached, defined at crates/transaction-pool/src/error.rs:212 and :216.
Per EIP-7702, the replayed authorization is skipped at execution. The victim's nonce and code do not change. The throttle is therefore fully decoupled from any real or impending delegation, confirming this is unintended behavior and not the intended sweep-protection.
The attacker transaction is propagated externally, with propagate: true at eth.rs:654-655. Every honest reth node independently recovers the victim and applies the throttle locally.
WHY THE EXISTING THROTTLE SAFETY CHECK DOES NOT PREVENT THIS
validate_auth includes an AuthorityReserved check at txpool.rs:970-986. It rejects the attacker transaction if the named authority already has more than max_inflight_delegated_slot_limit transactions pooled.
This does not protect an idle account. If the victim has less than or equal to the slot limit currently pooled, which is the common case for the vast majority of accounts at any given moment, the attacker transaction is accepted, the victim enters auths, and the victim's next transaction is throttled. The attacker can target idle accounts or wait for a victim's pool to drain.
ATTACK SCENARIO ON MAINNET WITH DEFAULT CONFIGURATION
The attacker collects any EIP-7702 authorization tuple the victim V has ever signed from a past type-4 transaction in the public mempool or on-chain history. Preferably this is now a guaranteed execution no-op, such as a stale-nonce authorization where V's account nonce advanced past auth.nonce, or a chain_id == 0 authorization.
From a funded attacker account, the attacker submits one type-4 transaction whose authorization_list contains V's harvested tuple and optionally many other victims' tuples. The transaction is pool-valid because Prague is active and the list is non-empty.
The cost is approximately 21,000 gas base plus approximately 12,500 gas per named victim, using the EIP-7702 PER_AUTH_BASE_COST.
On insertion, V is included in auths. To sustain the attack at near-zero cost, the attacker can make the transaction perpetually queued, for example nonce-gapped from the attacker's own account so it never executes and never pays gas, then resubmit when evicted or replaced. auths is populated for queued transactions too.
Until the attacker transaction leaves the pool, V's own ordinary transactions are throttled to 1 in-flight transaction. Any nonce-gapped V transaction is rejected with OutOfOrderTxFromDelegated or InflightTxLimitReached across all honest reth nodes, while on-chain V has no delegation and an unchanged nonce.
A direct unit test at the TxPool layer can drive steps 2 through 4 via add_transaction with a hand-built authority_ids value to assert:
The attacker transaction is accepted and V is present in auths.
V cannot exceed max_inflight_delegated_slot_limit pooled transactions and a nonce-gapped V transaction is rejected.
The on-chain V account is unchanged after the attacker transaction executes.
IMPACT
This allows cheap, targeted, sustained throttling of arbitrary victims' transaction inclusion through honest reth nodes' public mempool, decoupled from any real on-chain delegation.
Important amplifiers:
Many victims can be targeted per attacker transaction.
chain_id == 0 and stale authorizations are indefinitely replayable.
The sustained cost can be near zero via a perpetually queued poisoning transaction.
The only victim prerequisite is having ever signed a 7702 authorization, which is a routine post-Pectra user action, and being momentarily idle.
Severity scales inversely with max_inflight_delegated_slot_limit. The default of 1 is the worst case. The design also self-griefs because a throttled victim cannot escape by sending faster.
SUGGESTED REMEDIATION
Gate authority tracking, and/or the check_delegation_limit short-circuit, on authorization execution-plausibility using state already available at validation time.
Possible fixes:
When recovering authorities, additionally require chain_id to be either 0 or spec.chain_id, and require authorization.nonce == state_nonce(authority), before treating an address as a tracked authority. The stateful account nonce is already available in the stateful validation path.
Alternatively, only apply the delegation throttle to an authority that is also the sender of a pooled transaction, meaning an account with a genuinely impending delegation, rather than to any signature-matched address.
Alternatively, expire auths entries aggressively and key them only on execution-plausible authorizations.
Any fix should be evaluated against the throttle's original goal, which appears to be protecting against an account being swept by an unrelated party once it is genuinely delegated, so the fix does not reopen that issue.
Code of Conduct