Conversation
🦋 Changeset detectedLatest commit: db3ad1d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
80d70dc to
437adf2
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves performance for EDR-based networks by caching the computed genesis state/owned accounts and fixes network.connect() so it doesn’t unnecessarily re-resolve config when no overrides are provided (which previously prevented caching from being effective).
Changes:
- Add a shared cache for EDR genesis state + owned accounts keyed mostly by config references.
- Adjust network config resolution logic to avoid re-resolving when using the default chain type without overrides.
- Add regression tests for config re-resolution behavior and genesis-state caching; add changesets for both fixes.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts | Adds tests asserting stable config references across connect() calls and verifies config isn’t re-resolved without overrides. |
| v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/genesis-state.ts | Adds tests validating caching behavior for genesis state computation. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts | Updates config resolution short-circuiting logic to avoid unnecessary re-resolution in default-chain-type/no-override cases. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/genesis-state.ts | Introduces the cached genesis state + owned accounts computation for EDR providers. |
| v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts | Switches provider config building to use the new cached genesis-state helper. |
| .changeset/every-islands-relate.md | Changeset for the network.connect() config re-resolution bug fix. |
| .changeset/cyan-clowns-type.md | Changeset for the genesis state caching optimization. |
| const chainGenesisState = | ||
| forkingConfig !== undefined | ||
| ? [] // TODO: Add support for overriding remote fork state when the local fork is different | ||
| : chainType === OPTIMISM_CHAIN_TYPE | ||
| ? opGenesisState(opHardforkFromString(specId)) | ||
| : l1GenesisState(l1HardforkFromString(specId)); |
There was a problem hiding this comment.
This treats any defined forkingConfig as “forking enabled” and skips adding the chain’s default genesis state. However resolveForkingConfig returns an object even when enabled is false, and other code paths (e.g. hardhatForkingConfigToEdrForkConfig) explicitly check enabled === true. Consider using forkingConfig?.enabled === true (and potentially normalizing the cache key the same way) so enabled: false behaves like “no forking”.
| for (const account of chainGenesisState) { | ||
| const existingOverride = genesisState.get(account.address); | ||
| if (existingOverride !== undefined) { | ||
| // Favor the genesis state specified by the user | ||
| account.balance = account.balance ?? existingOverride.balance; | ||
| account.nonce = account.nonce ?? existingOverride.nonce; | ||
| account.code = account.code ?? existingOverride.code; | ||
| account.storage = account.storage ?? existingOverride.storage; | ||
| } else { | ||
| genesisState.set(account.address, account); | ||
| } |
There was a problem hiding this comment.
The collision/merge logic here is unlikely to work as intended: Map<Uint8Array, ...> uses reference equality for keys, so genesisState.get(account.address) will only hit if the exact same Uint8Array instance is used as the key. Also, even if it hits, the code mutates account but never updates the map entry, so the merged values aren’t reflected in genesisState. If you need to handle address collisions, consider doing lookups via a normalized key (e.g. hex string) and then rebuilding the final Map<Uint8Array, AccountOverride> from the merged overrides.
There was a problem hiding this comment.
This is intentional, and explained above.
v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts
Outdated
Show resolved
Hide resolved
v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts
Outdated
Show resolved
Hide resolved
v-next/hardhat/test/internal/builtin-plugins/network-manager/edr/genesis-state.ts
Outdated
Show resolved
Hide resolved
| async #resolveNetworkConfig<ChainTypeT extends ChainType | string>( | ||
| resolvedNetworkName: string, | ||
| networkConfigOverride: NetworkConfigOverride | undefined = {}, | ||
| networkConfigOverride: NetworkConfigOverride = {}, | ||
| resolvedChainType: ChainTypeT, |
There was a problem hiding this comment.
#resolveNetworkConfig now requires a NetworkConfigOverride, but its caller passes networkConfigOverride which is optional and can be undefined. With strictNullChecks this becomes a type error and also makes Object.keys(networkConfigOverride) unsafe if undefined is ever passed at runtime. Consider accepting NetworkConfigOverride | undefined again and normalizing to {} inside the method, or defaulting at the call site (networkConfigOverride ?? {}).
There was a problem hiding this comment.
This comment doesn't make sense. It has = {}, so passing undefined defaulted to {}.
For example, this test:
interface I {
ii?: number;
}
function test(i: {} | undefined = {}) {
console.log(i);
}
test();
test(undefined);Prints
{}
{}
| const genesisStateAndAccountsCache: WeakMap< | ||
| EdrNetworkAccountsConfig, | ||
| WeakMap< | ||
| EdrNetworkForkingConfig | typeof noForkingConfigCacheMarkerObject, | ||
| Map< | ||
| ChainType, | ||
| Map< | ||
| string, | ||
| { | ||
| genesisState: Map<Uint8Array, AccountOverride>; | ||
| ownedAccounts: Array<{ secretKey: string; balance: bigint }>; | ||
| } | ||
| > | ||
| > | ||
| > | ||
| > = new WeakMap(); | ||
|
|
There was a problem hiding this comment.
From my reading of this - we create a WeakMap based off the network accounts. This is in effect going to be scoped to the lifetime of the hre; in a test suite this will mainly be the global hre.
We weakmap to the forking config as well, but as we use a "no forking" marker object (the most common use case) which has module scope, for most cases this level will act just as a map.
The next levels are not objects but relevant keys.
My mental model here is that this cache is scoped to the hre object with some optimizations, but in general the genesis states will be cached for the lifetime of the HRE.
I think this makes sense.
There was a problem hiding this comment.
Your take is mostly correct.
The only case when it isn't is that when there are network config overrides the config gets re-resolved, so its lifetime is independent of the HRE. That means that this cache isn't hit for config overrides, because the references are different, and that those values can be GC'd after the network connection dies.
| ?.get(chainType) | ||
| ?.get(specId); |
There was a problem hiding this comment.
I might be tempted to collapse the chainType layer and specId into a single key + map.
I don't feel strongly on this.
| if (cached !== undefined) { | ||
| return cached; | ||
| } |
There was a problem hiding this comment.
We could use the same mutex trick here that you used for the ContractDecoder to ensure there is only one calculation.
| if (hasNoOverrides && isChainTypeDefault) { | ||
| return { | ||
| ...existingNetworkConfig, | ||
| /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- | ||
| TypeScript can't follow this case, but we are just providing the | ||
| default */ | ||
| chainType: resolvedChainType as ChainType, | ||
| }; | ||
| } |
There was a problem hiding this comment.
For this PR I think this works.
That it forces an ugly test later that can't rely on reference equality for the network config - that causes me to pause.
I think this suggests that network config within networkConfigs should have the chain type set. Or am I missing a subtlety of our config resolution - and the undefined has to be preserved?
Claude suggested memoizing here - but I think that is too complicated to satisfy my desire to get the same networkConfig object.
There was a problem hiding this comment.
I also found this surprising as well.
The reason chainType is not always resolved is that undefined lets you change it on connect().
Claude suggested memoizing here - but I think that is too complicated to satisfy my desire to get the same networkConfig object.
Not unreasonable to be honest, it would make the cache simpler. So the complexity would be moved somewhere else.
There was a problem hiding this comment.
This is not the only place where the network config is spread, so I added a better explanation in 93d953f
kanej
left a comment
There was a problem hiding this comment.
I have left comments - but I don't think any are blockers.
I rebased this onto the connectOnBefore branch and ran the OpenZeppelin test suite with one connection per file and everything passed.
…ire network config
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
d50fbff to
ff71fb0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
v-next/hardhat/test/internal/builtin-plugins/solidity/hooks.ts:578
- This whitespace-only line appears to be leftover from removing an eslint-disable comment. Please delete it to avoid trailing-whitespace/formatting lint failures and keep the test diff clean.
contracts: {},
sources: {},
errors: [],
|
|
||
| resolved.solidity.registeredCompilerTypes.push(...(types as any[])); |
There was a problem hiding this comment.
There are whitespace-only lines left behind (e.g. after removing eslint-disable comments). These typically trigger formatting/lint failures (e.g. trailing whitespace/no-multiple-empty-lines) and add noise to diffs. Please remove the stray indentation-only line(s).
This PR caches the creation of the genesis state of EDR providers, and fixes a bug in
network.connect()which was triggering the re-resolution of the config unnecessarily. The bug was preventing the cache from working.