@ethereumjs/evm
Version:
JavaScript Ethereum Virtual Machine (EVM) implementation
1,135 lines • 49.7 kB
JavaScript
"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