@tokamak-zk-evm/synthesizer
Version:
Tokamak zk-EVM Synthesizer - Processes Ethereum transactions into wire maps for Tokamak zk-SNARK proof generation
1,363 lines (1,218 loc) • 41.2 kB
text/typescript
import { ConsensusAlgorithm } from '@synthesizer-libs/common'
import {
Account,
BIGINT_0,
BIGINT_1,
BIGINT_2,
MAX_UINT64,
bigIntToHex,
bytesToBigInt,
bytesToHex,
equalsBytes,
} from "@synthesizer-libs/util"
import debugDefault from 'debug'
import readline from 'readline'
import { FORMAT, MAGIC, VERSION } from './eof/constants.js'
import { EOFContainerMode, validateEOF } from './eof/container.js'
import { setupEOF } from './eof/setup.js'
import { ContainerSectionType } from './eof/verify.js'
import { ERROR, EvmError } from './exceptions.js'
import { type EVMPerformanceLogger, type Timer } from './logger.js'
import { Memory } from './memory.js'
import { Message } from './message.js'
import { trap } from './opcodes/index.js'
import { Stack } from './stack.js'
import { MemoryPt } from './tokamak/pointers/memoryPt.js'
import { StackPt } from './tokamak/pointers/stackPt.js'
import { arrToStr, mapToStr } from './tokamak/utils/index.js'
import type { EVM } from './evm.js'
import type { Journal } from './journal.js'
import type { AsyncOpHandler, Opcode, OpcodeMapEntry } from './opcodes/index.js'
import type { Synthesizer } from './tokamak/core/index.js'
import type { MemoryPts } from './tokamak/pointers/memoryPt.js'
import type {
Block,
EOFEnv,
EVMMockBlockchainInterface,
EVMProfilerOpts,
EVMResult,
Log,
} from './types.js'
import type {
Common,
StateManagerInterface,
VerkleAccessWitnessInterface,
} from '@synthesizer-libs/common'
import type { Address, PrefixedHexString } from "@synthesizer-libs/util"
const debugGas = debugDefault('evm:gas')
export interface InterpreterOpts {
pc?: number
}
/**
* Immediate (unprocessed) result of running an EVM bytecode.
*/
export interface RunResult {
logs: Log[]
returnValue?: Uint8Array
/**
* A set of accounts to selfdestruct
*/
selfdestruct: Set<PrefixedHexString>
/**
* A map which tracks which addresses were created (used in EIP 6780)
*/
createdAddresses?: Set<PrefixedHexString>
returnMemoryPts?: MemoryPts
}
export interface Env {
address: Address
caller: Address
callData: Uint8Array
callValue: bigint
code: Uint8Array
isStatic: boolean
isCreate: boolean
depth: number
gasPrice: bigint
origin: Address
block: Block
contract: Account
codeAddress: Address /* Different than address for DELEGATECALL and CALLCODE */
gasRefund: bigint /* Current value (at begin of the frame) of the gas refund */
eof?: EOFEnv /* Optional EOF environment in case of EOF execution */
blobVersionedHashes: PrefixedHexString[] /** Versioned hashes for blob transactions */
createdAddresses?: Set<string>
accessWitness?: VerkleAccessWitnessInterface
chargeCodeAccesses?: boolean
callMemoryPts: MemoryPts
}
export interface RunState {
programCounter: number
programCounterPrev: number
opCode: number
memory: Memory
memoryPt: MemoryPt
memoryWordCount: bigint
highestMemCost: bigint
stack: Stack
stackPt: StackPt
code: Uint8Array
shouldDoJumpAnalysis: boolean
validJumps: Uint8Array // array of values where validJumps[index] has value 0 (default), 1 (jumpdest), 2 (beginsub)
cachedPushes: { [pc: number]: bigint }
stateManager: StateManagerInterface
blockchain: EVMMockBlockchainInterface
env: Env
messageGasLimit?: bigint // Cache value from `gas.ts` to save gas limit for a message call
interpreter: Interpreter
gasRefund: bigint // Tracks the current refund
gasLeft: bigint // Current gas left
returnBytes: Uint8Array /* Current bytes in the return Uint8Array. Cleared each time a CALL/CREATE is made in the current frame. */
synthesizer: Synthesizer
returnMemoryPts: MemoryPts
}
export interface InterpreterResult {
runState: RunState
exceptionError?: EvmError
}
export interface InterpreterStep {
gasLeft: bigint
gasRefund: bigint
stateManager: StateManagerInterface
stack: bigint[]
pc: number
depth: number
opcode: {
name: string
fee: number
dynamicFee?: bigint
isAsync: boolean
}
account: Account
address: Address
memory: Uint8Array
memoryWordCount: bigint
codeAddress: Address
}
/**
* Parses and executes EVM bytecode.
*/
export class Interpreter {
protected _vm: any
protected _runState: RunState
protected _stateManager: StateManagerInterface
protected common: Common
public _evm: EVM
public journal: Journal
protected _synthesizer: Synthesizer
_env: Env
// Keep track of this Interpreter run result
// TODO move into Env?
_result: RunResult
// Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...})
private opDebuggers: { [key: string]: (debug: string) => void } = {}
private profilerOpts?: EVMProfilerOpts
private performanceLogger: EVMPerformanceLogger
// TODO remove gasLeft as constructor argument
constructor(
evm: EVM,
stateManager: StateManagerInterface,
blockchain: EVMMockBlockchainInterface,
env: Env,
gasLeft: bigint,
journal: Journal,
performanceLogs: EVMPerformanceLogger,
synthesizer: Synthesizer,
profilerOpts?: EVMProfilerOpts,
) {
this._evm = evm
this._stateManager = stateManager
this.common = this._evm.common
if (
this.common.consensusType() === 'poa' &&
this._evm['_optsCached'].cliqueSigner === undefined
)
throw new Error(
'Must include cliqueSigner function if clique/poa is being used for consensus type',
)
this._runState = {
programCounter: 0,
programCounterPrev: 0,
opCode: 0xfe, // INVALID opcode
memory: new Memory(),
memoryPt: new MemoryPt(),
memoryWordCount: BIGINT_0,
highestMemCost: BIGINT_0,
stack: new Stack(),
stackPt: new StackPt(),
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),
synthesizer,
returnMemoryPts: [],
}
this.journal = journal
this._env = env
this._result = {
logs: [],
returnValue: undefined,
selfdestruct: new Set(),
returnMemoryPts: undefined,
}
this.profilerOpts = profilerOpts
this.performanceLogger = performanceLogs
this._synthesizer = synthesizer
}
async run(code: Uint8Array, opts: InterpreterOpts = {}): Promise<InterpreterResult> {
if (!this.common.isActivatedEIP(3540) || code[0] !== 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] !== MAGIC) {
// Bytecode contains invalid EOF magic byte
return {
runState: this._runState,
exceptionError: new EvmError(ERROR.INVALID_BYTECODE_RESULT),
}
}
if (code[2] !== VERSION) {
// Bytecode contains invalid EOF version number
return {
runState: this._runState,
exceptionError: new EvmError(ERROR.INVALID_EOF_FORMAT),
}
}
this._runState.code = code
const isTxCreate = this._env.isCreate && this._env.depth === 0
const eofMode = isTxCreate ? EOFContainerMode.TxInitmode : EOFContainerMode.Default
try {
setupEOF(this._runState, eofMode)
} catch (e) {
return {
runState: this._runState,
exceptionError: new EvmError(ERROR.INVALID_EOF_FORMAT), // TODO: verify if all gas should be consumed
}
}
if (isTxCreate) {
// Tx tries to deploy container
try {
validateEOF(
this._runState.code,
this._evm,
ContainerSectionType.InitCode,
EOFContainerMode.TxInitmode,
)
} catch (e) {
// Trying to deploy an invalid EOF container
return {
runState: this._runState,
exceptionError: new EvmError(ERROR.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 new Error('Internal error: program counter not in range')
}
let err
let cachedOpcodes: OpcodeMapEntry[]
let doJumpAnalysis = true
let timer: Timer | undefined
let overheadTimer: Timer | undefined
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: number
let opCodeObj: OpcodeMapEntry
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 verkle 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(6800) &&
// 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()
}
this._runState.programCounterPrev = this._runState.programCounter
await this.runStep(opCodeObj!)
if (overheadTimer !== undefined) {
this.performanceLogger.unpauseTimer(overheadTimer)
}
} catch (e: any) {
if (overheadTimer !== undefined) {
this.performanceLogger.unpauseTimer(overheadTimer)
}
// re-throw on non-VM errors
if (!('errorType' in e && e.errorType === 'EvmError')) {
throw e
}
// STOP is not an exception
if (e.error !== ERROR.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?: OpcodeMapEntry): Promise<void> {
const opEntry = opcodeObj ?? this.lookupOpInfo(this._runState.opCode)
const opInfo = opEntry.opcodeInfo
let timer: Timer
if (this.profilerOpts?.enabled === true) {
timer = this.performanceLogger.startTimer(opInfo.name)
}
let gas = opInfo.feeBigInt
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.common.isActivatedEIP(6800) && this._env.chargeCodeAccesses === true) {
const contract = this._runState.interpreter.getAddress()
const statelessGas =
this._runState.env.accessWitness!.touchCodeChunksRangeOnReadAndChargeGas(
contract,
this._runState.programCounter,
this._runState.programCounter,
)
gas += statelessGas
debugGas(`codechunk accessed statelessGas=${statelessGas} (-> ${gas})`)
}
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())
}
// Check for invalid opcode
if (opInfo.isInvalid) {
throw new EvmError(ERROR.INVALID_OPCODE)
}
// Reduce opcode's base fee
this.useGas(gas, opInfo)
// Advance program counter
this._runState.programCounter++
// Execute opcode handler
const opFn = opEntry.opHandler
// 2. Execute normal opcode
if (opInfo.isAsync) {
await (opFn as AsyncOpHandler).apply(null, [this._runState, this.common])
} else {
opFn.apply(null, [this._runState, this.common])
}
// Check stack consistency between EVM and Synthesizer
const stackVals = this._runState.stack.getStack();
const stackPtVals = this._runState.stackPt.getStack().map(dataPt => dataPt.value);
if (!(stackVals.length === stackPtVals.length && stackVals.every((val, index) => val === stackPtVals[index]))){
console.log(`Instruction: ${opInfo.name}`)
console.log(`Stack values(right-newest): ${stackVals}`)
console.log(`StackPt values(right-newest): ${stackPtVals}`)
throw new Error('Synthesizer: Stack mismatch between EVM and Synthesizer')
}
} 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: number): OpcodeMapEntry {
return (<any>this._evm)._opcodeMap[op]
}
async _runStepHook(dynamicFee: bigint, gasLeft: bigint): Promise<void> {
const opcodeInfo = this.lookupOpInfo(this._runState.opCode)
const opcode = opcodeInfo.opcodeInfo
const eventObj: InterpreterStep = {
pc: this._runState.programCounter,
gasLeft,
gasRefund: this._runState.gasRefund,
opcode: {
name: opcode.fullName,
fee: opcode.fee,
dynamicFee,
isAsync: opcode.isAsync,
},
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(this._runState.memoryWordCount) * 32),
memoryWordCount: this._runState.memoryWordCount,
codeAddress: this._env.codeAddress,
stateManager: this._runState.stateManager,
}
if (this._evm.DEBUG) {
// Create opTrace for debug functionality
let hexStack = []
hexStack = eventObj.stack.map((item: any) => {
return bigIntToHex(BigInt(item))
})
/*eslint-disable */
function waitForCommand(targetCommand = 'continue') {
return new Promise<void>((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
rl.question(`Type "${targetCommand}" to continue: `, (input) => {
if (input.trim() === targetCommand) {
rl.close()
resolve()
} else {
console.log(`Invalid command. Please type "${targetCommand}" to proceed.`)
rl.close()
waitForCommand(targetCommand).then(resolve) // wait again
}
})
})
}
const stringMemory = Buffer.from(this._runState.memory._store).toString('hex')
const stringMemoryPt = mapToStr(this._runState.memoryPt._storePt)
const name = eventObj.opcode.name
const opTrace = {
pc: eventObj.pc,
op: name,
gas: bigIntToHex(eventObj.gasLeft),
gasCost: bigIntToHex(dynamicFee),
stack: hexStack,
depth: eventObj.depth,
}
// console.log(`"memory": ${JSON.stringify(eventObj.memory)}\n`)
console.log(`"memory": ${stringMemory.slice(0, 256)}\n`)
console.log(`"stackPt": ${JSON.stringify(this._runState.stackPt.getStack(), arrToStr, 2)}\n`)
console.log(`"memoryPt": ${JSON.stringify(stringMemoryPt, null, 1)}\n`)
if (!(name in this.opDebuggers)) {
this.opDebuggers[name] = debugDefault(`evm:ops:${name}`)
}
this.opDebuggers[name](JSON.stringify(opTrace))
await waitForCommand('c')
}
/**
* 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 {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)
*/
await (this._evm as any)._emit('step', eventObj)
}
// Returns all valid jump and jumpsub destinations.
_getValidJumpDestinations(code: Uint8Array) {
const jumps = new Uint8Array(code.length)
const pushes: { [pc: number]: bigint } = {}
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 extraSteps = opcode - 0x5f
const push = bytesToBigInt(code.slice(i + 1, i + opcode - 0x5e))
pushes[i + 1] = push
i += extraSteps
} else if (opcode === 0x5b) {
// Define a JUMPDEST as a 1 in the valid jumps array
jumps[i] = 1
}
}
}
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: bigint, context?: string | Opcode): void {
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 < BIGINT_0) {
this._runState.gasLeft = BIGINT_0
trap(ERROR.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: bigint, context?: string): void {
if (this._evm.DEBUG) {
debugGas(
`${typeof context === 'string' ? context + ': ' : ''}refund ${amount} gas (-> ${
this._runState.gasRefund
})`,
)
}
this._runState.gasRefund += amount
}
/**
* 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: bigint, context?: string): void {
if (this._evm.DEBUG) {
debugGas(
`${typeof context === 'string' ? context + ': ' : ''}sub gas refund ${amount} (-> ${
this._runState.gasRefund
})`,
)
}
this._runState.gasRefund -= amount
if (this._runState.gasRefund < BIGINT_0) {
this._runState.gasRefund = BIGINT_0
trap(ERROR.REFUND_EXHAUSTED)
}
}
/**
* Increments the internal gasLeft counter. Used for adding callStipend.
* @param amount - Amount to add
*/
addStipend(amount: bigint): void {
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: Address): Promise<bigint> {
// 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 Account()
}
return account.balance
}
/**
* Store 256-bit a value in memory to persistent storage.
*/
async storageStore(key: Uint8Array, value: Uint8Array): Promise<void> {
await this._stateManager.putStorage(this._env.address, key, value)
const account = await this._stateManager.getAccount(this._env.address)
if (!account) {
throw new Error('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)
*/
async storageLoad(key: Uint8Array, original = false): Promise<Uint8Array> {
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: Uint8Array, value: Uint8Array): void {
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: Uint8Array): Uint8Array {
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: Uint8Array): void {
this._result.returnValue = returnData
trap(ERROR.STOP)
}
/**
* Set the memory pointers to the returning output data for the execution.
* @param returnMemoryPts - The MemoryPt contents for the output data to return
*/
finishPt(returnMemoryPts: MemoryPts): void {
this._result.returnMemoryPts = returnMemoryPts
}
/**
* 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: Uint8Array): void {
this._result.returnValue = returnData
trap(ERROR.REVERT)
}
/**
* Returns address of currently executing account.
*/
getAddress(): Address {
return this._env.address
}
/**
* Returns balance of self.
*/
getSelfBalance(): bigint {
return this._env.contract.balance
}
/**
* Returns the deposited value by the instruction/transaction
* responsible for this execution.
*/
getCallValue(): bigint {
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(): Uint8Array {
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(): bigint {
return BigInt(this._env.callData.length)
}
/**
* Returns caller address. This is the address of the account
* that is directly responsible for this execution.
*/
getCaller(): bigint {
return bytesToBigInt(this._env.caller.bytes)
}
/**
* Returns the size of code running in current environment.
*/
getCodeSize(): bigint {
return BigInt(this._env.code.length)
}
/**
* Returns the code running in current environment.
*/
getCode(): Uint8Array {
return this._env.code
}
/**
* Returns the current gasCounter.
*/
getGasLeft(): bigint {
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(): bigint {
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(): Uint8Array {
return this._runState.returnBytes
}
/**
* Returns the pointers to the aliased data in the current return data buffer. This contains the source pointers to 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.
*/
getReturnMemoryPts(): MemoryPts {
return this._runState.returnMemoryPts
}
/**
* Returns true if the current call must be executed statically.
*/
isStatic(): boolean {
return this._env.isStatic
}
/**
* Returns price of gas in current environment.
*/
getTxGasPrice(): bigint {
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(): bigint {
return bytesToBigInt(this._env.origin.bytes)
}
/**
* Returns the block's number.
*/
getBlockNumber(): bigint {
return this._env.block.header.number
}
/**
* Returns the block's beneficiary address.
*/
getBlockCoinbase(): bigint {
let coinbase: Address
if (this.common.consensusAlgorithm() === ConsensusAlgorithm.Clique) {
coinbase = this._evm['_optsCached'].cliqueSigner!(this._env.block.header)
} else {
coinbase = this._env.block.header.coinbase
}
return bytesToBigInt(coinbase.toBytes())
}
/**
* Returns the block's timestamp.
*/
getBlockTimestamp(): bigint {
return this._env.block.header.timestamp
}
/**
* Returns the block's difficulty.
*/
getBlockDifficulty(): bigint {
return this._env.block.header.difficulty
}
/**
* Returns the block's prevRandao field.
*/
getBlockPrevRandao(): bigint {
return bytesToBigInt(this._env.block.header.prevRandao)
}
/**
* Returns the block's gas limit.
*/
getBlockGasLimit(): bigint {
return this._env.block.header.gasLimit
}
/**
* Returns the Base Fee of the block as proposed in [EIP-3198](https://eips.ethereum.org/EIPS/eip-3198)
*/
getBlockBaseFee(): bigint {
const baseFee = this._env.block.header.baseFeePerGas
if (baseFee === undefined) {
// Sanity check
throw new Error('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(): bigint {
const blobBaseFee = this._env.block.header.getBlobGasPrice()
if (blobBaseFee === undefined) {
// Sanity check
throw new Error('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(): bigint {
return this.common.chainId()
}
/**
* Sends a message with arbitrary data to a given address path.
*/
async call(
gasLimit: bigint,
address: Address,
value: bigint,
data: Uint8Array,
memoryPts: MemoryPts,
): Promise<bigint> {
const msg = new 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,
memoryPts,
})
return this._baseCall(msg)
}
/**
* Message-call into this account with an alternative account's code.
*/
async callCode(
gasLimit: bigint,
address: Address,
value: bigint,
data: Uint8Array,
memoryPts: MemoryPts,
): Promise<bigint> {
const msg = new 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,
memoryPts,
})
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: bigint,
address: Address,
value: bigint,
data: Uint8Array,
memoryPts: MemoryPts,
): Promise<bigint> {
const msg = new 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,
memoryPts,
})
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: bigint,
address: Address,
value: bigint,
data: Uint8Array,
memoryPts: MemoryPts,
): Promise<bigint> {
const msg = new 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,
memoryPts,
})
return this._baseCall(msg)
}
async _baseCall(msg: Message): Promise<bigint> {
const selfdestruct = new Set(this._result.selfdestruct)
msg.selfdestruct = selfdestruct
msg.gasRefund = this._runState.gasRefund
// empty the return data Uint8Array
this._runState.returnBytes = new Uint8Array(0)
let createdAddresses: Set<PrefixedHexString>
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 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 === ERROR.REVERT)
) {
this._runState.returnBytes = results.execResult.returnValue
// For synthesizer
if (results.execResult.returnMemoryPts) {
this._runState.returnMemoryPts = results.execResult.returnMemoryPts
}
}
if (!results.execResult.exceptionError) {
for (const addressToSelfdestructHex of selfdestruct) {
this._result.selfdestruct.add(addressToSelfdestructHex)
}
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 new Error('could not read contract account')
}
this._env.contract = account
this._runState.gasRefund = results.execResult.gasRefund ?? BIGINT_0
}
return this._getReturnCode(results)
}
/**
* Creates a new contract with a given value.
*/
async create(
gasLimit: bigint,
value: bigint,
codeToRun: Uint8Array,
salt?: Uint8Array,
eofCallData?: Uint8Array,
): Promise<bigint> {
const selfdestruct = new Set(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)
// 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
) {
return BIGINT_0
}
// EIP-2681 check
if (this._env.contract.nonce >= MAX_UINT64) {
return BIGINT_0
}
this._env.contract.nonce += BIGINT_1
await this.journal.putAccount(this._env.address, this._env.contract)
if (this.common.isActivatedEIP(3860)) {
if (
codeToRun.length > Number(this.common.param('maxInitCodeSize')) &&
this._evm.allowUnlimitedInitCodeSize === false
) {
return BIGINT_0
}
}
const message = new Message({
caller,
gasLimit,
value,
data: codeToRun,
eofCallData,
salt,
depth,
selfdestruct,
gasRefund: this._runState.gasRefund,
blobVersionedHashes: this._env.blobVersionedHashes,
accessWitness: this._env.accessWitness,
})
let createdAddresses: Set<PrefixedHexString>
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 === ERROR.REVERT
) {
this._runState.returnBytes = results.execResult.returnValue
}
if (
!results.execResult.exceptionError ||
results.execResult.exceptionError.error === ERROR.CODESTORE_OUT_OF_GAS
) {
for (const addressToSelfdestructHex of selfdestruct) {
this._result.selfdestruct.add(addressToSelfdestructHex)
}
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 new Error('could not read contract account')
}
this._env.contract = account
this._runState.gasRefund = results.execResult.gasRefund ?? BIGINT_0
if (results.createdAddress) {
// push the created address to the stack
return 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: bigint,
value: bigint,
data: Uint8Array,
salt: Uint8Array,
): Promise<bigint> {
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: bigint,
value: bigint,
containerData: Uint8Array,
salt: Uint8Array,
callData: Uint8Array,
): Promise<bigint> {
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: Address): Promise<void> {
return this._selfDestruct(toAddress)
}
async _selfDestruct(toAddress: Address): Promise<void> {
// only add to refund if this is the first selfdestruct for the address
if (!this._result.selfdestruct.has(bytesToHex(this._env.address.bytes))) {
this.refundGas(this.common.param('selfdestructRefundGas'))
}
this._result.selfdestruct.add(bytesToHex(this._env.address.bytes))
const toSelf = equalsBytes(toAddress.bytes, this._env.address.bytes)
// Add to beneficiary balance
if (!toSelf) {
let toAccount = await this._stateManager.getAccount(toAddress)
if (!toAccount) {
toAccount = new Account()
}
toAccount.balance += this._env.contract.balance
await this.journal.putAccount(toAddress, toAccount)
}
// 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
}
}
// Set contract balance to 0
if (doModify) {
await this._stateManager.modifyAccountFields(this._env.address, {
balance: BIGINT_0,
})
}
trap(ERROR.STOP)
}
/**
* Creates a new log in the current environment.
*/
log(data: Uint8Array, numberOfTopics: number, topics: Uint8Array[]): void {
if (numberOfTopics < 0 || numberOfTopics > 4) {
trap(ERROR.OUT_OF_RANGE)
}
if (topics.length !== numberOfTopics) {
trap(ERROR.INTERNAL_ERROR)
}
const log: Log = [this._env.address.bytes, topics, data]
this._result.logs.push(log)
}
private _getReturnCode(results: EVMResult, isEOFCreate = false) {
if (this._runState.env.eof === undefined || isEOFCreate) {
if (results.execResult.exceptionError) {
return BIGINT_0
} else {
return BIGINT_1
}
} else {
// EOF mode, call was either EXTCALL / EXTDELEGATECALL / EXTSTATICCALL
if (results.execResult.exceptionError !== undefined) {
if (results.execResult.exceptionError.error === ERROR.REVERT) {
// Revert
return BIGINT_1
} else {
// Failure
return BIGINT_2
}
}
return BIGINT_0
}
}
}