Skip to content

Cache genesis state#8077

Merged
alcuadrado merged 10 commits intomainfrom
cache-genesis-state
Mar 24, 2026
Merged

Cache genesis state#8077
alcuadrado merged 10 commits intomainfrom
cache-genesis-state

Conversation

@alcuadrado
Copy link
Copy Markdown
Member

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.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 23, 2026

🦋 Changeset detected

Latest commit: db3ad1d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
hardhat Patch

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

@alcuadrado alcuadrado force-pushed the cache-genesis-state branch from 80d70dc to 437adf2 Compare March 23, 2026 17:17
@alcuadrado alcuadrado added no docs needed This PR doesn't require links to documentation no peer bump needed labels Mar 23, 2026
@alcuadrado alcuadrado requested review from Copilot and kanej March 23, 2026 17:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +126 to +131
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));
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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”.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +143
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);
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

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.

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 is intentional, and explained above.

Comment on lines 333 to 336
async #resolveNetworkConfig<ChainTypeT extends ChainType | string>(
resolvedNetworkName: string,
networkConfigOverride: NetworkConfigOverride | undefined = {},
networkConfigOverride: NetworkConfigOverride = {},
resolvedChainType: ChainTypeT,
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

#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 ?? {}).

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 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

{}
{}

Base automatically changed from single-contract-decoder to main March 24, 2026 11:42
Copilot AI review requested due to automatic review settings March 24, 2026 11:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Comment on lines +34 to +50
const genesisStateAndAccountsCache: WeakMap<
EdrNetworkAccountsConfig,
WeakMap<
EdrNetworkForkingConfig | typeof noForkingConfigCacheMarkerObject,
Map<
ChainType,
Map<
string,
{
genesisState: Map<Uint8Array, AccountOverride>;
ownedAccounts: Array<{ secretKey: string; balance: bigint }>;
}
>
>
>
> = new WeakMap();

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.

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.

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.

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.

Comment on lines +63 to +64
?.get(chainType)
?.get(specId);
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.

I might be tempted to collapse the chainType layer and specId into a single key + map.
I don't feel strongly on this.

Comment on lines +66 to +68
if (cached !== undefined) {
return cached;
}
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.

We could use the same mutex trick here that you used for the ContractDecoder to ensure there is only one calculation.

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.

That's a good point.

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.

Added it in db3ad1d

Comment on lines +351 to +359
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,
};
}
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.

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.

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.

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.

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 is not the only place where the network config is spread, so I added a better explanation in 93d953f

Copy link
Copy Markdown
Member

@kanej kanej left a comment

Choose a reason for hiding this comment

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

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.

Copilot AI review requested due to automatic review settings March 24, 2026 21:34
@alcuadrado alcuadrado force-pushed the cache-genesis-state branch from d50fbff to ff71fb0 Compare March 24, 2026 21:34
@alcuadrado alcuadrado enabled auto-merge March 24, 2026 21:35
@alcuadrado alcuadrado disabled auto-merge March 24, 2026 21:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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: [],

Comment on lines +55 to 56

resolved.solidity.registeredCompilerTypes.push(...(types as any[]));
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
@alcuadrado alcuadrado added this pull request to the merge queue Mar 24, 2026
Merged via the queue into main with commit 2a71290 Mar 24, 2026
213 checks passed
@alcuadrado alcuadrado deleted the cache-genesis-state branch March 24, 2026 21:57
@github-actions github-actions bot mentioned this pull request Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no docs needed This PR doesn't require links to documentation no peer bump needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants