NomicFoundation / NomicFoundation/hardhat

ethers' JS keccak is the hottest symbol in provider test suites - register a native implementation

Open
#8,564 1 comment 0 reactions 1 assignee View on GitHub

@ChristopherDedominici is already working on this.

Since Aug 26, 2026.

Dominant language
TypeScript
Stars
8.5k
Forks
1.7k
Avg merge
6d 20h
Merged PRs (30d)
35

Description

On the provider (mocha) test path, most hardhat test CPU is the JS side rather than EDR, and the ethers + @noble crypto stack is its largest component. Measured in the Hardhat 3 + EDR profiling campaign: that stack is ≈24% of wall-clock on openzeppelin-contracts (56% of active JS) and ≈20% of wall on lidofinance-core.

The single hottest symbol in openzeppelin's run is keccakP from @noble/hashes inside ethers v6 — 4.3% of total CPU ≈ ~4% of wall (~9% of active JS) — the keccak permutation executing as JIT'd JavaScript. Every ethers operation hashes: getAddress() checksums, ABI selectors and topics, transaction serialisation, CREATE address derivation. Node's OpenSSL cannot substitute: it provides standardised SHA3-256, whose padding differs from Ethereum's pre-standard keccak256.

ethers v6 makes this fixable without patching: keccak256.register(fn) exists precisely so environments can install faster implementations. JS ECDSA via @noble/curves is larger still (≈5% of wall, ~11% of active JS) and its signingKey hooks are registrable the same way.

Direction

Register a native keccak256 in hardhat-ethers when the plugin materialises its ethers instance — sourced ideally from a small @nomicfoundation/edr export, since EDR already links keccak natively. Requirements: bit-identical output (verified exhaustively; a mismatch is consensus-breaking), no registration into a user's separately imported ethers copy, silent fallback where no native binary exists, and a measured check that napi call overhead does not make small inputs slower. Native signing is a sensible stretch goal once keccak proves the pattern.

Suites on viem get no benefit — viem has no substitution hook. An upstream conversation with ethers about a native or WASM default would fix it for everyone, and is a separate follow-up.

Implementation plan

# Register a native keccak256 in hardhat-ethers via ethers' crypto hooks

## Problem

[Profiling Hardhat 3 e2e scenarios](https://app.notion.com/p/nomicfoundation/Runtime-Profiling-2026-08-05-3b3578cdeaf5808eafdff4e22ba425d0?source=copy_link) shows that on the provider (mocha) path, most `hardhat test` CPU is the JS side rather than EDR, and the ethers + `@noble` crypto stack is its largest component: ≈24% of wall-clock on openzeppelin-contracts (56% of active JS), ≈20% of wall on lidofinance-core.

The single hottest symbol in openzeppelin's run is `keccakP` from `@noble/hashes` inside ethers v6 — 4.3% of total CPU ≈ **~4% of wall** (~9% of active JS) — the keccak permutation running as JIT'd JavaScript. Every ethers operation hashes: `getAddress()` checksums, ABI selectors and topics, transaction serialisation, CREATE address derivation. Node's OpenSSL cannot help: it offers standardised SHA3-256, whose padding differs from Ethereum's pre-standard keccak256.

JS ECDSA via `@noble/curves` (`mod`/`invert`/`mul`) is larger still at ≈5% of wall (~11% of active JS), and ethers exposes `signingKey` hooks the same way — a stretch goal once keccak proves the pattern.

## Root cause

ethers v6 crypto functions are explicitly substitutable: `keccak256.register(fn)` (same for `sha256`, `computeHmac`, …) exists precisely so environments can install faster implementations. Hardhat registers nothing, so the pure-JS implementations run.

## Task

1. **Pick the native keccak source**, in preference order:
   - export one from `@nomicfoundation/edr` (`crates/edr_napi`): a sync `keccak256(input: Buffer | Uint8Array): Buffer` napi function — EDR already links keccak natively, so the binding is tiny, and it inherits any asm-backend work there;
   - a small, well-maintained prebuilt napi package as a `hardhat-ethers` dependency (weigh maintenance and supply-chain risk; the team already ships its own napi binaries, which argues for the EDR route).

   Micro-benchmark the choice against `@noble/hashes` on 32/64/128/1024-byte inputs: napi call overhead must not make small inputs slower. If it does for ≤32-byte inputs, keep JS below that size — measure, don't guess.
2. **Wire the registration in `packages/hardhat-ethers`** where the plugin materialises its ethers instance, respecting the lazy-init rules (not at `index.ts` import time — startup cost is a separate concern). Register only into the ethers instance the plugin itself resolves; never reach into a user's separately imported copy. If the native binary is missing, fall back to noble silently.
3. **Correctness gate**: exhaustive equivalence tests against `@noble/hashes` — random inputs from 0 bytes to several KB, plus empty input and >1 MB, compared byte-for-byte. Any mismatch is consensus-breaking. Confirm `isAddress`/`getAddress` checksums and typed-data hashing still match known vectors.
4. **Scope check**: confirm which ethers copy a scenario actually exercises (top-level `node_modules/ethers` vs a nested copy under `@nomicfoundation/hardhat-ethers`). If test code imports its own ethers, plugin-level registration may not reach it — document that limitation rather than monkey-patching module internals beyond the sanctioned `register()` API.
5. Run `pnpm lint`, `pnpm build`, `pnpm test` in `packages/hardhat-ethers` (plus EDR's verify set if an export was added there).

Suites on viem get no benefit — viem has no substitution hook. Upstream conversations with ethers about a native or WASM default are a separate, non-code follow-up.

## Verification (before/after)

Profile before and after with `pnpm profiler` (a bare `pnpm profiler` prints its usage; see `scripts/README.md`):

```bash
pnpm build
pnpm profiler --scenario ./end-to-end/openzeppelin-contracts \
  --prepare "cold compile" --command "test mocha" \
  --mode both --init --use-local
```

Expect: `keccakP` gone (or ≪1%) from `report.txt` and the `.cpuprofile`, replaced by a small native-hash entry; suite wall time down ~3–4%; identical test results (same pass count in `cmd.log`).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.