Skip to content

Commit cdf601c

Browse files
acolytec3jochem-brouwerholgerd77
authored
Add tracing to t8n (#3953)
* Add json traces to t8n tool support * Update trace output to be one step per line * move tracing to correct spot * address feedback * Clean up traces * Cache pre-gas computation memory size for 3155 * Add jsonifyTrace helper and test * more fixes * Have t8n use typescript where available * Partially fix gas calculation * Implicitly call STOP if end of bytecode is reached * spellcheck * fix tests and opcode function lookup logic * more fixes * address feedback * fix tests * fix test * Remove unneeded test data * where is the outlog? * REmove extra slash * lint * t8ntool: fix test * remove Nify from whitelist * client: remove console.log in test * evm/vm: lowercase stepTraceJSON * Move helpers to vm. Update tests. Add eip7756 formatted fields * Add test * Comment bytecode * Revert changes related to adding STOP code * spellcheck * Add remaining fields for eip 7756 * spellcheck * fix functionDepth reference * update comments * Add logic to track storage in step hook * memory is not optional * pad keys to 32 bytes for storage * Address feedback * Simplify immediates computation * Add eof test * Revise bytecode * Fix definition and presentation of immediates * fix intermediates issue * spellcheck * address feedback --------- Co-authored-by: Jochem Brouwer <[email protected]> Co-authored-by: Holger Drewes <[email protected]>
1 parent e1091ae commit cdf601c

File tree

12 files changed

+476
-29
lines changed

12 files changed

+476
-29
lines changed

config/cspell-ts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
}
2525
],
2626
"words": [
27+
"immediates",
2728
"unerasable",
2829
"bytelist",
2930
"bytestring",

packages/evm/src/eof/container.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ class EOFHeader {
246246
// Write the start of the first code section into `codeStartPos`
247247
// Note: in EVM, if one would set the Program Counter to this byte, it would start executing the bytecode of the first code section
248248
this.codeStartPos = [relativeOffset]
249+
this.getCodePosition(this.codeSizes.length - 1) // initialize code positions
249250
}
250251

251252
sections() {
@@ -269,6 +270,27 @@ class EOFHeader {
269270
}
270271
return offset
271272
}
273+
274+
// Returns the code section for a given program counter position
275+
getSectionFromProgramCounter(programCounter: number) {
276+
if (
277+
programCounter < 0 ||
278+
programCounter >
279+
this.codeStartPos[this.codeStartPos.lastIndex] + this.codeSizes[this.codeSizes.lastIndex]
280+
) {
281+
// If code position is outside the beginning or end of the code sections, return 0
282+
throw EthereumJSErrorWithoutCode('program counter out of bounds')
283+
}
284+
285+
for (let i = 0; i < this.codeSizes.length; i++) {
286+
if (programCounter < this.codeStartPos[i] + this.codeSizes[i]) {
287+
// We've found our section if the code position is less than the end of the current code section
288+
return i
289+
}
290+
}
291+
// This shouldn't happen so just error
292+
throw EthereumJSErrorWithoutCode(`Invalid program counter value: ${programCounter}`)
293+
}
272294
}
273295

274296
export interface TypeSection {

packages/evm/src/interpreter.ts

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
BIGINT_2,
77
EthereumJSErrorWithoutCode,
88
MAX_UINT64,
9+
bigIntToBytes,
910
bigIntToHex,
1011
bytesToBigInt,
1112
bytesToHex,
1213
equalsBytes,
14+
setLengthLeft,
1315
setLengthRight,
1416
} from '@ethereumjs/util'
1517
import debugDefault from 'debug'
@@ -32,6 +34,7 @@ import type {
3234
VerkleAccessWitnessInterface,
3335
} from '@ethereumjs/common'
3436
import type { Address, PrefixedHexString } from '@ethereumjs/util'
37+
import { stackDelta } from './eof/stackDelta.ts'
3538
import type { EVM } from './evm.ts'
3639
import type { Journal } from './journal.ts'
3740
import type { AsyncOpHandler, Opcode, OpcodeMapEntry } from './opcodes/index.ts'
@@ -108,6 +111,7 @@ export interface RunState {
108111
gasRefund: bigint // Tracks the current refund
109112
gasLeft: bigint // Current gas left
110113
returnBytes: Uint8Array /* Current bytes in the return Uint8Array. Cleared each time a CALL/CREATE is made in the current frame. */
114+
accessedStorage: Map<PrefixedHexString, PrefixedHexString>
111115
}
112116

113117
export interface InterpreterResult {
@@ -127,12 +131,18 @@ export interface InterpreterStep {
127131
fee: number
128132
dynamicFee?: bigint
129133
isAsync: boolean
134+
code: number // The hexadecimal representation of the opcode (e.g. 0x60 for PUSH1)
130135
}
131136
account: Account
132137
address: Address
133138
memory: Uint8Array
134139
memoryWordCount: bigint
135140
codeAddress: Address
141+
eofSection?: number // Current EOF section being executed
142+
immediate?: Uint8Array // Immediate argument of the opcode
143+
eofFunctionDepth?: number // Depth of CALLF return stack
144+
error?: Uint8Array // Error bytes returned if revert occurs
145+
storage?: [PrefixedHexString, PrefixedHexString][]
136146
}
137147

138148
/**
@@ -198,6 +208,7 @@ export class Interpreter {
198208
gasRefund: env.gasRefund,
199209
gasLeft,
200210
returnBytes: new Uint8Array(0),
211+
accessedStorage: new Map(), // Maps accessed storage keys to their values (i.e. SSTOREd and SLOADed values)
201212
}
202213
this.journal = journal
203214
this._env = env
@@ -283,7 +294,7 @@ export class Interpreter {
283294
while (this._runState.programCounter < this._runState.code.length) {
284295
const programCounter = this._runState.programCounter
285296
let opCode: number
286-
let opCodeObj: OpcodeMapEntry
297+
let opCodeObj: OpcodeMapEntry | undefined
287298
if (doJumpAnalysis) {
288299
opCode = this._runState.code[programCounter]
289300
// Only run the jump destination analysis if `code` actually contains a JUMP/JUMPI/JUMPSUB opcode
@@ -319,13 +330,13 @@ export class Interpreter {
319330
}
320331
}
321332

322-
this._runState.opCode = opCode!
333+
this._runState.opCode = opCode
323334

324335
try {
325336
if (overheadTimer !== undefined) {
326337
this.performanceLogger.pauseTimer()
327338
}
328-
await this.runStep(opCodeObj!)
339+
await this.runStep(opCodeObj)
329340
if (overheadTimer !== undefined) {
330341
this.performanceLogger.unpauseTimer(overheadTimer)
331342
}
@@ -374,6 +385,9 @@ export class Interpreter {
374385

375386
let gas = opInfo.feeBigInt
376387

388+
// Cache pre-gas memory size if doing tracing (EIP-7756)
389+
const memorySizeCache = this._runState.memoryWordCount
390+
377391
try {
378392
if (opInfo.dynamicGas) {
379393
// This function updates the gas in-place.
@@ -384,7 +398,7 @@ export class Interpreter {
384398
if (this._evm.events.listenerCount('step') > 0 || this._evm.DEBUG) {
385399
// Only run this stepHook function if there is an event listener (e.g. test runner)
386400
// or if the vm is running in debug mode (to display opcode debug logs)
387-
await this._runStepHook(gas, this.getGasLeft())
401+
await this._runStepHook(gas, this.getGasLeft(), memorySizeCache)
388402
}
389403

390404
if (
@@ -441,27 +455,72 @@ export class Interpreter {
441455
return this._evm['_opcodeMap'][op]
442456
}
443457

444-
async _runStepHook(dynamicFee: bigint, gasLeft: bigint): Promise<void> {
445-
const opcodeInfo = this.lookupOpInfo(this._runState.opCode)
446-
const opcode = opcodeInfo.opcodeInfo
458+
async _runStepHook(dynamicFee: bigint, gasLeft: bigint, memorySize: bigint): Promise<void> {
459+
const opcodeInfo = this.lookupOpInfo(this._runState.opCode).opcodeInfo
460+
const section = this._env.eof?.container.header.getSectionFromProgramCounter(
461+
this._runState.programCounter,
462+
)
463+
let error = undefined
464+
let immediate = undefined
465+
if (opcodeInfo.code === 0xfd) {
466+
// If opcode is REVERT, read error data and return in trace
467+
const [offset, length] = this._runState.stack.peek(2)
468+
error = new Uint8Array(0)
469+
if (length !== BIGINT_0) {
470+
error = this._runState.memory.read(Number(offset), Number(length))
471+
}
472+
}
473+
474+
// Add immediate if present (i.e. bytecode parameter for a preceding opcode like (RJUMP 01 - jumps to PC 1))
475+
if (
476+
stackDelta[opcodeInfo.code] !== undefined &&
477+
stackDelta[opcodeInfo.code].intermediates > 0
478+
) {
479+
immediate = this._runState.code.slice(
480+
this._runState.programCounter + 1, // immediates start "immediately" following current opcode
481+
this._runState.programCounter + 1 + stackDelta[opcodeInfo.code].intermediates,
482+
)
483+
}
484+
485+
if (opcodeInfo.name === 'SLOAD') {
486+
// Store SLOADed values for recording in trace
487+
const key = this._runState.stack.peek(1)
488+
const value = await this.storageLoad(setLengthLeft(bigIntToBytes(key[0]), 32))
489+
this._runState.accessedStorage.set(`0x${key[0].toString(16)}`, bytesToHex(value))
490+
}
491+
492+
if (opcodeInfo.name === 'SSTORE') {
493+
// Store SSTOREed values for recording in trace
494+
const [key, value] = this._runState.stack.peek(2)
495+
this._runState.accessedStorage.set(`0x${key.toString(16)}`, `0x${value.toString(16)}`)
496+
}
497+
498+
// Create event object for step
447499
const eventObj: InterpreterStep = {
448500
pc: this._runState.programCounter,
449501
gasLeft,
450502
gasRefund: this._runState.gasRefund,
451503
opcode: {
452-
name: opcode.fullName,
453-
fee: opcode.fee,
504+
name: opcodeInfo.fullName,
505+
fee: opcodeInfo.fee,
454506
dynamicFee,
455-
isAsync: opcode.isAsync,
507+
isAsync: opcodeInfo.isAsync,
508+
code: opcodeInfo.code,
456509
},
457510
stack: this._runState.stack.getStack(),
458511
depth: this._env.depth,
459512
address: this._env.address,
460513
account: this._env.contract,
461-
memory: this._runState.memory._store.subarray(0, Number(this._runState.memoryWordCount) * 32),
462-
memoryWordCount: this._runState.memoryWordCount,
514+
memory: this._runState.memory._store.subarray(0, Number(memorySize) * 32),
515+
memoryWordCount: memorySize,
463516
codeAddress: this._env.codeAddress,
464517
stateManager: this._runState.stateManager,
518+
eofSection: section,
519+
immediate,
520+
error,
521+
eofFunctionDepth:
522+
this._env.eof !== undefined ? this._env.eof?.eofRunState.returnStack.length + 1 : undefined,
523+
storage: Array.from(this._runState.accessedStorage.entries()),
465524
}
466525

467526
if (this._evm.DEBUG) {
@@ -499,6 +558,7 @@ export class Interpreter {
499558
* @property {fee} opcode.number Base fee of the opcode
500559
* @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee
501560
* @property {boolean} opcode.isAsync opcode is async
561+
* @property {number} opcode.code opcode code
502562
* @property {BigInt} gasLeft amount of gasLeft
503563
* @property {BigInt} gasRefund gas refund
504564
* @property {StateManager} stateManager a {@link StateManager} instance
@@ -510,6 +570,11 @@ export class Interpreter {
510570
* @property {Uint8Array} memory the memory of the EVM as a `Uint8Array`
511571
* @property {BigInt} memoryWordCount current size of memory in words
512572
* @property {Address} codeAddress the address of the code which is currently being ran (this differs from `address` in a `DELEGATECALL` and `CALLCODE` call)
573+
* @property {number} eofSection the current EOF code section referenced by the PC
574+
* @property {Uint8Array} immediate the immediate argument of the opcode
575+
* @property {Uint8Array} error the error data of the opcode (only present for REVERT)
576+
* @property {number} eofFunctionDepth the depth of the function call (only present for EOF)
577+
* @property {Array} storage an array of tuples, where each tuple contains a storage key and value
513578
*/
514579
await this._evm['_emit']('step', eventObj)
515580
}

packages/evm/test/eips/eof-runner.spec.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Account, Address, hexToBytes } from '@ethereumjs/util'
1+
import { Account, Address, type PrefixedHexString, hexToBytes } from '@ethereumjs/util'
22
import { assert, describe, it } from 'vitest'
33

4-
import { createEVM } from '../../src/index.ts'
4+
import { EOFContainer, createEVM } from '../../src/index.ts'
55

66
import { getCommon } from './eof-utils.ts'
77

@@ -38,4 +38,19 @@ describe('EOF: should run a simple contract', async () => {
3838
// This costs 4 gas
3939
assert.equal(result.execResult.executionGasUsed, BigInt(4))
4040
})
41+
it('should initialize code positions correctly', async () => {
42+
const code = hexToBytes(
43+
('0xef0001' +
44+
'010008' +
45+
'02' +
46+
'000200080003' +
47+
'040000' +
48+
'00' +
49+
'0080000100000001' + // Section 1
50+
'3050e30001600100' + // Section 2
51+
'3050e4') as PrefixedHexString,
52+
)
53+
const eofContainer = new EOFContainer(code)
54+
assert.doesNotThrow(() => eofContainer.header.getSectionFromProgramCounter(33))
55+
})
4156
})

packages/evm/test/verkle.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('verkle tests', () => {
112112
assert.equal(writtenChunks.length, 1)
113113
assert.equal(res.execResult.exceptionError?.error, undefined)
114114
})
115-
})
115+
}, 20000)
116116
describe('generate an execution witness', () => {
117117
it('should generate the correct execution witness from a prestate and changes', async () => {
118118
const preStateVKT: Record<PrefixedHexString, PrefixedHexString> = {
@@ -188,4 +188,4 @@ describe('generate an execution witness', () => {
188188
// Ensure sender account nonce is 1 in execution witness
189189
assert.equal(decodeVerkleLeafBasicData(hexToBytes(suffixDiff!.newValue!)).nonce, 1n)
190190
})
191-
})
191+
}, 20000)

packages/vm/test/api/EIPs/eip-4844-blobs.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,4 @@ describe('EIP4844 tests', () => {
100100
assert.deepEqual(result.stateRoot, block.header.stateRoot)
101101
assert.deepEqual(result.logsBloom, block.header.logsBloom)
102102
})
103-
})
103+
}, 20000)

packages/vm/test/api/runTx.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,4 +926,4 @@ describe('EIP 4844 transaction tests', () => {
926926
assert.equal(res.blobGasUsed, 131072n, 'returns correct blob gas used for 1 blob')
927927
Blockchain.prototype.getBlock = oldGetBlockFunction
928928
})
929-
})
929+
}, 20000)

0 commit comments

Comments
 (0)