Skip to content

Commit 5fb3095

Browse files
fvictorioXanewok
andauthored
Run solidity tracing logic in EDR (#5769)
* refactor: Remove the experimental MessageTrace hook This removes the `MessageTrace` from the (experimental) public API and only hides it behind our internal API, where it is used for stack trace logic. We want to move this type to Rust to decrease the overhead of the value passing across the FFI and it would allow us to remove/not export this type entirely from Rust when porting the stack trace logic. * Add await in provider creation * refactor: Use the new EDR internals for the stack trace decoding * refactor: Merge ContractsIdentifier into VmTraceDecoder * refactor: Adapt to the now removed Bytecode * fix: Use nested scopes in a stack trace entry message switch * Bump EDR version * Create happy-kiwis-draw.md --------- Co-authored-by: Igor Matuszewski <[email protected]>
1 parent 40d9339 commit 5fb3095

23 files changed

+220
-5311
lines changed

.changeset/happy-kiwis-draw.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Adapted Hardhat to trace Solidity logic on EDR. This resulted in a 10% performance improvement for most workloads.

packages/hardhat-core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
"dependencies": {
103103
"@ethersproject/abi": "^5.1.2",
104104
"@metamask/eth-sig-util": "^4.0.0",
105-
"@nomicfoundation/edr": "^0.5.2",
105+
"@nomicfoundation/edr": "^0.6.1",
106106
"@nomicfoundation/ethereumjs-common": "4.0.4",
107107
"@nomicfoundation/ethereumjs-tx": "5.0.4",
108108
"@nomicfoundation/ethereumjs-util": "9.0.4",

packages/hardhat-core/src/internal/hardhat-network/provider/provider.ts

+11-24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
import type {
1212
EdrContext,
1313
Provider as EdrProviderT,
14+
VmTraceDecoder as VmTraceDecoderT,
15+
VMTracer as VMTracerT,
1416
RawTrace,
1517
Response,
1618
SubscriptionEvent,
@@ -42,7 +44,6 @@ import { isErrorResponse } from "../../core/providers/http";
4244
import { getHardforkName } from "../../util/hardforks";
4345
import { createModelsAndDecodeBytecodes } from "../stack-traces/compiler-to-model";
4446
import { ConsoleLogger } from "../stack-traces/consoleLogger";
45-
import { ContractsIdentifier } from "../stack-traces/contracts-identifier";
4647
import {
4748
VmTraceDecoder,
4849
initializeVmTraceDecoder,
@@ -167,15 +168,15 @@ export class EdrProviderWrapper
167168
private _callOverrideCallback?: CallOverrideCallback;
168169

169170
/** Used for internal stack trace tests. */
170-
private _vmTracer?: VMTracer;
171+
private _vmTracer?: VMTracerT;
171172

172173
private constructor(
173174
private readonly _provider: EdrProviderT,
174175
// we add this for backwards-compatibility with plugins like solidity-coverage
175176
private readonly _node: {
176177
_vm: MinimalEthereumJsVm;
177178
},
178-
private readonly _vmTraceDecoder: VmTraceDecoder,
179+
private readonly _vmTraceDecoder: VmTraceDecoderT,
179180
// The common configuration for EthereumJS VM is not used by EDR, but tests expect it as part of the provider.
180181
private readonly _common: Common,
181182
tracingConfig?: TracingConfig
@@ -221,8 +222,7 @@ export class EdrProviderWrapper
221222
const printLineFn = loggerConfig.printLineFn ?? printLine;
222223
const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine;
223224

224-
const contractsIdentifier = new ContractsIdentifier();
225-
const vmTraceDecoder = new VmTraceDecoder(contractsIdentifier);
225+
const vmTraceDecoder = new VmTraceDecoder();
226226

227227
const hardforkName = getHardforkName(config.hardfork);
228228

@@ -368,6 +368,9 @@ export class EdrProviderWrapper
368368
if (needsTraces) {
369369
const rawTraces = responseObject.traces;
370370
for (const rawTrace of rawTraces) {
371+
this._vmTracer?.observe(rawTrace);
372+
373+
// For other consumers in JS we need to marshall the entire trace over FFI
371374
const trace = rawTrace.trace();
372375

373376
// beforeTx event
@@ -384,8 +387,6 @@ export class EdrProviderWrapper
384387
edrTracingStepToMinimalInterpreterStep(traceItem)
385388
);
386389
}
387-
388-
this._vmTracer?.addStep(traceItem);
389390
}
390391
// afterMessage event
391392
else if ("executionResult" in traceItem) {
@@ -395,8 +396,6 @@ export class EdrProviderWrapper
395396
edrTracingMessageResultToMinimalEVMResult(traceItem)
396397
);
397398
}
398-
399-
this._vmTracer?.addAfterMessage(traceItem.executionResult);
400399
}
401400
// beforeMessage event
402401
else {
@@ -406,8 +405,6 @@ export class EdrProviderWrapper
406405
edrTracingMessageToMinimalMessage(traceItem)
407406
);
408407
}
409-
410-
this._vmTracer?.addBeforeMessage(traceItem);
411408
}
412409
}
413410

@@ -474,7 +471,7 @@ export class EdrProviderWrapper
474471
*
475472
* Used for internal stack traces integration tests.
476473
*/
477-
public setVmTracer(vmTracer?: VMTracer) {
474+
public setVmTracer(vmTracer?: VMTracerT) {
478475
this._vmTracer = vmTracer;
479476
}
480477

@@ -552,7 +549,7 @@ export class EdrProviderWrapper
552549
);
553550

554551
log(
555-
"ContractsIdentifier failed to be updated. Please report this to help us improve Hardhat.\n",
552+
"VmTraceDecoder failed to be updated. Please report this to help us improve Hardhat.\n",
556553
error
557554
);
558555

@@ -578,17 +575,7 @@ export class EdrProviderWrapper
578575
rawTrace: RawTrace
579576
): Promise<SolidityStackTrace | undefined> {
580577
const vmTracer = new VMTracer();
581-
582-
const trace = rawTrace.trace();
583-
for (const traceItem of trace) {
584-
if ("pc" in traceItem) {
585-
vmTracer.addStep(traceItem);
586-
} else if ("executionResult" in traceItem) {
587-
vmTracer.addAfterMessage(traceItem.executionResult);
588-
} else {
589-
vmTracer.addBeforeMessage(traceItem);
590-
}
591-
}
578+
vmTracer.observe(rawTrace);
592579

593580
let vmTrace = vmTracer.getLastTopLevelMessageTrace();
594581
const vmTracerError = vmTracer.getLastError();
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,7 @@
1-
import { bytesToBigInt } from "@nomicfoundation/ethereumjs-util";
2-
import { assertHardhatInvariant } from "../../core/errors";
1+
import { requireNapiRsModule } from "../../../common/napi-rs";
32

4-
const { rawDecode } = require("ethereumjs-abi");
3+
const { ReturnData } = requireNapiRsModule(
4+
"@nomicfoundation/edr"
5+
) as typeof import("@nomicfoundation/edr");
56

6-
// selector of Error(string)
7-
const ERROR_SELECTOR = "08c379a0";
8-
// selector of Panic(uint256)
9-
const PANIC_SELECTOR = "4e487b71";
10-
11-
/**
12-
* Represents the returnData of a transaction, whose contents are unknown.
13-
*/
14-
export class ReturnData {
15-
private _selector: string | undefined;
16-
17-
constructor(public value: Uint8Array) {
18-
if (value.length >= 4) {
19-
this._selector = Buffer.from(value.slice(0, 4)).toString("hex");
20-
}
21-
}
22-
23-
public isEmpty(): boolean {
24-
return this.value.length === 0;
25-
}
26-
27-
public matchesSelector(selector: Uint8Array): boolean {
28-
if (this._selector === undefined) {
29-
return false;
30-
}
31-
32-
return this._selector === Buffer.from(selector).toString("hex");
33-
}
34-
35-
public isErrorReturnData(): boolean {
36-
return this._selector === ERROR_SELECTOR;
37-
}
38-
39-
public isPanicReturnData(): boolean {
40-
return this._selector === PANIC_SELECTOR;
41-
}
42-
43-
public decodeError(): string {
44-
if (this.isEmpty()) {
45-
return "";
46-
}
47-
48-
assertHardhatInvariant(
49-
this._selector === ERROR_SELECTOR,
50-
"Expected return data to be a Error(string)"
51-
);
52-
53-
const [decodedError] = rawDecode(["string"], this.value.slice(4)) as [
54-
string
55-
];
56-
57-
return decodedError;
58-
}
59-
60-
public decodePanic(): bigint {
61-
assertHardhatInvariant(
62-
this._selector === PANIC_SELECTOR,
63-
"Expected return data to be a Panic(uint256)"
64-
);
65-
66-
// we are assuming that panic codes are smaller than Number.MAX_SAFE_INTEGER
67-
const errorCode = bytesToBigInt(this.value.slice(4));
68-
69-
return errorCode;
70-
}
71-
72-
public getSelector(): string | undefined {
73-
return this._selector;
74-
}
75-
}
7+
export { ReturnData };
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,7 @@
1-
import type { ExceptionalHalt, SuccessReason } from "@nomicfoundation/edr";
2-
31
import { requireNapiRsModule } from "../../../../common/napi-rs";
42

5-
export enum ExitCode {
6-
SUCCESS,
7-
REVERT,
8-
OUT_OF_GAS,
9-
INTERNAL_ERROR,
10-
INVALID_OPCODE,
11-
STACK_UNDERFLOW,
12-
CODESIZE_EXCEEDS_MAXIMUM,
13-
CREATE_COLLISION,
14-
STATIC_STATE_CHANGE,
15-
}
16-
17-
export class Exit {
18-
public static fromEdrSuccessReason(reason: SuccessReason): Exit {
19-
const { SuccessReason } = requireNapiRsModule(
20-
"@nomicfoundation/edr"
21-
) as typeof import("@nomicfoundation/edr");
22-
23-
switch (reason) {
24-
case SuccessReason.Stop:
25-
case SuccessReason.Return:
26-
case SuccessReason.SelfDestruct:
27-
case SuccessReason.EofReturnContract:
28-
return new Exit(ExitCode.SUCCESS);
29-
}
30-
31-
const _exhaustiveCheck: never = reason;
32-
}
33-
34-
public static fromEdrExceptionalHalt(halt: ExceptionalHalt): Exit {
35-
const { ExceptionalHalt } = requireNapiRsModule(
36-
"@nomicfoundation/edr"
37-
) as typeof import("@nomicfoundation/edr");
38-
39-
switch (halt) {
40-
case ExceptionalHalt.OutOfGas:
41-
return new Exit(ExitCode.OUT_OF_GAS);
42-
43-
case ExceptionalHalt.OpcodeNotFound:
44-
case ExceptionalHalt.InvalidFEOpcode:
45-
// Returned when an opcode is not implemented for the hardfork
46-
case ExceptionalHalt.NotActivated:
47-
return new Exit(ExitCode.INVALID_OPCODE);
48-
49-
case ExceptionalHalt.StackUnderflow:
50-
return new Exit(ExitCode.STACK_UNDERFLOW);
51-
52-
case ExceptionalHalt.CreateCollision:
53-
return new Exit(ExitCode.CREATE_COLLISION);
54-
55-
case ExceptionalHalt.CreateContractSizeLimit:
56-
return new Exit(ExitCode.CODESIZE_EXCEEDS_MAXIMUM);
57-
58-
default: {
59-
// eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error
60-
throw new Error(`Unmatched EDR exceptional halt: ${halt}`);
61-
}
62-
}
63-
}
64-
65-
constructor(public kind: ExitCode) {}
66-
67-
public isError(): boolean {
68-
return this.kind !== ExitCode.SUCCESS;
69-
}
70-
71-
public getReason(): string {
72-
switch (this.kind) {
73-
case ExitCode.SUCCESS:
74-
return "Success";
75-
case ExitCode.REVERT:
76-
return "Reverted";
77-
case ExitCode.OUT_OF_GAS:
78-
return "Out of gas";
79-
case ExitCode.INTERNAL_ERROR:
80-
return "Internal error";
81-
case ExitCode.INVALID_OPCODE:
82-
return "Invalid opcode";
83-
case ExitCode.STACK_UNDERFLOW:
84-
return "Stack underflow";
85-
case ExitCode.CODESIZE_EXCEEDS_MAXIMUM:
86-
return "Codesize exceeds maximum";
87-
case ExitCode.CREATE_COLLISION:
88-
return "Create collision";
89-
case ExitCode.STATIC_STATE_CHANGE:
90-
return "Static state change";
91-
}
3+
const { ExitCode } = requireNapiRsModule(
4+
"@nomicfoundation/edr"
5+
) as typeof import("@nomicfoundation/edr");
926

93-
const _exhaustiveCheck: never = this.kind;
94-
}
95-
}
7+
export { ExitCode };

0 commit comments

Comments
 (0)