Skip to content

Add tracing to t8n #3953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
504a7bd
Add json traces to t8n tool support
acolytec3 Mar 31, 2025
45d3cdc
Update trace output to be one step per line
acolytec3 Mar 31, 2025
d577e67
move tracing to correct spot
acolytec3 Apr 1, 2025
ecbe65d
address feedback
acolytec3 Apr 1, 2025
7263442
Clean up traces
acolytec3 Apr 1, 2025
a357fb6
Cache pre-gas computation memory size for 3155
acolytec3 Apr 1, 2025
fed9ce1
Add jsonifyTrace helper and test
acolytec3 Apr 1, 2025
ab2f22c
more fixes
acolytec3 Apr 1, 2025
d87e42d
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 2, 2025
bc88658
Have t8n use typescript where available
acolytec3 Apr 2, 2025
87afde8
Partially fix gas calculation
acolytec3 Apr 2, 2025
9fc4d27
Implicitly call STOP if end of bytecode is reached
acolytec3 Apr 3, 2025
410e7a0
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 3, 2025
2ee5d97
spellcheck
acolytec3 Apr 3, 2025
91bc624
fix tests and opcode function lookup logic
acolytec3 Apr 3, 2025
571a4f9
more fixes
acolytec3 Apr 3, 2025
8bd5ae6
address feedback
acolytec3 Apr 3, 2025
c52404f
fix tests
acolytec3 Apr 3, 2025
e88bca7
fix test
acolytec3 Apr 3, 2025
2d977d5
Remove unneeded test data
acolytec3 Apr 3, 2025
29e4313
where is the outlog?
acolytec3 Apr 3, 2025
4adfdb5
REmove extra slash
acolytec3 Apr 3, 2025
62b336f
lint
acolytec3 Apr 3, 2025
238f6cc
t8ntool: fix test
jochem-brouwer Apr 3, 2025
db31d14
remove Nify from whitelist
jochem-brouwer Apr 3, 2025
f6572c0
client: remove console.log in test
jochem-brouwer Apr 3, 2025
d797a92
evm/vm: lowercase stepTraceJSON
jochem-brouwer Apr 3, 2025
30923fa
Move helpers to vm. Update tests. Add eip7756 formatted fields
acolytec3 Apr 4, 2025
d09eeff
Add test
acolytec3 Apr 4, 2025
ee28d8c
Comment bytecode
acolytec3 Apr 4, 2025
1e76835
Revert changes related to adding STOP code
acolytec3 Apr 4, 2025
5ae3eff
spellcheck
acolytec3 Apr 4, 2025
5982f97
Add remaining fields for eip 7756
acolytec3 Apr 7, 2025
fb0f72c
spellcheck
acolytec3 Apr 7, 2025
11247a6
fix functionDepth reference
acolytec3 Apr 7, 2025
90c4cba
update comments
acolytec3 Apr 7, 2025
5819a48
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 7, 2025
4e51407
Add logic to track storage in step hook
acolytec3 Apr 8, 2025
1defe8b
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 8, 2025
a3c0b12
memory is not optional
acolytec3 Apr 8, 2025
8c83887
pad keys to 32 bytes for storage
acolytec3 Apr 8, 2025
23010e0
Merge branch 'master' into add-tracing-to-t8n
acolytec3 Apr 8, 2025
fdd6f7a
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 10, 2025
810990f
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 10, 2025
e2ef053
Address feedback
acolytec3 Apr 10, 2025
cc48f0c
Simplify immediates computation
acolytec3 Apr 11, 2025
7befd98
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 15, 2025
d119247
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 21, 2025
3ef669f
Add eof test
acolytec3 Apr 22, 2025
cfd9c31
Revise bytecode
acolytec3 Apr 22, 2025
0c285c0
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 22, 2025
43477c3
Fix definition and presentation of immediates
acolytec3 Apr 22, 2025
380de1b
Merge remote-tracking branch 'origin/master' into add-tracing-to-t8n
acolytec3 Apr 23, 2025
ae3c0e0
fix intermediates issue
acolytec3 Apr 23, 2025
86f6e77
spellcheck
acolytec3 Apr 23, 2025
62ae8fd
address feedback
acolytec3 Apr 23, 2025
ae8cd6a
Merge branch 'master' into add-tracing-to-t8n
holgerd77 Apr 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/cspell-ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
}
],
"words": [
"Nify",
"bytelist",
"bytestring",
"binarytree",
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export {
RustBN254,
validateEOF,
}

export { JSONifyStepTrace } from './opcodes/util.ts'
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion to rename this to StepTraceJSON. Why is this exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use it in t8n and that's in vm 🙃

Copy link
Member

Choose a reason for hiding this comment

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

Yer its fine, sorry for some reason I thought it was a type and not a method, and AFAIK you dont have to explicitly export types in order to be picked up by the IDE

export * from './binaryTreeAccessWitness.ts'
export * from './constructors.ts'
export * from './params.ts'
Expand Down
34 changes: 24 additions & 10 deletions packages/evm/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export interface InterpreterStep {
fee: number
dynamicFee?: bigint
isAsync: boolean
code: number
}
account: Account
address: Address
Expand Down Expand Up @@ -279,13 +280,13 @@ export class Interpreter {
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) {
// Iterate through the given ops until we hit STOP or some exception occurs
while (true) {
const programCounter = this._runState.programCounter
let opCode: number
let opCodeObj: OpcodeMapEntry
let opCodeObj: OpcodeMapEntry | undefined
if (doJumpAnalysis) {
opCode = this._runState.code[programCounter]
opCode = this._runState.code[programCounter] ?? 0 // Yellow Paper calls for explicitly executing STOP if no opcode found at PC (i.e. PC is beyond end of code)
// 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(
Expand All @@ -299,6 +300,10 @@ export class Interpreter {
}
} else {
opCodeObj = cachedOpcodes![programCounter]
if (opCodeObj === undefined) {
// If programCounter is out of bounds, set opCodeObj to STOP as above
opCodeObj = this.lookupOpInfo(0)
}
opCode = opCodeObj.opcodeInfo.code
}

Expand All @@ -319,13 +324,13 @@ export class Interpreter {
}
}

this._runState.opCode = opCode!
this._runState.opCode = opCode

try {
if (overheadTimer !== undefined) {
this.performanceLogger.pauseTimer()
}
await this.runStep(opCodeObj!)
await this.runStep(opCodeObj)
if (overheadTimer !== undefined) {
this.performanceLogger.unpauseTimer(overheadTimer)
}
Expand Down Expand Up @@ -374,6 +379,13 @@ export class Interpreter {

let gas = opInfo.feeBigInt

// Cache pre-gas memory size if doing tracing (EIP-3155)
let memorySize = BIGINT_0

if (this._evm.events.listenerCount('step') > 0 || this._evm.DEBUG) {
memorySize = this._runState.memoryWordCount
}

try {
if (opInfo.dynamicGas) {
// This function updates the gas in-place.
Expand All @@ -384,7 +396,7 @@ export class Interpreter {
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())
await this._runStepHook(gas, this.getGasLeft(), memorySize)
}

if (
Expand Down Expand Up @@ -441,7 +453,7 @@ export class Interpreter {
return this._evm['_opcodeMap'][op]
}

async _runStepHook(dynamicFee: bigint, gasLeft: bigint): Promise<void> {
async _runStepHook(dynamicFee: bigint, gasLeft: bigint, memorySize: bigint): Promise<void> {
const opcodeInfo = this.lookupOpInfo(this._runState.opCode)
const opcode = opcodeInfo.opcodeInfo
const eventObj: InterpreterStep = {
Expand All @@ -453,13 +465,14 @@ export class Interpreter {
fee: opcode.fee,
dynamicFee,
isAsync: opcode.isAsync,
code: opcode.code,
},
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,
memory: this._runState.memory._store.subarray(0, Number(memorySize) * 32),
memoryWordCount: memorySize,
codeAddress: this._env.codeAddress,
stateManager: this._runState.stateManager,
}
Expand Down Expand Up @@ -499,6 +512,7 @@ export class Interpreter {
* @property {fee} opcode.number Base fee of the opcode
* @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee
* @property {boolean} opcode.isAsync opcode is async
* @property {number} opcode.code opcode code
* @property {BigInt} gasLeft amount of gasLeft
* @property {BigInt} gasRefund gas refund
* @property {StateManager} stateManager a {@link StateManager} instance
Expand Down
29 changes: 28 additions & 1 deletion packages/evm/src/opcodes/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
BIGINT_64,
BIGINT_160,
BIGINT_NEG1,
bigIntToHex,
bytesToHex,
createAddressFromBigInt,
equalsBytes,
Expand All @@ -20,7 +21,7 @@
import type { Common } from '@ethereumjs/common'
import type { Address } from '@ethereumjs/util'
import type { ERROR } from '../exceptions.ts'
import type { RunState } from '../interpreter.ts'
import type { InterpreterStep, RunState } from '../interpreter.ts'

const MASK_160 = (BIGINT_1 << BIGINT_160) - BIGINT_1

Expand Down Expand Up @@ -271,3 +272,29 @@
return common.param('sstoreSetGas')
}
}

/**
* Formats an individual EVM step trace as a JSON object
* @param step an {@link InterpreterStep} emitted by the EVM `step` event
* @param memory whether to include the memory in the trace
* @returns a JSON object that matches teh EIP-3155 trace format
*/
export const JSONifyStepTrace = (step: InterpreterStep, memory: boolean = false) => {
let hexStack = []
hexStack = step.stack.map((item: bigint) => {
return '0x' + item.toString(16)
})
const opTrace = {
pc: step.pc,
op: step.opcode.code,
gas: bigIntToHex(step.gasLeft),
gasCost: bigIntToHex(step.opcode.dynamicFee ?? BigInt(step.opcode.fee)), // if `dynamicFee` is set, it includes base fee
memory: memory ? (step.memory.length > 0 ? bytesToHex(step.memory) : '0x') : undefined,
memSize: Number(step.memoryWordCount) * 32,
stack: hexStack,
depth: step.depth + 1, // Other clients start depth at 1
refund: Number(step.gasRefund),
opName: step.opcode.name,
}
return opTrace
}

Check warning on line 300 in packages/evm/src/opcodes/util.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/opcodes/util.ts#L283-L300

Added lines #L283 - L300 were not covered by tests
50 changes: 47 additions & 3 deletions packages/evm/test/runCall.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import {
Account,
Address,
BIGINT_0,
MAX_UINT64,
bigIntToHex,
bytesToBigInt,
bytesToHex,
concatBytes,
Expand All @@ -18,11 +20,12 @@

import { defaultBlock } from '../src/evm.ts'
import { ERROR } from '../src/exceptions.ts'
import { createEVM } from '../src/index.ts'

import { eip4844Data } from './testdata/eip4844.ts'
import { JSONifyStepTrace, createEVM } from '../src/index.ts'

import { readFileSync } from 'fs'

Check failure on line 25 in packages/evm/test/runCall.spec.ts

View workflow job for this annotation

GitHub Actions / browser / test-all-browser

test/runCall.spec.ts

Error: Module "fs" has been externalized for browser compatibility. Cannot access "fs.readFileSync" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details. ❯ Object.get ../../../../../../../@id/__vite-browser-external:fs:3:11 ❯ test/runCall.spec.ts:25:29
import { MerkleStateManager } from '@ethereumjs/statemanager'
import type { EVMRunCallOpts } from '../src/types.ts'
import { eip4844Data } from './testdata/eip4844.ts'

// Non-protected Create2Address generator. Does not check if Uint8Arrays have the right padding.
function create2address(sourceAddress: Address, codeHash: Uint8Array, salt: Uint8Array): Address {
Expand Down Expand Up @@ -753,3 +756,44 @@
}
})
})
describe('JSON traces', () => {
it.skip('should produce a trace that matches EIP 3155 spec', async () => {
// Test case provided in the EIP-3155 spec -- doesn't actually match spec with regard to representation of memory
const gethTrace = readFileSync(__dirname + '/testdata/besuTrace.jsonl', 'utf-8')
const common = new Common({ chain: Mainnet, hardfork: Hardfork.Istanbul })
const stateManager = new MerkleStateManager({
common,
})
const evm = await createEVM({
stateManager,
common,
})
const bytecodeHex = '0x604080536040604055604060006040600060025afa6040f3'
const bytecode = hexToBytes(bytecodeHex)
const runCallArgs = {
data: bytecode,
gasLimit: BigInt(0x2540be400),
}

const traces: string[] = []
evm.events.on('step', (e) => {
const trace = JSONifyStepTrace(e, true)
traces.push(JSON.stringify(trace))
})
evm.events.on('afterMessage', async (e) => {
const summary = {
stateRoot: bytesToHex(await evm.stateManager.getStateRoot()),
output: e.execResult.returnValue.length > 0 ? bytesToHex(e.execResult.returnValue) : '',
gasUsed: bigIntToHex(
e.execResult.executionGasUsed + (e.execResult.blobGasUsed ?? BIGINT_0), // TODO: figure out how to get total gas used
),
pass: e.execResult.exceptionError === undefined,
fork: evm.common.hardfork(),
}
traces.push(JSON.stringify(summary))
})
await evm.runCall(runCallArgs)
const traceString = traces.join('\n')
assert.equal(traceString, gethTrace)
})
})
2 changes: 1 addition & 1 deletion packages/evm/test/runCode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const JUMP = '56'
const JUMPDEST = '5b'

const testCases = [
{ code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST], pc: 1, resultPC: 6 },
{ code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST], pc: 1, resultPC: 7 },
{
code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST],
pc: -1,
Expand Down
16 changes: 16 additions & 0 deletions packages/evm/test/testdata/besuTrace.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{"pc":0,"op":96,"gas":"0x2540be400","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":2,"op":128,"gas":"0x2540be3fd","gasCost":"0x3","memSize":0,"stack":["0x40"],"depth":1,"refund":0,"opName":"DUP1"}
{"pc":3,"op":83,"gas":"0x2540be3fa","gasCost":"0xc","memSize":0,"stack":["0x40","0x40"],"depth":1,"refund":0,"opName":"MSTORE8"}
{"pc":4,"op":96,"gas":"0x2540be3ee","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":6,"op":96,"gas":"0x2540be3eb","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":8,"op":85,"gas":"0x2540be3e8","gasCost":"0x4e20","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x40"],"depth":1,"refund":0,"opName":"SSTORE"}
{"pc":9,"op":96,"gas":"0x2540b95c8","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":[],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":11,"op":96,"gas":"0x2540b95c5","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":13,"op":96,"gas":"0x2540b95c2","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x0"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":15,"op":96,"gas":"0x2540b95bf","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x0","0x40"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":17,"op":96,"gas":"0x2540b95bc","gasCost":"0x3","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x0","0x40","0x0"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":19,"op":90,"gas":"0x2540b95b9","gasCost":"0x2","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x0","0x40","0x0","0x2"],"depth":1,"refund":0,"opName":"GAS"}
{"pc":20,"op":250,"gas":"0x2540b95b7","gasCost":"0x24abb676c","memory":"0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x40","0x0","0x40","0x0","0x2","0x2540b95b7"],"depth":1,"refund":0,"opName":"STATICCALL"}
{"pc":21,"op":96,"gas":"0x2540b92a7","gasCost":"0x3","memory":"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x1"],"returnData":"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","depth":1,"refund":0,"opName":"PUSH1"}
{"pc":23,"op":243,"gas":"0x2540b92a4","gasCost":"0x0","memory":"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000","memSize":96,"stack":["0x1","0x40"],"returnData":"0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","depth":1,"refund":0,"opName":"RETURN"}
{"stateRoot":"0x8fa0dcc7f1d2383c89e5737c2843632db881c0946e80b71fe7175365e6538797","output":"0x40","gasUsed":"0x515c","pass":true,"fork":"Istanbul"}
2 changes: 1 addition & 1 deletion packages/vm/test/t8n/ethereumjs-t8ntool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ if [[ "$1" == "--version" ]]; then
fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
export NODE_OPTIONS="--max-old-space-size=4096"
npx tsx "$SCRIPT_DIR/launchT8N.ts" "$@"
npx tsx --conditions=typescript "$SCRIPT_DIR/launchT8N.ts" "$@"
7 changes: 6 additions & 1 deletion packages/vm/test/t8n/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export function getArguments() {
type: 'boolean',
default: false,
})
.option('trace', {
describe: 'Write EVM traces as JSON to trace-<tx-number>-<tx-hash>.json',
type: 'boolean',
default: false,
})
.strict()
.help().argv

Expand All @@ -84,7 +89,7 @@ export function getArguments() {
reward: BigInt((args as any)['state.reward']),
chainid: BigInt((args as any)['state.chainid']),
}

args.trace = (<any>args)['trace']
Copy link
Member

Choose a reason for hiding this comment

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

Note for myself: I should update these types, not sure why the types are not cast or set. Should do something similar as in the client where the CLI arguments are correctly typed (not this PR)

return args
}

Expand Down
Loading
Loading