diff --git a/packages/util/src/errors.ts b/packages/util/src/errors.ts new file mode 100644 index 0000000000..45dc7133d0 --- /dev/null +++ b/packages/util/src/errors.ts @@ -0,0 +1,100 @@ +enum ErrorCode { + INVALID_PARAM = 'INVALID_PARAM', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', +} + +interface GeneralError extends Partial { + code?: T +} + +interface InvalidParamError extends GeneralError { + param?: string +} + +interface UnknownError extends GeneralError { + [key: string]: any +} + +// Convert an ErrorCode into its Typed Error +type CodedGeneralError = T extends ErrorCode.INVALID_PARAM + ? InvalidParamError + : T extends ErrorCode.UNKNOWN_ERROR + ? UnknownError + : never + +function isError>( + error: GeneralError, + code: K +): error is T { + if (error && (error).code === code) { + return true + } + return false +} + +function isInvalidParamError(error: GeneralError): error is InvalidParamError { + return isError(error, ErrorCode.INVALID_PARAM) +} + +function isUnknownError(error: GeneralError): error is UnknownError { + return isError(error, ErrorCode.UNKNOWN_ERROR) +} + +export class ErrorLogger { + static errors = ErrorCode + + makeError(codedError: CodedGeneralError): Error { + const { code, message, ...params } = codedError + const messageDetails: Array = [] + + if (isInvalidParamError(codedError) && typeof params.param !== 'undefined') { + messageDetails.push(`Invalid param=${codedError.param}`) + } + + if (isUnknownError(codedError)) { + Object.keys(params).forEach((key) => { + const value = codedError[key] + try { + messageDetails.push(key + '=' + JSON.stringify(value)) + } catch { + messageDetails.push(key + '=' + JSON.stringify(codedError[key].toString())) + } + }) + } + + messageDetails.push(`code=${code}`) + + let errorMessage = message ?? '' + + if (messageDetails.length) { + errorMessage += ' | Details: ' + messageDetails.join(', ') + } + + const error = new Error(errorMessage) as CodedGeneralError + error.code = codedError.code + + Object.keys(params).forEach((key) => { + const typedKey = key as keyof typeof codedError + error[typedKey] = codedError[typedKey] + }) + + // captureStackTrace is not defined in some browsers, notably Firefox + if (Error.captureStackTrace) { + Error.captureStackTrace(error, this.throwError) + } + + throw error + } + + throwError( + code?: T, + params?: Omit, 'code' | 'stack'> + ): void { + this.makeError({ + code: code ?? ErrorCode.UNKNOWN_ERROR, + ...params, + } as CodedGeneralError) + } +} + +export const errorLog = new ErrorLogger() diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index bda39a3f6c..d079db4297 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -13,6 +13,11 @@ export * from './account' */ export * from './address' +/** + * Errors + */ +export * from './errors' + /** * Hash functions */ diff --git a/packages/util/test/errors.spec.ts b/packages/util/test/errors.spec.ts new file mode 100644 index 0000000000..3621c38ffe --- /dev/null +++ b/packages/util/test/errors.spec.ts @@ -0,0 +1,125 @@ +import tape from 'tape' +import { ErrorLogger } from '../src/errors' + +tape('ErrorLogger', (t) => { + const errorLog = new ErrorLogger() + + t.test('should assign the UNKNOWN_ERROR code to errors without code', (st) => { + let error: any + + try { + errorLog.throwError() + } catch (e) { + error = e + } + + st.equal(error.code, ErrorLogger.errors.UNKNOWN_ERROR) + st.end() + }), + t.test('should preserve the stack trace of the throwError context', (st) => { + let error: any + + try { + errorLog.throwError() + } catch (e) { + error = e + } + + // captureStackTrace is not defined in some browsers, notably Firefox, so behavior can't be implemented/tested there + // @ts-ignore + if (Error.captureStackTrace) { + st.ok(/Error: {2}\| Details: code=UNKNOWN_ERROR\n {4}at Test./.test(error.stack)) + } + + st.end() + }), + t.test('should populate an error with UNKNOWN_ERROR code with all provided params', (st) => { + let error: any + + try { + errorLog.throwError(ErrorLogger.errors.UNKNOWN_ERROR, { + errorInfo1: 'Information on the error', + errorInfo2: 'More information on the error', + }) + } catch (e) { + error = e + } + + st.equal(error.code, ErrorLogger.errors.UNKNOWN_ERROR) + st.equal(error.errorInfo1, 'Information on the error') + st.equal(error.errorInfo2, 'More information on the error') + st.end() + }), + t.test( + 'should add all error params of UNKNOWN_ERROR to error mes21 02 2022 17:42:12.866:ERROR [launcher]: No binary for ChromeHeadless browser on your platform.sage details', + (st) => { + let error: any + + try { + errorLog.throwError(ErrorLogger.errors.UNKNOWN_ERROR, { + errorInfo1: 'Information on the error', + errorInfo2: 'More information on the error', + }) + } catch (e) { + error = e + } + + st.equal( + error.message, + ' | Details: errorInfo1="Information on the error", errorInfo2="More information on the error", code=UNKNOWN_ERROR' + ) + st.end() + } + ), + t.test('should append all error details to provided error message', (st) => { + let error: any + + try { + errorLog.throwError(ErrorLogger.errors.UNKNOWN_ERROR, { + message: 'Error Message', + errorInfo1: 'Information on the error', + errorInfo2: 'More information on the error', + }) + } catch (e) { + error = e + } + + st.equal( + error.message, + 'Error Message | Details: errorInfo1="Information on the error", errorInfo2="More information on the error", code=UNKNOWN_ERROR' + ) + st.end() + }) + t.test('should populate an error with INVALID_PARAM with the "param" prop', (st) => { + let error: any + + try { + errorLog.throwError(ErrorLogger.errors.INVALID_PARAM, { + param: 'difficulty', + }) + } catch (e) { + error = e + } + + st.equal(error.param, 'difficulty') + st.end() + }), + t.test('should add the "param" prop to the INVALID_PARAM error message', (st) => { + let error: any + + try { + errorLog.throwError(ErrorLogger.errors.INVALID_PARAM, { + message: 'Gas limit higher than maximum', + param: 'gasLimit', + }) + } catch (e) { + error = e + } + + st.equal( + error.message, + 'Gas limit higher than maximum | Details: Invalid param=gasLimit, code=INVALID_PARAM' + ) + st.end() + }) +}) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 5728cceb44..a074a87074 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,6 +1,6 @@ import { debug as createDebugLogger } from 'debug' import { BaseTrie as Trie } from 'merkle-patricia-tree' -import { Account, Address, BN, intToBuffer, rlp } from 'ethereumjs-util' +import { Account, Address, BN, errorLog, ErrorLogger, intToBuffer, rlp } from 'ethereumjs-util' import { Block } from '@ethereumjs/block' import { ConsensusType } from '@ethereumjs/common' import VM from './index' @@ -209,8 +209,11 @@ export default async function runBlock(this: VM, opts: RunBlockOpts): Promise API parameter usage/data errors', async (t) => { await vm .runBlock({ block }) .then(() => t.fail('should have returned error')) - .catch((e) => t.ok(e.message.includes('Invalid block'))) + .catch((e) => t.ok(e.code === ErrorLogger.errors.INVALID_PARAM && e.param === 'gasLimit')) }) t.test('should fail when block validation fails', async (t) => {