-
-
Notifications
You must be signed in to change notification settings - Fork 57
feat(evm): record executions with an inspector #375
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
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| "ox": minor | ||
| --- | ||
|
|
||
| Added `Evm.setInspector` and `Evm.clearInspector`, which record what an execution did and report it as a trace on the result. | ||
|
|
||
| ```ts | ||
| import { Evm, Inspector } from 'ox/evm' | ||
|
|
||
| Evm.setInspector(evm, {}) | ||
|
|
||
| const result = Evm.callTx(evm, transaction) | ||
| const [root] = Inspector.tree(result.trace) | ||
| root.calls.length | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| import type * as Address from '../core/Address.js' | ||
| import type * as Hex from '../core/Hex.js' | ||
| import type * as codec from './internal/codec.js' | ||
|
|
||
| /** | ||
| * A recorded execution. | ||
| * | ||
| * Events keep the order the engine produced them in, one per hook it called. | ||
| * Nothing is interpreted: {@link ox#Inspector.(tree:function)} builds the call | ||
| * tree from this, so the shape can change without a new engine. | ||
| */ | ||
| export type Trace = codec.Trace | ||
|
|
||
| /** One recorded hook call. */ | ||
| export type Event = codec.TraceEvent | ||
|
|
||
| /** What an inspector records. */ | ||
| export type Options = { | ||
| /** | ||
| * Largest trace to keep, in bytes. | ||
| * | ||
| * Recording stops at the limit and the trace reports `truncated`, keeping what | ||
| * ran first: a trace is always a prefix of the execution, never a stream with | ||
| * gaps. Execution is unaffected either way. | ||
| * | ||
| * @default 1_048_576 | ||
| */ | ||
| limit?: number | undefined | ||
| /** Records memory size on each instruction. Requires `steps`. */ | ||
| memory?: boolean | undefined | ||
| /** Records the stack on each instruction. Requires `steps`. */ | ||
| stack?: boolean | undefined | ||
| /** | ||
| * Records every instruction. | ||
| * | ||
| * Off by default, and worth leaving off: a mainnet transaction runs millions of | ||
| * instructions, where calls, creates, and logs number in the tens. Turn this on | ||
| * to debug a specific execution, not to observe one in production. | ||
| * | ||
| * @default false | ||
| */ | ||
| steps?: boolean | undefined | ||
| } | ||
|
|
||
| /** | ||
| * A call or create in the tree, with whatever it did inside it. | ||
| * | ||
| * The shape {@link ox#Inspector.(tree:function)} produces. | ||
| */ | ||
| export type Frame = { | ||
| /** Account that made the call. */ | ||
| caller: Address.Address | ||
| /** Calls and creates this frame made, in order. */ | ||
| calls: readonly Frame[] | ||
| /** Account whose code ran. */ | ||
| codeAddress: Address.Address | ||
| /** Address a create deployed, when it succeeded. */ | ||
| createdAddress?: Address.Address | undefined | ||
| /** How deep this frame sits. */ | ||
| depth: number | ||
| /** Account the call was addressed to. */ | ||
| destination: Address.Address | ||
| /** Gas the frame was given. */ | ||
| gasLimit: bigint | ||
| /** Gas the frame consumed. Absent when the trace ended before it returned. */ | ||
| gasSpent?: bigint | undefined | ||
| /** Calldata, or initcode for a create. */ | ||
| input: Hex.Hex | ||
| /** | ||
| * Which instruction produced this frame. | ||
| * | ||
| * `'unknown'` for a kind a later evm2 revision added, which this artifact | ||
| * reports rather than folding into `'call'`. | ||
| */ | ||
| kind: (typeof codec.messageKinds)[number] | typeof codec.unknownMessageKind | ||
| /** Logs this frame emitted directly. */ | ||
| logs: readonly { | ||
| address: Address.Address | ||
| data: Hex.Hex | ||
| topics: readonly Hex.Hex[] | ||
| }[] | ||
| /** Returned data, or revert data. Absent when the frame never returned. */ | ||
| output?: Hex.Hex | undefined | ||
| /** Accounts this frame self-destructed, with where their balance went. */ | ||
| selfdestructs: readonly { | ||
| contract: Address.Address | ||
| target: Address.Address | ||
| value: bigint | ||
| }[] | ||
| /** Why the frame stopped, as evm2's discriminant. Absent when unfinished. */ | ||
| stop?: number | undefined | ||
| /** Value transferred. */ | ||
| value: bigint | ||
| } | ||
|
|
||
| /** | ||
| * Builds the call tree a trace describes. | ||
| * | ||
| * The engine records a flat sequence; nesting is recovered here from the order of | ||
| * the call and return events. A truncated trace yields the frames it captured, | ||
| * with the unfinished ones missing their output and gas. | ||
| * | ||
| * @example | ||
| * ```ts twoslash | ||
| * // @noErrors | ||
| * import { Evm, Inspector } from 'ox/evm' | ||
| * | ||
| * Evm.setInspector(evm, {}) | ||
| * const result = Evm.callTx(evm, transaction) | ||
| * | ||
| * const [root] = Inspector.tree(result.trace) | ||
| * root.calls.length | ||
| * ``` | ||
| * | ||
| * @param trace - Recorded execution. | ||
| * @returns The frames the trace describes, outermost first. | ||
| */ | ||
| export function tree(trace: Trace | undefined): readonly Frame[] { | ||
| if (!trace) return [] | ||
|
|
||
| const roots: Frame[] = [] | ||
| // Frames still open, innermost last. A return event closes the last one. | ||
| const open: Frame[] = [] | ||
|
|
||
| const current = () => open[open.length - 1] | ||
|
|
||
| for (const event of trace.events) { | ||
| if (event.kind === 'call' || event.kind === 'create') { | ||
| const frame: Frame = { | ||
| caller: event.caller, | ||
| calls: [], | ||
| codeAddress: event.codeAddress, | ||
| depth: event.depth, | ||
| destination: event.destination, | ||
| gasLimit: event.gasLimit, | ||
| input: event.input, | ||
| kind: event.messageKind, | ||
| logs: [], | ||
| selfdestructs: [], | ||
| value: event.value, | ||
| } | ||
| const parent = current() | ||
| if (parent) (parent.calls as Frame[]).push(frame) | ||
| else roots.push(frame) | ||
| open.push(frame) | ||
| continue | ||
| } | ||
|
|
||
| if (event.kind === 'callEnd' || event.kind === 'createEnd') { | ||
| const frame = open.pop() | ||
| if (!frame) continue | ||
| Object.assign(frame, { | ||
| gasSpent: event.gasSpent, | ||
| output: event.output, | ||
| stop: event.stop, | ||
| ...(event.createdAddress | ||
| ? { createdAddress: event.createdAddress } | ||
| : {}), | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| // Logs and self-destructs belong to whichever frame is running. | ||
| const frame = current() | ||
| if (!frame) continue | ||
| if (event.kind === 'log') | ||
| (frame.logs as Frame['logs'][number][]).push({ | ||
| address: event.address, | ||
| data: event.data, | ||
| topics: event.topics, | ||
| }) | ||
| else if (event.kind === 'selfdestruct') | ||
| (frame.selfdestructs as Frame['selfdestructs'][number][]).push({ | ||
| contract: event.contract, | ||
| target: event.target, | ||
| value: event.value, | ||
| }) | ||
| } | ||
|
|
||
| return roots | ||
| } | ||
|
|
||
| /** | ||
| * Returns the instructions a trace recorded, in order. | ||
| * | ||
| * Empty unless the inspector recorded steps. | ||
| * | ||
| * @example | ||
| * ```ts twoslash | ||
| * // @noErrors | ||
| * import { Inspector } from 'ox/evm' | ||
| * | ||
| * const steps = Inspector.steps(result.trace) | ||
| * steps[0]?.opcode | ||
| * ``` | ||
| * | ||
| * @param trace - Recorded execution. | ||
| * @returns Each instruction, with its program counter, gas, and stack. | ||
| */ | ||
| export function steps( | ||
| trace: Trace | undefined, | ||
| ): readonly Extract<Event, { kind: 'step' }>[] { | ||
| if (!trace) return [] | ||
| return trace.events.filter( | ||
| (event): event is Extract<Event, { kind: 'step' }> => event.kind === 'step', | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For any
SELFDESTRUCT, the raw event includes bothcontract(the account being destroyed) andtarget(the beneficiary), but the tree view dropscontractand exposes only the beneficiary/value. Consumers usingInspector.treetherefore cannot tell which account was removed, and guessing from the frame is wrong for contexts such as delegated execution; includeevent.contractin the frame's selfdestruct entry.Useful? React with 👍 / 👎.