@nomicfoundation/ethereumjs-vm
Version:
An Ethereum VM implementation
352 lines • 14.5 kB
JavaScript
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';
/**
* 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 {
/**
* 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
*/
constructor(opts = {}) {
this._isInitialized = false;
/**
* 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
*/
this.DEBUG = false;
this.events = new AsyncEventEmitter();
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({ 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, data) => {
return new Promise((resolve) => this.events.emit(topic, 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;
}
/**
* VM async constructor. Creates engine instance and initializes it.
*
* @param opts VM engine constructor options
*/
static async create(opts = {}) {
const vm = new this(opts);
const genesisStateOpts = opts.stateManager === undefined && opts.genesisState === undefined
? { genesisState: {} }
: undefined;
await vm.init({ ...genesisStateOpts, ...opts });
return vm;
}
async init({ genesisState } = {}) {
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 this.blockchain._init === 'function') {
await this.blockchain._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) {
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) {
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) {
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) {
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._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) {
hf = 'error';
}
const errorStr = `vm hf=${hf}`;
return errorStr;
}
/**
* Emit EVM profile logs
* @param logs
* @param profileTitle
* @hidden
*/
emitEVMProfile(logs, profileTitle) {
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, leftpad) {
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) {
return padStr(str, 0).length - 2;
}
// Step one: calculate the length of each colum
const colLength = [];
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 = 30000000; // 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);
}
}
//# sourceMappingURL=vm.js.map