UNPKG

@polygon-hermez/vm

Version:
429 lines (390 loc) 13.8 kB
import { CheckpointTrie as Trie } from 'merkle-patricia-tree' import { Account, Address, BNLike } from 'ethereumjs-util' import Blockchain from '@ethereumjs/blockchain' import Common, { Chain } from '@ethereumjs/common' import { StateManager, DefaultStateManager } from './state/index' import { default as runCode, RunCodeOpts } from './runCode' import { default as runCall, RunCallOpts } from './runCall' import { default as runTx, RunTxOpts, RunTxResult } from './runTx' import { default as runBlock, RunBlockOpts, RunBlockResult } from './runBlock' import { default as buildBlock, BuildBlockOpts, BlockBuilder } from './buildBlock' import { EVMResult, ExecResult } from './evm/evm' import { OpcodeList, getOpcodesForHF } from './evm/opcodes' import { precompiles } from './evm/precompiles' import runBlockchain from './runBlockchain' const AsyncEventEmitter = require('async-eventemitter') import { promisify } from 'util' // very ugly way to detect if we are running in a browser const isBrowser = new Function('try {return this===window;}catch(e){ return false;}') let mcl: any let mclInitPromise: any if (!isBrowser()) { mcl = require('mcl-wasm') mclInitPromise = mcl.init(mcl.BLS12_381) } /** * Options for instantiating a {@link VM}. */ export interface VMOpts { /** * Use a {@link Common} instance * if you want to change the chain setup. * * ### Possible Values * * - `chain`: all chains supported by `Common` or a custom chain * - `hardfork`: `mainnet` hardforks up to the `MuirGlacier` hardfork * - `eips`: `2537` (usage e.g. `eips: [ 2537, ]`) * * ### Supported EIPs * * - [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - Fee Market * - [EIP-2315](https://eips.ethereum.org/EIPS/eip-2315) - VM simple subroutines * - [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) (`experimental`) - BLS12-381 precompiles * - [EIP-2565](https://eips.ethereum.org/EIPS/eip-2565) - ModExp Gas Cost * - [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) - Typed Transactions * - [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) - Gas cost increases for state access opcodes * - [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) - Access List Transaction Type * - [EIP-3198](https://eips.ethereum.org/EIPS/eip-3198) - BASEFEE opcode * - [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529) - Reduction in refunds * - [EIP-3541](https://eips.ethereum.org/EIPS/eip-3541) - Reject new contracts starting with the 0xEF byte * - [EIP-3855](https://eips.ethereum.org/EIPS/eip-3855) - PUSH0 instruction * * *Annotations:* * * - `experimental`: behaviour can change on patch versions * * ### Default Setup * * Default setup if no `Common` instance is provided: * * - `chain`: `mainnet` * - `hardfork`: `istanbul` * - `eips`: `[]` */ common?: Common /** * A {@link StateManager} instance to use as the state store (Beta API) */ stateManager?: StateManager /** * A {@link CheckpointTrie} instance for the state tree (ignored if stateManager is passed) * @deprecated - will be removed in next major version release */ state?: Trie /** * A {@link Blockchain} object for storing/retrieving blocks */ blockchain?: Blockchain /** * If true, create entries in the state tree for the precompiled contracts, saving some gas the * first time each of them is called. * * If this parameter is false, the first call to each of them has to pay an extra 25000 gas * for creating the account. * * Setting this to true has the effect of precompiled contracts' gas costs matching mainnet's from * the very first call, which is intended for testing networks. * * Default: `false` */ activatePrecompiles?: boolean /** * Allows unlimited contract sizes while debugging. By setting this to `true`, the check for * contract size limit of 24KB (see [EIP-170](https://git.io/vxZkK)) is bypassed. * * Default: `false` [ONLY set to `true` during debugging] */ allowUnlimitedContractSize?: boolean /** * Select hardfork based upon block number. This automatically switches to the right hard fork based upon the block number. * * Default: `false` */ hardforkByBlockNumber?: boolean /** * Select the HF by total difficulty (Merge HF) * * This option is a superset of `hardforkByBlockNumber` (so only use one of both options) * and determines the HF by both the block number and the TD. * * Since the TD is only a threshold the block number will in doubt take precedence (imagine * e.g. both Merge and Shanghai HF blocks set and the block number from the block provided * pointing to a Shanghai block: this will lead to set the HF as Shanghai and not the Merge). */ hardforkByTD?: BNLike } /** * 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 default class VM extends AsyncEventEmitter { /** * The StateManager used by the VM */ readonly stateManager: StateManager /** * The blockchain the VM operates on */ readonly blockchain: Blockchain readonly _common: Common protected readonly _opts: VMOpts protected _isInitialized: boolean = false protected readonly _allowUnlimitedContractSize: boolean protected _opcodes: OpcodeList protected readonly _hardforkByBlockNumber: boolean protected readonly _hardforkByTD?: BNLike /** * Cached emit() function, not for public usage * set to public due to implementation internals * @hidden */ public readonly _emit: (topic: string, data: any) => Promise<void> /** * Pointer to the mcl package, not for public usage * set to public due to implementation internals * @hidden */ public readonly _mcl: any // /** * 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 */ protected 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) await vm.init() return vm } /** * Instantiates a new {@link VM} Object. * @param opts */ constructor(opts: VMOpts = {}) { super() this._opts = opts // Throw on chain or hardfork options removed in latest major release // to prevent implicit chain setup on a wrong chain if ('chain' in opts || 'hardfork' in opts) { throw new Error('Chain/hardfork options are not allowed any more on initialization') } if (opts.common) { // Supported EIPs const supportedEIPs = [1559, 2315, 2537, 2565, 2718, 2929, 2930, 3198, 3529, 3541, 3607, 3855] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { throw new Error(`EIP-${eip} is not supported by the VM`) } } this._common = opts.common } else { const DEFAULT_CHAIN = Chain.Mainnet const supportedHardforks = [ 'chainstart', 'homestead', 'dao', 'tangerineWhistle', 'spuriousDragon', 'byzantium', 'constantinople', 'petersburg', 'istanbul', 'muirGlacier', 'berlin', 'arrowGlacier', ] this._common = new Common({ chain: DEFAULT_CHAIN, supportedHardforks, }) } this._common.on('hardforkChanged', () => { this._opcodes = getOpcodesForHF(this._common) }) // Set list of opcodes based on HF // TODO: make this EIP-friendly this._opcodes = getOpcodesForHF(this._common) if (opts.stateManager) { this.stateManager = opts.stateManager } else { const trie = opts.state ?? new Trie() this.stateManager = new DefaultStateManager({ trie, common: this._common, }) } this.blockchain = opts.blockchain ?? new Blockchain({ common: this._common }) this._allowUnlimitedContractSize = opts.allowUnlimitedContractSize ?? false if (opts.hardforkByBlockNumber !== undefined && opts.hardforkByTD !== undefined) { throw new Error( `The hardforkByBlockNumber and hardforkByTD options can't be used in conjunction` ) } this._hardforkByBlockNumber = opts.hardforkByBlockNumber ?? false this._hardforkByTD = opts.hardforkByTD if (this._common.isActivatedEIP(2537)) { if (isBrowser()) { throw new Error('EIP-2537 is currently not supported in browsers') } else { this._mcl = mcl } } // Safeguard if "process" is not available (browser) if (process !== undefined && process.env.DEBUG) { this.DEBUG = true } // We cache this promisified function as it's called from the main execution loop, and // promisifying each time has a huge performance impact. this._emit = promisify(this.emit.bind(this)) } async init(): Promise<void> { if (this._isInitialized) { return } await this.blockchain.initPromise if (this._opts.activatePrecompiles && !this._opts.stateManager) { await this.stateManager.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. await Promise.all( Object.keys(precompiles) .map((k: string): Address => new Address(Buffer.from(k, 'hex'))) .map(async (address: Address) => { const account = Account.fromAccountData({ balance: 1 }) await this.stateManager.putAccount(address, account) }) ) await this.stateManager.commit() } if (this._common.isActivatedEIP(2537)) { if (isBrowser()) { throw new Error('EIP-2537 is currently not supported in browsers') } else { const mcl = this._mcl await mclInitPromise // ensure that mcl is initialized. mcl.setMapToMode(mcl.IRTF) // set the right map mode; otherwise mapToG2 will return wrong values. mcl.verifyOrderG1(1) // subgroup checks for G1 mcl.verifyOrderG2(1) // subgroup checks for G2 } } this._isInitialized = true } /** * Processes blocks and adds them to the blockchain. * * This method modifies the state. * * @param blockchain - A {@link Blockchain} object to process */ async runBlockchain(blockchain?: Blockchain, maxBlocks?: number): Promise<void | number> { await this.init() return runBlockchain.bind(this)(blockchain, maxBlocks) } /** * 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> { await this.init() 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> { await this.init() return runTx.bind(this)(opts) } /** * runs a call (or create) operation. * * This method modifies the state. * * @param {RunCallOpts} opts */ async runCall(opts: RunCallOpts): Promise<EVMResult> { await this.init() return runCall.bind(this)(opts) } /** * Runs EVM code. * * This method modifies the state. * * @param {RunCodeOpts} opts */ async runCode(opts: RunCodeOpts): Promise<ExecResult> { await this.init() return runCode.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> { await this.init() return buildBlock.bind(this)(opts) } /** * Returns a list with the currently activated opcodes * available for VM execution */ getActiveOpcodes(): OpcodeList { return getOpcodesForHF(this._common) } /** * Returns a copy of the {@link VM} instance. */ copy(): VM { return new VM({ stateManager: this.stateManager.copy(), blockchain: this.blockchain.copy(), common: this._common.copy(), }) } /** * 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 } }