hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
667 lines (586 loc) • 20.4 kB
text/typescript
import type {
Artifacts,
BoundExperimentalHardhatNetworkMessageTraceHook,
CompilerInput,
CompilerOutput,
EIP1193Provider,
EthSubscription,
HardhatNetworkChainsConfig,
RequestArguments,
} from "../../../types";
import type {
EdrContext,
Provider as EdrProviderT,
RawTrace,
Response,
SubscriptionEvent,
} from "@nomicfoundation/edr";
import { Common } from "@nomicfoundation/ethereumjs-common";
import chalk from "chalk";
import debug from "debug";
import { EventEmitter } from "events";
import fsExtra from "fs-extra";
import * as t from "io-ts";
import semver from "semver";
import { requireNapiRsModule } from "../../../common/napi-rs";
import {
HARDHAT_NETWORK_RESET_EVENT,
HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT,
} from "../../constants";
import {
rpcCompilerInput,
rpcCompilerOutput,
} from "../../core/jsonrpc/types/input/solc";
import { validateParams } from "../../core/jsonrpc/types/input/validation";
import {
InvalidArgumentsError,
InvalidInputError,
ProviderError,
} from "../../core/providers/errors";
import { isErrorResponse } from "../../core/providers/http";
import { getHardforkName } from "../../util/hardforks";
import { createModelsAndDecodeBytecodes } from "../stack-traces/compiler-to-model";
import { ConsoleLogger } from "../stack-traces/consoleLogger";
import { ContractsIdentifier } from "../stack-traces/contracts-identifier";
import {
VmTraceDecoder,
initializeVmTraceDecoder,
} from "../stack-traces/vm-trace-decoder";
import { FIRST_SOLC_VERSION_SUPPORTED } from "../stack-traces/constants";
import { encodeSolidityStackTrace } from "../stack-traces/solidity-errors";
import { SolidityStackTrace } from "../stack-traces/solidity-stack-trace";
import { SolidityTracer } from "../stack-traces/solidityTracer";
import { VMTracer } from "../stack-traces/vm-tracer";
import { getPackageJson } from "../../util/packageInfo";
import {
ForkConfig,
GenesisAccount,
IntervalMiningConfig,
MempoolOrder,
NodeConfig,
TracingConfig,
} from "./node-types";
import {
edrRpcDebugTraceToHardhat,
edrTracingMessageResultToMinimalEVMResult,
edrTracingMessageToMinimalMessage,
edrTracingStepToMinimalInterpreterStep,
ethereumjsIntervalMiningConfigToEdr,
ethereumjsMempoolOrderToEdrMineOrdering,
ethereumsjsHardforkToEdrSpecId,
} from "./utils/convertToEdr";
import { makeCommon } from "./utils/makeCommon";
import { LoggerConfig, printLine, replaceLastLine } from "./modules/logger";
import { MinimalEthereumJsVm, getMinimalEthereumJsVm } from "./vm/minimal-vm";
const log = debug("hardhat:core:hardhat-network:provider");
/* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */
export const DEFAULT_COINBASE = "0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e";
let _globalEdrContext: EdrContext | undefined;
// Lazy initialize the global EDR context.
export function getGlobalEdrContext(): EdrContext {
const { EdrContext } = requireNapiRsModule(
"@nomicfoundation/edr"
) as typeof import("@nomicfoundation/edr");
if (_globalEdrContext === undefined) {
// Only one is allowed to exist
_globalEdrContext = new EdrContext();
}
return _globalEdrContext;
}
interface HardhatNetworkProviderConfig {
hardfork: string;
chainId: number;
networkId: number;
blockGasLimit: number;
minGasPrice: bigint;
automine: boolean;
intervalMining: IntervalMiningConfig;
mempoolOrder: MempoolOrder;
chains: HardhatNetworkChainsConfig;
genesisAccounts: GenesisAccount[];
allowUnlimitedContractSize: boolean;
throwOnTransactionFailures: boolean;
throwOnCallFailures: boolean;
allowBlocksWithSameTimestamp: boolean;
initialBaseFeePerGas?: number;
initialDate?: Date;
coinbase?: string;
experimentalHardhatNetworkMessageTraceHooks?: BoundExperimentalHardhatNetworkMessageTraceHook[];
forkConfig?: ForkConfig;
forkCachePath?: string;
enableTransientStorage: boolean;
}
export function getNodeConfig(
config: HardhatNetworkProviderConfig,
tracingConfig?: TracingConfig
): NodeConfig {
return {
automine: config.automine,
blockGasLimit: config.blockGasLimit,
minGasPrice: config.minGasPrice,
genesisAccounts: config.genesisAccounts,
allowUnlimitedContractSize: config.allowUnlimitedContractSize,
tracingConfig,
initialBaseFeePerGas: config.initialBaseFeePerGas,
mempoolOrder: config.mempoolOrder,
hardfork: config.hardfork,
chainId: config.chainId,
networkId: config.networkId,
initialDate: config.initialDate,
forkConfig: config.forkConfig,
forkCachePath:
config.forkConfig !== undefined ? config.forkCachePath : undefined,
coinbase: config.coinbase ?? DEFAULT_COINBASE,
chains: config.chains,
allowBlocksWithSameTimestamp: config.allowBlocksWithSameTimestamp,
enableTransientStorage: config.enableTransientStorage,
};
}
class EdrProviderEventAdapter extends EventEmitter {}
type CallOverrideCallback = (
address: Buffer,
data: Buffer
) => Promise<
{ result: Buffer; shouldRevert: boolean; gas: bigint } | undefined
>;
export class EdrProviderWrapper
extends EventEmitter
implements EIP1193Provider
{
private _failedStackTraces = 0;
// temporarily added to make smock work with HH+EDR
private _callOverrideCallback?: CallOverrideCallback;
/** Used for internal stack trace tests. */
private _vmTracer?: VMTracer;
private constructor(
private readonly _provider: EdrProviderT,
// we add this for backwards-compatibility with plugins like solidity-coverage
private readonly _node: {
_vm: MinimalEthereumJsVm;
},
private readonly _eventAdapter: EdrProviderEventAdapter,
private readonly _vmTraceDecoder: VmTraceDecoder,
// The common configuration for EthereumJS VM is not used by EDR, but tests expect it as part of the provider.
private readonly _common: Common,
tracingConfig?: TracingConfig
) {
super();
if (tracingConfig !== undefined) {
initializeVmTraceDecoder(this._vmTraceDecoder, tracingConfig);
}
}
public static async create(
config: HardhatNetworkProviderConfig,
loggerConfig: LoggerConfig,
tracingConfig?: TracingConfig
): Promise<EdrProviderWrapper> {
const { Provider } = requireNapiRsModule(
"@nomicfoundation/edr"
) as typeof import("@nomicfoundation/edr");
const coinbase = config.coinbase ?? DEFAULT_COINBASE;
let fork;
if (config.forkConfig !== undefined) {
fork = {
jsonRpcUrl: config.forkConfig.jsonRpcUrl,
blockNumber:
config.forkConfig.blockNumber !== undefined
? BigInt(config.forkConfig.blockNumber)
: undefined,
};
}
const initialDate =
config.initialDate !== undefined
? BigInt(Math.floor(config.initialDate.getTime() / 1000))
: undefined;
// To accomodate construction ordering, we need an adapter to forward events
// from the EdrProvider callback to the wrapper's listener
const eventAdapter = new EdrProviderEventAdapter();
const printLineFn = loggerConfig.printLineFn ?? printLine;
const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine;
const contractsIdentifier = new ContractsIdentifier();
const vmTraceDecoder = new VmTraceDecoder(contractsIdentifier);
const hardforkName = getHardforkName(config.hardfork);
const provider = await Provider.withConfig(
getGlobalEdrContext(),
{
allowBlocksWithSameTimestamp:
config.allowBlocksWithSameTimestamp ?? false,
allowUnlimitedContractSize: config.allowUnlimitedContractSize,
bailOnCallFailure: config.throwOnCallFailures,
bailOnTransactionFailure: config.throwOnTransactionFailures,
blockGasLimit: BigInt(config.blockGasLimit),
chainId: BigInt(config.chainId),
chains: Array.from(config.chains, ([chainId, hardforkConfig]) => {
return {
chainId: BigInt(chainId),
hardforks: Array.from(
hardforkConfig.hardforkHistory,
([hardfork, blockNumber]) => {
return {
blockNumber: BigInt(blockNumber),
specId: ethereumsjsHardforkToEdrSpecId(
getHardforkName(hardfork)
),
};
}
),
};
}),
cacheDir: config.forkCachePath,
coinbase: Buffer.from(coinbase.slice(2), "hex"),
fork,
hardfork: ethereumsjsHardforkToEdrSpecId(hardforkName),
genesisAccounts: config.genesisAccounts.map((account) => {
return {
secretKey: account.privateKey,
balance: BigInt(account.balance),
};
}),
initialDate,
initialBaseFeePerGas:
config.initialBaseFeePerGas !== undefined
? BigInt(config.initialBaseFeePerGas!)
: undefined,
minGasPrice: config.minGasPrice,
mining: {
autoMine: config.automine,
interval: ethereumjsIntervalMiningConfigToEdr(config.intervalMining),
memPool: {
order: ethereumjsMempoolOrderToEdrMineOrdering(config.mempoolOrder),
},
},
networkId: BigInt(config.networkId),
},
{
enable: loggerConfig.enabled,
decodeConsoleLogInputsCallback: (inputs: Buffer[]) => {
const consoleLogger = new ConsoleLogger();
return consoleLogger.getDecodedLogs(inputs);
},
getContractAndFunctionNameCallback: (
code: Buffer,
calldata?: Buffer
) => {
return vmTraceDecoder.getContractAndFunctionNamesForCall(
code,
calldata
);
},
printLineCallback: (message: string, replace: boolean) => {
if (replace) {
replaceLastLineFn(message);
} else {
printLineFn(message);
}
},
},
(event: SubscriptionEvent) => {
eventAdapter.emit("ethEvent", event);
}
);
const minimalEthereumJsNode = {
_vm: getMinimalEthereumJsVm(provider),
};
const common = makeCommon(getNodeConfig(config));
const wrapper = new EdrProviderWrapper(
provider,
minimalEthereumJsNode,
eventAdapter,
vmTraceDecoder,
common,
tracingConfig
);
// Pass through all events from the provider
eventAdapter.addListener(
"ethEvent",
wrapper._ethEventListener.bind(wrapper)
);
return wrapper;
}
public async request(args: RequestArguments): Promise<unknown> {
if (args.params !== undefined && !Array.isArray(args.params)) {
throw new InvalidInputError(
"Hardhat Network doesn't support JSON-RPC params sent as an object"
);
}
const params = args.params ?? [];
if (args.method === "hardhat_addCompilationResult") {
return this._addCompilationResultAction(
...this._addCompilationResultParams(params)
);
} else if (args.method === "hardhat_getStackTraceFailuresCount") {
return this._getStackTraceFailuresCountAction(
...this._getStackTraceFailuresCountParams(params)
);
}
const stringifiedArgs = JSON.stringify({
method: args.method,
params,
});
const responseObject: Response = await this._provider.handleRequest(
stringifiedArgs
);
const response = JSON.parse(responseObject.json);
const needsTraces =
this._node._vm.evm.events.eventNames().length > 0 ||
this._node._vm.events.eventNames().length > 0 ||
this._vmTracer !== undefined;
if (needsTraces) {
const rawTraces = responseObject.traces;
for (const rawTrace of rawTraces) {
const trace = rawTrace.trace();
// beforeTx event
if (this._node._vm.events.listenerCount("beforeTx") > 0) {
this._node._vm.events.emit("beforeTx");
}
for (const traceItem of trace) {
// step event
if ("pc" in traceItem) {
if (this._node._vm.evm.events.listenerCount("step") > 0) {
this._node._vm.evm.events.emit(
"step",
edrTracingStepToMinimalInterpreterStep(traceItem)
);
}
this._vmTracer?.addStep(traceItem);
}
// afterMessage event
else if ("executionResult" in traceItem) {
if (this._node._vm.evm.events.listenerCount("afterMessage") > 0) {
this._node._vm.evm.events.emit(
"afterMessage",
edrTracingMessageResultToMinimalEVMResult(traceItem)
);
}
this._vmTracer?.addAfterMessage(traceItem.executionResult);
}
// beforeMessage event
else {
if (this._node._vm.evm.events.listenerCount("beforeMessage") > 0) {
this._node._vm.evm.events.emit(
"beforeMessage",
edrTracingMessageToMinimalMessage(traceItem)
);
}
this._vmTracer?.addBeforeMessage(traceItem);
}
}
// afterTx event
if (this._node._vm.events.listenerCount("afterTx") > 0) {
this._node._vm.events.emit("afterTx");
}
}
}
if (isErrorResponse(response)) {
let error;
const solidityTrace = responseObject.solidityTrace;
let stackTrace: SolidityStackTrace | undefined;
if (solidityTrace !== null) {
stackTrace = await this._rawTraceToSolidityStackTrace(solidityTrace);
}
if (stackTrace !== undefined) {
error = encodeSolidityStackTrace(response.error.message, stackTrace);
// Pass data and transaction hash from the original error
(error as any).data = response.error.data?.data ?? undefined;
(error as any).transactionHash =
response.error.data?.transactionHash ?? undefined;
} else {
if (response.error.code === InvalidArgumentsError.CODE) {
error = new InvalidArgumentsError(response.error.message);
} else {
error = new ProviderError(
response.error.message,
response.error.code
);
}
error.data = response.error.data;
}
// eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error
throw error;
}
if (args.method === "hardhat_reset") {
this.emit(HARDHAT_NETWORK_RESET_EVENT);
} else if (args.method === "evm_revert") {
this.emit(HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT);
}
// Override EDR version string with Hardhat version string with EDR backend,
// e.g. `HardhatNetwork/2.19.0/@nomicfoundation/edr/0.2.0-dev`
if (args.method === "web3_clientVersion") {
return clientVersion(response.result);
} else if (
args.method === "debug_traceTransaction" ||
args.method === "debug_traceCall"
) {
return edrRpcDebugTraceToHardhat(response.result);
} else {
return response.result;
}
}
/**
* Sets a `VMTracer` that observes EVM throughout requests.
*
* Used for internal stack traces integration tests.
*/
public setVmTracer(vmTracer?: VMTracer) {
this._vmTracer = vmTracer;
}
// temporarily added to make smock work with HH+EDR
private _setCallOverrideCallback(callback: CallOverrideCallback) {
this._callOverrideCallback = callback;
this._provider.setCallOverrideCallback(
async (address: Buffer, data: Buffer) => {
return this._callOverrideCallback?.(address, data);
}
);
}
private _setVerboseTracing(enabled: boolean) {
this._provider.setVerboseTracing(enabled);
}
private _ethEventListener(event: SubscriptionEvent) {
const subscription = `0x${event.filterId.toString(16)}`;
const results = Array.isArray(event.result) ? event.result : [event.result];
for (const result of results) {
this._emitLegacySubscriptionEvent(subscription, result);
this._emitEip1193SubscriptionEvent(subscription, result);
}
}
private _emitLegacySubscriptionEvent(subscription: string, result: any) {
this.emit("notification", {
subscription,
result,
});
}
private _emitEip1193SubscriptionEvent(subscription: string, result: unknown) {
const message: EthSubscription = {
type: "eth_subscription",
data: {
subscription,
result,
},
};
this.emit("message", message);
}
private _addCompilationResultParams(
params: any[]
): [string, CompilerInput, CompilerOutput] {
return validateParams(
params,
t.string,
rpcCompilerInput,
rpcCompilerOutput
);
}
private async _addCompilationResultAction(
solcVersion: string,
compilerInput: CompilerInput,
compilerOutput: CompilerOutput
): Promise<boolean> {
let bytecodes;
try {
bytecodes = createModelsAndDecodeBytecodes(
solcVersion,
compilerInput,
compilerOutput
);
} catch (error) {
console.warn(
chalk.yellow(
"The Hardhat Network tracing engine could not be updated. Run Hardhat with --verbose to learn more."
)
);
log(
"ContractsIdentifier failed to be updated. Please report this to help us improve Hardhat.\n",
error
);
return false;
}
for (const bytecode of bytecodes) {
this._vmTraceDecoder.addBytecode(bytecode);
}
return true;
}
private _getStackTraceFailuresCountParams(params: any[]): [] {
return validateParams(params);
}
private _getStackTraceFailuresCountAction(): number {
return this._failedStackTraces;
}
private async _rawTraceToSolidityStackTrace(
rawTrace: RawTrace
): Promise<SolidityStackTrace | undefined> {
const vmTracer = new VMTracer();
const trace = rawTrace.trace();
for (const traceItem of trace) {
if ("pc" in traceItem) {
vmTracer.addStep(traceItem);
} else if ("executionResult" in traceItem) {
vmTracer.addAfterMessage(traceItem.executionResult);
} else {
vmTracer.addBeforeMessage(traceItem);
}
}
let vmTrace = vmTracer.getLastTopLevelMessageTrace();
const vmTracerError = vmTracer.getLastError();
if (vmTrace !== undefined) {
vmTrace = this._vmTraceDecoder.tryToDecodeMessageTrace(vmTrace);
}
try {
if (vmTrace === undefined || vmTracerError !== undefined) {
throw vmTracerError;
}
const solidityTracer = new SolidityTracer();
return solidityTracer.getStackTrace(vmTrace);
} catch (err) {
this._failedStackTraces += 1;
log(
"Could not generate stack trace. Please report this to help us improve Hardhat.\n",
err
);
}
}
}
async function clientVersion(edrClientVersion: string): Promise<string> {
const hardhatPackage = await getPackageJson();
const edrVersion = edrClientVersion.split("/")[1];
return `HardhatNetwork/${hardhatPackage.version}/@nomicfoundation/edr/${edrVersion}`;
}
export async function createHardhatNetworkProvider(
hardhatNetworkProviderConfig: HardhatNetworkProviderConfig,
loggerConfig: LoggerConfig,
artifacts?: Artifacts
): Promise<EIP1193Provider> {
return EdrProviderWrapper.create(
hardhatNetworkProviderConfig,
loggerConfig,
await makeTracingConfig(artifacts)
);
}
async function makeTracingConfig(
artifacts: Artifacts | undefined
): Promise<TracingConfig | undefined> {
if (artifacts !== undefined) {
const buildInfos = [];
const buildInfoFiles = await artifacts.getBuildInfoPaths();
try {
for (const buildInfoFile of buildInfoFiles) {
const buildInfo = await fsExtra.readJson(buildInfoFile);
if (semver.gte(buildInfo.solcVersion, FIRST_SOLC_VERSION_SUPPORTED)) {
buildInfos.push(buildInfo);
}
}
return {
buildInfos,
};
} catch (error) {
console.warn(
chalk.yellow(
"Stack traces engine could not be initialized. Run Hardhat with --verbose to learn more."
)
);
log(
"Solidity stack traces disabled: Failed to read solc's input and output files. Please report this to help us improve Hardhat.\n",
error
);
}
}
}