diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 0d7ab1790b7..a259a9ffd19 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -404,14 +404,13 @@ export class BlockHeader { */ protected _consensusFormatValidation() { const { nonce, uncleHash, difficulty, extraData, number } = this - const hardfork = this.common.hardfork() // Consensus type dependent checks if (this.common.consensusAlgorithm() === ConsensusAlgorithm.Ethash) { // PoW/Ethash if ( number > BigInt(0) && - this.extraData.length > this.common.paramByHardfork('vm', 'maxExtraDataSize', hardfork) + this.extraData.length > this.common.param('vm', 'maxExtraDataSize') ) { // Check length of data on all post-genesis blocks const msg = this._errorMsg('invalid amount of extra data') @@ -506,10 +505,8 @@ export class BlockHeader { parentGasLimit = parentGasLimit * elasticity } const gasLimit = this.gasLimit - const hardfork = this.common.hardfork() - const a = - parentGasLimit / this.common.paramByHardfork('gasConfig', 'gasLimitBoundDivisor', hardfork) + const a = parentGasLimit / this.common.param('gasConfig', 'gasLimitBoundDivisor') const maxGasLimit = parentGasLimit + a const minGasLimit = parentGasLimit - a @@ -523,10 +520,8 @@ export class BlockHeader { throw new Error(msg) } - if (gasLimit < this.common.paramByHardfork('gasConfig', 'minGasLimit', hardfork)) { - const msg = this._errorMsg( - `gas limit decreased below minimum gas limit for hardfork=${hardfork}` - ) + if (gasLimit < this.common.param('gasConfig', 'minGasLimit')) { + const msg = this._errorMsg(`gas limit decreased below minimum gas limit`) throw new Error(msg) } } @@ -704,18 +699,16 @@ export class BlockHeader { ) throw new Error(msg) } - const hardfork = this.common.hardfork() const blockTs = this.timestamp const { timestamp: parentTs, difficulty: parentDif } = parentBlockHeader - const minimumDifficulty = this.common.paramByHardfork('pow', 'minimumDifficulty', hardfork) - const offset = - parentDif / this.common.paramByHardfork('pow', 'difficultyBoundDivisor', hardfork) + const minimumDifficulty = this.common.param('pow', 'minimumDifficulty') + const offset = parentDif / this.common.param('pow', 'difficultyBoundDivisor') let num = this.number // We use a ! here as TS cannot follow this hardfork-dependent logic, but it always gets assigned let dif!: bigint - if (this.common.hardforkGteHardfork(hardfork, Hardfork.Byzantium) === true) { + if (this.common.gteHardfork(Hardfork.Byzantium) === true) { // max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) // 9), -99) (EIP100) const uncleAddend = equalsBytes(parentBlockHeader.uncleHash, KECCAK256_RLP_ARRAY) ? 1 : 2 let a = BigInt(uncleAddend) - (blockTs - parentTs) / BigInt(9) @@ -727,13 +720,13 @@ export class BlockHeader { dif = parentDif + offset * a } - if (this.common.hardforkGteHardfork(hardfork, Hardfork.Byzantium) === true) { + if (this.common.gteHardfork(Hardfork.Byzantium) === true) { // Get delay as parameter from common num = num - this.common.param('pow', 'difficultyBombDelay') if (num < BigInt(0)) { num = BigInt(0) } - } else if (this.common.hardforkGteHardfork(hardfork, Hardfork.Homestead) === true) { + } else if (this.common.gteHardfork(Hardfork.Homestead) === true) { // 1 - (block_timestamp - parent_timestamp) // 10 let a = BigInt(1) - (blockTs - parentTs) / BigInt(10) const cutoff = BigInt(-99) @@ -744,7 +737,7 @@ export class BlockHeader { dif = parentDif + offset * a } else { // pre-homestead - if (parentTs + this.common.paramByHardfork('pow', 'durationLimit', hardfork) > blockTs) { + if (parentTs + this.common.param('pow', 'durationLimit') > blockTs) { dif = offset + parentDif } else { dif = parentDif - offset diff --git a/packages/client/bin/cli.ts b/packages/client/bin/cli.ts index b8ff58b8e6c..8fc16dc9a98 100755 --- a/packages/client/bin/cli.ts +++ b/packages/client/bin/cli.ts @@ -344,6 +344,16 @@ const args: ClientOpts = yargs(hideBin(process.argv)) 'To run client in single node configuration without need to discover the sync height from peer. Particularly useful in test configurations. This flag is automically activated in the "dev" mode', boolean: true, }) + .option('vmProfileBlocks', { + describe: 'Report the VM profile after running each block', + boolean: true, + default: false, + }) + .option('vmProfileTxs', { + describe: 'Report the VM profile after running each block', + boolean: true, + default: false, + }) .option('loadBlocksFromRlp', { describe: 'path to a file of RLP encoded blocks', string: true, @@ -816,6 +826,8 @@ async function run() { mine, minerCoinbase: args.minerCoinbase, isSingleNode, + vmProfileBlocks: args.vmProfileBlocks, + vmProfileTxs: args.vmProfileTxs, minPeers: args.minPeers, multiaddrs, port: args.port, diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 1258dbe9ec2..4ec1890c102 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -11,7 +11,7 @@ import type { Logger } from './logging' import type { EventBusType, MultiaddrLike } from './types' import type { BlockHeader } from '@ethereumjs/block' import type { Address } from '@ethereumjs/util' -import type { VM } from '@ethereumjs/vm' +import type { VM, VMProfilerOpts } from '@ethereumjs/vm' import type { Multiaddr } from 'multiaddr' export enum DataDirectory { @@ -251,6 +251,16 @@ export interface ConfigOptions { */ isSingleNode?: boolean + /** + * Whether to profile VM blocks + */ + vmProfileBlocks?: boolean + + /** + * Whether to profile VM txs + */ + vmProfileTxs?: boolean + /** * Unlocked accounts of form [address, privateKey] * Currently only the first account is used to seal mined PoA blocks @@ -376,6 +386,7 @@ export class Config { public readonly isSingleNode: boolean public readonly accounts: [address: Address, privKey: Uint8Array][] public readonly minerCoinbase?: Address + public readonly vmProfilerOpts?: VMProfilerOpts public readonly safeReorgDistance: number public readonly skeletonFillCanonicalBackStep: number @@ -434,6 +445,14 @@ export class Config { this.debugCode = options.debugCode ?? Config.DEBUGCODE_DEFAULT this.mine = options.mine ?? false this.isSingleNode = options.isSingleNode ?? false + + if (options.vmProfileBlocks !== undefined || options.vmProfileTxs !== undefined) { + this.vmProfilerOpts = { + reportAfterBlock: options.vmProfileBlocks !== false, + reportAfterTx: options.vmProfileTxs !== false, + } + } + this.accounts = options.accounts ?? [] this.minerCoinbase = options.minerCoinbase diff --git a/packages/client/src/execution/vmexecution.ts b/packages/client/src/execution/vmexecution.ts index 4cd6901c9e6..65cad68777f 100644 --- a/packages/client/src/execution/vmexecution.ts +++ b/packages/client/src/execution/vmexecution.ts @@ -76,6 +76,7 @@ export class VMExecution extends Execution { common: this.config.execCommon, blockchain: this.chain.blockchain, stateManager, + profilerOpts: this.config.vmProfilerOpts, }) } else { this.vm = this.config.vm diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 0bdb47d7707..879b7a8ab0d 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -157,5 +157,7 @@ export interface ClientOpts { txLookupLimit?: number startBlock?: number isSingleNode?: boolean + vmProfileBlocks?: boolean + vmProfileTxs?: boolean loadBlocksFromRlp?: string } diff --git a/packages/common/src/chains.ts b/packages/common/src/chains.ts index 93b2680b827..aa560aa17c4 100644 --- a/packages/common/src/chains.ts +++ b/packages/common/src/chains.ts @@ -543,8 +543,7 @@ export const chains: ChainsDict = { { name: 'cancun', block: null, - timestamp: '2000000000', - forkHash: '0xffab2acd', + forkHash: null, }, ], bootstrapNodes: [ diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 890de04692e..93a26edd9f2 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -25,16 +25,22 @@ import type { CliqueConfig, CommonOpts, CustomCommonOpts, + EIPConfig, + EIPOrHFConfig, EthashConfig, GenesisBlockConfig, GethConfigOpts, HardforkByOpts, + HardforkConfig, HardforkTransitionConfig, } from './types.js' import type { BigIntLike, PrefixedHexString } from '@ethereumjs/util' type HardforkSpecKeys = string // keyof typeof HARDFORK_SPECS type HardforkSpecValues = typeof HARDFORK_SPECS[HardforkSpecKeys] + +type ParamsCacheConfig = Omit + /** * Common class to access chain and hardfork parameters and to provide * a unified and shared view on the network and hardfork state. @@ -46,12 +52,15 @@ type HardforkSpecValues = typeof HARDFORK_SPECS[HardforkSpecKeys] export class Common { readonly DEFAULT_HARDFORK: string | Hardfork - private _chainParams: ChainConfig - private _hardfork: string | Hardfork - private _eips: number[] = [] - private _customChains: ChainConfig[] + protected _chainParams: ChainConfig + protected _hardfork: string | Hardfork + protected _eips: number[] = [] + protected _customChains: ChainConfig[] - private HARDFORK_CHANGES: [HardforkSpecKeys, HardforkSpecValues][] + protected _paramsCache: ParamsCacheConfig = {} + protected _activatedEIPsCache: number[] = [] + + protected HARDFORK_CHANGES: [HardforkSpecKeys, HardforkSpecValues][] public events: EventEmitter @@ -197,7 +206,7 @@ export class Common { return Boolean((initializedChains['names'] as ChainName)[chainId.toString()]) } - private static _getChainParams( + protected static _getChainParams( chain: string | number | Chain | bigint, customChains?: ChainConfig[] ): ChainConfig { @@ -238,6 +247,10 @@ export class Common { if (opts.eips) { this.setEIPs(opts.eips) } + if (Object.keys(this._paramsCache).length === 0) { + this._buildParamsCache() + this._buildActivatedEIPsCache() + } } /** @@ -283,6 +296,8 @@ export class Common { if (hfChanges[0] === hardfork) { if (this._hardfork !== hardfork) { this._hardfork = hardfork + this._buildParamsCache() + this._buildActivatedEIPsCache() this.events.emit('hardforkChanged', hardfork) } existing = true @@ -435,7 +450,7 @@ export class Common { * @param hardfork Hardfork name * @returns Dictionary with hardfork params or null if hardfork not on chain */ - private _getHardfork(hardfork: string | Hardfork): HardforkTransitionConfig | null { + protected _getHardfork(hardfork: string | Hardfork): HardforkTransitionConfig | null { const hfs = this.hardforks() for (const hf of hfs) { if (hf['name'] === hardfork) return hf @@ -458,6 +473,12 @@ export class Common { `${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}` ) } + } + this._eips = eips + this._buildParamsCache() + this._buildActivatedEIPsCache() + + for (const eip of eips) { if ((EIPs as any)[eip].requiredEIPs !== undefined) { for (const elem of (EIPs as any)[eip].requiredEIPs) { if (!(eips.includes(elem) || this.isActivatedEIP(elem))) { @@ -466,14 +487,85 @@ export class Common { } } } - this._eips = eips + } + + /** + * Internal helper for _buildParamsCache() + */ + protected _mergeWithParamsCache(params: HardforkConfig | EIPConfig) { + this._paramsCache['gasConfig'] = { + ...this._paramsCache['gasConfig'], + ...params['gasConfig'], + } + this._paramsCache['gasPrices'] = { + ...this._paramsCache['gasPrices'], + ...params['gasPrices'], + } + this._paramsCache['pow'] = { + ...this._paramsCache['pow'], + ...params['pow'], + } + this._paramsCache['sharding'] = { + ...this._paramsCache['sharding'], + ...params['sharding'], + } + this._paramsCache['vm'] = { + ...this._paramsCache['vm'], + ...params['vm'], + } + } + + /** + * Build up a cache for all parameter values for the current HF and all activated EIPs + */ + protected _buildParamsCache() { + this._paramsCache = {} + // Iterate through all hardforks up to hardfork set + const hardfork = this.hardfork() + for (const hfChanges of this.HARDFORK_CHANGES) { + // EIP-referencing HF config (e.g. for berlin) + if ('eips' in hfChanges[1]) { + const hfEIPs = hfChanges[1]['eips'] + for (const eip of hfEIPs!) { + if (!(eip in EIPs)) { + throw new Error(`${eip} not supported`) + } + + this._mergeWithParamsCache(EIPs[eip]) + } + // Parameter-inlining HF config (e.g. for istanbul) + } else { + this._mergeWithParamsCache(hfChanges[1]) + } + if (hfChanges[0] === hardfork) break + } + // Iterate through all additionally activated EIPs + for (const eip of this._eips) { + if (!(eip in EIPs)) { + throw new Error(`${eip} not supported`) + } + + this._mergeWithParamsCache(EIPs[eip]) + } + } + + protected _buildActivatedEIPsCache() { + this._activatedEIPsCache = [] + + for (const hfChanges of this.HARDFORK_CHANGES) { + const hf = hfChanges[1] + if (this.gteHardfork(hf['name']) && 'eips' in hf) { + this._activatedEIPsCache = this._activatedEIPsCache.concat(hf['eips'] as number[]) + } + } + this._activatedEIPsCache = this._activatedEIPsCache.concat(this._eips) } /** * Returns a parameter for the current chain setup * * If the parameter is present in an EIP, the EIP always takes precedence. - * Otherwise the parameter if taken from the latest applied HF with + * Otherwise the parameter is taken from the latest applied HF with * a change on the respective parameter. * * @param topic Parameter topic ('gasConfig', 'gasPrices', 'vm', 'pow') @@ -483,12 +575,14 @@ export class Common { param(topic: string, name: string): bigint { // TODO: consider the case that different active EIPs // can change the same parameter - let value - for (const eip of this._eips) { - value = this.paramByEIP(topic, name, eip) - if (value !== undefined) return value + let value = null + if ( + (this._paramsCache as any)[topic] !== undefined && + (this._paramsCache as any)[topic][name] !== undefined + ) { + value = (this._paramsCache as any)[topic][name].v } - return this.paramByHardfork(topic, name, this._hardfork) + return BigInt(value ?? 0) } /** @@ -501,14 +595,14 @@ export class Common { paramByHardfork(topic: string, name: string, hardfork: string | Hardfork): bigint { let value = null for (const hfChanges of this.HARDFORK_CHANGES) { - // EIP-referencing HF file (e.g. berlin.json) + // EIP-referencing HF config (e.g. for berlin) if ('eips' in hfChanges[1]) { const hfEIPs = hfChanges[1]['eips'] for (const eip of hfEIPs!) { const valueEIP = this.paramByEIP(topic, name, eip) value = typeof valueEIP === 'bigint' ? valueEIP : value } - // Parameter-inlining HF file (e.g. istanbul.json) + // Parameter-inlining HF config (e.g. for istanbul) } else { if ( (hfChanges[1] as any)[topic] !== undefined && @@ -575,17 +669,9 @@ export class Common { * @param eip */ isActivatedEIP(eip: number): boolean { - if (this.eips().includes(eip)) { + if (this._activatedEIPsCache.includes(eip)) { return true } - for (const hfChanges of this.HARDFORK_CHANGES) { - const hf = hfChanges[1] - if (this.gteHardfork(hf['name']) && 'eips' in hf) { - if ((hf['eips'] as number[]).includes(eip)) { - return true - } - } - } return false } @@ -755,7 +841,7 @@ export class Common { * @param genesisHash Genesis block hash of the chain * @returns Fork hash as hex string */ - private _calcForkHash(hardfork: string | Hardfork, genesisHash: Uint8Array): PrefixedHexString { + protected _calcForkHash(hardfork: string | Hardfork, genesisHash: Uint8Array): PrefixedHexString { let hfBytes = new Uint8Array(0) let prevBlockOrTime = 0 for (const hf of this.hardforks()) { @@ -905,7 +991,8 @@ export class Common { } /** - * Returns the active EIPs + * Returns the additionally activated EIPs + * (by using the `eips` constructor option) * @returns List of EIPs */ eips(): number[] { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index c882be2bc44..00b4908d55c 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -133,7 +133,7 @@ type ParamDict = { d: string } -type EIPOrHFConfig = { +export type EIPOrHFConfig = { comment: string url: string status: string diff --git a/packages/common/test/params.spec.ts b/packages/common/test/params.spec.ts index f82d24918e0..83d55e82dfb 100644 --- a/packages/common/test/params.spec.ts +++ b/packages/common/test/params.spec.ts @@ -80,6 +80,22 @@ describe('[Common]: Parameter access for param(), paramByHardfork()', () => { ) }) + it('Access on copied Common instances', () => { + const c = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai }) + let msg = 'Should correctly access param with param() on original Common' + assert.equal(c.param('pow', 'minerReward'), BigInt(2000000000000000000), msg) + + const cCopy = c.copy() + cCopy.setHardfork(Hardfork.Chainstart) + + msg = 'Should correctly access param with param() on copied Common with hardfork changed' + assert.equal(cCopy.param('pow', 'minerReward'), BigInt(5000000000000000000), msg) + + msg = + 'Should correctly access param with param() on original Common after copy and HF change on copied Common' + assert.equal(c.param('pow', 'minerReward'), BigInt(2000000000000000000), msg) + }) + it('EIP param access, paramByEIP()', () => { const c = new Common({ chain: Chain.Mainnet }) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 9dcda8c6048..5130dcaa5d6 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -20,13 +20,15 @@ import { EOF, getEOFCode } from './eof.js' import { ERROR, EvmError } from './exceptions.js' import { Interpreter } from './interpreter.js' import { Journal } from './journal.js' +import { EVMPerformanceLogger } from './logger.js' import { Message } from './message.js' import { getOpcodesForHF } from './opcodes/index.js' -import { getActivePrecompiles } from './precompiles/index.js' +import { getActivePrecompiles, getPrecompileName } from './precompiles/index.js' import { TransientStorage } from './transientStorage.js' import { DefaultBlockchain } from './types.js' import type { InterpreterOpts } from './interpreter.js' +import type { Timer } from './logger.js' import type { MessageWithTo } from './message.js' import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/gas.js' import type { OpHandler, OpcodeList } from './opcodes/index.js' @@ -108,6 +110,8 @@ export class EVM implements EVMInterface { protected readonly _optsCached: EVMOpts + protected performanceLogger: EVMPerformanceLogger + public get precompiles() { return this._precompiles } @@ -191,6 +195,8 @@ export class EVM implements EVMInterface { return new Promise((resolve) => this.events.emit(topic as keyof EVMEvents, data, resolve)) } + this.performanceLogger = new EVMPerformanceLogger() + // 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 = @@ -265,11 +271,23 @@ export class EVM implements EVMInterface { let result: ExecResult if (message.isCompiled) { + let timer: Timer + let target: string + if (this._optsCached.profiler?.enabled === true) { + target = bytesToUnprefixedHex(message.codeAddress.bytes) + // TODO: map target precompile not to address, but to a name + target = getPrecompileName(target) ?? target.slice(20) + timer = this.performanceLogger.startTimer(target) + } result = await this.runPrecompile( message.code as PrecompileFunc, message.data, message.gasLimit ) + + if (this._optsCached.profiler?.enabled === true) { + this.performanceLogger.stopTimer(timer!, Number(result.executionGasUsed), 'precompiles') + } result.gasRefund = message.gasRefund } else { if (this.DEBUG) { @@ -557,7 +575,9 @@ export class EVM implements EVMInterface { this.blockchain, env, message.gasLimit, - this.journal + this.journal, + this.performanceLogger, + this._optsCached.profiler ) if (message.selfdestruct) { interpreter._result.selfdestruct = message.selfdestruct @@ -885,6 +905,14 @@ export class EVM implements EVMInterface { ;(opts.stateManager as any).common = common return new EVM(opts) } + + public getPerformanceLogs() { + return this.performanceLogger.getLogs() + } + + public clearPerformanceLogs() { + this.performanceLogger.clear() + } } export function OOGResult(gasLimit: bigint): ExecResult { diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index 6afb6f76e08..4a9fbac4286 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -18,8 +18,9 @@ import { Stack } from './stack.js' import type { EVM } from './evm.js' import type { Journal } from './journal.js' +import type { EVMPerformanceLogger, Timer } from './logger.js' import type { AsyncOpHandler, OpHandler, Opcode } from './opcodes/index.js' -import type { Block, Blockchain, EVMResult, Log } from './types.js' +import type { Block, Blockchain, EVMProfilerOpts, EVMResult, Log } from './types.js' import type { Common, EVMStateManagerInterface } from '@ethereumjs/common' import type { Address } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -133,6 +134,9 @@ export class Interpreter { // Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...}) private opDebuggers: { [key: string]: (debug: string) => void } = {} + private profilerOpts?: EVMProfilerOpts + private performanceLogger: EVMPerformanceLogger + // TODO remove gasLeft as constructor argument constructor( evm: EVM, @@ -140,7 +144,9 @@ export class Interpreter { blockchain: Blockchain, env: Env, gasLeft: bigint, - journal: Journal + journal: Journal, + performanceLogs: EVMPerformanceLogger, + profilerOpts?: EVMProfilerOpts ) { this._evm = evm this._stateManager = stateManager @@ -171,6 +177,8 @@ export class Interpreter { returnValue: undefined, selfdestruct: new Set(), } + this.profilerOpts = profilerOpts + this.performanceLogger = performanceLogs } async run(code: Uint8Array, opts: InterpreterOpts = {}): Promise { @@ -259,41 +267,54 @@ export class Interpreter { async runStep(): Promise { const opInfo = this.lookupOpInfo(this._runState.opCode) - let gas = BigInt(opInfo.fee) - // clone the gas limit; call opcodes can add stipend, - // which makes it seem like the gas left increases - const gasLimitClone = this.getGasLeft() - - if (opInfo.dynamicGas) { - const dynamicGasHandler = (this._evm as any)._dynamicGasHandlers.get(this._runState.opCode)! - // This function updates the gas in-place. - // It needs the base fee, for correct gas limit calculation for the CALL opcodes - gas = await dynamicGasHandler(this._runState, gas, this.common) - } + let timer: Timer - if (this._evm.events.listenerCount('step') > 0 || this._evm.DEBUG) { - // Only run this stepHook function if there is an event listener (e.g. test runner) - // or if the vm is running in debug mode (to display opcode debug logs) - await this._runStepHook(gas, gasLimitClone) + if (this.profilerOpts?.enabled === true) { + timer = this.performanceLogger.startTimer(opInfo.name) } - // Check for invalid opcode - if (opInfo.name === 'INVALID') { - throw new EvmError(ERROR.INVALID_OPCODE) - } + let gas = BigInt(opInfo.fee) - // Reduce opcode's base fee - this.useGas(gas, `${opInfo.name} fee`) - // Advance program counter - this._runState.programCounter++ + try { + // clone the gas limit; call opcodes can add stipend, + // which makes it seem like the gas left increases + const gasLimitClone = this.getGasLeft() - // Execute opcode handler - const opFn = this.getOpHandler(opInfo) + if (opInfo.dynamicGas) { + const dynamicGasHandler = (this._evm as any)._dynamicGasHandlers.get(this._runState.opCode)! + // This function updates the gas in-place. + // It needs the base fee, for correct gas limit calculation for the CALL opcodes + gas = await dynamicGasHandler(this._runState, gas, this.common) + } - if (opInfo.isAsync) { - await (opFn as AsyncOpHandler).apply(null, [this._runState, this.common]) - } else { - opFn.apply(null, [this._runState, this.common]) + if (this._evm.events.listenerCount('step') > 0 || this._evm.DEBUG) { + // Only run this stepHook function if there is an event listener (e.g. test runner) + // or if the vm is running in debug mode (to display opcode debug logs) + await this._runStepHook(gas, gasLimitClone) + } + + // Check for invalid opcode + if (opInfo.name === 'INVALID') { + throw new EvmError(ERROR.INVALID_OPCODE) + } + + // Reduce opcode's base fee + this.useGas(gas, `${opInfo.name} fee`) + // Advance program counter + this._runState.programCounter++ + + // Execute opcode handler + const opFn = this.getOpHandler(opInfo) + + if (opInfo.isAsync) { + await (opFn as AsyncOpHandler).apply(null, [this._runState, this.common]) + } else { + opFn.apply(null, [this._runState, this.common]) + } + } finally { + if (this.profilerOpts?.enabled === true) { + this.performanceLogger.stopTimer(timer!, Number(gas), 'opcodes') + } } } @@ -872,7 +893,14 @@ export class Interpreter { return BigInt(0) } + let timer: Timer + if (this.profilerOpts?.enabled === true) { + timer = this.performanceLogger.pauseTimer() + } const results = await this._evm.runCall({ message: msg }) + if (this.profilerOpts?.enabled === true) { + this.performanceLogger.unpauseTimer(timer!) + } if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs) @@ -971,7 +999,14 @@ export class Interpreter { message.createdAddresses = createdAddresses } + let timer: Timer + if (this.profilerOpts?.enabled === true) { + timer = this.performanceLogger.pauseTimer() + } const results = await this._evm.runCall({ message }) + if (this.profilerOpts?.enabled === true) { + this.performanceLogger.unpauseTimer(timer!) + } if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs) diff --git a/packages/evm/src/logger.ts b/packages/evm/src/logger.ts new file mode 100644 index 00000000000..b31dfc40667 --- /dev/null +++ b/packages/evm/src/logger.ts @@ -0,0 +1,143 @@ +type EVMPerformanceLogEntry = { + calls: number + time: number + gasUsed: number +} + +export type EVMPerformanceLogOutput = { + calls: number // Amount this opcode/precompile was called + totalTime: number // Amount of seconds taken for this opcode/precompile (rounded to 3 digits) + avgTimePerCall: number // Avg time per call of this opcode/precompile (rounded to 3 digits) + gasUsed: number // Total amount of gas used by this opcode/precompile + millionGasPerSecond: number // How much million gas is executed per second (rounded to 3 digits) + tag: string // opcode/precompile tag +} + +type EVMPerformanceLogs = { + [tag: string]: EVMPerformanceLogEntry +} + +export class Timer { + private startTime: number + private runTime = 0 + tag: string + + constructor(tag: string) { + this.tag = tag + this.startTime = performance.now() + } + + pause() { + this.runTime = this.runTime + performance.now() - this.startTime + } + + unpause() { + this.startTime = performance.now() + } + + time() { + return (performance.now() - this.startTime + this.runTime) / 1000 + } +} + +export class EVMPerformanceLogger { + private opcodes!: EVMPerformanceLogs + private precompiles!: EVMPerformanceLogs + + private currentTimer?: Timer + + constructor() { + this.clear() + } + + clear() { + this.opcodes = {} + this.precompiles = {} + } + + getLogs() { + // Return nicely formatted logs + function getLogsFor(obj: EVMPerformanceLogs) { + const output: EVMPerformanceLogOutput[] = [] + for (const key in obj) { + const field = obj[key] + const entry = { + calls: field.calls, + totalTime: Math.round(field.time * 1e6) / 1e3, + avgTimePerCall: Math.round((field.time / field.calls) * 1e6) / 1e3, + gasUsed: field.gasUsed, + millionGasPerSecond: Math.round(field.gasUsed / field.time / 1e3) / 1e3, + tag: key, + } + output.push(entry) + } + + output.sort((a, b) => { + return b.millionGasPerSecond - a.millionGasPerSecond + }) + + return output + } + + return { + opcodes: getLogsFor(this.opcodes), + precompiles: getLogsFor(this.precompiles), + } + } + + // Start a new timer + // Only one timer can be timing at the same time + startTimer(tag: string) { + if (this.currentTimer !== undefined) { + throw new Error('Cannot have two timers running at the same time') + } + + this.currentTimer = new Timer(tag) + return this.currentTimer + } + + // Pauses current timer and returns that timer + pauseTimer() { + const timer = this.currentTimer + if (timer === undefined) { + throw new Error('No timer to pause') + } + timer.pause() + this.currentTimer = undefined + return timer + } + + // Unpauses current timer and returns that timer + unpauseTimer(timer: Timer) { + if (this.currentTimer !== undefined) { + throw new Error('Cannot unpause timer: another timer is already running') + } + timer.unpause() + this.currentTimer = timer + } + + // Stops a timer from running + stopTimer(timer: Timer, gasUsed: number, targetTimer: 'precompiles' | 'opcodes' = 'opcodes') { + if (this.currentTimer !== undefined && this.currentTimer !== timer) { + throw new Error('Cannot unpause timer: another timer is already running') + } + const time = timer.time() + const tag = timer.tag + this.currentTimer = undefined + + // Update the fields + const target = this[targetTimer] + if (target[tag] === undefined) { + target[tag] = { + calls: 0, + time: 0, + gasUsed: 0, + } + } + const obj = target[tag] + + obj.calls++ + obj.time += time + obj.gasUsed += gasUsed + } +} diff --git a/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts b/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts index 918ecd32051..9b00ef55690 100644 --- a/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts +++ b/packages/evm/src/precompiles/0a-kzg-point-evaluation.ts @@ -41,8 +41,8 @@ export async function precompile0a(opts: PrecompileInput): Promise { return EvmErrorResult(new EvmError(ERROR.INVALID_INPUT_LENGTH), opts.gasLimit) } - const version = Number(opts.common.paramByEIP('sharding', 'blobCommitmentVersionKzg', 4844)) - const fieldElementsPerBlob = opts.common.paramByEIP('sharding', 'fieldElementsPerBlob', 4844)! + const version = Number(opts.common.param('sharding', 'blobCommitmentVersionKzg')) + const fieldElementsPerBlob = opts.common.param('sharding', 'fieldElementsPerBlob') const versionedHash = opts.data.subarray(0, 32) const z = opts.data.subarray(32, 64) const y = opts.data.subarray(64, 96) diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index d3dacfd1ad1..e7477ba1693 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -19,6 +19,7 @@ interface PrecompileEntry { address: string check: PrecompileAvailabilityCheckType precompile: PrecompileFunc + name: string } interface Precompiles { @@ -54,6 +55,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Chainstart, }, precompile: precompile01, + name: 'ECRECOVER (0x01)', }, { address: '0000000000000000000000000000000000000002', @@ -62,6 +64,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Chainstart, }, precompile: precompile02, + name: 'SHA256 (0x02)', }, { address: '0000000000000000000000000000000000000003', @@ -70,6 +73,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Chainstart, }, precompile: precompile03, + name: 'RIPEMD160 (0x03)', }, { address: '0000000000000000000000000000000000000004', @@ -78,6 +82,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Chainstart, }, precompile: precompile04, + name: 'Identity (0x04)', }, { address: '0000000000000000000000000000000000000005', @@ -86,6 +91,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Byzantium, }, precompile: precompile05, + name: 'MODEXP (0x05)', }, { address: '0000000000000000000000000000000000000006', @@ -94,6 +100,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Byzantium, }, precompile: precompile06, + name: 'ECADD (0x06)', }, { address: '0000000000000000000000000000000000000007', @@ -102,6 +109,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Byzantium, }, precompile: precompile07, + name: 'ECMUL (0x07)', }, { address: '0000000000000000000000000000000000000008', @@ -110,6 +118,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Byzantium, }, precompile: precompile08, + name: 'ECPAIR (0x08)', }, { address: '0000000000000000000000000000000000000009', @@ -118,6 +127,7 @@ const precompileEntries: PrecompileEntry[] = [ param: Hardfork.Istanbul, }, precompile: precompile09, + name: 'BLAKE2f (0x09)', }, { address: '000000000000000000000000000000000000000a', @@ -126,6 +136,7 @@ const precompileEntries: PrecompileEntry[] = [ param: 4844, }, precompile: precompile0a, + name: 'KZG (0x0a)', }, ] @@ -183,6 +194,20 @@ function getActivePrecompiles( return precompileMap } -export { getActivePrecompiles, precompileEntries, precompiles, ripemdPrecompileAddress } +function getPrecompileName(addressUnprefixedStr: string) { + for (const entry of precompileEntries) { + if (entry.address === addressUnprefixedStr) { + return entry.name + } + } +} + +export { + getActivePrecompiles, + getPrecompileName, + precompileEntries, + precompiles, + ripemdPrecompileAddress, +} export type { AddPrecompile, CustomPrecompile, DeletePrecompile, PrecompileFunc, PrecompileInput } diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index 5cec8e597f9..40926c3b5df 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -158,6 +158,11 @@ export interface EVMInterface { events?: AsyncEventEmitter } +export type EVMProfilerOpts = { + enabled: boolean + // extra options here (such as use X hardfork for gas) +} + /** * Options for instantiating a {@link EVM}. */ @@ -254,6 +259,11 @@ export interface EVMOpts { * */ blockchain?: Blockchain + + /** + * + */ + profiler?: EVMProfilerOpts } /** diff --git a/packages/statemanager/src/stateManager.ts b/packages/statemanager/src/stateManager.ts index a8f842dff2d..2893d0b5fd4 100644 --- a/packages/statemanager/src/stateManager.ts +++ b/packages/statemanager/src/stateManager.ts @@ -926,6 +926,9 @@ export class DefaultStateManager implements EVMStateManagerInterface { * 2. Cache values are generally not copied along */ shallowCopy(): DefaultStateManager { + const common = this.common.copy() + common.setHardfork(this.common.hardfork()) + const trie = this._trie.shallowCopy(false) const prefixCodeHashes = this._prefixCodeHashes let accountCacheOpts = { ...this._accountCacheSettings } @@ -938,6 +941,7 @@ export class DefaultStateManager implements EVMStateManagerInterface { } return new DefaultStateManager({ + common, trie, prefixCodeHashes, accountCacheOpts, diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 9e71e038757..8845f21811a 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -3,7 +3,6 @@ import { Address, MAX_INTEGER, MAX_UINT64, - SECP256K1_ORDER_DIV_2, bigIntToHex, bytesToBigInt, bytesToHex, @@ -179,28 +178,6 @@ export abstract class BaseTransaction return errors.length === 0 } - protected _validateYParity() { - const { v } = this - if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) { - const msg = this._errorMsg('The y-parity of the transaction should either be 0 or 1') - throw new Error(msg) - } - } - - /** - * EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid. - * Reasoning: https://ethereum.stackexchange.com/a/55728 - */ - protected _validateHighS() { - const { s } = this - if (this.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) { - const msg = this._errorMsg( - 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid' - ) - throw new Error(msg) - } - } - /** * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) */ diff --git a/packages/tx/src/capabilities/eip1559.ts b/packages/tx/src/capabilities/eip1559.ts new file mode 100644 index 00000000000..8c51884994f --- /dev/null +++ b/packages/tx/src/capabilities/eip1559.ts @@ -0,0 +1,13 @@ +import type { Transaction, TransactionType } from '../types.js' + +type TypedTransactionEIP1559 = + | Transaction[TransactionType.BlobEIP4844] + | Transaction[TransactionType.FeeMarketEIP1559] + +export function getUpfrontCost(tx: TypedTransactionEIP1559, baseFee: bigint): bigint { + const prio = tx.maxPriorityFeePerGas + const maxBase = tx.maxFeePerGas - baseFee + const inclusionFeePerGas = prio < maxBase ? prio : maxBase + const gasPrice = inclusionFeePerGas + baseFee + return tx.gasLimit * gasPrice + tx.value +} diff --git a/packages/tx/src/capabilities/eip2930.ts b/packages/tx/src/capabilities/eip2930.ts new file mode 100644 index 00000000000..c97261c0839 --- /dev/null +++ b/packages/tx/src/capabilities/eip2930.ts @@ -0,0 +1,42 @@ +import { RLP } from '@ethereumjs/rlp' +import { concatBytes, hexToBytes } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' + +import { BaseTransaction } from '../baseTransaction.js' +import { type TypedTransaction } from '../types.js' +import { AccessLists } from '../util.js' + +import type { Transaction, TransactionType } from '../types.js' + +type TypedTransactionEIP2930 = Exclude + +/** + * The amount of gas paid for the data in this tx + */ +export function getDataFee(tx: TypedTransactionEIP2930): bigint { + if (tx['cache'].dataFee && tx['cache'].dataFee.hardfork === tx.common.hardfork()) { + return tx['cache'].dataFee.value + } + + let cost = BaseTransaction.prototype.getDataFee.bind(tx)() + cost += BigInt(AccessLists.getDataFeeEIP2930(tx.accessList, tx.common)) + + if (Object.isFrozen(tx)) { + tx['cache'].dataFee = { + value: cost, + hardfork: tx.common.hardfork(), + } + } + + return cost +} + +export function getHashedMessageToSign(tx: TypedTransactionEIP2930): Uint8Array { + return keccak256(tx.getMessageToSign()) +} + +export function serialize(tx: TypedTransactionEIP2930): Uint8Array { + const base = tx.raw() + const txTypeBytes = hexToBytes('0x' + tx.type.toString(16).padStart(2, '0')) + return concatBytes(txTypeBytes, RLP.encode(base)) +} diff --git a/packages/tx/src/capabilities/generic.ts b/packages/tx/src/capabilities/generic.ts new file mode 100644 index 00000000000..b57dc7e7d3e --- /dev/null +++ b/packages/tx/src/capabilities/generic.ts @@ -0,0 +1,85 @@ +import { SECP256K1_ORDER_DIV_2, bigIntToUnpaddedBytes, ecrecover } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' + +import { Capability, type TypedTransaction } from '../types.js' + +export function isSigned(tx: TypedTransaction): boolean { + const { v, r, s } = tx + if (v === undefined || r === undefined || s === undefined) { + return false + } else { + return true + } +} + +export function errorMsg(tx: TypedTransaction, msg: string) { + return `${msg} (${tx.errorStr()})` +} + +export function hash(tx: TypedTransaction): Uint8Array { + if (!tx.isSigned()) { + const msg = errorMsg(tx, 'Cannot call hash method if transaction is not signed') + throw new Error(msg) + } + + if (Object.isFrozen(tx)) { + if (!tx['cache'].hash) { + tx['cache'].hash = keccak256(tx.serialize()) + } + return tx['cache'].hash + } + + return keccak256(tx.serialize()) +} + +/** + * EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid. + * Reasoning: https://ethereum.stackexchange.com/a/55728 + */ +export function validateHighS(tx: TypedTransaction): void { + const { s } = tx + if (tx.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) { + const msg = errorMsg( + tx, + 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid' + ) + throw new Error(msg) + } +} + +export function validateYParity(tx: TypedTransaction) { + const { v } = tx + if (v !== undefined && v !== BigInt(0) && v !== BigInt(1)) { + const msg = errorMsg(tx, 'The y-parity of the transaction should either be 0 or 1') + throw new Error(msg) + } +} + +export function getSenderPublicKey(tx: TypedTransaction): Uint8Array { + if (tx['cache'].senderPubKey !== undefined) { + return tx['cache'].senderPubKey + } + + const msgHash = tx.getMessageToVerifySignature() + + const { v, r, s } = tx + + validateHighS(tx) + + try { + const sender = ecrecover( + msgHash, + v!, + bigIntToUnpaddedBytes(r!), + bigIntToUnpaddedBytes(s!), + tx.supports(Capability.EIP155ReplayProtection) ? tx.common.chainId() : undefined + ) + if (Object.isFrozen(tx)) { + tx['cache'].senderPubKey = sender + } + return sender + } catch (e: any) { + const msg = errorMsg(tx, 'Invalid Signature') + throw new Error(msg) + } +} diff --git a/packages/tx/src/eip1559Transaction.ts b/packages/tx/src/eip1559Transaction.ts index 4d7f7a2094a..13635a692ef 100644 --- a/packages/tx/src/eip1559Transaction.ts +++ b/packages/tx/src/eip1559Transaction.ts @@ -6,15 +6,16 @@ import { bytesToBigInt, bytesToHex, concatBytes, - ecrecover, equalsBytes, hexToBytes, toBytes, validateNoLeadingZeroes, } from '@ethereumjs/util' -import { keccak256 } from 'ethereum-cryptography/keccak.js' import { BaseTransaction } from './baseTransaction.js' +import * as EIP1559 from './capabilities/eip1559.js' +import * as EIP2930 from './capabilities/eip2930.js' +import * as Generic from './capabilities/generic.js' import { TransactionType } from './types.js' import { AccessLists } from './util.js' @@ -188,8 +189,8 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction toBytes(vh)) - this._validateYParity() - this._validateHighS() + Generic.validateYParity(this) + Generic.validateHighS(this) for (const hash of this.versionedHashes) { if (hash.length !== 32) { const msg = this._errorMsg('versioned hash is invalid length') throw new Error(msg) } - if ( - BigInt(hash[0]) !== this.common.paramByEIP('sharding', 'blobCommitmentVersionKzg', 4844) - ) { + if (BigInt(hash[0]) !== this.common.param('sharding', 'blobCommitmentVersionKzg')) { const msg = this._errorMsg('versioned hash does not start with KZG commitment version') throw new Error(msg) } @@ -352,7 +351,7 @@ export class BlobEIP4844Transaction extends BaseTransaction { const msg = this._errorMsg('gas limit * gasPrice cannot exceed MAX_INTEGER (2^256-1)') throw new Error(msg) } + this._validateCannotExceedMaxInteger({ gasPrice: this.gasPrice }) BaseTransaction._validateNotArray(txData) @@ -256,19 +257,7 @@ export class LegacyTransaction extends BaseTransaction { * Use {@link Transaction.getMessageToSign} to get a tx hash for the purpose of signing. */ hash(): Uint8Array { - if (!this.isSigned()) { - const msg = this._errorMsg('Cannot call hash method if transaction is not signed') - throw new Error(msg) - } - - if (Object.isFrozen(this)) { - if (!this.cache.hash) { - this.cache.hash = keccak256(RLP.encode(this.raw())) - } - return this.cache.hash - } - - return keccak256(RLP.encode(this.raw())) + return Generic.hash(this) } /** @@ -286,32 +275,7 @@ export class LegacyTransaction extends BaseTransaction { * Returns the public key of the sender */ getSenderPublicKey(): Uint8Array { - if (this.cache.senderPubKey !== undefined) { - return this.cache.senderPubKey - } - - const msgHash = this.getMessageToVerifySignature() - - const { v, r, s } = this - - this._validateHighS() - - try { - const sender = ecrecover( - msgHash, - v!, - bigIntToUnpaddedBytes(r!), - bigIntToUnpaddedBytes(s!), - this.supports(Capability.EIP155ReplayProtection) ? this.common.chainId() : undefined - ) - if (Object.isFrozen(this)) { - this.cache.senderPubKey = sender - } - return sender - } catch (e: any) { - const msg = this._errorMsg('Invalid Signature') - throw new Error(msg) - } + return Generic.getSenderPublicKey(this) } /** @@ -413,6 +377,6 @@ export class LegacyTransaction extends BaseTransaction { * @hidden */ protected _errorMsg(msg: string) { - return `${msg} (${this.errorStr()})` + return Generic.errorMsg(this, msg) } } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 293ed9280be..92bcc4c87f2 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -30,7 +30,7 @@ import type { TxReceipt, } from './types.js' import type { VM } from './vm.js' -import type { EVMInterface } from '@ethereumjs/evm' +import type { EVM, EVMInterface } from '@ethereumjs/evm' const { debug: createDebugLogger } = debugDefault @@ -224,6 +224,14 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promisethis.evm).getPerformanceLogs() + const tag = ' - Block ' + Number(block.header.number) + ' (' + bytesToHex(block.hash()) + ')' + this.emitEVMProfile(logs.precompiles, 'Precompile performance ' + tag) + this.emitEVMProfile(logs.opcodes, 'Opcodes performance' + tag) + ;(this.evm).clearPerformanceLogs() + } + return results } diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 7ca159fa8c8..2ccb326f5f3 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -27,6 +27,7 @@ import type { } from './types.js' import type { VM } from './vm.js' import type { AccessList, AccessListItem } from '@ethereumjs/common' +import type { EVM } from '@ethereumjs/evm' import type { AccessListEIP2930Transaction, FeeMarketEIP1559Transaction, @@ -162,6 +163,13 @@ export async function runTx(this: VM, opts: RunTxOpts): Promise { this.evm.journal.cleanJournal() } this.evm.stateManager.originalStorageCache.clear() + if (this._opts.profilerOpts?.reportAfterTx === true) { + const logs = (this.evm).getPerformanceLogs() + const tag = ' - Tx ' + bytesToHex(opts.tx.hash()) + this.emitEVMProfile(logs.precompiles, 'Precompile performance ' + tag) + this.emitEVMProfile(logs.opcodes, 'Opcodes performance' + tag) + ;(this.evm).clearPerformanceLogs() + } } } diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 182f74bea4e..0e7c5b92584 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -64,6 +64,11 @@ export interface EIP4844BlobTxReceipt extends PostByzantiumTxReceipt { blobGasPrice: bigint } +export type EVMProfilerOpts = { + enabled: boolean + // extra options here (such as use X hardfork for gas) +} + export type VMEvents = { beforeBlock: (data: Block, resolve?: (result?: any) => void) => void afterBlock: (data: AfterBlockEvent, resolve?: (result?: any) => void) => void @@ -71,6 +76,12 @@ export type VMEvents = { afterTx: (data: AfterTxEvent, resolve?: (result?: any) => void) => void } +export type VMProfilerOpts = { + //evmProfilerOpts: EVMProfilerOpts + reportAfterTx?: boolean + reportAfterBlock?: boolean +} + /** * Options for instantiating a {@link VM}. */ @@ -140,6 +151,8 @@ export interface VMOpts { * Use a custom EVM to run Messages on. If this is not present, use the default EVM. */ evm?: EVMInterface + + profilerOpts?: VMProfilerOpts } /** diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index f51a9231f1d..4946058797f 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -21,6 +21,7 @@ import type { import type { BlockchainInterface } from '@ethereumjs/blockchain' import type { EVMStateManagerInterface } from '@ethereumjs/common' import type { EVMInterface } from '@ethereumjs/evm' +import type { EVMPerformanceLogOutput } from '@ethereumjs/evm/dist/cjs/logger.js' import type { BigIntLike, GenesisState } from '@ethereumjs/util' /** @@ -113,14 +114,28 @@ export class VM { 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 { + const enableProfiler = + this._opts.profilerOpts?.reportAfterBlock ?? this._opts.profilerOpts?.reportAfterTx ?? false this.evm = new EVM({ common: this.common, stateManager: this.stateManager, blockchain: this.blockchain, + profiler: { + enabled: enableProfiler, + }, }) } @@ -239,6 +254,7 @@ export class VM { common, evm: evmCopy, setHardfork: this._setHardfork, + profilerOpts: this._opts.profilerOpts, }) } @@ -255,4 +271,81 @@ export class VM { 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 + } + const colOrder = [ + 'tag', + 'calls', + 'avgTimePerCall', + 'totalTime', + 'gasUsed', + 'millionGasPerSecond', + ] + const colNames = ['tag', 'calls', 'ms/call', 'total (ms)', 'gas used', 'Mgas/s'] + function padStr(str: string | number, leftpad: number) { + return ' ' + str.toString().padStart(leftpad, ' ') + ' ' + } + function strLen(str: string | number) { + return padStr(str, 0).length - 2 + } + + 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 + colLength[ins] = Math.max(colLength[ins] ?? 0, strLen(entry[key])) + ins++ + } + } + + for (const i in colLength) { + colLength[i] = Math.max(colLength[i] ?? 0, strLen(colNames[i])) + } + + const headerLength = colLength.reduce((pv, cv) => pv + cv, 0) + colLength.length * 3 - 1 + // eslint-disable-next-line + console.log('+== ' + profileTitle + ' ==+') + + const header = '|' + '-'.repeat(headerLength) + '|' + // eslint-disable-next-line + console.log(header) + + let str = '' + for (const i in colLength) { + str += '|' + padStr(colNames[i], colLength[i]) + } + str += '|' + + // eslint-disable-next-line + console.log(str) + + for (const entry of logs) { + let str = '' + let i = 0 + for (const key of colOrder) { + //@ts-ignore + str += '|' + padStr(entry[key], colLength[i]) + i++ + } + str += '|' + // eslint-disable-next-line + console.log(str) + } + + const footer = '+' + '-'.repeat(headerLength) + '+' + // eslint-disable-next-line + console.log(footer) + } } diff --git a/packages/vm/test/tester/config.ts b/packages/vm/test/tester/config.ts index f64ca57c973..fc6aaad7202 100644 --- a/packages/vm/test/tester/config.ts +++ b/packages/vm/test/tester/config.ts @@ -38,60 +38,16 @@ export const SKIP_PERMANENT = [ * tests running slow (run from time to time) */ export const SKIP_SLOW = [ - 'Call50000_sha256', - 'static_Call50000', - 'static_Call50000_ecrec', - 'static_Call50000_identity', - 'static_Call50000_identity2', - 'static_Call50000_rip160', - 'static_Return50000_2', - 'Return50000', - 'Return50000_2', - 'static_Call50000_sha256', - 'CALLBlake2f_MaxRounds', + 'Call50000_sha256', // Last check: 2023-08-24, Constantinople HF, still slow (1-2 minutes per block execution) + 'CALLBlake2f_MaxRounds', // Last check: 2023-08-24, Berlin HF, still very slow (several minutes per block execution) + 'Return50000', // Last check: 2023-08-24, Constantinople HF, still slow (1-2 minutes per block execution) + 'Return50000_2', // Last check: 2023-08-24, Constantinople HF, still slow (1-2 minutes per block execution) + 'static_Call50000_sha256', // Last check: 2023-08-24, Berlin HF, still slow (30-60 secs per block execution) // vmPerformance tests - 'loopMul', - 'loopExp', + 'loopMul', // Last check: 2023-08-24, Berlin HF, still very slow (up to minutes per block execution) + 'loopExp', // Last check: 2023-08-24, Berlin HF, somewhat slow (5-10 secs per block execution) ] -/** - * VMTests have been deprecated, see https://github.com/ethereum/tests/issues/593 - * skipVM test list is currently not used but might be useful in the future since VMTests - * have now been converted to BlockchainTests, see https://github.com/ethereum/tests/pull/680 - * Disabling this due to ESLint, but will keep it here for possible future reference - */ -/*const SKIP_VM = [ - // slow performance tests - 'loop-mul', - 'loop-add-10M', - 'loop-divadd-10M', - 'loop-divadd-unr100-10M', - 'loop-exp-16b-100k', - 'loop-exp-1b-1M', - 'loop-exp-2b-100k', - 'loop-exp-32b-100k', - 'loop-exp-4b-100k', - 'loop-exp-8b-100k', - 'loop-exp-nop-1M', - 'loop-mulmod-2M', - 'ABAcalls0', - 'ABAcallsSuicide0', - 'ABAcallsSuicide1', - 'sha3_bigSize', - 'CallRecursiveBomb0', - 'CallToNameRegistrator0', - 'CallToPrecompiledContract', - 'CallToReturn1', - 'PostToNameRegistrator0', - 'PostToReturn1', - 'callcodeToNameRegistrator0', - 'callcodeToReturn1', - 'callstatelessToNameRegistrator0', - 'callstatelessToReturn1', - 'createNameRegistrator', - 'randomTest643', -]*/ - /** * Returns an alias for specified hardforks to meet test dependencies requirements/assumptions. * @param {String} forkConfig - the name of the hardfork for which an alias should be returned @@ -409,8 +365,8 @@ const expectedTestsFull: { Byzantium: 15703, Constantinople: 33146, Petersburg: 33128, - Istanbul: 38773, - MuirGlacier: 38773, + Istanbul: 38340, + MuirGlacier: 38340, Berlin: 41365, London: 61197, ArrowGlacier: 0, diff --git a/packages/vm/test/tester/index.ts b/packages/vm/test/tester/index.ts index a71e798aa96..7b694f0a0d6 100755 --- a/packages/vm/test/tester/index.ts +++ b/packages/vm/test/tester/index.ts @@ -200,7 +200,6 @@ async function runTests() { tape(name, async (t) => { let testIdentifier: string const failingTests: Record = {} - ;(t as any).on('result', (o: any) => { if ( typeof o.ok !== 'undefined' && @@ -218,6 +217,7 @@ async function runTests() { // https://github.com/ethereum/tests/releases/tag/v7.0.0-beta.1 const dirs = getTestDirs(FORK_CONFIG_VM, name) + console.time('Total (including setup)') for (const dir of dirs) { await new Promise((resolve, reject) => { if (argv.customTestsPath !== undefined) { @@ -263,6 +263,9 @@ async function runTests() { t.ok(assertCount >= expectedTests, `expected ${expectedTests} checks, got ${assertCount}`) } + console.log() + console.timeEnd('Total (including setup)') + t.end() }) } diff --git a/packages/wallet/docs/classes/hdkey.EthereumHDKey.md b/packages/wallet/docs/classes/hdkey.EthereumHDKey.md index 2d60198d120..778ee9a95eb 100644 --- a/packages/wallet/docs/classes/hdkey.EthereumHDKey.md +++ b/packages/wallet/docs/classes/hdkey.EthereumHDKey.md @@ -34,7 +34,7 @@ #### Defined in -[hdkey.ts:23](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L23) +[hdkey.ts:27](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L27) ## Methods @@ -56,7 +56,7 @@ Derive a node based on a child index #### Defined in -[hdkey.ts:52](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L52) +[hdkey.ts:56](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L56) ___ @@ -78,7 +78,7 @@ Derives a node based on a path (e.g. m/44'/0'/0/1) #### Defined in -[hdkey.ts:45](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L45) +[hdkey.ts:49](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L49) ___ @@ -94,7 +94,7 @@ Return a `Wallet` instance as seen above #### Defined in -[hdkey.ts:59](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L59) +[hdkey.ts:63](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L63) ___ @@ -110,7 +110,7 @@ Returns a BIP32 extended private key (xprv) #### Defined in -[hdkey.ts:28](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L28) +[hdkey.ts:32](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L32) ___ @@ -126,7 +126,7 @@ Return a BIP32 extended public key (xpub) #### Defined in -[hdkey.ts:38](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L38) +[hdkey.ts:42](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L42) ___ @@ -148,7 +148,7 @@ Create an instance based on a BIP32 extended private or public key. #### Defined in -[hdkey.ts:19](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L19) +[hdkey.ts:23](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L23) ___ @@ -158,9 +158,6 @@ ___ Creates an instance based on a seed. -For the seed we suggest to use [bip39](https://npmjs.org/package/bip39) to -create one from a BIP39 mnemonic. - #### Parameters | Name | Type | @@ -173,4 +170,27 @@ create one from a BIP39 mnemonic. #### Defined in -[hdkey.ts:12](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L12) +[hdkey.ts:9](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L9) + +___ + +### fromMnemonic + +▸ `Static` **fromMnemonic**(`mnemonic`, `_passphrase`): [`EthereumHDKey`](hdkey.EthereumHDKey.md) + +Creates an instance based on a [BIP39 Mnemonic phrases](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki). + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `mnemonic` | `string` | +| `_passphrase` | `string` | + +#### Returns + +[`EthereumHDKey`](hdkey.EthereumHDKey.md) + +#### Defined in + +[hdkey.ts:16](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/wallet/src/hdkey.ts#L16) diff --git a/packages/wallet/src/hdkey.ts b/packages/wallet/src/hdkey.ts index 2bf9527d95e..b4fddac7eaf 100644 --- a/packages/wallet/src/hdkey.ts +++ b/packages/wallet/src/hdkey.ts @@ -1,3 +1,4 @@ +import { mnemonicToSeedSync } from 'ethereum-cryptography/bip39/index.js' import { HDKey } from 'ethereum-cryptography/hdkey.js' import { Wallet } from './wallet.js' @@ -5,14 +6,18 @@ import { Wallet } from './wallet.js' export class EthereumHDKey { /** * Creates an instance based on a seed. - * - * For the seed we suggest to use [bip39](https://npmjs.org/package/bip39) to - * create one from a BIP39 mnemonic. */ public static fromMasterSeed(seedBuffer: Uint8Array): EthereumHDKey { return new EthereumHDKey(HDKey.fromMasterSeed(seedBuffer)) } + /** + * Creates an instance based on BIP39 mnemonic phrases + */ + public static fromMnemonic(mnemonic: string, passphrase?: string): EthereumHDKey { + return EthereumHDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase)) + } + /** * Create an instance based on a BIP32 extended private or public key. */ diff --git a/packages/wallet/test/hdkey.spec.ts b/packages/wallet/test/hdkey.spec.ts index 04024b3640b..920f390854f 100644 --- a/packages/wallet/test/hdkey.spec.ts +++ b/packages/wallet/test/hdkey.spec.ts @@ -8,6 +8,9 @@ const fixtureseed = hexToBytes( '0x747f302d9c916698912d5f70be53a6cf53bc495803a5523d3a7c3afa2afba94ec3803f838b3e1929ab5481f9da35441372283690fdcf27372c38f40ba134fe03' ) const fixturehd = EthereumHDKey.fromMasterSeed(fixtureseed) +const fixtureMnemonic = 'awake book subject inch gentle blur grant damage process float month clown' +const fixtureAddress = '0x4dcccf58c6573eb896250b0c9647a40c1673af44' +const fixturePrivateKey = '0xf62a8ea4ab7025d151ccd84981c66278d0d3cd58ff837467cdc51229915a22d1' describe('HD Key tests', () => { it('.fromMasterSeed()', () => { assert.doesNotThrow(function () { @@ -15,6 +18,15 @@ describe('HD Key tests', () => { }) }) + it('.fromMnemonic()', () => { + const path = "m/44'/60'/0'/0/0" + const hdWallet = EthereumHDKey.fromMnemonic(fixtureMnemonic) + const wallet = hdWallet.derivePath(path).getWallet() + + assert.strictEqual(wallet.getPrivateKeyString(), fixturePrivateKey) + assert.strictEqual(wallet.getAddressString(), fixtureAddress) + }) + it('.privateExtendedKey()', () => { assert.deepEqual( fixturehd.privateExtendedKey(),