$ npm install @ethereumjs/evm
TypeScript implementation of the Ethereum EVM. |
---|
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 will become an integrated part of the EVM functionality with the activation of the point evaluation precompile. It is therefore strongly recommended to always run the EVM with a KZG library installed and initialized, see KZG Setup for instructions.
With the v2 release (Summer 2023) the EVM/VM packages have been further decoupled and it now possible to run the EVM package in isolation with reasonable defaults.
The following is the simplest example for an EVM instantiation:
// ./examples/simple.ts
import { hexToBytes } from '@ethereumjs/util'
import { EVM } from '@ethereumjs/evm'
const main = async () => {
const evm = await EVM.create()
const res = await evm.runCode({ code: hexToBytes('0x6001') }) // PUSH1 01 -- simple bytecode to push 1 onto the stack
console.log(res.executionGasUsed) // 3n
}
main()
Note: with the switch from v2 to v3 the old direct new EVM()
constructor usage has been deprecated and an EVM
now has to be instantiated with the async static EVM.create()
constructor.
If the EVM should run on a certain state an @ethereumjs/statemanager
is needed. An @ethereumjs/blockchain
instance can be passed in to provide access to external interface information like a blockhash:
// ./examples/withBlockchain.ts
import { Blockchain } from '@ethereumjs/blockchain'
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { EVM } from '@ethereumjs/evm'
import { DefaultStateManager } from '@ethereumjs/statemanager'
import { bytesToHex } from '@ethereumjs/util'
const main = async () => {
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai })
const stateManager = new DefaultStateManager()
const blockchain = await Blockchain.create()
const evm = await EVM.create({
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: Buffer.from(code.join(''), 'hex'),
gasLimit: BigInt(0xffff),
})
console.log(`Returned: ${bytesToHex(results.returnValue)}`)
console.log(`gasUsed: ${results.executionGasUsed.toString()}`)
}
void main()
Additionally this usage example shows the use of events to listen on the inner workings and procedural updates
(step
event) of the EVM.
This library support all EVM precompiles up to the Prague
hardfork.
The following code allows to run precompiles in isolation, e.g. for testing purposes:
// ./examples/precompile.ts
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { EVM, getActivePrecompiles } from '@ethereumjs/evm'
import { bytesToHex, hexToBytes } from '@ethereumjs/util'
const main = async () => {
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Prague })
// Taken from test/eips/precompiles/bls/add_G1_bls.json
const data = hexToBytes(
'0x0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb0000000000000000000000000000000008b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e100000000000000000000000000000000112b98340eee2777cc3c14163dea3ec97977ac3dc5c70da32e6e87578f44912e902ccef9efe28d4a78b8999dfbca942600000000000000000000000000000000186b28d92356c4dfec4b5201ad099dbdede3781f8998ddf929b4cd7756192185ca7b8f4ef7088f813270ac3d48868a21'
)
const gasLimit = BigInt(5000000)
const evm = await EVM.create({ common })
const precompile = getActivePrecompiles(common).get('000000000000000000000000000000000000000b')!
const callData = {
data,
gasLimit,
common,
_EVM: evm,
}
const result = await precompile(callData)
console.log(`Precompile result:${bytesToHex(result.returnValue)}`)
}
main()
Starting with v3.1.0
the EVM support the BLS precompiles introduced with EIP-2537. 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 EVM.create({ common, bls })
This projects contain the following examples:
All of the examples have their own README.md
explaining how to run them.
With the breaking release round in Summer 2023 we have added hybrid ESM/CJS builds for all our libraries (see section below) and have eliminated many of the caveats which had previously prevented a frictionless browser usage.
It is now 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 from 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.
With the breaking releases from Summer 2023 we have removed all Node.js specific Buffer
usages from our libraries and replace these with Uint8Array representations, which are available both in Node.js and the browser (Buffer
is a subclass of Uint8Array
).
We have converted existing Buffer conversion methods to Uint8Array conversion methods in the @ethereumjs/util bytes
module, see the respective README section for guidance.
Starting with v1 the usage of BN.js for big numbers has been removed from the library and replaced with the usage of the native JS BigInt data type (introduced in ES2020
).
Please note that number-related API signatures have changed along with this version update and the minimal build target has been updated to ES2020
.
This package contains the inner Ethereum Virtual Machine core functionality which was included in the @ethereumjs/vm package up till 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 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)homestead
tangerineWhistle
spuriousDragon
byzantium
constantinople
petersburg
istanbul
muirGlacier
(only mainnet
)berlin
(v5.2.0
+)london
(v5.4.0
+)arrowGlacier
(only mainnet
) (v5.6.0
+)merge
shanghai
(v2.0.0
+)cancun
(v2.0.0
+)Default: shanghai
(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 { Chain, Common } from '@ethereumjs/common'
import { EVM } from '@ethereumjs/evm'
const main = async () => {
const common = new Common({ chain: Chain.Mainnet, eips: [3074] })
const evm = await EVM.create({ common })
console.log(`EIP 3074 is active - ${evm.common.isActivatedEIP(3074)}`)
}
main()
Currently supported EIPs:
outdated
)outdated
)outdated
)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 with e.g. a more performant WASM implementation by using a shared common
instance.
This library supports the blob transaction type introduced with EIP-4844.
To run EVM related EIP-4844 functionality you have to active the EIP in the associated @ethereumjs/common
library:
// ./examples/4844.ts
import { Common, Chain, Hardfork } from '@ethereumjs/common'
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [4844] })
EIP-4844 comes with a new opcode BLOBHASH
(Attention! Renamed from DATAHASH
) and adds a new point evaluation precompile at address 0x0a
(moved from 0x14
at some point along spec updates).
Note: Usage of the point evaluation precompile needs a manual KZG library installation and global initialization, see KZG Setup for instructions.
The EVM has a public property events
which instantiates an AsyncEventEmitter and events are submitted along major execution steps which you can listen to.
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.An example for the step
event can be found in the initial usage example in this README
.
You can perform asynchronous operations from within an event handler and prevent the EVM to keep running until they finish.
In order to do that, your event handler has to accept two arguments. The first one will be the event object, and the second one a function. The EVM won't continue until you call this function.
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 perform synchronous operations, you don't need to receive a function as the handler's second argument, nor call it.
Note that if your event handler receives multiple arguments, the second one will be the continuation function, and it must be called.
If an exception is 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 throw from within event handlers.
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 CL 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 control flow, CALL or CREATE message execution |
evm:gas |
EVM gas logger |
evm:eei:gas |
EEI gas logger |
evm:ops |
Opcode traces |
evm:ops:[Lower-case opcode name] |
Traces on a specific opcode |
Here are some examples for 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
The EVM processes state changes at many levels.
The opFns for CREATE
, CALL
, and CALLCODE
call back up to runCall
.
TODO: this section likely needs an update.
Starting with the v2.1.0
release the EVM comes with build-in profiling capabilities to detect performance bottlenecks and to generally support the targeted evolution of the JavaScript EVM performance.
While the EVM now has a dedicated profiler
setting to activate, the profiler can best and most useful be 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 blindedly taken over without a deeper understanding/grasping of the underlying measurement conditions.
Happy EVM Profiling! 🎉 🤩
See @ethereumjs/vm README.
See our organizational documentation for an introduction to EthereumJS
as well as information on current standards and best practices. If you want to join for work or carry out improvements on the libraries, please review our contribution guidelines first.
© 2010 - cnpmjs.org x YWFE | Home | YWFE