Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Tevm Architecture Overview

Tevm is an Ethereum execution node embedded in JavaScript. The batteries-included MemoryClient presents a viem-compatible API, while a TevmNode owns the EVM, state, blockchain, transaction pool, receipts, mining policy, and optional fork connection underneath it.

The Runtime at a Glance

Application, viem action, or EIP-1193 consumer

                 MemoryClient
        (viem public/wallet/test + tevm* actions)

                 Tevm transport

                    TevmNode
       ┌───────────────┼────────────────┐
       │               │                │
    TxPool         Mining policy    ReceiptsManager
       │               │                ▲
       └──────────► VM / EVM ───────────┘

             StateManager + Blockchain

            optional fork transport proxy

                 upstream JSON-RPC

createMemoryClient() constructs this stack synchronously. The underlying node initializes lazily; client.tevmReady() lets callers eagerly wait for initialization, which is useful for forks.

MemoryClient and the Transport Boundary

MemoryClient is a viem client configured with a Tevm transport. It includes:

  • viem public actions such as getBlock, getBalance, getLogs, and getTransactionReceipt;
  • viem wallet and Anvil-compatible test actions such as sendTransaction, setBalance, mine, snapshot, and revert;
  • Tevm actions such as tevmCall, tevmSetAccount, tevmDumpState, and tevmMine.

Each action becomes an EIP-1193-style request or a direct Tevm action at the transport boundary. The transport routes it to the same TevmNode, so viem, Ethers, raw request, and Tevm actions observe one chain.

import { createMemoryClient } from 'tevm'
 
const client = createMemoryClient()
 
const blockNumber = await client.getBlockNumber()
const chainId = await client.request({ method: 'eth_chainId' })
 
console.log(blockNumber, chainId)

The request shape is { method, params? }. JSON-RPC envelope fields such as id and jsonrpc belong to HTTP serialization, not EIP-1193 calls.

StateManager and the EVM

The StateManager owns Ethereum account state: nonce, balance, bytecode, and storage. It supports checkpoints, commits, reverts, state dumps, and fork-backed reads.

The EVM executes bytecode against that state. A call can:

  1. checkpoint the current state;
  2. execute opcodes and precompiles;
  3. collect logs, created addresses, gas data, and an optional trace;
  4. commit when execution becomes canonical or revert when it is only a simulation.

tevmCall is a simulation by default. With addToMempool: true, Tevm also creates a pending transaction. State only becomes canonical when that transaction is mined.

TxPool, Mining, Blocks, and Receipts

The txpool holds valid pending transactions and orders executable transactions by sender nonce and fees. The configured mining policy decides when a miner drains it:

  • { type: 'manual' } keeps transactions pending until a mine action.
  • { type: 'auto' } mines submitted transactions immediately.
  • { type: 'interval', blockTime: seconds } mines on a timer.

During mining, Tevm selects executable transactions, runs each through the VM, commits the resulting state root, builds a canonical block, removes included transactions from the pool, and indexes receipts and logs. This ordering is why a receipt is unavailable before mining and queryable afterward.

import { createMemoryClient, PREFUNDED_ACCOUNTS } from 'tevm'
 
const client = createMemoryClient({
  miningConfig: { type: 'manual' },
})
 
const { txHash } = await client.tevmCall({
  from: PREFUNDED_ACCOUNTS[0].address,
  to: '0x1111111111111111111111111111111111111111',
  value: 1n,
  addToMempool: true,
})
 
if (!txHash) throw new Error('transaction was not added to the txpool')
 
const beforeMining = await client.request({ method: 'txpool_status' })
await client.tevmMine({ blockCount: 1 })
const transaction = await client.getTransaction({ hash: txHash })
const receipt = await client.getTransactionReceipt({ hash: txHash })
const block = await client.getBlock({ blockHash: receipt.blockHash })
 
console.log(beforeMining.pending, transaction.blockNumber, receipt.status, block.number)

Tevm exposes two deliberately different mining parameter shapes:

  • Tevm action: client.tevmMine({ blockCount: 2, interval: 1 })
  • viem test action: client.mine({ blocks: 2, interval: 1 })

The receipt manager stores receipts by block and transaction hash and provides the log index used by eth_getLogs and viem's getLogs.

The Forking Proxy

A fork does not download the entire remote chain. It keeps a local overlay and asks the upstream transport for missing data:

  1. the local blockchain resolves the fork block and older remote blocks through the transport;
  2. the state manager fetches missing accounts, code, and storage at the fork block;
  3. fetched values are cached locally;
  4. local writes, pending transactions, and mined blocks live in the overlay and never modify the upstream chain.
import { createMemoryClient, http } from 'tevm'
import { mainnet } from 'tevm/common'
 
const client = createMemoryClient({
  common: mainnet,
  fork: {
    transport: http('https://eth.drpc.org')({}),
    blockTag: 20_000_000n,
  },
})
 
await client.tevmReady()
 
const remoteBalance = await client.getBalance({
  address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  blockNumber: 20_000_000n,
})
 
await client.setBalance({
  address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  value: remoteBalance + 1n,
})

Setting common avoids a chain-ID discovery request. Pin blockTag in repeatable tests; omit it when the latest upstream state is intentional.

Low-Level Access

TevmNode is the ownership boundary for the runtime. Advanced integrations can inspect its components directly:

import { createTevmNode } from 'tevm'
 
const node = createTevmNode()
await node.ready()
 
const vm = await node.getVm()
const txPool = await node.getTxPool()
const receipts = await node.getReceiptsManager()
const head = await vm.blockchain.getCanonicalHeadBlock()
 
console.log(head.header.number, txPool.pool.size, receipts.GET_LOGS_LIMIT)

Prefer MemoryClient for application code. Low-level components use Ethereum-native byte arrays, address objects, typed transactions, and explicit checkpoint rules; they are intended for custom execution tools and Tevm contributors.

Execution and Data Flow

Read

getBalance → viem action → Tevm transport → node handler → state manager → local cache or fork proxy.

Simulate

tevmCall → checkpoint → EVM execution → result and optional trace → revert checkpoint.

Submit and Mine

sendTransaction or tevmCall({ addToMempool: true }) → txpool → mining policy → VM execution → state commit → block → receipt and log indexes.

Query History

getBlock, getTransactionReceipt, or getLogs → local blockchain and receipt indexes, falling back to the upstream transport only for fork history that is not local.

Where to Go Next