diff --git a/packages/evm/CHANGELOG.md b/packages/evm/CHANGELOG.md index 67b2bfa73d0..57cf890af02 100644 --- a/packages/evm/CHANGELOG.md +++ b/packages/evm/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) (modification: no type change headlines) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + +- Add optional `pushData` on `InterpreterStep` for `PUSH0`–`PUSH32` opcode traces, see Issue [#2418](https://github.com/ethereumjs/ethereumjs-monorepo/issues/2418) + ## 10.1.1 - 2025-01-28 - Ensure `codeAddress` in step event is correctly set, see PR [#4189](https://github.com/ethereumjs/ethereumjs-monorepo/pull/4189) diff --git a/packages/evm/README.md b/packages/evm/README.md index 24e997c0790..06e09f008e0 100644 --- a/packages/evm/README.md +++ b/packages/evm/README.md @@ -491,7 +491,7 @@ You can subscribe to the following events: - `beforeMessage`: Emits a `Message` right after running it. - `afterMessage`: Emits an `EVMResult` right after running a message. -- `step`: Emits an `InterpreterStep` right before running an EVM step. +- `step`: Emits an `InterpreterStep` right before running an EVM step. For `PUSH0`–`PUSH32`, `InterpreterStep.pushData` contains the bytes placed on the stack (one `0x00` byte for `PUSH0`, otherwise the immediate bytes from bytecode). - `newContract`: Emits a `NewContractEvent` right before creating a contract. This event contains the deployment code, not the deployed code, as the creation message may not return such a code. #### Event listeners diff --git a/packages/evm/src/interpreter.ts b/packages/evm/src/interpreter.ts index f1fcc64f4f0..1eaa8fabb4b 100644 --- a/packages/evm/src/interpreter.ts +++ b/packages/evm/src/interpreter.ts @@ -148,6 +148,11 @@ export interface InterpreterStep { codeAddress: Address eofSection?: number // Current EOF section being executed immediate?: Uint8Array // Immediate argument of the opcode + /** + * Bytes placed on the stack by `PUSH0`–`PUSH32` (present only on those steps). + * For `PUSH0` this is a single `0x00` byte; for `PUSHn` it is the `n` immediate bytes from code. + */ + pushData?: Uint8Array eofFunctionDepth?: number // Depth of CALLF return stack error?: Uint8Array // Error bytes returned if revert occurs storage?: [PrefixedHexString, PrefixedHexString][] @@ -489,6 +494,16 @@ export class Interpreter { ) } + let pushData: Uint8Array | undefined + const opCodeNum = opcodeInfo.code + if (opCodeNum >= 0x5f && opCodeNum <= 0x7f) { + if (opCodeNum === 0x5f) { + pushData = new Uint8Array([0]) + } else { + pushData = immediate + } + } + // Create event object for step const eventObj: InterpreterStep = { pc: this._runState.programCounter, @@ -511,6 +526,7 @@ export class Interpreter { stateManager: this._runState.stateManager, eofSection: section, immediate, + pushData, error, eofFunctionDepth: this._env.eof !== undefined ? this._env.eof?.eofRunState.returnStack.length + 1 : undefined, @@ -565,6 +581,7 @@ export class Interpreter { * @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} pushData bytes pushed by PUSH0–PUSH32 (only present for those opcodes) * @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 diff --git a/packages/evm/test/stepPushData.spec.ts b/packages/evm/test/stepPushData.spec.ts new file mode 100644 index 00000000000..354fbc39900 --- /dev/null +++ b/packages/evm/test/stepPushData.spec.ts @@ -0,0 +1,56 @@ +import { equalsBytes, hexToBytes } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { createEVM } from '../src/index.ts' + +import type { InterpreterStep } from '../src/index.ts' + +describe('step event pushData', () => { + it('sets pushData for PUSH0', async () => { + const evm = await createEVM() + const steps: InterpreterStep[] = [] + evm.events.on('step', (e) => { + steps.push(e) + }) + await evm.runCode({ code: hexToBytes('0x5f00'), gasLimit: 100000n }) + const pushStep = steps.find((s) => s.opcode.code === 0x5f) + assert.isDefined(pushStep?.pushData) + assert.isTrue(equalsBytes(pushStep!.pushData!, new Uint8Array([0]))) + }) + + it('sets pushData for PUSH1 to the immediate byte(s)', async () => { + const evm = await createEVM() + const steps: InterpreterStep[] = [] + evm.events.on('step', (e) => { + steps.push(e) + }) + await evm.runCode({ code: hexToBytes('0x60ab00'), gasLimit: 100000n }) + const pushStep = steps.find((s) => s.opcode.code === 0x60) + assert.isDefined(pushStep?.pushData) + assert.isTrue(equalsBytes(pushStep!.pushData!, new Uint8Array([0xab]))) + }) + + it('sets pushData length 32 for PUSH32', async () => { + const evm = await createEVM() + const steps: InterpreterStep[] = [] + evm.events.on('step', (e) => { + steps.push(e) + }) + const imm = '01'.repeat(32) + await evm.runCode({ code: hexToBytes(`0x7f${imm}00`), gasLimit: 100000n }) + const pushStep = steps.find((s) => s.opcode.code === 0x7f) + assert.isDefined(pushStep?.pushData) + assert.strictEqual(pushStep!.pushData!.length, 32) + }) + + it('does not set pushData for non-PUSH opcodes', async () => { + const evm = await createEVM() + const steps: InterpreterStep[] = [] + evm.events.on('step', (e) => { + steps.push(e) + }) + await evm.runCode({ code: hexToBytes('0x600160020100'), gasLimit: 100000n }) + const addStep = steps.find((s) => s.opcode.code === 0x01) + assert.isUndefined(addStep?.pushData) + }) +})