Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/legal-roses-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Optimize the initialization of EDR Network Connections by only processing the build outputs once.
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ interface EdrProviderConfig {
chainDescriptors: ChainDescriptorsConfig;
networkConfig: RequireField<EdrNetworkConfig, "chainType">;
loggerConfig?: LoggerConfig;
tracingConfig?: TracingConfigWithBuffers;
contractDecoder: ContractDecoder;
jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction;
coverageConfig?: CoverageConfig;
gasReportConfig?: GasReportConfig;
Expand All @@ -147,14 +147,20 @@ export class EdrProvider extends BaseProvider {
#provider: Provider | undefined;
#nextRequestId = 1;

public static async createContractDecoder(
tracingConfig: TracingConfigWithBuffers,
): Promise<ContractDecoder> {
return ContractDecoder.withContracts(tracingConfig);
}

/**
* Creates a new instance of `EdrProvider`.
*/
public static async create({
chainDescriptors,
networkConfig,
loggerConfig = { enabled: false },
tracingConfig = {},
contractDecoder,
jsonRpcRequestWrapper,
coverageConfig,
gasReportConfig,
Expand All @@ -174,8 +180,6 @@ export class EdrProvider extends BaseProvider {
// We need to catch errors here, as the provider creation can panic unexpectedly,
// and we want to make sure such a crash is propagated as a ProviderError.
try {
const contractDecoder = ContractDecoder.withContracts(tracingConfig);

const context = await getGlobalEdrContext();
const provider = await context.createProvider(
hardhatChainTypeToEdrChainType(networkConfig.chainType),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import type {
JsonRpcRequest,
JsonRpcResponse,
} from "../../../types/providers.js";
import type { GasReportConfig } from "@nomicfoundation/edr";
import type { ContractDecoder, GasReportConfig } from "@nomicfoundation/edr";

import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { exists, readBinaryFile } from "@nomicfoundation/hardhat-utils/fs";
import { deepMerge } from "@nomicfoundation/hardhat-utils/lang";
import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization";

import { resolveUserConfigToHardhatConfig } from "../../core/hre.js";
import { isSupportedChainType } from "../../edr/chain-type.js";
Expand Down Expand Up @@ -57,6 +58,8 @@ export class NetworkManagerImplementation implements NetworkManager {
readonly #projectRoot: string;

#nextConnectionId = 0;
readonly #contractDecoderMutex = new AsyncMutex();
#contractDecoder: ContractDecoder | undefined;

constructor(
defaultNetwork: string,
Expand Down Expand Up @@ -233,6 +236,40 @@ export class NetworkManagerImplementation implements NetworkManager {
};
}

// We load the build infos and their outputs to create a contract
// decoder when the first provider is created. Successive providers will
// reuse the same decoder as a performance optimization.
//
// The trade-off here is that if you create an EDR provider, then
// compile new contracts, and create a new provider, the new contracts
// won't be loaded.
//
// Even without this optimization, we already had the problem of new
// contracts not being visible to existing providers.
Comment thread
kanej marked this conversation as resolved.
//
// In practice, most workflows compile everything before creating
// any network connection.
Comment thread
kanej marked this conversation as resolved.
if (this.#contractDecoder === undefined) {
// We want to ensure that only one contract decoder is created so we
// protect the initialization with a mutex.
await this.#contractDecoderMutex.exclusiveRun(async () => {
// We check again if the decoder is undefined because another async
// execution context could have already initialized it while we were
// waiting for the mutex.
if (this.#contractDecoder === undefined) {
this.#contractDecoder = await EdrProvider.createContractDecoder({
buildInfos: await this.#getBuildInfosAndOutputsAsBuffers(),
ignoreContracts: false,
});
}
});
}
Comment on lines +252 to +266
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The lazy initialization of #contractDecoder isn't concurrency-safe: if two connections create an EDR provider at the same time, both can enter this if block and build/load the decoder in parallel (defeating the "once per NetworkManager" goal). Consider caching a single in-flight Promise (e.g., #contractDecoderPromise) or guarding initialization with a simple mutex to ensure only one initialization occurs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The AsyncMutex here and the double guard should avoid the race condition. The call to EdrProvider.createContractDecoder happens with the mutex and must be complete before another callback can reach here because of the mutex.

Comment thread
kanej marked this conversation as resolved.

assertHardhatInvariant(
this.#contractDecoder !== undefined,
"Contract decoder should have been initialized before creating the provider",
);

return EdrProvider.create({
chainDescriptors: this.#chainDescriptors,
// The resolvedNetworkConfig can have its chainType set to `undefined`
Expand All @@ -249,10 +286,7 @@ export class NetworkManagerImplementation implements NetworkManager {
chainType: resolvedChainType as ChainType,
},
jsonRpcRequestWrapper,
tracingConfig: {
buildInfos: await this.#getBuildInfosAndOutputsAsBuffers(),
ignoreContracts: false,
},
contractDecoder: this.#contractDecoder,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

this.#contractDecoder is typed as ContractDecoder | undefined, but EdrProvider.create now requires a non-optional contractDecoder. This will fail type-checking at this call site. Consider storing the initialized decoder in a local const (or changing the field to a non-optional type and using definite assignment) so the argument is guaranteed to be ContractDecoder here.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this type checks already

coverageConfig,
gasReportConfig,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
resolveEdrNetwork,
resolveHttpNetwork,
} from "../../../../src/internal/builtin-plugins/network-manager/config-resolution.js";
import { EdrProvider } from "../../../../src/internal/builtin-plugins/network-manager/edr/edr-provider.js";
import {
getCurrentHardfork,
getHardforks,
Expand Down Expand Up @@ -2512,8 +2513,8 @@ describe("NetworkManagerImplementation", () => {
for (const hardfork of Object.values(OpHardforkName)) {
const validationErrors = await validateNetworkUserConfig({
...edrConfig({ hardfork }),
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Type assertion needed because changing defaultChainType requires module
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Type assertion needed because changing defaultChainType requires module
augmentation, which can't be done in test files */
defaultChainType: OPTIMISM_CHAIN_TYPE as any,
});
Expand Down Expand Up @@ -2556,8 +2557,8 @@ describe("NetworkManagerImplementation", () => {
...edrConfig({
hardfork: L1HardforkName.OSAKA,
}),
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Type assertion needed because changing defaultChainType requires module
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Type assertion needed because changing defaultChainType requires module
augmentation, which can't be done in test files */
defaultChainType: OPTIMISM_CHAIN_TYPE as any,
});
Expand Down Expand Up @@ -2959,4 +2960,24 @@ describe("NetworkManagerImplementation", () => {
});
});
});

describe("ContractDecoder caching", () => {
it("should create the ContractDecoder only once across multiple EDR connections", async (t) => {
const spy = t.mock.method(EdrProvider, "createContractDecoder");

await networkManager.connect({
network: "edrNetwork",
});

await networkManager.connect({
network: "edrNetwork",
});

assert.equal(
spy.mock.callCount(),
1,
"createContractDecoder should be called exactly once",
);
});
});
});
Loading