Skip to content

feat(l1): periodic mempool sweep for stale and dormant transactions#6610

Open
ilitteri wants to merge 8 commits into
mainfrom
feat/mempool-sweep-tasks
Open

feat(l1): periodic mempool sweep for stale and dormant transactions#6610
ilitteri wants to merge 8 commits into
mainfrom
feat/mempool-sweep-tasks

Conversation

@ilitteri
Copy link
Copy Markdown
Collaborator

@ilitteri ilitteri commented May 12, 2026

Motivation

Two failure modes accumulate dead state in the mempool over time:

  1. TTL: A tx whose nonce will never land (sender abandoned the key, replaced with an unrelated tx elsewhere, etc.) sits in the pool indefinitely — wasting capacity that productive txs could use.
  2. Dormant senders: A sender with a permanent nonce gap relative to on-chain state (e.g. high-nonce-wall attack) holds pool slots forever because the txs can never execute.

Add a low-frequency background sweep that evicts both classes.

Description

  • 1-minute periodic sweep spawned during L1 + L2 initialisation via Blockchain::spawn_mempool_sweep. Holds only a Weak<Blockchain> so the task tears itself down on shutdown. MissedTickBehavior::Skip so a slow iteration doesn't trigger a burst of catch-up ticks.
  • Mempool::evict_stale(ttl) — drops txs older than ttl based on the existing MempoolTransaction.timestamp.
  • Mempool::evict_dormant(store, max_nonce_gap, dormancy) — flags senders whose lowest pending nonce exceeds the on-chain nonce by more than max_nonce_gap AND every queue entry is older than dormancy. The lowest-nonce gap (rather than top-nonce - on-chain) avoids false positives on long contiguous queues. The eviction is atomic with respect to fresh arrivals: the read-lock snapshot records exact (hash, timestamp) pairs and the write-lock removal step only drops hashes whose pool entry STILL has the snapshot timestamp — txs that arrived between snapshot and removal are safe.
  • CLI: --mempool.lifetime (default 3h), --mempool.max-nonce-gap (default 64), --mempool.dormancy (default 3h). Duration defaults render as 3h in --help, not raw seconds.

Behavioral change

Pool occupancy drops over time as inactive senders' txs age out. Sweep is rate-limited (1-minute tick) and bounded (per-sender scan via txs_by_sender_nonce index, one pass).

Deferred

End-to-end test of the spawned sweep loop (timing-dependent). The eviction methods themselves have direct unit-test coverage including the contiguous-queue regression case.

Add a 1-minute background task that evicts mempool entries on two criteria:
TTL (drop txs older than --mempool.lifetime, default 3h) and dormancy
(drop all entries from senders whose top pending nonce exceeds the on-chain
nonce by more than --mempool.max-nonce-gap and have been dormant for
--mempool.dormancy, defaults 64 and 3h).

The sweep is spawned in init_blockchain for both L1 and L2; it holds a
Weak<Blockchain> so it terminates automatically once the blockchain is
dropped. MempoolTransaction already tracks insertion time, so the evict
methods read tx.time() directly; a new_with_timestamp constructor is added
for test-time injection. Constants DEFAULT_MEMPOOL_LIFETIME, DEFAULT_DORMANCY,
DEFAULT_MAX_NONCE_GAP, and MEMPOOL_SWEEP_INTERVAL live in
crates/blockchain/constants.rs and back both the CLI defaults and the
BlockchainOptions defaults. Duration flags are parsed via the existing
ethrex_common::serde_utils::parse_duration.
Copilot AI review requested due to automatic review settings May 12, 2026 02:42
@ilitteri ilitteri requested review from a team, ManuelBilbao and avilagaston9 as code owners May 12, 2026 02:42
@github-actions github-actions Bot added the L1 Ethereum client label May 12, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: Solid implementation of mempool eviction logic with good test coverage and clear documentation. Two main issues need attention: blocking synchronization in async context and inefficient sender-based lookup.

Critical Issues

1. Blocking locks in async context (Performance/Risk)

File: crates/blockchain/blockchain.rs, lines 3197-3241
File: crates/blockchain/mempool.rs, lines 506, 565, 625

The mempool uses std::sync::RwLock (via self.read()/self.write()) but is accessed from async code in run_mempool_sweep. This blocks the Tokio executor thread when the lock is contended.

Recommendation: Either:

  • Use tokio::sync::RwLock for the mempool to allow async-friendly locking, or
  • Wrap the eviction calls in tokio::task::spawn_blocking:
// In run_mempool_sweep
let result = tokio::task::spawn_blocking({
    let mempool = chain.mempool.clone();
    move || mempool.evict_stale(ttl)
}).await.expect("spawn_blocking failed");

2. Inefficient sender lookup in dormancy sweep (Performance)

File: crates/blockchain/mempool.rs, lines 619-627

evict_dormant iterates the entire transaction_pool for each dormant sender (O(N×S) complexity). If the mempool has an index by sender address (e.g., txs_by_address: FxHashMap<Address, Vec<H256>>), use it instead:

// Instead of scanning all transactions:
let hashes: Vec<H256> = inner
    .txs_by_address // Use index if available
    .get(&sender)
    .cloned()
    .unwrap_or_default();

Minor Issues

3. Fragile test path

File: crates/blockchain/mempool.rs, line 656
The relative path ../../fixtures/genesis/l1.json assumes a specific crate nesting depth. Use CARGO_MANIFEST_DIR or ensure the path works when tests are run from different locations.

let genesis = serde_json::from_str::<Genesis>(include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../fixtures/genesis/l1.json"
))).expect("parse genesis");

4. Clock drift sensitivity

File: crates/blockchain/mempool.rs, line 643
SystemTime::now() can go backwards if the system clock is adjusted, potentially preventing eviction. Consider documenting this behavior or using a monotonic clock (Instant) for the eviction logic, though SystemTime is acceptable for this use case.

Positive Feedback

  • Weak reference pattern: Correct use of Weak<Blockchain> in spawn_mempool_sweep (line 3209) ensures the task doesn't prevent blockchain shutdown.
  • Lock scope minimization: Good practice in evict_dormant releasing the read lock before async storage calls (lines 590-591).
  • Constants: Well-documented defaults in constants.rs with sensible values (3h TTL, 64 nonce gap).
  • CLI integration: Clean use of helper functions (default_mempool_lifetime_str) to keep defaults synchronized between constants and clap.
  • Test coverage: Comprehensive tests for both TTL and dormancy eviction paths including edge cases (recent activity preservation, small gap preservation).

Nitpick

File: cmd/ethrex/cli.rs, lines 71-78
The return type clap::builder::OsStr is overly specific; returning impl Into<OsStr> or just String would be more idiomatic, though the current implementation works correctly.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 12, 2026

Lines of code report

Total lines added: 357
Total lines removed: 0
Total lines changed: 357

Detailed view
+-------------------------------------------+-------+------+
| File                                      | Lines | Diff |
+-------------------------------------------+-------+------+
| ethrex/cmd/ethrex/cli.rs                  | 1167  | +59  |
+-------------------------------------------+-------+------+
| ethrex/cmd/ethrex/initializers.rs         | 659   | +4   |
+-------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/initializers.rs      | 385   | +4   |
+-------------------------------------------+-------+------+
| ethrex/cmd/ethrex/utils.rs                | 186   | +4   |
+-------------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs    | 2554  | +46  |
+-------------------------------------------+-------+------+
| ethrex/crates/blockchain/constants.rs     | 22    | +5   |
+-------------------------------------------+-------+------+
| ethrex/crates/blockchain/mempool.rs       | 692   | +228 |
+-------------------------------------------+-------+------+
| ethrex/crates/common/types/transaction.rs | 3410  | +7   |
+-------------------------------------------+-------+------+

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

Adds a periodic, low-frequency mempool sweep to evict (1) stale transactions by TTL and (2) “dormant” senders with large nonce gaps whose pool entries have all aged past a dormancy window. This is wired into both L1 and L2 initialization and exposed via new CLI flags with documented defaults.

Changes:

  • Implemented Mempool::evict_stale(ttl) and async Mempool::evict_dormant(store, max_nonce_gap, dormancy), plus unit tests.
  • Spawned a background sweep loop in Blockchain (weakly-referenced) running every MEMPOOL_SWEEP_INTERVAL.
  • Added CLI flags/env vars and docs for --mempool.lifetime, --mempool.max-nonce-gap, --mempool.dormancy, backed by new constants.

Reviewed changes

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

Show a summary per file
File Description
docs/CLI.md Documents the new mempool sweep flags and defaults for both node modes.
crates/common/types/transaction.rs Adds MempoolTransaction::new_with_timestamp to support timestamp injection in tests.
crates/blockchain/mempool.rs Implements TTL + dormancy eviction logic and adds unit tests for both sweeps.
crates/blockchain/constants.rs Defines default durations/thresholds and sweep interval constants.
crates/blockchain/Cargo.toml Adds serde_json dev-dependency for new mempool tests.
crates/blockchain/blockchain.rs Adds sweep task spawning and the periodic sweep loop.
cmd/ethrex/utils.rs Adds a duration parser wrapper for clap value parsing.
cmd/ethrex/l2/initializers.rs Wires new mempool options into L2 BlockchainOptions and spawns the sweep task.
cmd/ethrex/initializers.rs Wires new mempool options into L1 BlockchainOptions and spawns the sweep task.
cmd/ethrex/cli.rs Adds new CLI flags/env vars, defaults, and passes options into blockchain init.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/blockchain/mempool.rs Outdated
Comment on lines +612 to +616
let mut inner = self.write()?;
let mut evicted = 0usize;
for sender in dormant_senders {
let hashes: Vec<H256> = inner
.transaction_pool
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +613 to +629
let mut evicted = 0usize;
for sender in dormant_senders {
let hashes: Vec<H256> = inner
.transaction_pool
.iter()
.filter_map(|(hash, tx)| {
if tx.sender() == sender {
Some(*hash)
} else {
None
}
})
.collect();
for hash in &hashes {
inner.remove_transaction_with_lock(hash)?;
evicted += 1;
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread crates/blockchain/mempool.rs Outdated
Comment on lines +560 to +564
// Snapshot per-sender (top_nonce, oldest_timestamp) under the read
// lock so we can do async storage lookups without holding it.
struct SenderStats {
top_nonce: u64,
newest_timestamp: u128,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +3207 to +3211
let mut interval = tokio::time::interval(MEMPOOL_SWEEP_INTERVAL);
// Drop the first immediate tick — let the node finish initialising before
// doing any work.
interval.tick().await;
loop {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have everything I need for a thorough review.


Review: feat(l1): periodic mempool sweep for stale and dormant transactions

The overall architecture is sound — Weak<Blockchain> for automatic teardown is elegant, and the two-phase locking in evict_dormant (read → async I/O → write) is the correct pattern. The test coverage is solid. A few issues need attention before merging.


pub test constructor leaks into production API

crates/common/types/transaction.rs

pub fn new_with_timestamp(tx: Transaction, sender: Address, timestamp: u128) -> Self {

This constructor is documented as "intended for tests" but has no #[cfg(test)] guard. Any downstream crate can call it to create a MempoolTransaction with an arbitrary timestamp — including one in the future or distant past — and inject it into the pool via add_transaction. This bypasses the normal clock-based admission time entirely.

Either gate it:

#[cfg(test)]
pub fn new_with_timestamp(...) -> Self { ... }

or restrict visibility: pub(crate) if it needs to cross module boundaries within the same crate. Making it pub(crate) would require moving the test module or adjusting the call site, so #[cfg(test)] is cleaner given it's only called from mempool.rs tests.


txs_order deque not cleaned by the new eviction paths

crates/blockchain/mempool.rs, remove_transaction_with_lock (line ~61)

remove_transaction_with_lock removes from transaction_pool, txs_by_sender_nonce, and broadcast_pool — but not from txs_order. The existing remove_oldest_transaction tolerates this because it iterates txs_order in a loop and gracefully skips absent hashes. However, after a large sweep eviction, txs_order retains all the evicted hashes indefinitely.

The lazy cleanup in add_transaction only fires when txs_order.len() > mempool_prune_threshold (1.5× max_mempool_size). If the pool is heavily evicted and then sits idle, txs_order accumulates dead entries forever. In a pathological case where 10 000 transactions are evicted and no new ones arrive, the deque never shrinks.

This is a pre-existing pattern, but the new sweep methods are the first callers that can cause mass removal outside of remove_oldest_transaction. Consider triggering a txs_order compaction at the end of evict_stale / evict_dormant when the number of evictions is non-trivial:

// After mass eviction, compact the order queue while the lock is still held.
if evicted > 0 {
    let txpool = &inner.transaction_pool;
    inner.txs_order.retain(|h| txpool.contains_key(h));
}

MissedTickBehavior not set on the sweep interval

crates/blockchain/blockchain.rs, run_mempool_sweep

let mut interval = tokio::time::interval(MEMPOOL_SWEEP_INTERVAL);

tokio::time::interval uses MissedTickBehavior::Burst by default: if a sweep tick takes longer than 60 s (e.g., during a heavy evict_dormant with many storage lookups under load), subsequent ticks fire back-to-back to "catch up." For a maintenance task, this is undesirable — it could hammer the store with rapid sweeps.

let mut interval = tokio::time::interval(MEMPOOL_SWEEP_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

TOCTOU window in evict_dormant

crates/blockchain/mempool.rs, evict_dormant

The function releases the read lock, does async storage lookups, then re-acquires a write lock. In that window, a sender classified as dormant could submit a fresh transaction — resetting their actual newest_timestamp — but the snapshot still carries the stale value, so they'd be incorrectly evicted.

The practical impact is low (the gap between snapshot and write lock is only a few storage round-trips), and the 3-hour dormancy default makes false positives rare. But it's worth a code comment acknowledging the race so future reviewers don't accidentally "fix" the two-phase lock approach without understanding the trade-off:

// Note: snapshot may lag a few async round-trips behind the live pool.
// A sender who submits a fresh tx in that window risks incorrect eviction.
// Given the 3h dormancy default, false-positive eviction is extremely rare.

O(senders × pool_size) scan for dormant sender hashes

crates/blockchain/mempool.rs, evict_dormant, write-lock phase (~line 480)

for sender in dormant_senders {
    let hashes: Vec<H256> = inner
        .transaction_pool
        .iter()
        .filter_map(|(hash, tx)| {
            if tx.sender() == sender { Some(*hash) } else { None }
        })
        .collect();
    ...
}

This iterates the full pool once per dormant sender. If there are k dormant senders and n pool entries, this is O(k×n). A single pass suffices:

let dormant_set: FxHashSet<Address> = dormant_senders.into_iter().collect();
let hashes: Vec<H256> = inner
    .transaction_pool
    .iter()
    .filter_map(|(hash, tx)| dormant_set.contains(&tx.sender()).then_some(*hash))
    .collect();

With a bounded 10 000-entry pool and sweeps running only once a minute, the current code is not a real performance concern, but the single-pass version is also simpler.


Default duration displayed as 10800s instead of 3h

cmd/ethrex/cli.rs and docs/CLI.md

fn default_mempool_lifetime_str() -> clap::builder::OsStr {
    format!("{}s", DEFAULT_MEMPOOL_LIFETIME.as_secs()).into()
}

This prints [default: 10800s] in --help output and the CLI docs. 3h is what a human would type and is accepted by parse_duration. A small helper would keep it readable:

fn duration_to_human(d: Duration) -> String {
    let secs = d.as_secs();
    if secs % 3600 == 0 { format!("{}h", secs / 3600) }
    else if secs % 60 == 0 { format!("{}m", secs / 60) }
    else { format!("{}s", secs) }
}

Minor: s.to_string() allocation in parse_duration

cmd/ethrex/utils.rs

ethrex_common::serde_utils::parse_duration(s.to_string())

The underlying function takes String, so the allocation is unavoidable given the current API. If ethrex_common::serde_utils::parse_duration ever accepts &str, drop the .to_string() call. No action needed now.


What's good

  • Using Weak<Blockchain> so the sweep task self-terminates on blockchain drop is a clean design with no extra cancellation tokens needed.
  • Two-phase locking in evict_dormant (read lock for snapshot → async I/O → write lock) correctly avoids holding a lock across await points.
  • The dormancy condition (newest_timestamp >= cutoff means "at least one entry is fresh, so skip the sender") is logically correct.
  • Test coverage covers all four relevant cases. The use of empty_store() with an in-memory engine and real genesis is more reliable than pure mock-based tests.
  • current_unix_micros() returning 0 on clock error is safe: no transaction can have time() < 0 (u128), so the eviction correctly becomes a no-op when the clock is broken.

Automated review by Claude (Anthropic) · sonnet · custom prompt

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR adds a 1-minute background task that periodically evicts stale and dormant transactions from the mempool, addressing two failure modes: unbounded lifetime of abandoned transactions and senders holding pool capacity with unreachable nonces. Two configurable sweeps are introduced — a TTL sweep (evict_stale) and a nonce-gap dormancy sweep (evict_dormant) — backed by new CLI flags and BlockchainOptions fields with documented defaults.

  • evict_stale removes any transaction older than --mempool.lifetime (default 3h), correctly walking the pool under a single write lock.
  • evict_dormant identifies senders whose pool entries have all aged past --mempool.dormancy (3h) and whose nonce gap exceeds --mempool.max-nonce-gap (64), then evicts all their entries; this method has two logic issues described in the inline comments.
  • The sweep task uses a Weak<Blockchain> reference for automatic teardown when the node shuts down, and the new MempoolTransaction::new_with_timestamp constructor enables clean unit-test coverage of both eviction paths.

Confidence Score: 3/5

The sweep infrastructure and TTL eviction are safe, but the dormancy sweep has two correctness defects that can drop valid transactions in production before the fix is applied.

The nonce gap check compares the sender's top pool nonce against the on-chain nonce rather than the minimum pool nonce, meaning a sender who queued 65+ sequential transactions starting at nonce 1 would be incorrectly evicted after 3 hours. In the same function, the read-lock snapshot is released before async storage lookups and re-acquired as a write lock, so any transaction submitted by the sender in that window is swept without the dormancy check being re-applied.

crates/blockchain/mempool.rs — both the gap-computation logic and the snapshot/eviction lock split in evict_dormant need attention before merge.

Important Files Changed

Filename Overview
crates/blockchain/mempool.rs Core eviction logic added — two bugs: gap uses top_nonce instead of min_nonce (evicts valid consecutive-nonce queues), and TOCTOU between snapshot read-lock and write-lock eviction can drop fresh transactions.
crates/blockchain/blockchain.rs Adds spawn_mempool_sweep() and run_mempool_sweep(); Weak lifecycle and interval handling look correct.
cmd/ethrex/cli.rs Adds three new CLI flags and wires them into BlockchainOptions; default duration helpers display as raw seconds (e.g. 10800s) instead of the more readable 3h form.
crates/blockchain/constants.rs Adds well-documented sweep constants; all values look sensible.
crates/common/types/transaction.rs Adds new_with_timestamp constructor for test injection of synthetic timestamps; correctly scoped and documented.
cmd/ethrex/initializers.rs Passes new options into BlockchainOptions and calls spawn_mempool_sweep() for L1; straightforward wiring.
cmd/ethrex/l2/initializers.rs Same pattern as L1 initializer — passes options and spawns sweep for L2.
cmd/ethrex/utils.rs Thin wrapper around ethrex_common::serde_utils::parse_duration for clap value_parser; correct and well-documented.
crates/blockchain/Cargo.toml Adds serde_json as a dev-dependency for genesis parsing in tests; appropriate placement.
docs/CLI.md Documents the three new CLI flags in both L1 and L2 sections; defaults shown as 10800s instead of 3h, mirroring the CLI helper issue.

Sequence Diagram

sequenceDiagram
    participant Init as init_blockchain
    participant BC as Blockchain (Arc)
    participant Sweep as run_mempool_sweep (Weak)
    participant MP as Mempool
    participant Store as Storage

    Init->>BC: spawn_mempool_sweep()
    BC->>Sweep: tokio::spawn(run_mempool_sweep(Weak))
    Note over Sweep: interval tick (dropped — init grace)

    loop Every MEMPOOL_SWEEP_INTERVAL (60s)
        Sweep->>BC: Weak::upgrade()
        alt Arc still alive
            BC-->>Sweep: "Arc<Blockchain>"
            Sweep->>MP: evict_stale(ttl)
            MP-->>Sweep: Ok(n_evicted)
            Sweep->>MP: evict_dormant(store, max_gap, dormancy)
            Note over MP,Store: Read lock - snapshot sender stats, Release lock
            MP->>Store: get_latest_block_number()
            loop per sender in snapshot
                MP->>Store: get_account_info(block, sender)
                Store-->>MP: Option AccountInfo
                MP->>MP: check gap and dormancy
            end
            Note over MP: Write lock - evict dormant senders
            MP-->>Sweep: Ok(n_evicted)
        else Arc dropped
            Sweep->>Sweep: return (task tears down)
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/blockchain/mempool.rs:567-606
**Incorrect nonce used for gap check — flags legitimate senders**

`SenderStats` tracks `top_nonce`, and the gap is computed as `top_nonce - on_chain_nonce`. This catches the described "high-nonce wall" attack, but it also flags any sender whose queue of **consecutive** transactions happens to be longer than `max_nonce_gap`. For example: on-chain nonce = 0, pool nonces = 1 … 65. Gap = 65 > 64, but nonce 1 is the very next executable tx — nothing is unreachable. These transactions would be evicted even though they're entirely valid.

The check that correctly distinguishes an unreachable wall from a large-but-valid queue is `min_nonce_in_pool - on_chain_nonce > max_nonce_gap`. A min-nonce field needs to be added to `SenderStats` and tracked in the accumulation loop the same way `top_nonce` is.

### Issue 2 of 3
crates/blockchain/mempool.rs:611-631
**TOCTOU: fresh transactions can be evicted after dormancy snapshot**

The function releases the read lock after taking the snapshot, does async storage lookups (potentially hundreds of milliseconds), then acquires a write lock to remove transactions. Any transaction submitted by a "dormant" sender in that window will be collected in the per-sender `hashes` vector and removed unconditionally, even though it was fresh and was never observed in the snapshot that triggered the eviction decision.

Re-checking `tx.time() < cutoff` for each hash inside the write-lock phase before calling `remove_transaction_with_lock` would close the race without needing to hold the read lock across the awaits.

### Issue 3 of 3
cmd/ethrex/cli.rs:69-79
The helpers convert the constant to `{secs}s`, so both `--mempool.lifetime` and `--mempool.dormancy` display as `10800s` in `--help` and in `CLI.md`. Expressing the default as `3h` directly matches the format the parser already accepts and is far more readable to operators.

```suggestion
/// Format the default mempool lifetime as a clap-compatible string,
/// reusing the [`DEFAULT_MEMPOOL_LIFETIME`] constant so it stays in sync.
fn default_mempool_lifetime_str() -> clap::builder::OsStr {
    let secs = DEFAULT_MEMPOOL_LIFETIME.as_secs();
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    match (h, m, s) {
        (h, 0, 0) => format!("{}h", h),
        (0, m, 0) => format!("{}m", m),
        (0, 0, s) => format!("{}s", s),
        (h, m, 0) => format!("{}h{}m", h, m),
        _ => format!("{}s", secs),
    }
    .into()
}

/// Format the default mempool dormancy as a clap-compatible string,
/// reusing the [`DEFAULT_DORMANCY`] constant so it stays in sync.
fn default_mempool_dormancy_str() -> clap::builder::OsStr {
    let secs = DEFAULT_DORMANCY.as_secs();
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    match (h, m, s) {
        (h, 0, 0) => format!("{}h", h),
        (0, m, 0) => format!("{}m", m),
        (0, 0, s) => format!("{}s", s),
        (h, m, 0) => format!("{}h{}m", h, m),
        _ => format!("{}s", secs),
    }
    .into()
}
```

Reviews (1): Last reviewed commit: "feat(l1): periodic mempool sweep for sta..." | Re-trigger Greptile

Comment on lines +567 to +606
let snapshot: FxHashMap<Address, SenderStats> = {
let inner = self.read()?;
let mut acc: FxHashMap<Address, SenderStats> =
FxHashMap::with_capacity_and_hasher(128, Default::default());
for tx in inner.transaction_pool.values() {
let entry = acc.entry(tx.sender()).or_insert(SenderStats {
top_nonce: tx.nonce(),
newest_timestamp: tx.time(),
});
if tx.nonce() > entry.top_nonce {
entry.top_nonce = tx.nonce();
}
if tx.time() > entry.newest_timestamp {
entry.newest_timestamp = tx.time();
}
}
acc
};

let latest_block_number = store.get_latest_block_number().await?;

let mut dormant_senders: Vec<Address> = Vec::new();
for (sender, stats) in &snapshot {
// Newest entry must itself be older than the dormancy window,
// which is equivalent to "all entries are older than dormancy".
if stats.newest_timestamp >= cutoff {
continue;
}

let on_chain_nonce = store
.get_account_info(latest_block_number, *sender)
.await?
.map(|info| info.nonce)
.unwrap_or(0);

let gap = stats.top_nonce.saturating_sub(on_chain_nonce);
if gap > max_nonce_gap {
dormant_senders.push(*sender);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Incorrect nonce used for gap check — flags legitimate senders

SenderStats tracks top_nonce, and the gap is computed as top_nonce - on_chain_nonce. This catches the described "high-nonce wall" attack, but it also flags any sender whose queue of consecutive transactions happens to be longer than max_nonce_gap. For example: on-chain nonce = 0, pool nonces = 1 … 65. Gap = 65 > 64, but nonce 1 is the very next executable tx — nothing is unreachable. These transactions would be evicted even though they're entirely valid.

The check that correctly distinguishes an unreachable wall from a large-but-valid queue is min_nonce_in_pool - on_chain_nonce > max_nonce_gap. A min-nonce field needs to be added to SenderStats and tracked in the accumulation loop the same way top_nonce is.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/mempool.rs
Line: 567-606

Comment:
**Incorrect nonce used for gap check — flags legitimate senders**

`SenderStats` tracks `top_nonce`, and the gap is computed as `top_nonce - on_chain_nonce`. This catches the described "high-nonce wall" attack, but it also flags any sender whose queue of **consecutive** transactions happens to be longer than `max_nonce_gap`. For example: on-chain nonce = 0, pool nonces = 1 … 65. Gap = 65 > 64, but nonce 1 is the very next executable tx — nothing is unreachable. These transactions would be evicted even though they're entirely valid.

The check that correctly distinguishes an unreachable wall from a large-but-valid queue is `min_nonce_in_pool - on_chain_nonce > max_nonce_gap`. A min-nonce field needs to be added to `SenderStats` and tracked in the accumulation loop the same way `top_nonce` is.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +611 to +631

let mut inner = self.write()?;
let mut evicted = 0usize;
for sender in dormant_senders {
let hashes: Vec<H256> = inner
.transaction_pool
.iter()
.filter_map(|(hash, tx)| {
if tx.sender() == sender {
Some(*hash)
} else {
None
}
})
.collect();
for hash in &hashes {
inner.remove_transaction_with_lock(hash)?;
evicted += 1;
}
}
Ok(evicted)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 TOCTOU: fresh transactions can be evicted after dormancy snapshot

The function releases the read lock after taking the snapshot, does async storage lookups (potentially hundreds of milliseconds), then acquires a write lock to remove transactions. Any transaction submitted by a "dormant" sender in that window will be collected in the per-sender hashes vector and removed unconditionally, even though it was fresh and was never observed in the snapshot that triggered the eviction decision.

Re-checking tx.time() < cutoff for each hash inside the write-lock phase before calling remove_transaction_with_lock would close the race without needing to hold the read lock across the awaits.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/mempool.rs
Line: 611-631

Comment:
**TOCTOU: fresh transactions can be evicted after dormancy snapshot**

The function releases the read lock after taking the snapshot, does async storage lookups (potentially hundreds of milliseconds), then acquires a write lock to remove transactions. Any transaction submitted by a "dormant" sender in that window will be collected in the per-sender `hashes` vector and removed unconditionally, even though it was fresh and was never observed in the snapshot that triggered the eviction decision.

Re-checking `tx.time() < cutoff` for each hash inside the write-lock phase before calling `remove_transaction_with_lock` would close the race without needing to hold the read lock across the awaits.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread cmd/ethrex/cli.rs
Comment on lines +69 to +79
/// Format the default mempool lifetime as a clap-compatible string,
/// reusing the [`DEFAULT_MEMPOOL_LIFETIME`] constant so it stays in sync.
fn default_mempool_lifetime_str() -> clap::builder::OsStr {
format!("{}s", DEFAULT_MEMPOOL_LIFETIME.as_secs()).into()
}

/// Format the default mempool dormancy as a clap-compatible string,
/// reusing the [`DEFAULT_DORMANCY`] constant so it stays in sync.
fn default_mempool_dormancy_str() -> clap::builder::OsStr {
format!("{}s", DEFAULT_DORMANCY.as_secs()).into()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The helpers convert the constant to {secs}s, so both --mempool.lifetime and --mempool.dormancy display as 10800s in --help and in CLI.md. Expressing the default as 3h directly matches the format the parser already accepts and is far more readable to operators.

Suggested change
/// Format the default mempool lifetime as a clap-compatible string,
/// reusing the [`DEFAULT_MEMPOOL_LIFETIME`] constant so it stays in sync.
fn default_mempool_lifetime_str() -> clap::builder::OsStr {
format!("{}s", DEFAULT_MEMPOOL_LIFETIME.as_secs()).into()
}
/// Format the default mempool dormancy as a clap-compatible string,
/// reusing the [`DEFAULT_DORMANCY`] constant so it stays in sync.
fn default_mempool_dormancy_str() -> clap::builder::OsStr {
format!("{}s", DEFAULT_DORMANCY.as_secs()).into()
}
/// Format the default mempool lifetime as a clap-compatible string,
/// reusing the [`DEFAULT_MEMPOOL_LIFETIME`] constant so it stays in sync.
fn default_mempool_lifetime_str() -> clap::builder::OsStr {
let secs = DEFAULT_MEMPOOL_LIFETIME.as_secs();
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
match (h, m, s) {
(h, 0, 0) => format!("{}h", h),
(0, m, 0) => format!("{}m", m),
(0, 0, s) => format!("{}s", s),
(h, m, 0) => format!("{}h{}m", h, m),
_ => format!("{}s", secs),
}
.into()
}
/// Format the default mempool dormancy as a clap-compatible string,
/// reusing the [`DEFAULT_DORMANCY`] constant so it stays in sync.
fn default_mempool_dormancy_str() -> clap::builder::OsStr {
let secs = DEFAULT_DORMANCY.as_secs();
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = secs % 60;
match (h, m, s) {
(h, 0, 0) => format!("{}h", h),
(0, m, 0) => format!("{}m", m),
(0, 0, s) => format!("{}s", s),
(h, m, 0) => format!("{}h{}m", h, m),
_ => format!("{}s", secs),
}
.into()
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd/ethrex/cli.rs
Line: 69-79

Comment:
The helpers convert the constant to `{secs}s`, so both `--mempool.lifetime` and `--mempool.dormancy` display as `10800s` in `--help` and in `CLI.md`. Expressing the default as `3h` directly matches the format the parser already accepts and is far more readable to operators.

```suggestion
/// Format the default mempool lifetime as a clap-compatible string,
/// reusing the [`DEFAULT_MEMPOOL_LIFETIME`] constant so it stays in sync.
fn default_mempool_lifetime_str() -> clap::builder::OsStr {
    let secs = DEFAULT_MEMPOOL_LIFETIME.as_secs();
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    match (h, m, s) {
        (h, 0, 0) => format!("{}h", h),
        (0, m, 0) => format!("{}m", m),
        (0, 0, s) => format!("{}s", s),
        (h, m, 0) => format!("{}h{}m", h, m),
        _ => format!("{}s", secs),
    }
    .into()
}

/// Format the default mempool dormancy as a clap-compatible string,
/// reusing the [`DEFAULT_DORMANCY`] constant so it stays in sync.
fn default_mempool_dormancy_str() -> clap::builder::OsStr {
    let secs = DEFAULT_DORMANCY.as_secs();
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    match (h, m, s) {
        (h, 0, 0) => format!("{}h", h),
        (0, m, 0) => format!("{}m", m),
        (0, 0, s) => format!("{}s", s),
        (h, m, 0) => format!("{}h{}m", h, m),
        _ => format!("{}s", secs),
    }
    .into()
}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/blockchain/mempool.rs lines 596-604: gap = top_nonce - on_chain_nonce is not an actual nonce gap, it is just queue depth. A sender with a fully contiguous queue nonce = on_chain_nonce..on_chain_nonce+65 will satisfy this condition and get all txs evicted after dormancy, even though nothing is “stalled” or unfillable. If the intent is to evict real nonce holes, this needs to inspect missing nonces / contiguous span, not just the highest nonce.

  2. crates/blockchain/mempool.rs lines 567-628: evict_dormant() decides dormancy from a read snapshot, then does async store reads, then takes a write lock and removes all current txs for that sender. A fresh tx arriving during that window, including the gap-filling tx that makes the account no longer dormant, will still be deleted. This needs a re-check under the write lock, or deletion should be limited to the snapshotted hashes after confirming the sender still qualifies.

  3. crates/blockchain/blockchain.rs lines 3207-3212 and crates/blockchain/mempool.rs lines 588-599: the sweep loop uses tokio::time::interval() with the default missed-tick behavior (Burst), while each dormancy pass does one account lookup per sender. If a pass ever takes longer than MEMPOOL_SWEEP_INTERVAL, the task will immediately replay missed ticks and can pin the node into back-to-back sweeps. MissedTickBehavior::Skip or a sleep-based loop would be safer.

The diff does not touch EVM execution, gas accounting, trie updates, or RLP logic directly; the risk here is isolated to mempool policy/concurrency. I could not run cargo check/cargo test in this environment because the workspace depends on an uncached git dependency (libssz) and network access is blocked.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@ilitteri ilitteri mentioned this pull request May 12, 2026
12 tasks
… one-pass scan)

Phase 2 review (Copilot + greptile P1) flagged three related issues:
- The dormancy gap test used `top_nonce - on_chain_nonce`, which fires
  on senders with a long but CONTIGUOUS queue (e.g. on_chain=0,
  pool=1..=65 → gap=65>64) even though every nonce is executable.
- TOCTOU between the read-lock snapshot and the write-lock removal
  let freshly-arrived txs be evicted alongside the stale ones.
- The removal phase scanned `transaction_pool.iter()` once per dormant
  sender — O(pool_size × dormant_senders).

Single rewrite addresses all three:
- Compare against `lowest_pending_nonce` instead of `top_nonce`. A
  contiguous queue from on-chain+1 has lowest=on-chain+1 and gap=1, so
  it doesn't fire the rule. Only queues whose bottom is unreachable
  (>max_nonce_gap past on-chain) are flagged.
- The snapshot now records the exact `(hash, timestamp)` pairs per
  sender, and the write phase removes only those hashes whose pool
  entry still has the snapshot timestamp. Newly-arrived (or re-added)
  txs are safe.
- The hash collection is done in the single read-phase pass over
  `transaction_pool.iter()` — no per-sender re-scans.

New regression test `evict_dormant_keeps_long_contiguous_queue`
covers the false-positive case. Existing fixtures adjusted to the new
lowest-nonce semantics.
ilitteri added 3 commits May 12, 2026 15:30
Phase 2 review (Copilot): `tokio::time::interval` defaults to
`MissedTickBehavior::Burst`. If a sweep iteration ever runs longer than
`MEMPOOL_SWEEP_INTERVAL` (many senders, slow storage), the loop fires
back-to-back ticks trying to catch up — exactly when the node is
already under load. `Skip` instead drops the missed ticks; we'd rather
miss a sweep cycle than burst.

The "stale-vs-fresh timestamp" doc-comment mismatch flagged in the
same review was already addressed when the snapshot was rewritten in
the previous commit (`SenderStats.newest_timestamp` now matches the
code that reads `tx.time() > entry.newest_timestamp`).
Phase 2 review (greptile P2): `--mempool.lifetime` and
`--mempool.dormancy` defaults rendered as `10800s` in `--help` and
`docs/CLI.md`, but the parser already accepts `3h` / `30m` / `45s`.
Show the human-readable form so operators don't have to convert.

Added `duration_default_str(d: Duration)` that picks the largest
whole unit (`Nh`, `Nm`, or `Ns`) for the displayed default. Both
existing helpers route through it, and `docs/CLI.md` is regenerated
with the new defaults.
…cstring

Reviewer attribution belongs in PR bodies and commit messages, not in
test docstrings. Rephrased the comment to describe the failure mode it
guards against without naming the reviewer who flagged it.
let ttl_micros = ttl.as_micros();
let cutoff = now_micros.saturating_sub(ttl_micros);

let mut inner = self.write()?;
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.

evict_stale takes the write lock for the entire scan + remove, so admissions are blocked for the duration of the iteration. evict_dormant (line 580-599) deliberately uses a read-lock snapshot to avoid this same blocking, then re-acquires the write lock only for the actual removals with a per-entry timestamp double-check.

For consistency (and because evict_stale will face the same scan cost for a full pool), consider mirroring the snapshot-then-evict pattern:

let stale_with_ts: Vec<(H256, u128)> = {
    let inner = self.read()?;
    inner.transaction_pool.iter()
        .filter(|(_, tx)| tx.time() < cutoff)
        .map(|(h, tx)| (*h, tx.time()))
        .collect()
};
// then write-lock + per-entry timestamp double-check, same as evict_dormant

For a 10k-tx pool this drops the worst-case admission-block window from "O(N) under write lock" to "O(N) under read lock + O(K) under write lock" where K is the count actually evicted. Not blocking — the work per tx is already cheap — but it's a free consistency win with the dormancy path.

/// occupancy.
pub const DEFAULT_MEMPOOL_LIFETIME: Duration = Duration::from_secs(3 * 60 * 60);

/// Default maximum gap allowed between a sender's top pending nonce and
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.

Doc-vs-impl mismatch: this comment says "between a sender's top pending nonce and their on-chain nonce", but the evict_dormant implementation (mempool.rs:621) explicitly measures the gap at the lowest pending nonce — which the PR description confirms is intentional ("avoids false positives on long contiguous queues").

Swap "top" → "lowest" here so the constant's docstring matches the implementation. Otherwise a future reader tuning --mempool.max-nonce-gap will reason about the wrong queue end.

# Conflicts:
#	cmd/ethrex/utils.rs
#	crates/blockchain/blockchain.rs
#	crates/blockchain/constants.rs
#	crates/blockchain/mempool.rs
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

Hive — bal-devnet-6 Amsterdam consume-engine tests — 32 functions / 54 cases

Same root cause as the blockchain-runner skip list (see EF Tests —
Blockchain
below): snobal-devnet-6 fixtures expect bal-devnet-6 spec
semantics, but our impl runs ahead due to the bal-devnet-7-prep
set_delegation SELFDESTRUCT-style refund subtraction. These fixtures
are routed through hive's eels/consume-engine simulator and produce
the same failures. Excluded via KNOWN_EXCLUDED_TESTS (substring
match on test_<name>[fork_Amsterdam, anchoring to the Amsterdam fork
so legacy Prague/Osaka variants still run).

Affected EELS test functions (32)
  • test_auth_refund_block_gas_accounting
  • test_auth_refund_bypasses_one_fifth_cap
  • test_auth_state_gas_scales_with_cpsb
  • test_auth_with_calldata_and_access_list
  • test_auth_with_multiple_sstores
  • test_authorization_exact_state_gas_boundary
  • test_authorization_to_precompile_address
  • test_authorization_with_sstore
  • test_bal_7702_delegation_clear
  • test_bal_7702_delegation_create
  • test_bal_7702_delegation_update
  • test_bal_7702_double_auth_reset
  • test_bal_7702_double_auth_swap
  • test_bal_7702_null_address_delegation_no_code_change
  • test_bal_all_transaction_types
  • test_bal_selfdestruct_to_7702_delegation
  • test_bal_withdrawal_to_7702_delegation
  • test_duplicate_signer_authorizations
  • test_existing_account_auth_header_gas_used_uses_worst_case
  • test_existing_account_refund
  • test_existing_account_refund_enables_sstore
  • test_existing_auth_with_reverted_execution_preserves_intrinsic
  • test_many_authorizations_state_gas
  • test_mixed_auths_header_gas_used_uses_worst_case
  • test_mixed_new_and_existing_auths
  • test_mixed_valid_and_invalid_auths
  • test_multi_tx_block_auth_refund_and_sstore
  • test_multiple_refund_types_in_one_tx
  • test_simple_gas_accounting
  • test_sstore_state_gas_all_tx_types
  • test_transfer_with_all_tx_types
  • test_varying_calldata_costs

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-6+ semantics (and bal-devnet-7-prep
set_delegation re-application) that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

EF Tests — Blockchain bal-devnet-6 (Amsterdam fork) — 74 tests

snobal-devnet-6 fixtures expect bal-devnet-6 spec semantics, but our impl
runs ahead due to the bal-devnet-7-prep set_delegation SELFDESTRUCT-style
refund subtraction. Skipped in
tooling/ef_tests/blockchain/tests/all.rs::SKIPPED_BASE, anchored on
[fork_Amsterdam so legacy Prague / Osaka variants still run.

Bucket breakdown (74 total) and resolution path
EIP Bucket Count
EIP-7702 set_code_txs 24
EIP-7702 set_code_txs_2 15
EIP-7702 gas 1
EIP-8037 state_gas_set_code 17
EIP-8037 state_gas_pricing 1
EIP-8037 state_gas_sstore 1
EIP-7928 block_access_lists_eip7702 8
EIP-7928 block_access_lists 1
EIP-7778 gas_accounting 3
EIP-7708 transfer_logs 1
EIP-7976 refunds 1
EIP-1344 chainid (Amsterdam fork-transition fixture) 1
Total 74

Re-enable once we either:

  • (a) bump fixtures to a snobal-devnet-7 release that locks in the new
    accounting; or
  • (b) revert the bal-devnet-7-prep subtraction for bal-devnet-6
    compatibility.
Full test list (74)

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/

  • delegation_clearing
  • delegation_clearing_and_set
  • delegation_clearing_failing_tx
  • delegation_clearing_tx_to
  • eoa_tx_after_set_code
  • ext_code_on_chain_delegating_set_code
  • ext_code_on_self_delegating_set_code
  • ext_code_on_self_set_code
  • ext_code_on_set_code
  • many_delegations
  • nonce_overflow_after_first_authorization
  • nonce_validity
  • reset_code
  • self_code_on_set_code
  • self_sponsored_set_code
  • set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce
  • set_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce_self_sponsored
  • set_code_to_log
  • set_code_to_non_empty_storage_non_zero_nonce
  • set_code_to_self_destruct
  • set_code_to_self_destructing_account_deployed_in_same_tx
  • set_code_to_sstore
  • set_code_to_sstore_then_sload
  • set_code_to_system_contract

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/

  • call_pointer_to_created_from_create_after_oog_call_again
  • call_to_precompile_in_pointer_context
  • contract_storage_to_pointer_with_storage
  • delegation_replacement_call_previous_contract
  • double_auth
  • pointer_measurements
  • pointer_normal
  • pointer_reentry
  • pointer_resets_an_empty_code_account_with_storage
  • pointer_reverts
  • pointer_to_pointer
  • pointer_to_precompile
  • pointer_to_static
  • pointer_to_static_reentry
  • static_to_pointer

EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/gas/

  • account_warming

EIP-8037 — for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/

  • auth_refund_block_gas_accounting
  • auth_refund_bypasses_one_fifth_cap
  • auth_with_calldata_and_access_list
  • auth_with_multiple_sstores
  • authorization_exact_state_gas_boundary
  • authorization_to_precompile_address
  • authorization_with_sstore
  • duplicate_signer_authorizations
  • existing_account_auth_header_gas_used_uses_worst_case
  • existing_account_refund
  • existing_account_refund_enables_sstore
  • existing_auth_with_reverted_execution_preserves_intrinsic
  • many_authorizations_state_gas
  • mixed_auths_header_gas_used_uses_worst_case
  • mixed_new_and_existing_auths
  • mixed_valid_and_invalid_auths
  • multi_tx_block_auth_refund_and_sstore

EIP-8037 — state_gas_pricing/

  • auth_state_gas_scales_with_cpsb

EIP-8037 — state_gas_sstore/

  • sstore_state_gas_all_tx_types

EIP-7928 — for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/

  • bal_7702_delegation_clear
  • bal_7702_delegation_create
  • bal_7702_delegation_update
  • bal_7702_double_auth_reset
  • bal_7702_double_auth_swap
  • bal_7702_null_address_delegation_no_code_change
  • bal_selfdestruct_to_7702_delegation
  • bal_withdrawal_to_7702_delegation

EIP-7928 — block_access_lists/

  • bal_all_transaction_types

EIP-7778 — for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/

  • multiple_refund_types_in_one_tx
  • simple_gas_accounting
  • varying_calldata_costs

EIP-7708 — for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/

  • transfer_with_all_tx_types

EIP-7976 — for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/refunds/

  • gas_refunds_from_data_floor

EIP-1344 — for_amsterdam/istanbul/eip1344_chainid/chainid/

  • chainid (Amsterdam fork-transition fixture)

ilitteri added 2 commits May 13, 2026 18:28
Rust 1.91's clippy::manual_is_multiple_of lint rejects `secs % N == 0` when a
built-in method exists. Swaps the two checks in duration_default_str.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants