v10| TypeScript implementation of the Ethereum EVM. | | ———————————————- |
@Noble crypto)To obtain the latest version, simply require the project using npm:
npm install @ethereumjs/evm
This package provides the core Ethereum Virtual Machine (EVM) implementation which is capable of executing EVM-compatible bytecode. The package has been extracted from the @ethereumjs/vm package along the VM v6 release.
Note: Starting with the Dencun hardfork EIP-4844 related functionality has become an integrated part of the EVM functionality with the activation of the point evaluation precompile. For this precompile to work a separate installation of the KZG library is necessary (we decided not to bundle due to large bundle sizes), see KZG Setup for instructions.
The following is the simplest example for an EVM instantiation with reasonable defaults for state and blockchain information (like blockhashes):
// ./examples/simple.ts
import { createEVM } from '@ethereumjs/evm'
import { hexToBytes } from '@ethereumjs/util'
const main = async () => {
const evm = await createEVM()
const res = await evm.runCode({ code: hexToBytes('0x6001') }) // PUSH1 01 -- simple bytecode to push 1 onto the stack
console.log(res.executionGasUsed) // 3n
}
void main()
If you want the EVM to run against a specific state, you need an @ethereumjs/statemanager. An @ethereumjs/blockchain instance can be passed in to provide access to external interface information like a blockhash:
// ./examples/withBlockchain.ts
import { createBlockchain } from '@ethereumjs/blockchain'
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { createEVM } from '@ethereumjs/evm'
import { MerkleStateManager } from '@ethereumjs/statemanager'
import { bytesToHex, hexToBytes } from '@ethereumjs/util'
import type { PrefixedHexString } from '@ethereumjs/util'
const main = async () => {
const common = new Common({ chain: Mainnet, hardfork: Hardfork.Shanghai })
const stateManager = new MerkleStateManager()
const blockchain = await createBlockchain()
const evm = await createEVM({
common,
stateManager,
blockchain,
})
const STOP = '00'
const ADD = '01'
const PUSH1 = '60'
// Note that numbers added are hex values, so '20' would be '32' as decimal e.g.
const code = [PUSH1, '03', PUSH1, '05', ADD, STOP]
evm.events.on('step', function (data) {
// Note that data.stack is not immutable, i.e. it is a reference to the vm's internal stack object
console.log(`Opcode: ${data.opcode.name}\tStack: ${data.stack}`)
})
const results = await evm.runCode({
code: hexToBytes(('0x' + code.join('')) as PrefixedHexString),
gasLimit: BigInt(0xffff),
})
console.log(`Returned: ${bytesToHex(results.returnValue)}`)
console.log(`gasUsed: ${results.executionGasUsed.toString()}`)
}
void main()
Additionally, this example shows how to use events to listen to the inner workings and procedural updates
(step event) of the EVM.
This library by default uses JavaScript implementations for the basic standard crypto primitives like hashing or signature verification (for included txs). See @ethereumjs/common README for instructions on how to replace them with, e.g., a more performant WASM implementation by using a shared common instance.
The EVM records contract events as logs: a compact tuple reused across @ethereumjs/evm, @ethereumjs/vm, and (with field renaming) JSON-RPC.
type Log = [address: Uint8Array, topics: Uint8Array[], data: Uint8Array]
// emitter indexed fields unindexed payload
| Source | When |
|---|---|
LOG0–LOG4 opcodes |
Contract bytecode writes to memory, then logs topics + data |
| EIP-7708 (Amsterdam) | Synthetic Transfer / Burn logs on native ETH movement via runCall() |
runCode() / runCall()Both methods return an ExecResult with an optional logs array:
const result = await evm.runCode({ code, to: contractAddress, gasLimit: 100_000n })
for (const log of result.logs ?? []) {
const [address, topics, data] = log
// bytesToHex(address), topics.map(bytesToHex), bytesToHex(data)
}
See examples/emitLogs.ts for a minimal LOG1 bytecode snippet.
Notes:
message.to / contract address), not necessarily tx.origin.@ethereumjs/vm — see Receipts and event logs.See the examples folder for different meaningful examples on how to use the EVM package and invoke certain aspects of it, e.g. running a bytecode snippet, listening to events, or to activate an EVM with a certain EIP for experimental purposes. Opcode-focused samples live under examples/opcodes/ (e.g. EIP-8024 DUPN/SWAPN/EXCHANGE on Hardfork.Amsterdam).
Noteworthy examples:
examples/emitLogs.ts: Run LOG1 bytecode and read ExecResult.logs.examples/runCode.ts: Trace opcode execution with the step event.We provide hybrid ESM/CJS builds for all our libraries. With the v10 breaking release round from Spring 2025, all libraries are “pure-JS” by default and we have eliminated all hard-wired WASM code. Additionally we have substantially lowered the bundle sizes, reduced the number of dependencies, and cut out all usages of Node.js-specific primitives (like the Node.js event emitter).
It is easily possible to run a browser build of one of the EthereumJS libraries within a modern browser using the provided ESM build. For a setup example see ./examples/browser.html.
For documentation on EVM instantiation, exposed API and emitted events see generated API docs.
With the breaking releases from Summer 2023 we have started to ship our libraries with both CommonJS (cjs folder) and ESM builds (esm folder), see package.json for the detailed setup.
If you use an ES6-style import in your code files, the ESM build will be used:
import { EthereumJSClass } from '@ethereumjs/[PACKAGE_NAME]'
If you use Node.js specific require, the CJS build will be used:
const { EthereumJSClass } = require('@ethereumjs/[PACKAGE_NAME]')
Using ESM will give you additional advantages over CJS beyond browser usage like static code analysis / Tree Shaking which CJS can not provide.
This package contains the inner Ethereum Virtual Machine core functionality which was included in the @ethereumjs/vm package up to v5 and has been extracted along the v6 release.
This will make it easier to customize the inner EVM, which can now be passed as an optional argument to the outer VM instance.
For the EVM to properly work it needs access to a respective execution environment (to e.g. request on information like block hashes) as well as the connection to an outer account and contract state.
With the v2 release EVM, VM and StateManager have been substantially reworked in this regard, see PR #2649 and PR #2702 for further deepening context.
The interfaces (in a non-TypeScript sense) between these packages have been simplified and the EEI package has been completely removed. Most of the EEI related logic is now either handled internally or more generic functionality being taken over by the @ethereumjs/statemanager package.
This allows for both a standalone EVM instantiation with reasonable defaults as well as for a simplified EVM -> VM passing if a customized EVM is needed.
The package is organized around the bytecode-execution core:
evm.ts — the EVM class: message dispatch (runCall, runCode), _executeCall / _executeCreate, journal checkpointing, precompile dispatch and event emission.interpreter.ts — the Interpreter: the fetch-decode-execute loop (run), per-opcode gas charging and handler dispatch, jump-destination analysis and the step event.opcodes/ — the opcode table and handlers: codes.ts (table assembly per hardfork), functions.ts (opcode implementations), gas.ts (dynamic gas), plus per-EIP opcode modules (EIP1283.ts, EIP2200.ts, EIP2929.ts, EIP7928.ts, EIP8024.ts).precompiles/ — precompiled contracts, one file per address (01-ecrecover.ts … 100-p256verify.ts), with index.ts mapping address → implementation and bls12_381/ / bn254/ backends.eof/ — EOF (EIP-3540 et al.) container parsing, verification and setup.journal.ts — state journaling: checkpoint / commit / revert, touched/created-account tracking, forwarding to the StateManagerInterface.message.ts — the Message value object passed through call/create execution.memory.ts, stack.ts, transientStorage.ts — per-frame execution state.binaryTreeAccessWitness.ts — EIP-7864 access-witness generation.params.ts — paramsEVM, the EIP-indexed gas/parameter dictionary merged into Common.types.ts / constructors.ts — public types/option objects and the createEVM factory.The EVM is designed to be customized through createEVM / EVMOpts (src/types.ts):
customOpcodes?: CustomOpcode[] (src/types.ts:343): add, override or remove opcodes by number with your own handler and gas function.customPrecompiles?: CustomPrecompile[] (src/types.ts:351): add or override precompiled contracts at a given address.stateManager?: StateManagerInterface (src/types.ts:407): any implementation of the interface from @ethereumjs/common. If omitted, a SimpleStateManager is created by default (src/constructors.ts).Common — common?: Common (src/types.ts): drives hardfork/EIP gating and parameter resolution.params?: ParamsDict: override the values in paramsEVM (e.g. tweak a gas cost) without forking the package.bls? / bn254? (src/types.ts:370, :393): plug in native BLS12-381 / BN254 implementations for the relevant precompiles.The EthereumJS EVM implements all hardforks from Frontier (chainstart) up to the latest active mainnet hardfork.
Currently the following hardfork rules are supported:
chainstart (a.k.a. Frontier)homesteadtangerineWhistlespuriousDragonbyzantiumconstantinoplepetersburgistanbulmuirGlacier (only mainnet)berlin (v5.2.0+)london (v5.4.0+)arrowGlacier (only mainnet) (v5.6.0+)mergeshanghai (v2.0.0+)cancun (v2.0.0+)prague (v10+)osaka (v10.1.0+)amsterdam (IN DEVELOPMENT)Default: prague (taken from Common.DEFAULT_HARDFORK)
A specific hardfork EVM ruleset can be activated by passing in the hardfork
along the Common instance to the outer @ethereumjs/vm instance.
If you want to activate an EIP not currently active on the hardfork your common instance is set to, it is possible to individually activate EIP support in the EVM by specifying the desired EIPs using the eips property in your CommonOpts setup, e.g.:
// ./examples/eips.ts
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { createEVM } from '@ethereumjs/evm'
const main = async () => {
const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, eips: [7702] })
const evm = await createEVM({ common })
console.log(
`EIP 7702 is active in isolation on top of the Cancun HF - ${evm.common.isActivatedEIP(7702)}`,
)
}
void main()
Currently supported EIPs (sorted by EIP number):
Annotations:
(Prague)) indicate default activation on that fork(Amsterdam, experimental) and (experimental) mark unstable specs; behaviour may change on patch releases@ethereumjs/vmWhen EIP-7928 is active, BAL data accumulates on evm.blockLevelAccessList during execution. For typical usage see @ethereumjs/vm.
See the canonical Amsterdam overview in @ethereumjs/vm for release ↔ spec tracking.
EIP-8024 adds three backward-compatible stack manipulation opcodes, each with a single-byte immediate operand:
| Opcode | Byte | Effect |
|---|---|---|
DUPN |
0xe6 |
Duplicate the stack item at depth n (immediate encodes n) |
SWAPN |
0xe7 |
Swap the top item with the item at depth n |
EXCHANGE |
0xe8 |
Exchange items at depths x and y (pair immediate) |
The opcodes are active on Hardfork.Amsterdam and validated at decode time (invalid immediates trap). Gas costs: dupnGas, swapnGas, exchangeGas (default 3 each). They are supported in legacy bytecode and in EOF containers.
// ./examples/opcodes/0xe6-e8-eip8024-stack-opcodes.ts
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { type EVM, createEVM } from '@ethereumjs/evm'
// EIP-8024 stack opcodes (0xe6 DUPN, 0xe7 SWAPN, 0xe8 EXCHANGE)
// https://eips.ethereum.org/EIPS/eip-8024
//
// Active on Hardfork.Amsterdam. These extend DUP/SWAP to deep stack depths
// (n = 17..235) using a single-byte immediate per opcode.
//
// Run from packages/evm:
// npx tsx examples/opcodes/0xe6-e8-eip8024-stack-opcodes.ts
const DUPN = 0xe6
const SWAPN = 0xe7
const EXCHANGE = 0xe8
const STOP = 0x00
const PUSH1 = 0x60
/** Encode DUPN / SWAPN immediate for one-based depth n (17..235). Spec: n = (x + 145) mod 256 */
const encodeSingleImmediate = (n: number): number => {
if (n < 17 || n > 235) {
throw new Error(`DUPN/SWAPN depth must be 17..235, got ${n}`)
}
return (n - 145) & 0xff
}
/** Push consecutive values 1..count (bottom = 1, top = count) */
const buildPushSequence = (count: number): Uint8Array => {
const bytes = new Uint8Array(count * 2)
for (let i = 0; i < count; i++) {
bytes[i * 2] = PUSH1
bytes[i * 2 + 1] = i + 1
}
return bytes
}
const concatBytes = (...parts: Uint8Array[]): Uint8Array => {
const total = parts.reduce((sum, part) => sum + part.length, 0)
const out = new Uint8Array(total)
let offset = 0
for (const part of parts) {
out.set(part, offset)
offset += part.length
}
return out
}
const stackTop = (evmResult: Awaited<ReturnType<EVM['runCode']>>, n: number): number[] => {
const stack = evmResult.runState?.stack
if (!stack) {
throw new Error('Missing runState stack in result')
}
return stack.peek(n).map((word) => Number(word)).reverse()
}
const runCase = async (evm: EVM, label: string, code: Uint8Array) => {
const res = await evm.runCode({ code, gasLimit: 1_000_000n })
console.log('--------------------------------')
console.log(label)
console.log(`stack (top ${Math.min(5, stackTop(res, 5).length)} shown, top last):`, stackTop(res, 5))
console.log(`gas used: ${res.executionGasUsed}`)
}
const main = async () => {
const common = new Common({
chain: Mainnet,
hardfork: Hardfork.Amsterdam,
})
const evm = await createEVM({ common })
// Stack [1..18], top = 18. DUPN depth 17 duplicates the item at 17 (value 2) onto the top.
await runCase(
evm,
'DUPN (0xe6): duplicate stack item at depth 17',
concatBytes(
buildPushSequence(18),
Uint8Array.from([DUPN, encodeSingleImmediate(17), STOP]),
),
)
// Stack [1..18], top = 18. SWAPN depth 17 swaps top with the item at depth 17 (value 2).
await runCase(
evm,
'SWAPN (0xe7): swap top with item at depth 17',
concatBytes(
buildPushSequence(18),
Uint8Array.from([SWAPN, encodeSingleImmediate(17), STOP]),
),
)
// Stack [1..20], top = 20. EXCHANGE immediate 0x8e swaps the 1st and 2nd slots below the top (18 <-> 19).
await runCase(
evm,
'EXCHANGE (0xe8): swap 1st and 2nd slots below top (immediate 0x8e)',
concatBytes(buildPushSequence(20), Uint8Array.from([EXCHANGE, 0x8e, STOP])),
)
console.log('--------------------------------')
}
void main().catch((err) => {
console.error(err)
process.exitCode = 1
})
Run the full walkthrough (DUPN, SWAPN, and EXCHANGE) from packages/evm:
npx tsx examples/opcodes/0xe6-e8-eip8024-stack-opcodes.ts
See also CLZ (EIP-7939) for another Amsterdam/Osaka-era opcode example pattern.
See the canonical Amsterdam overview in @ethereumjs/vm for release ↔ spec tracking.
EIP-7954 raises the EVM size limits when active on Hardfork.Amsterdam:
| Parameter | Pre-7954 | Post-7954 |
|---|---|---|
maxCodeSize |
24 KiB (24576) | 32 KiB (32768) |
maxInitCodeSize |
48 KiB (49152) | 64 KiB (65536) |
These are Common parameters (common.param('maxCodeSize')) — no API changes beyond using the Amsterdam hardfork.
See the canonical Amsterdam overview in @ethereumjs/vm for release ↔ spec tracking.
State-gas accounting (EIP-8037) and ETH transfer/burn logs (EIP-7708) are implemented at the VM execution layer. EIP-2780 recipient/value/log extras are intrinsic (getIntrinsicGas() / the calldata floor); new-account state gas is charged at the top frame (pre-state). Inner CREATE/CREATE2 charges that state gas unless the target already has nonce or code, and a collision burns the 63/64 grant without a child frame. A child exceptional halt onto a balance-only target keeps the spilled NEW_ACCOUNT as regular gas and exceptional-halts the creating frame. CREATE new-account OOG is post-target (BAL records the created address); 7702 top-frame delegation OOG records the recipient and not the delegation target. runTx increments the sender nonce before that nested prep checkpoint (runCall({ skipNonceIncrement: true })), so a create-tx NEW_ACCOUNT OOG still bumps nonce and a self-signed 7702 auth compares against the already-bumped nonce. Top-frame NEW_ACCOUNT spill into gas_left is recorded on stateGasSpilled and credited on REVERT. 7702 ACCOUNT_WRITE is taken from leftover regular gas before runCall and counted in receipt gas. Top-frame delegation-target access is warm or cold (sender, coinbase, precompiles, access list). EIP-8038 SSTORE checks max(access_cost, stipend + 1) before the implicit storage read (cold access is 3000, above the 2300 stipend). See @ethereumjs/vm Amsterdam docs for RunTxResult fields, block gas dimensions, and receipt log behaviour.
This library supports the blob transaction type introduced with EIP-4844. EIP-4844 comes with a dedicated opcode BLOBHASH and has added a new point evaluation precompile at address 0x0a.
Note: Usage of the point evaluation precompile needs a manual KZG library installation and global initialization, see KZG Setup for instructions.
This library supports all EVM precompiles up to the Osaka hardfork.
In our examples folder we provide a helper function for simple direct precompile runs in the precompiles folder.
This is an example of a simple precompile run (BLS12_G1ADD precompile):
// ./examples/precompiles/0b-bls12-g1add.ts
import { runPrecompile } from './util.ts'
const main = async () => {
// BLS12_G1ADD precompile (address 0xb)
// Data taken from test/eips/precompiles/bls/add_G1_bls.json
// Input: G1 and G2 points (each 128 bytes = 256 hex characters)
const g1Point =
'0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1'
const g2Point =
'00000000000000000000000000000000112b98340eee2777cc3c14163dea3ec97977ac3dc5c70da32e6e87578f44912e902ccef9efe28d4a78b8999dfbca942600000000000000000000000000000000186b28d92356c4dfec4b5201ad099dbdede3781f8998ddf929b4cd7756192185ca7b8f4ef7088f813270ac3d48868a21'
const data = `0x${g1Point}${g2Point}`
await runPrecompile('BLS12_G1ADD', '0xb', data)
}
void main()
Starting with v10 the EVM supports the BLS precompiles introduced with EIP-2537 in its final version introduced with the Prague hardfork. These precompiles run natively using the @noble/curves library (❤️ to @paulmillr!).
An alternative WASM implementation (using bls-wasm) can be optionally used like this if needed for performance reasons:
import { EVM, MCLBLS } from '@ethereumjs/evm'
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Prague })
await mcl.init(mcl.BLS12_381)
const mclbls = new MCLBLS(mcl)
const evm = await createEVM({ common, bls })
The Osaka hardfork introduces some behavioral changes with EIP-7823 as well as a gas cost increase for the MODEXP precompile with EIP-7883.
You can use the following example as a starting point to compare on the changes between hardforks:
// ./examples/precompiles/05-modexp.ts
import { Hardfork } from '@ethereumjs/common'
import { runPrecompile } from './util.ts'
const main = async () => {
// MODEXP precompile (address 0x05)
// Calculate: 2^3 mod 5 = 8 mod 5 = 3
//
// Input format:
// - First 32 bytes: base length (0x01 = 1 byte)
// - Next 32 bytes: exponent length (0x01 = 1 byte)
// - Next 32 bytes: modulus length (0x01 = 1 byte)
// - Next 1 byte: base value (0x02 = 2)
// - Next 1 byte: exponent value (0x03 = 3)
// - Next 1 byte: modulus value (0x05 = 5)
const baseLen = '0000000000000000000000000000000000000000000000000000000000000001' // 1 byte
const expLen = '0000000000000000000000000000000000000000000000000000000000000001' // 1 byte
const modLen = '0000000000000000000000000000000000000000000000000000000000000001' // 1 byte
const base = '02' // 2
const exponent = '03' // 3
const modulus = '05' // 5
const data = `0x${baseLen}${expLen}${modLen}${base}${exponent}${modulus}`
await runPrecompile('MODEXP', '0x05', data)
await runPrecompile('MODEXP', '0x05', data, Hardfork.Cancun)
}
void main()
The Osaka hardfork introduces a new precompile for secp256r1 curve support with EIP-7951.
The following example code allows you to generate input values for the precompile using Noble Curves v2.0.0 or later.
// No direct examples integration (library version not taken in as a dependency)
import { p256 } from '@noble/curves/nist.js'
import { sha256 } from '@noble/hashes/sha2.js'
import { bigIntToHex, bytesToHex } from '@ethereumjs/util'
// Private/public key
const { secretKey, publicKey } = p256.keygen()
const pointPubKey = p256.Point.fromBytes(publicKey)
const pointX = bigIntToHex(pointPubKey.X)
const pointY = bigIntToHex(pointPubKey.Y)
// Message (hash) / signature
const msg = new TextEncoder().encode('Hello Fusaka!')
const sig = p256.sign(msg, secretKey, { lowS: false, prehash: false })
const msgHash = bytesToHex(sha256(msg))
const sigR = bytesToHex(sig).substring(2, 64 + 2)
const sigS = bytesToHex(sig).substring(64 + 2)
The EVM supports registering custom precompiles at arbitrary addresses. Custom precompiles can add new precompiles, override existing ones, or delete built-in precompiles.
Pass an array of CustomPrecompile entries to the customPrecompiles option when creating the EVM:
// ./examples/precompiles/customPrecompile.ts
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'
import { createEVM } from '@ethereumjs/evm'
import {
bigIntToBytes,
bytesToBigInt,
bytesToHex,
createAddressFromString,
setLengthLeft,
} from '@ethereumjs/util'
import type { ExecResult, PrecompileInput } from '@ethereumjs/evm'
// Custom precompile that adds two 32-byte big-endian unsigned integers (mod 2^256).
const ADDITION_GAS = 15n
function additionPrecompile(input: PrecompileInput): ExecResult {
const a = bytesToBigInt(input.data.subarray(0, 32))
const b = bytesToBigInt(input.data.subarray(32, 64))
const sum = (a + b) % 2n ** 256n
return {
executionGasUsed: ADDITION_GAS,
returnValue: setLengthLeft(bigIntToBytes(sum), 32),
}
}
const main = async () => {
const common = new Common({ chain: Mainnet, hardfork: Hardfork.Prague })
const ADDRESS = '0x000000000000000000000000000000000000ff01'
// Register the custom precompile with a hex string address
const evm = await createEVM({
common,
customPrecompiles: [{ address: ADDRESS, function: additionPrecompile }],
})
// Verify it is registered
const fn = evm.getPrecompile(ADDRESS)
console.log(`Precompile registered at ${ADDRESS}: ${fn !== undefined}`)
// Build call data: two 32-byte values (7 + 35)
const a = setLengthLeft(bigIntToBytes(7n), 32)
const b = setLengthLeft(bigIntToBytes(35n), 32)
const callData = new Uint8Array(64)
callData.set(a, 0)
callData.set(b, 32)
// Execute via runCall
const result = await evm.runCall({
to: createAddressFromString(ADDRESS),
gasLimit: BigInt(30000),
data: callData,
})
console.log('--------------------------------')
console.log('Custom Addition Precompile')
console.log(`Input : 7 + 35`)
console.log(`Result : ${bytesToBigInt(result.execResult.returnValue)} (${bytesToHex(result.execResult.returnValue)})`)
console.log(`Gas used : ${result.execResult.executionGasUsed}`)
console.log('--------------------------------')
}
void main()
The address for custom precompiles can be specified as either an Address instance or a 0x-prefixed hex string. All relevant types (CustomPrecompile, AddPrecompile, DeletePrecompile, PrecompileFunc, PrecompileInput) are exported from @ethereumjs/evm.
You can use evm.getPrecompile(address) to retrieve a registered precompile function at any address (works for both built-in and custom precompiles):
const fn = evm.getPrecompile('0x0000000000000000000000000000000000000002') // SHA256
const custom = evm.getPrecompile('0x000000000000000000000000000000000000ff01') // custom
To override a built-in precompile, register a custom precompile at the same address. To delete a precompile, pass an entry with only the address field (no function):
const evm = await createEVM({
customPrecompiles: [
{ address: '0x0000000000000000000000000000000000000002' }, // deletes SHA256
],
})
The EVM emits events that support async listeners (using EventEmitter3).
You can subscribe to the following events:
beforeMessage: Emits a Message right after running it.afterMessage: Emits an EVMResult right after running a message.step: Emits an InterpreterStep right before running an EVM step.newContract: Emits a NewContractEvent right before creating a contract. This event contains the deployment code, not the deployed code, as the creation message may not return such a code.You can perform asynchronous operations from within an event handler and prevent the EVM from continuing until they finish.
If subscribing to events with an async listener, specify the second
parameter of your listener as a resolve function that must be called once your listener code has finished.
See below for example usage:
// ./examples/eventListener.ts#L7-L14
evm.events.on('beforeMessage', (event) => {
console.log('synchronous listener to beforeMessage', event)
})
evm.events.on('afterMessage', (event, resolve) => {
console.log('asynchronous listener to beforeMessage', event)
// we need to call resolve() to avoid the event listener hanging
resolve?.()
})
If an exception is passed to that function, or thrown from within the handler or a function called by it, the exception will bubble into the EVM and interrupt it, possibly corrupting its state. It’s strongly recommended not to do that.
If you want to understand your EVM runs we have added a hierarchically structured list of debug loggers for your convenience which can be activated in arbitrary combinations. We also use these loggers internally for development and testing. These loggers use the debug library and can be activated on the CLI with DEBUG=ethjs,[Logger Selection] node [Your Script to Run].js and produce output like the following:

The following loggers are currently available:
| Logger | Description |
|---|---|
evm:evm |
EVM control flow, CALL or CREATE message execution |
evm:gas |
EVM gas logger |
evm:precompiles |
EVM precompiles logger |
evm:journal |
EVM journal logger |
evm:ops |
Opcode traces |
evm:ops:[Lower-case opcode name] |
Traces on a specific opcode |
Here are some examples of useful logger combinations.
Run one specific logger:
DEBUG=ethjs,evm tsx test.ts
Run all loggers currently available:
DEBUG=ethjs,evm:*,evm:*:* tsx test.ts
Run only the gas loggers:
DEBUG=ethjs,evm:*:gas tsx test.ts
Excluding the ops logger:
DEBUG=ethjs,evm:*,evm:*:*,-evm:ops tsx test.ts
Run some specific loggers including a logger specifically logging the SSTORE executions from the EVM (this is from the screenshot above):
DEBUG=ethjs,evm,evm:ops:sstore,evm:*:gas tsx test.ts
ethjs must be included in the DEBUG environment variables to enable any logs.
Additional log selections can be added with a comma separated list (no spaces). Logs with extensions can be enabled with a colon :, and * can be used to include all extensions.
DEBUG=ethjs,evm:journal,evm:ops:* npx vitest test/runCall.spec.ts
The EVM processes state changes through a hierarchical flow of execution:
runCall)The runCall method handles the execution of messages, which can be either contract calls or contract creations:
_executeCall or _executeCreate based on whether the message has a to address_executeCall and _executeCreate call into runInterpreter to actually execute the bytecodebeforeMessage, afterMessage)runCode / runInterpreter)The runCode method is a helper for directly running EVM bytecode (e.g., for testing or utility purposes) without the full message/transaction context:
runInterpreter to execute the provided bytecoderunCallThe runInterpreter method is used by both runCall (via _executeCall/_executeCreate) and runCode to process the actual bytecode.
The Interpreter class is the core bytecode processor:
Each opcode has an associated handler function that:
CALL, CREATE, DELEGATECALL create a new message and call back to the EVM’s runCall methodThis layered architecture provides separation of concerns while allowing for the complex interactions needed to execute smart contracts on the Ethereum platform.
The EthereumJS EVM comes with built-in profiling capabilities to detect performance bottlenecks and to generally support the targeted evolution of the JavaScript EVM performance.
While the EVM has a dedicated profiler setting to activate, the profiler is most useful when run through the EthereumJS client since this gives the most realistic conditions providing both real-world txs and a meaningful state size.
To repeatedly run the EVM profiler within the client sync the client on mainnet or a larger testnet to the desired block. Then the profiler should be run without sync (to not distort the results) by using the --executeBlocks and the --vmProfileBlocks (or --vmProfileTxs) flags in conjunction like:
npm run client:start -- --sync=none --vmProfileBlocks --executeBlocks=962720
This will give a profile output like the following:

The total (ms) column gives you a good overview what takes the most significant amount of time, to be put in relation with the number of calls.
The number to optimize for is the Mgas/s value. This value indicates how much gas (being a measure for the computational cost for an opcode) can be processed by the second.
A good measure to putting this relation with is by taking both the Ethereum gas limit (the max amount of “computation” per block) and the time/slot into account. With a gas limit of 30 Mio and a 12 sec slot time this leads to a following (very) minimum Mgas/s value:
30M / 12 sec = 2.5 Million gas per second
Note that this is nevertheless a very theoretical value but pretty valuable for some first rough orientation though.
Another note: profiler results for at least some opcodes are heavily distorted, first to mention the SSTORE opcode where the major “cost” occurs after block execution on checkpoint commit, which is not taken into account by the profiler.
Generally all results should rather encourage and need “self thinking” 😋 and are not suited to be blindly taken over without a deeper understanding/grasping of the underlying measurement conditions.
Happy EVM Profiling! 🎉 🤩
See @ethereumjs/vm README.
The EthereumJS GitHub organization and its repositories are managed by members of the former Ethereum Foundation JavaScript team and the broader Ethereum community. If you want to join for work or carry out improvements on the libraries see the developer docs for an overview of current standards and tools and review our code of conduct.