UNPKG

@nomicfoundation/ethereumjs-vm

Version:
447 lines (396 loc) 14.5 kB
import { Blockchain } from '@nomicfoundation/ethereumjs-blockchain' import { Chain, Common } from '@nomicfoundation/ethereumjs-common' import { EVM, getActivePrecompiles } from '@nomicfoundation/ethereumjs-evm' import { DefaultStateManager } from '@nomicfoundation/ethereumjs-statemanager' import { Account, Address, AsyncEventEmitter, unprefixedHexToBytes, } from '@nomicfoundation/ethereumjs-util' import { buildBlock } from './buildBlock.js' import { runBlock } from './runBlock.js' import { runTx } from './runTx.js' import type { BlockBuilder } from './buildBlock.js' import type { BuildBlockOpts, RunBlockOpts, RunBlockResult, RunTxOpts, RunTxResult, VMEvents, VMOpts, } from './types.js' import type { BlockchainInterface } from '@nomicfoundation/ethereumjs-blockchain' import type { EVMStateManagerInterface } from '@nomicfoundation/ethereumjs-common' import type { EVMInterface } from '@nomicfoundation/ethereumjs-evm' import type { EVMPerformanceLogOutput } from '@nomicfoundation/ethereumjs-evm/dist/cjs/logger.js' import type { BigIntLike, GenesisState } from '@nomicfoundation/ethereumjs-util' /** * Execution engine which can be used to run a blockchain, individual * blocks, individual transactions, or snippets of EVM bytecode. * * This class is an AsyncEventEmitter, please consult the README to learn how to use it. */ export class VM { /** * The StateManager used by the VM */ readonly stateManager: EVMStateManagerInterface /** * The blockchain the VM operates on */ readonly blockchain: BlockchainInterface readonly common: Common readonly events: AsyncEventEmitter<VMEvents> /** * The EVM used for bytecode execution */ readonly evm: EVMInterface protected readonly _opts: VMOpts protected _isInitialized: boolean = false protected readonly _setHardfork: boolean | BigIntLike /** * Cached emit() function, not for public usage * set to public due to implementation internals * @hidden */ public readonly _emit: (topic: string, data: any) => Promise<void> /** * VM is run in DEBUG mode (default: false) * Taken from DEBUG environment variable * * Safeguards on debug() calls are added for * performance reasons to avoid string literal evaluation * @hidden */ readonly DEBUG: boolean = false /** * VM async constructor. Creates engine instance and initializes it. * * @param opts VM engine constructor options */ static async create(opts: VMOpts = {}): Promise<VM> { const vm = new this(opts) const genesisStateOpts = opts.stateManager === undefined && opts.genesisState === undefined ? { genesisState: {} } : undefined await vm.init({ ...genesisStateOpts, ...opts }) return vm } /** * Instantiates a new {@link VM} Object. * * @deprecated The direct usage of this constructor is discouraged since * non-finalized async initialization might lead to side effects. Please * use the async {@link VM.create} constructor instead (same API). * @param opts */ protected constructor(opts: VMOpts = {}) { this.events = new AsyncEventEmitter<VMEvents>() this._opts = opts if (opts.common) { this.common = opts.common } else { const DEFAULT_CHAIN = Chain.Mainnet this.common = new Common({ chain: DEFAULT_CHAIN }) } if (opts.stateManager) { this.stateManager = opts.stateManager } else { this.stateManager = new DefaultStateManager({ common: this.common }) } this.blockchain = opts.blockchain ?? new (Blockchain as any)({ common: this.common }) if (this._opts.profilerOpts !== undefined) { const profilerOpts = this._opts.profilerOpts if (profilerOpts.reportAfterBlock === true && profilerOpts.reportAfterTx === true) { throw new Error( 'Cannot have `reportProfilerAfterBlock` and `reportProfilerAfterTx` set to `true` at the same time' ) } } // TODO tests if (opts.evm) { this.evm = opts.evm } else { let enableProfiler = false if ( this._opts.profilerOpts?.reportAfterBlock === true || this._opts.profilerOpts?.reportAfterTx === true ) { enableProfiler = true } this.evm = new EVM({ common: this.common, stateManager: this.stateManager, blockchain: this.blockchain, profiler: { enabled: enableProfiler, }, }) } this._setHardfork = opts.setHardfork ?? false this._emit = async (topic: string, data: any): Promise<void> => { return new Promise((resolve) => this.events.emit(topic as keyof VMEvents, data, resolve)) } // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables // Additional window check is to prevent vite browser bundling (and potentially other) to break this.DEBUG = typeof window === 'undefined' ? process?.env?.DEBUG?.includes('ethjs') ?? false : false } async init({ genesisState }: { genesisState?: GenesisState } = {}): Promise<void> { if (this._isInitialized) return if (genesisState !== undefined) { await this.stateManager.generateCanonicalGenesis(genesisState) } else if (this._opts.stateManager === undefined) { throw Error('genesisState state required to set genesis for stateManager') } if (typeof (<any>this.blockchain)._init === 'function') { await (this.blockchain as any)._init({ genesisState }) } if (this._opts.activatePrecompiles === true && typeof this._opts.stateManager === 'undefined') { await this.evm.journal.checkpoint() // put 1 wei in each of the precompiles in order to make the accounts non-empty and thus not have them deduct `callNewAccount` gas. for (const [addressStr] of getActivePrecompiles(this.common)) { const address = new Address(unprefixedHexToBytes(addressStr)) let account = await this.stateManager.getAccount(address) // Only do this if it is not overridden in genesis // Note: in the case that custom genesis has storage fields, this is preserved if (account === undefined) { account = new Account() const newAccount = Account.fromAccountData({ balance: 1, storageRoot: account.storageRoot, }) await this.stateManager.putAccount(address, newAccount) } } await this.evm.journal.commit() } this._isInitialized = true } /** * Processes the `block` running all of the transactions it contains and updating the miner's account * * This method modifies the state. If `generate` is `true`, the state modifications will be * reverted if an exception is raised. If it's `false`, it won't revert if the block's header is * invalid. If an error is thrown from an event handler, the state may or may not be reverted. * * @param {RunBlockOpts} opts - Default values for options: * - `generate`: false */ async runBlock(opts: RunBlockOpts): Promise<RunBlockResult> { return runBlock.bind(this)(opts) } /** * Process a transaction. Run the vm. Transfers eth. Checks balances. * * This method modifies the state. If an error is thrown, the modifications are reverted, except * when the error is thrown from an event handler. In the latter case the state may or may not be * reverted. * * @param {RunTxOpts} opts */ async runTx(opts: RunTxOpts): Promise<RunTxResult> { return runTx.bind(this)(opts) } /** * Build a block on top of the current state * by adding one transaction at a time. * * Creates a checkpoint on the StateManager and modifies the state * as transactions are run. The checkpoint is committed on {@link BlockBuilder.build} * or discarded with {@link BlockBuilder.revert}. * * @param {BuildBlockOpts} opts * @returns An instance of {@link BlockBuilder} with methods: * - {@link BlockBuilder.addTransaction} * - {@link BlockBuilder.build} * - {@link BlockBuilder.revert} */ async buildBlock(opts: BuildBlockOpts): Promise<BlockBuilder> { return buildBlock.bind(this)(opts) } /** * Returns a copy of the {@link VM} instance. * * Note that the returned copy will share the same db as the original for the blockchain and the statemanager. * * Associated caches will be deleted and caches will be re-initialized for a more short-term focused * usage, being less memory intense (the statemanager caches will switch to using an ORDERED_MAP cache * datastructure more suitable for short-term usage, the trie node LRU cache will not be activated at all). * To fine-tune this behavior (if the shallow-copy-returned object has a longer life span e.g.) you can set * the `downlevelCaches` option to `false`. * * @param downlevelCaches Downlevel (so: adopted for short-term usage) associated state caches (default: true) */ async shallowCopy(downlevelCaches = true): Promise<VM> { const common = this.common.copy() common.setHardfork(this.common.hardfork()) const blockchain = this.blockchain.shallowCopy() const stateManager = this.stateManager.shallowCopy(downlevelCaches) const evmOpts = { ...(this.evm as any)._optsCached, common, blockchain, stateManager, } const evmCopy = new EVM(evmOpts) // TODO fixme (should copy the EVMInterface, not default EVM) return VM.create({ stateManager, blockchain: this.blockchain, common, evm: evmCopy, setHardfork: this._setHardfork, profilerOpts: this._opts.profilerOpts, }) } /** * Return a compact error string representation of the object */ errorStr() { let hf = '' try { hf = this.common.hardfork() } catch (e: any) { hf = 'error' } const errorStr = `vm hf=${hf}` return errorStr } /** * Emit EVM profile logs * @param logs * @param profileTitle * @hidden */ emitEVMProfile(logs: EVMPerformanceLogOutput[], profileTitle: string) { if (logs.length === 0) { return } // Track total calls / time (ms) / gas let calls = 0 let totalMs = 0 let totalGas = 0 // Order of columns to report (see `EVMPerformanceLogOutput` type) const colOrder = [ 'tag', 'calls', 'avgTimePerCall', 'totalTime', 'staticGasUsed', 'dynamicGasUsed', 'gasUsed', 'staticGas', 'millionGasPerSecond', 'blocksPerSlot', ] // The name of this column to report (saves space) const colNames = [ 'tag', 'calls', 'ms/call', 'total (ms)', 'sgas', 'dgas', 'total (s+d)', 'static fee', 'Mgas/s', 'BpS', ] // Special padStr method which inserts whitespace left and right // This ensures that there is at least one whitespace between the columns (denoted by pipe `|` chars) function padStr(str: string | number, leftpad: number) { return ' ' + str.toString().padStart(leftpad, ' ') + ' ' } // Returns the string length of this column. Used to calculate how big the header / footer should be function strLen(str: string | number) { return padStr(str, 0).length - 2 } // Step one: calculate the length of each colum const colLength: number[] = [] for (const entry of logs) { let ins = 0 colLength[ins] = Math.max(colLength[ins] ?? 0, strLen(colNames[ins])) for (const key of colOrder) { // @ts-ignore if (entry[key] !== undefined) { // If entry is available, max out the current column length (this will be the longest string of this column) //@ts-ignore colLength[ins] = Math.max(colLength[ins] ?? 0, strLen(entry[key])) ins++ // In this switch statement update the total calls / time / gas used switch (key) { case 'calls': calls += entry[key] break case 'totalTime': totalMs += entry[key] break case 'gasUsed': totalGas += entry[key] break } } } } // Ensure that the column names also fit on the column length for (const i in colLength) { colLength[i] = Math.max(colLength[i] ?? 0, strLen(colNames[i])) } // Calculate the total header length // This is done by summing all columns together, plus adding three extra chars per column (two whitespace, one pipe) // Remove the final pipe character since this is included in the header string (so subtract one) const headerLength = colLength.reduce((pv, cv) => pv + cv, 0) + colLength.length * 3 - 1 const blockGasLimit = 30_000_000 // Block gas limit const slotTime = 12000 // Time in milliseconds (!) per slot // Normalize constant to check if execution time is above one block per slot (>=1) or not (<1) const bpsNormalizer = blockGasLimit / slotTime const avgGas = totalGas / totalMs // Gas per millisecond const mGasSAvg = Math.round(avgGas) / 1e3 const bpSAvg = Math.round((avgGas / bpsNormalizer) * 1e3) / 1e3 // Write the profile title // eslint-disable-next-line console.log('+== ' + profileTitle + ' ==+') // Write the summary of this profile // eslint-disable-next-line console.log( `+== Calls: ${calls}, Total time: ${ Math.round(totalMs * 1e3) / 1e3 }ms, Total gas: ${totalGas}, MGas/s: ${mGasSAvg}, Blocks per Slot (BpS): ${bpSAvg} ==+` ) // Generate and write the header const header = '|' + '-'.repeat(headerLength) + '|' // eslint-disable-next-line console.log(header) // Write the columns let str = '' for (const i in colLength) { str += '|' + padStr(colNames[i], colLength[i]) } str += '|' // eslint-disable-next-line console.log(str) // Write each profile entry for (const entry of logs) { let str = '' let i = 0 for (const key of colOrder) { //@ts-ignore if (entry[key] !== undefined) { //@ts-ignore str += '|' + padStr(entry[key], colLength[i]) i++ } } str += '|' // eslint-disable-next-line console.log(str) } // Finally, write the footer const footer = '+' + '-'.repeat(headerLength) + '+' // eslint-disable-next-line console.log(footer) } }