UNPKG

@ethereumjs/evm

Version:
1,135 lines 49.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Interpreter = void 0; const common_1 = require("@ethereumjs/common"); const util_1 = require("@ethereumjs/util"); const debug_1 = require("debug"); const eip7708_ts_1 = require("./eip7708.js"); const eip8037_ts_1 = require("./eip8037.js"); const constants_ts_1 = require("./eof/constants.js"); const container_ts_1 = require("./eof/container.js"); const setup_ts_1 = require("./eof/setup.js"); const verify_ts_1 = require("./eof/verify.js"); const errors_ts_1 = require("./errors.js"); const memory_ts_1 = require("./memory.js"); const message_ts_1 = require("./message.js"); const EIP7928_ts_1 = require("./opcodes/EIP7928.js"); const EIP8024_ts_1 = require("./opcodes/EIP8024.js"); const index_ts_1 = require("./opcodes/index.js"); const stack_ts_1 = require("./stack.js"); const stackDelta_ts_1 = require("./eof/stackDelta.js"); const debugGas = (0, debug_1.default)('evm:gas'); /** * Parses and executes EVM bytecode. */ class Interpreter { // TODO remove gasLeft as constructor argument constructor(evm, stateManager, blockchain, env, gasLeft, journal, performanceLogs, profilerOpts) { // Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...}) this.opDebuggers = {}; this._evm = evm; this._stateManager = stateManager; this.common = this._evm.common; if (this.common.consensusType() === 'poa' && this._evm['_optsCached'].cliqueSigner === undefined) throw (0, util_1.EthereumJSErrorWithoutCode)('Must include cliqueSigner function if clique/poa is being used for consensus type'); this._runState = { programCounter: 0, opCode: 0xfe, // INVALID opcode memory: new memory_ts_1.Memory(), memoryWordCount: util_1.BIGINT_0, highestMemCost: util_1.BIGINT_0, stack: new stack_ts_1.Stack(), code: new Uint8Array(0), validJumps: Uint8Array.from([]), cachedPushes: {}, stateManager: this._stateManager, blockchain, env, shouldDoJumpAnalysis: true, interpreter: this, gasRefund: env.gasRefund, gasLeft, returnBytes: new Uint8Array(0), }; this.journal = journal; this._env = env; this._result = { logs: env.initialLogs ? [...env.initialLogs] : [], returnValue: undefined, selfdestruct: new Map(), }; this.profilerOpts = profilerOpts; this.performanceLogger = performanceLogs; } async run(code, opts = {}) { if (!this.common.isActivatedEIP(3540) || code[0] !== constants_ts_1.FORMAT) { // EIP-3540 isn't active and first byte is not 0xEF - treat as legacy bytecode this._runState.code = code; } else if (this.common.isActivatedEIP(3540)) { if (code[1] !== constants_ts_1.MAGIC) { // Bytecode contains invalid EOF magic byte return { runState: this._runState, exceptionError: new errors_ts_1.EVMError(errors_ts_1.EVMError.errorMessages.INVALID_BYTECODE_RESULT), }; } if (code[2] !== constants_ts_1.VERSION) { // Bytecode contains invalid EOF version number return { runState: this._runState, exceptionError: new errors_ts_1.EVMError(errors_ts_1.EVMError.errorMessages.INVALID_EOF_FORMAT), }; } this._runState.code = code; const isTxCreate = this._env.isCreate && this._env.depth === 0; const eofMode = isTxCreate ? container_ts_1.EOFContainerMode.TxInitmode : container_ts_1.EOFContainerMode.Default; try { (0, setup_ts_1.setupEOF)(this._runState, eofMode); } catch { return { runState: this._runState, exceptionError: new errors_ts_1.EVMError(errors_ts_1.EVMError.errorMessages.INVALID_EOF_FORMAT), // TODO: verify if all gas should be consumed }; } if (isTxCreate) { // Tx tries to deploy container try { (0, container_ts_1.validateEOF)(this._runState.code, this._evm, verify_ts_1.ContainerSectionType.InitCode, container_ts_1.EOFContainerMode.TxInitmode); } catch { // Trying to deploy an invalid EOF container return { runState: this._runState, exceptionError: new errors_ts_1.EVMError(errors_ts_1.EVMError.errorMessages.INVALID_EOF_FORMAT), // TODO: verify if all gas should be consumed }; } } } this._runState.programCounter = opts.pc ?? this._runState.programCounter; // Check that the programCounter is in range const pc = this._runState.programCounter; if (pc !== 0 && (pc < 0 || pc >= this._runState.code.length)) { throw (0, util_1.EthereumJSErrorWithoutCode)('Internal error: program counter not in range'); } let err; let cachedOpcodes; let doJumpAnalysis = true; let timer; let overheadTimer; if (this.profilerOpts?.enabled === true && this.performanceLogger.hasTimer()) { timer = this.performanceLogger.pauseTimer(); overheadTimer = this.performanceLogger.startTimer('Overhead'); } // Iterate through the given ops until something breaks or we hit STOP while (this._runState.programCounter < this._runState.code.length) { const programCounter = this._runState.programCounter; let opCode; let opCodeObj; if (doJumpAnalysis) { opCode = this._runState.code[programCounter]; // Only run the jump destination analysis if `code` actually contains a JUMP/JUMPI/JUMPSUB opcode if (opCode === 0x56 || opCode === 0x57 || opCode === 0x5e) { const { jumps, pushes, opcodesCached } = this._getValidJumpDestinations(this._runState.code); this._runState.validJumps = jumps; this._runState.cachedPushes = pushes; this._runState.shouldDoJumpAnalysis = false; cachedOpcodes = opcodesCached; doJumpAnalysis = false; } } else { opCodeObj = cachedOpcodes[programCounter]; opCode = opCodeObj.opcodeInfo.code; } // if its an invalid opcode with binary activated, then check if its because of a missing code // chunk in the witness, and throw appropriate error to distinguish from an actual invalid opcode if (opCode === 0xfe && this.common.isActivatedEIP(7864) && // is this a code loaded from state using witnesses this._runState.env.chargeCodeAccesses === true) { const contract = this._runState.interpreter.getAddress(); if (!(await this._runState.stateManager.checkChunkWitnessPresent(contract, programCounter))) { throw Error(`Invalid witness with missing codeChunk for pc=${programCounter}`); } } this._runState.opCode = opCode; try { if (overheadTimer !== undefined) { this.performanceLogger.pauseTimer(); } await this.runStep(opCodeObj); if (overheadTimer !== undefined) { this.performanceLogger.unpauseTimer(overheadTimer); } } catch (e) { // Revert access witness changes if we revert - per EIP-4762 this._runState.env.accessWitness?.revert(); if (overheadTimer !== undefined) { this.performanceLogger.unpauseTimer(overheadTimer); } // re-throw on non-VM errors if (!('errorType' in e && e.errorType === errors_ts_1.EVMErrorTypeString)) { throw e; } // STOP is not an exception if (e.error !== errors_ts_1.EVMError.errorMessages.STOP) { err = e; } break; } } if (timer !== undefined) { this.performanceLogger.stopTimer(overheadTimer, 0); this.performanceLogger.unpauseTimer(timer); } return { runState: this._runState, exceptionError: err, }; } /** * Executes the opcode to which the program counter is pointing, * reducing its base gas cost, and increments the program counter. */ async runStep(opcodeObj) { const opEntry = opcodeObj ?? this.lookupOpInfo(this._runState.opCode); const opInfo = opEntry.opcodeInfo; let timer; if (this.profilerOpts?.enabled === true) { timer = this.performanceLogger.startTimer(opInfo.name); } let gas = opInfo.feeBigInt; // Cache pre-gas memory size if doing tracing (EIP-7756) const memorySizeCache = this._runState.memoryWordCount; try { if (opInfo.dynamicGas) { // This function updates the gas in-place. // It needs the base fee, for correct gas limit calculation for the CALL opcodes gas = await opEntry.gasHandler(this._runState, gas, 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, this.getGasLeft(), memorySizeCache); } if ((this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) && this._env.chargeCodeAccesses === true) { const contract = this._runState.interpreter.getAddress(); const statelessGas = this._runState.env.accessWitness.readAccountCodeChunks(contract, this._runState.programCounter, this._runState.programCounter); gas += statelessGas; debugGas(`codechunk accessed statelessGas=${statelessGas} (-> ${gas})`); } // Check for invalid opcode if (opInfo.isInvalid) { throw new errors_ts_1.EVMError(errors_ts_1.EVMError.errorMessages.INVALID_OPCODE); } // Reduce opcode's base fee this.useGas(gas, opInfo); // Advance program counter this._runState.programCounter++; (0, EIP7928_ts_1.consumeEip7928PostTargetCallOog)(this._runState); // Execute opcode handler const opFn = opEntry.opHandler; if (opInfo.isAsync) { await opFn.apply(null, [this._runState, this.common]); } else { opFn.apply(null, [this._runState, this.common]); } this._runState.env.accessWitness?.commit(); } finally { if (this.profilerOpts?.enabled === true) { this.performanceLogger.stopTimer(timer, Number(gas), 'opcodes', opInfo.fee, Number(gas) - opInfo.fee); } } } /** * Get info for an opcode from EVM's list of opcodes. */ lookupOpInfo(op) { return this._evm['_opcodeMap'][op]; } async _runStepHook(dynamicFee, gasLeft, memorySize) { const opcodeInfo = this.lookupOpInfo(this._runState.opCode).opcodeInfo; const section = this._env.eof?.container.header.getSectionFromProgramCounter(this._runState.programCounter); let error = undefined; let immediate = undefined; if (opcodeInfo.code === 0xfd) { // If opcode is REVERT, read error data and return in trace const [offset, length] = this._runState.stack.peek(2); error = new Uint8Array(0); if (length !== util_1.BIGINT_0) { error = this._runState.memory.read(Number(offset), Number(length)); } } // Add immediate if present (i.e. bytecode parameter for a preceding opcode like (RJUMP 01 - jumps to PC 1)) if (stackDelta_ts_1.stackDelta[opcodeInfo.code] !== undefined && stackDelta_ts_1.stackDelta[opcodeInfo.code].intermediates > 0) { immediate = this._runState.code.slice(this._runState.programCounter + 1, // immediates start "immediately" following current opcode this._runState.programCounter + 1 + stackDelta_ts_1.stackDelta[opcodeInfo.code].intermediates); } // Create event object for step const eventObj = { pc: this._runState.programCounter, gasLeft, gasRefund: this._runState.gasRefund, opcode: { name: opcodeInfo.fullName, fee: opcodeInfo.fee, dynamicFee, isAsync: opcodeInfo.isAsync, code: opcodeInfo.code, }, stack: this._runState.stack.getStack(), depth: this._env.depth, address: this._env.address, account: this._env.contract, memory: this._runState.memory._store.subarray(0, Number(memorySize) * 32), memoryWordCount: memorySize, codeAddress: this._env.codeAddress, stateManager: this._runState.stateManager, eofSection: section, immediate, error, eofFunctionDepth: this._env.eof !== undefined ? this._env.eof?.eofRunState.returnStack.length + 1 : undefined, }; if (this._evm.DEBUG) { // Create opTrace for debug functionality let hexStack = []; hexStack = eventObj.stack.map((item) => { return (0, util_1.bigIntToHex)(BigInt(item)); }); const name = eventObj.opcode.name; const opTrace = { pc: eventObj.pc, op: name, gas: (0, util_1.bigIntToHex)(eventObj.gasLeft), gasCost: (0, util_1.bigIntToHex)(dynamicFee), stack: hexStack, depth: eventObj.depth, }; if (!(name in this.opDebuggers)) { this.opDebuggers[name] = (0, debug_1.default)(`evm:ops:${name}`); } this.opDebuggers[name](JSON.stringify(opTrace)); } /** * The `step` event for trace output * * @event Event: step * @type {Object} * @property {Number} pc representing the program counter * @property {Object} opcode the next opcode to be ran * @property {string} opcode.name * @property {fee} opcode.number Base fee of the opcode * @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee * @property {boolean} opcode.isAsync opcode is async * @property {number} opcode.code opcode code * @property {BigInt} gasLeft amount of gasLeft * @property {BigInt} gasRefund gas refund * @property {StateManager} stateManager a {@link StateManager} instance * @property {Array} stack an `Array` of `Uint8Arrays` containing the stack * @property {Array} returnStack the return stack * @property {Account} account the Account which owns the code running * @property {Address} address the address of the `account` * @property {Number} depth the current number of calls deep the contract is * @property {Uint8Array} memory the memory of the EVM as a `Uint8Array` * @property {BigInt} memoryWordCount current size of memory in words * @property {Address} codeAddress the address of the code which is currently being ran (this differs from `address` in a `DELEGATECALL` and `CALLCODE` call) * @property {number} eofSection the current EOF code section referenced by the PC * @property {Uint8Array} immediate the immediate argument of the opcode * @property {Uint8Array} error the error data of the opcode (only present for REVERT) * @property {number} eofFunctionDepth the depth of the function call (only present for EOF) * @property {Array} storage an array of tuples, where each tuple contains a storage key and value */ await this._evm['_emit']('step', eventObj); } // Returns all valid jump and jumpsub destinations. _getValidJumpDestinations(code) { const jumps = new Uint8Array(code.length); const pushes = {}; const opcodesCached = Array(code.length); for (let i = 0; i < code.length; i++) { const opcode = code[i]; opcodesCached[i] = this.lookupOpInfo(opcode); // skip over PUSH0-32 since no jump destinations in the middle of a push block if (opcode <= 0x7f) { if (opcode >= 0x60) { const bytesToPush = opcode - 0x5f; let pushBytes = code.subarray(i + 1, i + opcode - 0x5e); if (pushBytes.length < bytesToPush) { pushBytes = (0, util_1.setLengthRight)(pushBytes, bytesToPush); } const push = (0, util_1.bytesToBigInt)(pushBytes); pushes[i + 1] = push; i += bytesToPush; } else if (opcode === 0x5b) { // Define a JUMPDEST as a 1 in the valid jumps array jumps[i] = 1; } } else if (this.common.isActivatedEIP(8024) && (opcode === 0xe6 || opcode === 0xe7 || opcode === 0xe8)) { const immediate = code[i + 1]; if (immediate === undefined) { continue; } const skipImmediate = opcode === 0xe8 ? (0, EIP8024_ts_1.isEIP8024PairImmediateValid)(immediate) : (0, EIP8024_ts_1.isEIP8024SingleImmediateValid)(immediate); if (skipImmediate === true) { i++; } } } return { jumps, pushes, opcodesCached }; } /** * Subtracts an amount from the gas counter. * @param amount - Amount of gas to consume * @param context - Usage context for debugging * @throws if out of gas */ useGas(amount, context) { this._runState.gasLeft -= amount; if (this._evm.DEBUG) { let tempString = ''; if (typeof context === 'string') { tempString = context + ': '; } else if (context !== undefined) { tempString = `${context.name} fee: `; } debugGas(`${tempString}used ${amount} gas (-> ${this._runState.gasLeft})`); } if (this._runState.gasLeft < util_1.BIGINT_0) { this._runState.gasLeft = util_1.BIGINT_0; (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.OUT_OF_GAS); } } /** * Adds a positive amount to the gas counter. * @param amount - Amount of gas refunded * @param context - Usage context for debugging */ refundGas(amount, context) { if (this._evm.DEBUG) { debugGas(`${typeof context === 'string' ? context + ': ' : ''}refund ${amount} gas (-> ${this._runState.gasRefund})`); } this._runState.gasRefund += amount; } /** * EIP-8037: Charge state-gas. Draws from the per-tx state-gas reservoir * first; if the reservoir is exhausted, the remainder is taken from the * regular `gasLeft` (which may OOG). Increments `execution_state_gas_used` * by the full charged amount. * @param amount - Amount of state gas to charge * @param context - Usage context for debugging * @throws if both pools combined are insufficient (out of gas) */ chargeStateGas(amount, context) { if (amount === util_1.BIGINT_0) return; const evm = this._evm; let remaining = amount; if (evm.stateGasReservoir > util_1.BIGINT_0) { const fromReservoir = remaining < evm.stateGasReservoir ? remaining : evm.stateGasReservoir; evm.stateGasReservoir -= fromReservoir; remaining -= fromReservoir; } if (remaining > util_1.BIGINT_0) { this.useGas(remaining, context !== undefined ? `state-gas spill: ${context}` : 'state-gas spill'); } evm.executionStateGasUsed += amount; if (evm.DEBUG) { debugGas(`${context !== undefined ? context + ': ' : ''}charged ${amount} state gas (reservoir=${evm.stateGasReservoir}, executionStateGasUsed=${evm.executionStateGasUsed}, gasLeft=${this._runState.gasLeft})`); } } /** * EIP-8037: Refill the state-gas reservoir, e.g. on revert / exceptional * halt or on SSTORE clear-back-to-zero where the slot was zero at tx start. * Increments `stateGasReservoir` and decrements `execution_state_gas_used` * by the same amount. Refills always go to the reservoir, regardless of * whether the original charge spilled to gas_left. * @param amount - Amount of state gas to refill * @param context - Usage context for debugging */ refillStateGasReservoir(amount, context) { if (amount === util_1.BIGINT_0) return; const evm = this._evm; evm.stateGasReservoir += amount; evm.executionStateGasUsed -= amount; if (evm.DEBUG) { debugGas(`${context !== undefined ? context + ': ' : ''}refilled ${amount} state gas (reservoir=${evm.stateGasReservoir}, executionStateGasUsed=${evm.executionStateGasUsed})`); } } /** * Reduces amount of gas to be refunded by a positive value. * @param amount - Amount to subtract from gas refunds * @param context - Usage context for debugging */ subRefund(amount, context) { if (this._evm.DEBUG) { debugGas(`${typeof context === 'string' ? context + ': ' : ''}sub gas refund ${amount} (-> ${this._runState.gasRefund})`); } this._runState.gasRefund -= amount; if (this._runState.gasRefund < util_1.BIGINT_0) { this._runState.gasRefund = util_1.BIGINT_0; (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.REFUND_EXHAUSTED); } } /** * Increments the internal gasLeft counter. Used for adding callStipend. * @param amount - Amount to add */ addStipend(amount) { if (this._evm.DEBUG) { debugGas(`add stipend ${amount} (-> ${this._runState.gasLeft})`); } this._runState.gasLeft += amount; } /** * Returns balance of the given account. * @param address - Address of account */ async getExternalBalance(address) { // Track address access for EIP-7928 BAL if (this._evm.common.isActivatedEIP(7928)) { this._evm.blockLevelAccessList?.addAddress(address.toString()); } // shortcut if current account if (address.equals(this._env.address)) { return this._env.contract.balance; } let account = await this._stateManager.getAccount(address); if (!account) { account = new util_1.Account(); } return account.balance; } /** * Store 256-bit a value in memory to persistent storage. */ async storageStore(key, value) { // EIP-7928: Get the original (pre-transaction) value BEFORE storing // This is needed to detect no-op writes (where new value equals original value) let originalValue; if (this._evm.common.isActivatedEIP(7928)) { originalValue = await this._stateManager.originalStorageCache.get(this._env.address, key); } await this._stateManager.putStorage(this._env.address, key, value); if (this._evm.common.isActivatedEIP(7928)) { this._evm.blockLevelAccessList?.addStorageWrite(this._env.address.toString(), key, value, this._evm.blockLevelAccessList.blockAccessIndex, originalValue); } const account = await this._stateManager.getAccount(this._env.address); if (!account) { throw (0, util_1.EthereumJSErrorWithoutCode)('could not read account while persisting memory'); } this._env.contract = account; } /** * Loads a 256-bit value to memory from persistent storage. * @param key - Storage key * @param original - If true, return the original storage value (default: false) * @param trackBAL - If true, track in BAL storageReads (default: true). Set to false for * implicit reads (e.g., SSTORE gas calculation) that should not appear in BAL. */ async storageLoad(key, original = false, trackBAL = true) { if (this._evm.common.isActivatedEIP(7928) && trackBAL) { this._evm.blockLevelAccessList?.addStorageRead(this._env.address.toString(), key); } if (original) { return this._stateManager.originalStorageCache.get(this._env.address, key); } else { return this._stateManager.getStorage(this._env.address, key); } } /** * Store 256-bit a value in memory to transient storage. * @param address Address to use * @param key Storage key * @param value Storage value */ transientStorageStore(key, value) { return this._evm.transientStorage.put(this._env.address, key, value); } /** * Loads a 256-bit value to memory from transient storage. * @param address Address to use * @param key Storage key */ transientStorageLoad(key) { return this._evm.transientStorage.get(this._env.address, key); } /** * Set the returning output data for the execution. * @param returnData - Output data to return */ finish(returnData) { this._result.returnValue = returnData; (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.STOP); } /** * Set the returning output data for the execution. This will halt the * execution immediately and set the execution result to "reverted". * @param returnData - Output data to return */ revert(returnData) { this._result.returnValue = returnData; (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.REVERT); } /** * Returns address of currently executing account. */ getAddress() { return this._env.address; } /** * Returns balance of self. */ getSelfBalance() { return this._env.contract.balance; } /** * Returns the deposited value by the instruction/transaction * responsible for this execution. */ getCallValue() { return this._env.callValue; } /** * Returns input data in current environment. This pertains to the input * data passed with the message call instruction or transaction. */ getCallData() { return this._env.callData; } /** * Returns size of input data in current environment. This pertains to the * input data passed with the message call instruction or transaction. */ getCallDataSize() { return BigInt(this._env.callData.length); } /** * Returns caller address. This is the address of the account * that is directly responsible for this execution. */ getCaller() { return (0, util_1.bytesToBigInt)(this._env.caller.bytes); } /** * Returns the size of code running in current environment. */ getCodeSize() { return BigInt(this._env.code.length); } /** * Returns the code running in current environment. */ getCode() { return this._env.code; } /** * Returns the current gasCounter. */ getGasLeft() { return this._runState.gasLeft; } /** * Returns size of current return data buffer. This contains the return data * from the last executed call, callCode, callDelegate, callStatic or create. * Note: create only fills the return data buffer in case of a failure. */ getReturnDataSize() { return BigInt(this._runState.returnBytes.length); } /** * Returns the current return data buffer. This contains the return data * from last executed call, callCode, callDelegate, callStatic or create. * Note: create only fills the return data buffer in case of a failure. */ getReturnData() { return this._runState.returnBytes; } /** * Returns true if the current call must be executed statically. */ isStatic() { return this._env.isStatic; } /** * Returns price of gas in current environment. */ getTxGasPrice() { return this._env.gasPrice; } /** * Returns the execution's origination address. This is the * sender of original transaction; it is never an account with * non-empty associated code. */ getTxOrigin() { return (0, util_1.bytesToBigInt)(this._env.origin.bytes); } /** * Returns the block's number. */ getBlockNumber() { return this._env.block.header.number; } /** * Returns the block's beneficiary address. */ getBlockCoinbase() { let coinbase; if (this.common.consensusAlgorithm() === common_1.ConsensusAlgorithm.Clique) { coinbase = this._evm['_optsCached'].cliqueSigner(this._env.block.header); } else { coinbase = this._env.block.header.coinbase; } return (0, util_1.bytesToBigInt)(coinbase.toBytes()); } /** * Returns the block's timestamp. */ getBlockTimestamp() { return this._env.block.header.timestamp; } /** * Returns the block's difficulty. */ getBlockDifficulty() { return this._env.block.header.difficulty; } /** * Returns the block's prevRandao field. */ getBlockPrevRandao() { return (0, util_1.bytesToBigInt)(this._env.block.header.prevRandao); } /** * Returns the block's gas limit. */ getBlockGasLimit() { return this._env.block.header.gasLimit; } /** * Returns the block's slot number (EIP-7843). */ getBlockSlotNumber() { if (this._env.block.header.slotNumber === undefined) { throw (0, util_1.EthereumJSErrorWithoutCode)('slotNumber is not available on this block'); } return this._env.block.header.slotNumber; } /** * Returns the Base Fee of the block as proposed in [EIP-3198](https://eips.ethereum.org/EIPS/eip-3198) */ getBlockBaseFee() { const baseFee = this._env.block.header.baseFeePerGas; if (baseFee === undefined) { // Sanity check throw (0, util_1.EthereumJSErrorWithoutCode)('Block has no Base Fee'); } return baseFee; } /** * Returns the Blob Base Fee of the block as proposed in [EIP-7516](https://eips.ethereum.org/EIPS/eip-7516) */ getBlobBaseFee() { const blobBaseFee = this._env.block.header.getBlobGasPrice(); if (blobBaseFee === undefined) { // Sanity check throw (0, util_1.EthereumJSErrorWithoutCode)('Block has no Blob Base Fee'); } return blobBaseFee; } /** * Returns the chain ID for current chain. Introduced for the * CHAINID opcode proposed in [EIP-1344](https://eips.ethereum.org/EIPS/eip-1344). */ getChainId() { return this.common.chainId(); } /** * Sends a message with arbitrary data to a given address path. */ async call(gasLimit, address, value, data) { const msg = new message_ts_1.Message({ caller: this._env.address, gasLimit, to: address, value, data, isStatic: this._env.isStatic, depth: this._env.depth + 1, blobVersionedHashes: this._env.blobVersionedHashes, accessWitness: this._env.accessWitness, }); return this._baseCall(msg); } /** * Message-call into this account with an alternative account's code. */ async callCode(gasLimit, address, value, data) { const msg = new message_ts_1.Message({ caller: this._env.address, gasLimit, to: this._env.address, codeAddress: address, value, data, isStatic: this._env.isStatic, depth: this._env.depth + 1, blobVersionedHashes: this._env.blobVersionedHashes, accessWitness: this._env.accessWitness, }); return this._baseCall(msg); } /** * Sends a message with arbitrary data to a given address path, but disallow * state modifications. This includes log, create, selfdestruct and call with * a non-zero value. */ async callStatic(gasLimit, address, value, data) { const msg = new message_ts_1.Message({ caller: this._env.address, gasLimit, to: address, value, data, isStatic: true, depth: this._env.depth + 1, blobVersionedHashes: this._env.blobVersionedHashes, accessWitness: this._env.accessWitness, }); return this._baseCall(msg); } /** * Message-call into this account with an alternative account's code, but * persisting the current values for sender and value. */ async callDelegate(gasLimit, address, value, data) { const msg = new message_ts_1.Message({ caller: this._env.caller, gasLimit, to: this._env.address, codeAddress: address, value, data, isStatic: this._env.isStatic, delegatecall: true, depth: this._env.depth + 1, blobVersionedHashes: this._env.blobVersionedHashes, accessWitness: this._env.accessWitness, }); return this._baseCall(msg); } async _baseCall(msg) { const selfdestruct = new Map(this._result.selfdestruct); msg.selfdestruct = selfdestruct; msg.gasRefund = this._runState.gasRefund; // empty the return data Uint8Array this._runState.returnBytes = new Uint8Array(0); let createdAddresses; if (this.common.isActivatedEIP(6780)) { createdAddresses = new Set(this._result.createdAddresses); msg.createdAddresses = createdAddresses; } // empty the return data Uint8Array this._runState.returnBytes = new Uint8Array(0); // Check if account has enough ether and max depth not exceeded if (this._env.depth >= Number(this.common.param('stackLimit')) || (msg.delegatecall !== true && this._env.contract.balance < msg.value)) { return util_1.BIGINT_0; } const results = await this._evm.runCall({ message: msg }); if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs); } // this should always be safe this.useGas(results.execResult.executionGasUsed, 'CALL, STATICCALL, DELEGATECALL, CALLCODE'); // Set return value if (results.execResult.returnValue !== undefined && (!results.execResult.exceptionError || results.execResult.exceptionError.error === errors_ts_1.EVMError.errorMessages.REVERT)) { this._runState.returnBytes = results.execResult.returnValue; } if (!results.execResult.exceptionError) { for (const [addressToSelfdestructHex, beneficiaryHex] of selfdestruct) { this._result.selfdestruct.set(addressToSelfdestructHex, beneficiaryHex); } if (this.common.isActivatedEIP(6780)) { // copy over the items to result via iterator for (const item of createdAddresses) { this._result.createdAddresses.add(item); } } // update stateRoot on current contract const account = await this._stateManager.getAccount(this._env.address); if (!account) { throw (0, util_1.EthereumJSErrorWithoutCode)('could not read contract account'); } this._env.contract = account; this._runState.gasRefund = results.execResult.gasRefund ?? util_1.BIGINT_0; } return this._getReturnCode(results); } /** * Creates a new contract with a given value. */ async create(gasLimit, value, codeToRun, salt, eofCallData) { const selfdestruct = new Map(this._result.selfdestruct); const caller = this._env.address; const depth = this._env.depth + 1; // empty the return data buffer this._runState.returnBytes = new Uint8Array(0); // EIP-8037: helper to refund the pre-charged NEW_ACCOUNT state-gas // when the CREATE short-circuits BEFORE a child frame is spawned // (depth limit, insufficient balance, EIP-2681 nonce overflow, // EIP-3860 oversized initcode). The pre-charge happened in // opcodes/gas.ts; the runCall revert handler won't fire here since // no child frame is created, so refund explicitly. const refundCreatePreCharge = () => { if (!this.common.isActivatedEIP(8037)) return; const stateBytesPerNewAccount = this.common.param('stateBytesPerNewAccount'); const blockGasLimit = this._env.block.header.gasLimit; const costPerStateByte = (0, eip8037_ts_1.activeCostPerStateByte)(this.common, blockGasLimit); const newAccountStateGas = stateBytesPerNewAccount * costPerStateByte; this._evm.stateGasReservoir += newAccountStateGas; this._evm.executionStateGasUsed -= newAccountStateGas; }; // Check if account has enough ether and max depth not exceeded if (this._env.depth >= Number(this.common.param('stackLimit')) || this._env.contract.balance < value) { refundCreatePreCharge(); return util_1.BIGINT_0; } // EIP-2681 check if (this._env.contract.nonce >= util_1.MAX_UINT64) { refundCreatePreCharge(); return util_1.BIGINT_0; } this._env.contract.nonce += util_1.BIGINT_1; await this.journal.putAccount(this._env.address, this._env.contract); if (this.common.isActivatedEIP(7928)) { this._evm.blockLevelAccessList.addNonceChange(this._env.address.toString(), this._env.contract.nonce, this._evm.blockLevelAccessList.blockAccessIndex); } if (this.common.isActivatedEIP(3860)) { if (codeToRun.length > Number(this.common.param('maxInitCodeSize')) && this._evm.allowUnlimitedInitCodeSize === false) { refundCreatePreCharge(); return util_1.BIGINT_0; } } const message = new message_ts_1.Message({ caller, gasLimit, value, data: codeToRun, eofCallData, salt, depth, selfdestruct, gasRefund: this._runState.gasRefund, blobVersionedHashes: this._env.blobVersionedHashes, accessWitness: this._env.accessWitness, }); let createdAddresses; if (this.common.isActivatedEIP(6780)) { createdAddresses = new Set(this._result.createdAddresses); message.createdAddresses = createdAddresses; } const results = await this._evm.runCall({ message }); if (results.execResult.logs) { this._result.logs = this._result.logs.concat(results.execResult.logs); } // this should always be safe this.useGas(results.execResult.executionGasUsed, 'CREATE'); // Set return buffer in case revert happened if (results.execResult.exceptionError && results.execResult.exceptionError.error === errors_ts_1.EVMError.errorMessages.REVERT) { this._runState.returnBytes = results.execResult.returnValue; } if (!results.execResult.exceptionError || results.execResult.exceptionError.error === errors_ts_1.EVMError.errorMessages.CODESTORE_OUT_OF_GAS) { for (const [addressToSelfdestructHex, beneficiaryHex] of selfdestruct) { this._result.selfdestruct.set(addressToSelfdestructHex, beneficiaryHex); } if (this.common.isActivatedEIP(6780)) { // copy over the items to result via iterator for (const item of createdAddresses) { this._result.createdAddresses.add(item); } } // update stateRoot on current contract const account = await this._stateManager.getAccount(this._env.address); if (!account) { throw (0, util_1.EthereumJSErrorWithoutCode)('could not read contract account'); } this._env.contract = account; this._runState.gasRefund = results.execResult.gasRefund ?? util_1.BIGINT_0; if (results.createdAddress) { // push the created address to the stack return (0, util_1.bytesToBigInt)(results.createdAddress.bytes); } } return this._getReturnCode(results, true); } /** * Creates a new contract with a given value. Generates * a deterministic address via CREATE2 rules. */ async create2(gasLimit, value, data, salt) { return this.create(gasLimit, value, data, salt); } /** * Creates a new contract with a given value. Generates * a deterministic address via EOFCREATE rules. */ async eofcreate(gasLimit, value, containerData, salt, callData) { return this.create(gasLimit, value, containerData, salt, callData); } /** * Mark account for later deletion and give the remaining balance to the * specified beneficiary address. This will cause a trap and the * execution will be aborted immediately. * @param toAddress - Beneficiary address */ async selfDestruct(toAddress) { return this._selfDestruct(toAddress); } async _selfDestruct(toAddress) { // only add to refund if this is the first selfdestruct for the address const selfdestructAddressHex = (0, util_1.bytesToHex)(this._env.address.bytes); if (!this._result.selfdestruct.has(selfdestructAddressHex)) { this.refundGas(this.common.param('selfdestructRefundGas')); } this._result.selfdestruct.set(selfdestructAddressHex, toAddress.toString()); const toSelf = (0, util_1.equalsBytes)(toAddress.bytes, this._env.address.bytes); const contractBalance = this._env.contract.balance; // EIP-7708: Emit ETH transfer log for SELFDESTRUCT with value to a different account if (this.common.isActivatedEIP(7708) && contractBalance > util_1.BIGINT_0 && !toSelf) { // Transfer log: from contract to beneficiary const fromTopic = (0, util_1.setLengthLeft)(this._env.address.bytes, 32); const toTopic = (0, util_1.setLengthLeft)(toAddress.bytes, 32); const data = (0, util_1.setLengthLeft)((0, util_1.bigIntToBytes)(contractBalance), 32); const transferLog = [ eip7708_ts_1.EIP7708_SYSTEM_ADDRESS, [eip7708_ts_1.EIP7708_TRANSFER_TOPIC, fromTopic, toTopic], data, ]; this._result.logs.push(transferLog); } // Add to beneficiary balance let beneficiaryWasNew = false; if (!toSelf) { let toAccount = await this._stateManager.getAccount(toAddress); beneficiaryWasNew = toAccount === undefined || toAccount.isEmpty(); if (!toAccount) { toAccount = new util_1.Account(); } const originalBalance = toAccount.balance; toAccount.balance += contractBalance; await this.journal.putAccount(toAddress, toAccount); if (this.common.isActivatedEIP(7928)) { this._evm.blockLevelAccessList.addBalanceChange(toAddress.toString(), toAccount.balance, this._evm.blockLevelAccessList.blockAccessIndex, originalBalance); } } // EIP-8037: SELFDESTRUCT that transfers value to a non-existent beneficiary // creates a new account. Charge stateBytesPerNewAccount * costPerStateByte. if (this.common.isActivatedEIP(8037) && !toSelf && beneficiaryWasNew && contractBalance > util_1.BIGINT_0) { const stateBytesPerNewAccount = this.common.param('stateBytesPerNewAccount'); const blockGasLimit = this._env.block.header.gasLimit; const costPerStateByte = (0, eip8037_ts_1.activeCostPerStateByte)(this.common, blockGasLimit); const charge = stateBytesPerNewAccount * costPerStateByte; this.chargeStateGas(charge, 'SELFDESTRUCT new beneficiary'); } // Modify the account (set balance to 0) flag let doModify = !this.common.isActivatedEIP(6780); // Always do this if 6780 is not active if (!doModify) { // If 6780 is active, check if current address is being created. If so // old behavior of SELFDESTRUCT exists and balance should be set to 0 of this account // (i.e. burn the ETH in current account) doModify = this._env.createdAddresses.has(this._env.address.toString()); // If contract is not being created in this tx... if (!doModify) { // Check if ETH being sent to another account (thus set balance to 0) doModify = !toSelf; } } // EIP-7708: Emit a Burn log (LOG2) for SELFDESTRUCT to self only when the balance // is actually zeroed (doModify=true, i.e. same-tx contract creation). Pre-existing // contracts where EIP-6780 prevents the burn should not emit a log. if (this.common.isActivatedEIP(7708) && contractBalance > util_1.BIGINT_0 && toSelf && doModify) { const contractTopic = (0, util_1.setLengthLeft)(this._env.address.bytes, 32); const data = (0, util_1.setLengthLeft)((0, util_1.bigIntToBytes)(contractBalance), 32); const burnLog = [eip7708_ts_1.EIP7708_SYSTEM_ADDRESS, [eip7708_ts_1.EIP7708_BURN_TOPIC, contractTopic], data]; this._result.logs.push(burnLog); } // Set contract balance to 0 if (doModify) { const originalBalance = this._env.contract.balance; await this._stateManager.modifyAccountFields(this._env.address, { balance: util_1.BIGINT_0, }); if (this.common.isActivatedEIP(7928)) { this._evm.blockLevelAccessList.addBalanceChange(this._env.address.toString(), util_1.BIGINT_0, this._evm.blockLevelAccessList.blockAccessIndex, originalBalance); } } (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.STOP); } /** * Creates a new log in the current environment. */ log(data, numberOfTopics, topics) { if (numberOfTopics < 0 || numberOfTopics > 4) { (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.OUT_OF_RANGE); } if (topics.length !== numberOfTopics) { (0, index_ts_1.trap)(errors_ts_1.EVMError.errorMessages.INTERNAL_ERROR); } const log = [this._env.address.bytes, topics, data]; this._result.logs.push(log); } _getReturnCode(results, isEOFCreate = false) { if (this._runState.env.eof === undefined || isEOFCreate) { if (results.execResult.exceptionError) { return util_1.BIGINT_0; } else { return util_1.BIGINT_1; } } else { // EOF mode, call was either EXTCALL / EXTDELEGATECALL / EXTSTATICCALL if (results.execResult.exceptionError !== undefined) { if (results.execResult.exceptionError.error === errors_ts_1.EVMError.errorMessages.REVERT) { // Revert return util_1.BIGINT_1; } else { // Failure return util_1.BIGINT_2; } } return util_1.BIGINT_0; } } } exports.Interpreter = Interpreter; //# sourceMappingURL=interpreter.js.map