Skip to content
← Inside Revm — Reading the EVM Engine
Inside Revm
Lesson 15 of 17·CONTENT24 min50 XP
Course
Inside Revm — Reading the EVM Engine
Lesson role
CONTENT
Sequence
15 / 17

Lesson 14 — Parallel execution — beyond the serial interpreter loop

Question

Can revm execute multiple transactions in parallel? Not by itself — revm executes one transaction at a time; parallel EVMs are built around it, typically with Block-STM optimistic concurrency layered on its Database trait. Solana does it differently (pre-declared account locks). Each has trade-offs.

  • Direct answer (revm 36 / reth eb4c15e): revm has no Block-STM scheduler and there is no revm_parallel crate. Reth executes a block's transactions sequentially, but runs them in parallel beforehand only to prewarm caches (crates/engine/tree/src/tree/payload_processor/prewarm.rs), computes the state root in parallel (crates/trie/parallel), and already carries EIP-7928 Block-Level Access List plumbing (payload_processor/bal.rs; revm-state 10's bal module).
  • Where a real Block-STM-on-revm lives: RISE's risechain/pevm (work in progress, not production ready per its README).

Principle (minimum model)

  • Serial baseline. Revm's default: execute tx-by-tx in order. Easy to reason about; cannot use multiple cores.
  • block-stm pattern. Execute multiple txs optimistically; track read/write sets; if a conflict arises, re-execute the conflicting tx. Introduced by Aptos for Move.
  • Conflict detection. Each tx has a read_set + write_set. If tx_a writes a slot tx_b read, tx_b must re-execute with the new value.
  • Re-execution. Track per-tx state version; on conflict, roll back tx_b's state changes and retry.
  • block-stm around revm. revm itself ships no parallel executor. Projects such as RISE's pevm implement revm's Database trait with a read-set-tracking wrapper (VmDb in crates/pevm/src/vm.rs) plus a multi-version memory and scheduler.
  • Solana's deterministic parallelism. Solana requires tx to pre-declare its account locks. No conflict possible at runtime; pure parallelism. Trade-off: harder DX.
  • Block-stm vs deterministic. Block-stm = easier DX (no lock declarations) but extra runtime cost on conflict. Deterministic = harder DX but predictable cost.
  • Production examples. Aptos uses block-stm. Solana uses deterministic. In EVM land, Polygon PoS (Bor, core/blockstm) and Sei (sei-cosmos/tasks/scheduler.go) ship Go implementations of the optimistic approach; Monad (an L1, own C++ client) uses its own optimistic parallel design.
  • Speedup. Workload-dependent. pevm's README reports its Block-STM version at ~2× sequential on average for Ethereum mainnet blocks and ~22× on a gigagas block of independent Uniswap swaps; heavy contention pushes it back toward serial.

Worked example + steps

Parallel execution — beyond the serial interpreter loop

Every lesson in this course so far has treated execution as serial: one transaction at a time, through the interpreter, then commit, then next. This is how Reth and revm ship today and how mainnet has worked for a decade. It is also the single biggest performance ceiling on the EVM, and several chains work around it: Aptos introduced Block-STM (for Move), Polygon Bor and Sei ship parallel EVM execution (both in Go), Monad runs its own optimistic parallel design in C++, and MegaETH's stateless-validator README describes its sequencer as a parallel, JIT-compiled executor. Reth itself keeps block execution serial but parallelises around it (prewarming, parallel state root, EIP-7928 BAL support). This lesson teaches you the model.

📌 Honest framing. Parallel EVM is a moving target. The specific implementations differ across Polygon, Sei, Monad, RISE (pevm), and Ethereum's own BAL work. What stays constant — and what this lesson teaches — is the block-stm pattern (optimistic concurrency control with read/write conflict detection), plus how it maps onto the Database trait you've already walked.

1. Why serial execution is the ceiling

A block contains N transactions. The interpreter executes them one at a time because each tx might read state another tx wrote. If tx 5 reads slot X and tx 3 wrote slot X, you have to run 3 before 5.

In practice, many transactions in a block are independent — DEX swaps on different pools, transfers to different accounts, oracle updates that nobody else reads in that block — yet they are still executed serially. The exact conflict rate varies block to block (one EVM-specific catch: every tx pays gas to the same beneficiary account, so naive tracking makes all txs conflict; pevm works around this by updating the beneficiary balance lazily). That independent share is the prize parallel EVM goes for.

2. The block-stm pattern (the dominant approach)

Block-STM (Gelashvili et al., 2022) was introduced by Aptos for Move. EVM adaptations include Polygon's Bor and Sei (both in Go) and RISE's pevm (Rust, on revm); Monad uses its own optimistic parallel design. The core idea:

1. Speculatively execute all N transactions in parallel (multiple workers).
2. While each tx runs, record what it reads (read set) and what it writes (write set).
3. After execution, validate in block order:
   - Re-check every location tx i read. If a lower-indexed tx j < i has since written
     a different version of it, i used stale data → abort and re-execute i.
   - (Block-STM marks the aborted tx's writes as ESTIMATE so later txs wait on them
     instead of reading stale values.)
4. Repeat validation + re-execution until all N transactions are valid in serial order.

This is optimistic concurrency control: assume conflicts are rare, run in parallel, fix the ones that actually conflict. Non-conflicting txs run once in parallel; conflicting ones are re-executed and effectively run as a serial chain. Net speedup depends entirely on the workload (see the pevm numbers above).

Example: a 100-tx block with 90 independent txs and 10 sandwich-arb txs on the same pool. The 90 independent ones execute in parallel and validate on the first pass. The 10 sandwich-arb ones all read and write the same pool state — they get speculatively executed in parallel, then 9 of them fail validation because they read stale pool state and must re-execute, each after the one before it. Total wall-clock: ~1 parallel pass + a 10-tx serial chain, instead of 100 serial steps. The conflicts cost you, but only proportionally.

🛣️ The road not taken (Solana): Solana solves the same throughput problem with the opposite bet. Every Solana transaction declares its read / write account set upfront, encoded in the tx message itself. The runtime (Banking Stage scheduler) then groups non-overlapping tx and runs them in parallel with zero speculation and zero retry — you never execute a tx whose dependencies are wrong, because the dependencies are known statically before execution. EVM's block-stm takes the opposite bet: keep the developer model permissive (no declared lock sets, contracts can touch arbitrary storage), and pay for it at the runtime layer with conflict tracking + re-execution. Two valid SE answers to the same problem; the choice between them is a tradeoff between developer ergonomics and runtime cost, not "right vs wrong."

3. Where this lives in the Database trait

Look back at the Database trait you walked in the Database chain:

// revm-database-interface 10.0.0 (revm 36), src/lib.rs
pub trait Database {
    type Error: DBErrorMarker;
    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>;
    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error>;
    fn storage(&mut self, address: Address, index: StorageKey)
        -> Result<StorageValue, Self::Error>;
    // default method: forwards to self.storage(address, storage_key)
    fn storage_by_account_id(&mut self, address: Address, account_id: usize, storage_key: StorageKey)
        -> Result<StorageValue, Self::Error> { /* ... */ }
    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error>;
}

In serial execution, the executor just calls these and gets values. In parallel execution, every basic / storage call is also a read-set entry, and every state mutation is a write-set entry. The block-stm scheduler wraps the standard Database with a tracking layer:

pub struct TrackedDatabase<D: Database> {
    inner: D,
    read_set: HashSet<(Address, StorageKey)>,         // populated by storage()
    read_accounts: HashSet<Address>,                  // populated by basic()
    // write_set lives in the executor's journaling layer
}

impl<D: Database> Database for TrackedDatabase<D> {
    type Error = D::Error;
    fn storage(&mut self, addr: Address, key: StorageKey) -> Result<StorageValue, Self::Error> {
        self.read_set.insert((addr, key));            // ← record the read
        self.inner.storage(addr, key)                 // ← delegate to real source
    }
    // ... basic(), code_by_hash(), block_hash() similarly track
}

This is why the Database trait shape pays off so heavily here. The lesson that walked it didn't say "this is also the seam where parallel execution plugs in," but it is. Any code that depends on Database works in parallel as long as the wrapper tracks reads; no executor or interpreter rewrite required.

(This wrapper compiles against revm 36 once basic / code_by_hash / block_hash are filled in the same way; storage_by_account_id can keep its default.) Real block-stm needs more than a read set — reads must be served from a multi-version memory so a tx sees the writes of lower-indexed txs — but the seam is the same.

🔍 Find in repo. bluealloy/revm itself has no Block-STM code. Open risechain/pevm instead: crates/pevm/src/vm.rs has struct VmDb with a read_set and impl<S: Storage> Database for VmDb<'_, S> — exactly this tracking-wrapper pattern, backed by mv_memory.rs and scheduler.rs. Read 50 lines of it. (pevm's main branch is on a newer revm and its README says it is moving to an inline EVM.)

4. The hard parts

Block-stm sounds easy in the abstract; the production complexity lives in:

ProblemWhat makes it hard
Write set propagationtx i's speculative writes must be visible to higher-indexed txs, and retracted if i is re-executed and writes something different. Requires a multi-version memory (think MVCC).
Re-execution orderingAfter invalidation, re-execute in dependency order. A naive "re-run failed ones" loop can thrash if you re-execute in the wrong order.
Estimating read/write sets cheaplyA tx's read set is only known after it executes. Pre-execution prediction (via static analysis or hints) is a research area.
Hot contractsAll sandwich txs on the same pool form an unavoidable serialization. block-stm doesn't help, and wasted re-executions can make it slower than plain serial. The fix is at the application layer (multiple pools, AMM redesign).
Gas accountingGas used must equal the serial result exactly; re-executions cost CPU, not gas. So the protocol doesn't charge anyone for contention, and shared accounts every tx touches (the fee beneficiary) need special handling to avoid false conflicts.
DeterminismMultiple workers executing in parallel must produce the exact same state root every time. Race conditions in the tracker = consensus bug = chain split.

The last row is the consensus-critical one. Parallel EVM's bug surface is huge compared to serial; this is why production rollouts have been cautious.

5. What different chains actually do

  • Polygon PoS (Bor) and Sei: Go implementations of optimistic parallel execution (0xPolygon/bor core/blockstm; Sei's sei-cosmos/tasks/scheduler.go + store/multiversion). Sei's EVM is a go-ethereum fork, not revm. pevm's README notes these Go ports have shown limited speedup.
  • Monad: an L1 with parallel execution from day one. Custom C++ client (not revm), but the read/write-set tracking model is similar.
  • RISE pevm: Block-STM in Rust on revm (§3), work in progress.
  • Reth: block execution is serial. Parallelism sits around it: parallel prewarming of caches, parallel state root (crates/trie/parallel), and EIP-7928 BAL handling.
  • Mainnet Ethereum: serial today. EIP-7928 Block-Level Access Lists — listed as Scheduled for Inclusion in Glamsterdam (EIP-7773) — makes blocks carry every accessed account/slot plus post-tx values, explicitly to enable parallel disk reads, parallel tx validation and parallel state root computation.

Mainnet's conservatism is about consensus risk and backward compatibility. A bug in parallel execution affects every node simultaneously and could split the chain — and mainnet secures hundreds of billions of dollars. New chains can ship aggressive parallel execution because their TVL starts low and they iterate fast. The same code, same risk profile, different cost of failure. That is also why mainnet's route (BAL) moves the dependency information into the block itself instead of trusting each client's speculative scheduler.

6. What this means for your career

Parallel EVM is one of the most active research and engineering frontiers in 2026. Teams working on it want engineers who can read a revm tracking-wrapper, understand block-stm, and contribute to either the scheduler or the conflict-detection layer. None of this work is possible without the Revm and Reth source-reading foundation you have now.

The path forward, if you want to make this your specialization:

  1. Read pevm's VmDb / mv_memory.rs / scheduler.rs for block-stm on revm (open source)
  2. Read Aptos's original block-stm paper for the theoretical foundation
  3. Build a toy parallel executor against revm using the Database tracking pattern from §3 (a weekend project for someone with this curriculum's foundations)
  4. Follow Reth's EIP-7928 BAL and parallel state-root work, or contribute to pevm

Drill

  1. Read a real block-stm implementation. Open risechain/pevm → crates/pevm/src/vm.rs. Find struct VmDb and its impl Database. Map its methods to the Database trait you walked. 30 minutes.
  2. Identify a parallelization-friendly block. Open Etherscan, pick a recent mainnet block. Skim the tx list. Count how many touch unique contracts (independent) vs how many touch the top 3 contracts (likely conflicting). What's your estimate of the parallel speedup ceiling for that block? 30 minutes.
  3. Read the original block-stm paper. arxiv:2203.06871. Skim sections 1, 3, 4. The Aptos model is for Move, but section 3's optimistic execution + validation diagram is the universal pattern. 45 minutes.
  4. Sketch your own tracking wrapper. In your fork of revm, write a TrackedDb that implements Database and prints every read. Run a small bytecode through it (like PUSH1 0x42 PUSH1 0x00 SSTORE STOP from earlier drills). See exactly which reads happen. This is the data block-stm uses to decide what conflicts. 1.5 hours.

Why this matters before the next lesson

The next lesson covers JIT/AOT compilation via revmc. Parallel and JIT are the two "beyond interpretation" frontiers — they attack the same throughput ceiling from different angles (parallel: more cores, JIT: faster cores). High-performance EVM teams can combine both. After both lessons you have the vocabulary to read either path's source and understand what trade-offs each team picked.

📺 Further reading

Summary (3 lines)

  • Parallel execution = block-stm optimistic (track read/write sets; re-execute on conflict) or Solana-style deterministic (pre-declared locks).
  • revm itself is serial; block-stm is built around its Database trait (e.g. RISE pevm: ~2× avg on mainnet blocks per its README). Reth stays serial but prewarms and roots in parallel; mainnet's path is EIP-7928 BAL.
  • Solana = deterministic = harder DX, predictable cost. Aptos introduced block-stm; Polygon / Sei / pevm adapted it to the EVM. Next: JIT/AOT.