Skip to content
49 changes: 39 additions & 10 deletions packages/evm/src/exceptions.ts → packages/evm/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export enum ERROR {
import { EthereumJSError } from '@ethereumjs/util'

import type { EOFError } from './eof/errors.js'

// TODO: merge EOF errors in here
export enum RuntimeErrorMessage {
OUT_OF_GAS = 'out of gas',
CODESTORE_OUT_OF_GAS = 'code store out of gas',
CODESIZE_EXCEEDS_MAXIMUM = 'code size to deposit exceeds maximum code size',
Expand All @@ -15,13 +20,11 @@ export enum ERROR {
REFUND_EXHAUSTED = 'refund exhausted',
VALUE_OVERFLOW = 'value overflow',
INSUFFICIENT_BALANCE = 'insufficient balance',
INVALID_BEGINSUB = 'invalid BEGINSUB',
INVALID_RETURNSUB = 'invalid RETURNSUB',
INVALID_JUMPSUB = 'invalid JUMPSUB',
INVALID_BYTECODE_RESULT = 'invalid bytecode deployed',
INITCODE_SIZE_VIOLATION = 'initcode exceeds max initcode size',
INVALID_INPUT_LENGTH = 'invalid input length',
INVALID_EOF_FORMAT = 'invalid EOF format',
INVALID_PRECOMPILE = 'invalid precompile',

// BLS errors
BLS_12_381_INVALID_INPUT_LENGTH = 'invalid input length',
Expand All @@ -38,12 +41,38 @@ export enum ERROR {
INVALID_PROOF = 'kzg proof invalid',
}

export class EvmError {
error: ERROR
errorType: string
export enum EvmErrorCode {
UNSUPPORTED_FEATURE = 'EVM_ERROR_UNSUPPORTED_FEATURE',
RUNTIME_ERROR = 'EVM_ERROR_RUNTIME_ERROR',
}

type EvmRuntimeErrorType = {
code: EvmErrorCode.RUNTIME_ERROR
reason: RuntimeErrorMessage | EOFError
} & (
| { reason: RuntimeErrorMessage.REVERT; revertBytes: Uint8Array }
| { reason: Exclude<RuntimeErrorMessage, RuntimeErrorMessage.REVERT> | EOFError }
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this scalable? I'm wondering how this would look if we had multiple error codes with corresponding different formats. Right now we're just having one case for "RuntimeErrorMessage.REVERT" and another excluding "RuntimeErrorMessage.REVERT", but I wonder if that might become too burdensome if we need to exclude more

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was some experimenting with the typescript typing I did. It ensures that if reason is RuntimeErrorMessage.REVERT it has the mandatory revertBytes. If is NOT a REVERT then the reason could be any of the RuntimeErrorMessage or an EOFError.

Note: EOFError has not been migrated to the EvmError, likely when I do that this typing (the second line) does not have to be there (will check)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typing (I checked) is just to type the evm errors such that the REVERT error has a revertBytes field. It is straightforward to add new error codes or new error reasons, or the inject specific error which adds context (like the mandatory revertBytes field in the REVERT error)


export type EvmErrorType = { code: EvmErrorCode.UNSUPPORTED_FEATURE } | EvmRuntimeErrorType

constructor(error: ERROR) {
this.error = error
this.errorType = 'EvmError'
export class EvmError extends EthereumJSError<EvmErrorType> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note, but I think we might want to migrate to the EVM (rather than Evm) capitalization as we've done for some others (Json -> JSON, etc.)

constructor(type: EvmErrorType, message?: string) {
super(type, message)
}
}

export function getRuntimeError(error: EvmError): RuntimeErrorMessage | EOFError | undefined {
if (error.type.code === EvmErrorCode.RUNTIME_ERROR) {
return error.type.reason
}
}

/*
throw new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.REVERT
})


*/
76 changes: 58 additions & 18 deletions packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import debugDefault from 'debug'

import { FORMAT } from './eof/constants.js'
import { isEOF } from './eof/util.js'
import { ERROR, EvmError } from './exceptions.js'
import { EvmError, EvmErrorCode, RuntimeErrorMessage, getRuntimeError } from './errors.js'
import { Interpreter } from './interpreter.js'
import { Journal } from './journal.js'
import { EVMPerformanceLogger } from './logger.js'
Expand Down Expand Up @@ -184,12 +184,18 @@ export class EVM implements EVMInterface {

for (const eip of this.common.eips()) {
if (!supportedEIPs.includes(eip)) {
throw new Error(`EIP-${eip} is not supported by the EVM`)
throw new EvmError(
{ code: EvmErrorCode.UNSUPPORTED_FEATURE },
`EIP-${eip} is not supported by the EVM`,
)
}
}

if (!EVM.supportedHardforks.includes(this.common.hardfork() as Hardfork)) {
throw new Error(
throw new EvmError(
{
code: EvmErrorCode.UNSUPPORTED_FEATURE,
},
`Hardfork ${this.common.hardfork()} not set as supported in supportedHardforks`,
)
}
Expand Down Expand Up @@ -418,7 +424,10 @@ export class EVM implements EVMInterface {
createdAddress: message.to,
execResult: {
returnValue: new Uint8Array(0),
exceptionError: new EvmError(ERROR.INITCODE_SIZE_VIOLATION),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.INITCODE_SIZE_VIOLATION,
}),
executionGasUsed: message.gasLimit,
},
}
Expand Down Expand Up @@ -475,7 +484,10 @@ export class EVM implements EVMInterface {
createdAddress: message.to,
execResult: {
returnValue: new Uint8Array(0),
exceptionError: new EvmError(ERROR.CREATE_COLLISION),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.CREATE_COLLISION,
}),
executionGasUsed: message.gasLimit,
},
}
Expand Down Expand Up @@ -770,8 +782,8 @@ export class EVM implements EVMInterface {
let gasUsed = message.gasLimit - interpreterRes.runState!.gasLeft
if (interpreterRes.exceptionError) {
if (
interpreterRes.exceptionError.error !== ERROR.REVERT &&
interpreterRes.exceptionError.error !== ERROR.INVALID_EOF_FORMAT
getRuntimeError(interpreterRes.exceptionError) !== RuntimeErrorMessage.REVERT &&
getRuntimeError(interpreterRes.exceptionError) !== RuntimeErrorMessage.INVALID_EOF_FORMAT
) {
gasUsed = message.gasLimit
}
Expand Down Expand Up @@ -907,7 +919,7 @@ export class EVM implements EVMInterface {
const { executionGasUsed, exceptionError, returnValue } = result.execResult
debug(
`Received message execResult: [ gasUsed=${executionGasUsed} exceptionError=${
exceptionError ? `'${exceptionError.error}'` : 'none'
exceptionError ? `'${getRuntimeError(exceptionError)}'` : 'none'
} returnValue=${short(returnValue)} gasRefund=${result.execResult.gasRefund ?? 0} ]`,
)
}
Expand All @@ -917,14 +929,17 @@ export class EVM implements EVMInterface {
// There is one exception: if the CODESTORE_OUT_OF_GAS error is thrown
// (this only happens the Frontier/Chainstart fork)
// then the error is dismissed
if (err && err.error !== ERROR.CODESTORE_OUT_OF_GAS) {
if (err && getRuntimeError(err) !== RuntimeErrorMessage.CODESTORE_OUT_OF_GAS) {
result.execResult.selfdestruct = new Set()
result.execResult.createdAddresses = new Set()
result.execResult.gasRefund = BIGINT_0
}
if (
err &&
!(this.common.hardfork() === Hardfork.Chainstart && err.error === ERROR.CODESTORE_OUT_OF_GAS)
!(
this.common.hardfork() === Hardfork.Chainstart &&
getRuntimeError(err) === RuntimeErrorMessage.CODESTORE_OUT_OF_GAS
)
) {
result.execResult.logs = []
await this.journal.revert()
Expand Down Expand Up @@ -993,7 +1008,10 @@ export class EVM implements EVMInterface {
gasLimit: bigint,
): Promise<ExecResult> | ExecResult {
if (typeof code !== 'function') {
throw new Error('Invalid precompile')
throw new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.INVALID_PRECOMPILE,
})
}

const opts = {
Expand Down Expand Up @@ -1053,7 +1071,10 @@ export class EVM implements EVMInterface {
protected async _reduceSenderBalance(account: Account, message: Message): Promise<void> {
account.balance -= message.value
if (account.balance < BIGINT_0) {
throw new EvmError(ERROR.INSUFFICIENT_BALANCE)
throw new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.INSUFFICIENT_BALANCE,
})
}
const result = this.journal.putAccount(message.caller, account)
if (this.DEBUG) {
Expand All @@ -1065,7 +1086,10 @@ export class EVM implements EVMInterface {
protected async _addToBalance(toAccount: Account, message: MessageWithTo): Promise<void> {
const newBalance = toAccount.balance + message.value
if (newBalance > MAX_INTEGER) {
throw new EvmError(ERROR.VALUE_OVERFLOW)
throw new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.VALUE_OVERFLOW,
})
}
toAccount.balance = newBalance
// putAccount as the nonce may have changed for contract creation
Expand Down Expand Up @@ -1114,43 +1138,59 @@ export class EVM implements EVMInterface {
}
}

// TODO clean me up
export function OOGResult(gasLimit: bigint): ExecResult {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasLimit,
exceptionError: new EvmError(ERROR.OUT_OF_GAS),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.OUT_OF_GAS,
}),
}
}
// CodeDeposit OOG Result
export function COOGResult(gasUsedCreateCode: bigint): ExecResult {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasUsedCreateCode,
exceptionError: new EvmError(ERROR.CODESTORE_OUT_OF_GAS),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.OUT_OF_GAS,
}),
}
}

export function INVALID_BYTECODE_RESULT(gasLimit: bigint): ExecResult {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasLimit,
exceptionError: new EvmError(ERROR.INVALID_BYTECODE_RESULT),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.INVALID_BYTECODE_RESULT,
}),
}
}

export function INVALID_EOF_RESULT(gasLimit: bigint): ExecResult {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasLimit,
exceptionError: new EvmError(ERROR.INVALID_EOF_FORMAT),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.INVALID_EOF_FORMAT,
}),
}
}

export function CodesizeExceedsMaximumError(gasUsed: bigint): ExecResult {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasUsed,
exceptionError: new EvmError(ERROR.CODESIZE_EXCEEDS_MAXIMUM),
exceptionError: new EvmError({
code: EvmErrorCode.RUNTIME_ERROR,
reason: RuntimeErrorMessage.CODESIZE_EXCEEDS_MAXIMUM,
}),
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/evm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { EOFContainer, validateEOF } from './eof/container.js'
import {
RuntimeErrorMessage as EVMRuntimeErrorMessage,
EvmError,
EvmErrorCode,
EvmErrorType,
} from './errors.js'
import { EVM } from './evm.js'
import { ERROR as EVMErrorMessage, EvmError } from './exceptions.js'
import { Message } from './message.js'
import { getOpcodesForHF } from './opcodes/index.js'
import {
Expand Down Expand Up @@ -47,8 +52,10 @@ export {
EOFContainer,
EVM,
EvmError,
EVMErrorMessage,
EvmErrorCode,
EvmErrorType,
EVMMockBlockchain,
EVMRuntimeErrorMessage,
getActivePrecompiles,
getOpcodesForHF,
MCLBLS,
Expand Down
Loading
Loading