Skip to content

Circular chunk dependency causes TDZ error with @noble/curves + @noble/hashes (viem/wagmi) #9225

@sembrestels

Description

@sembrestels

Bug Description

Rolldown (1.0.0-rc.17 via Vite 8.0.10) splits @noble/hashes and @noble/curves into separate chunks that form a static circular dependency. This causes a runtime error because @noble/curves/secp256k1 reads sha256 from @noble/hashes at module evaluation time (top-level curve construction), but the live binding is still undefined due to the circular chunk initialisation order.

This works correctly with Vite 7 / Rollup.

Error

Error: param hash is invalid. Expected hash, got undefined
    at r (utils-BeVtMw_R.js:1)
    at k (utils-BeVtMw_R.js:1)
    at we (secp256k1-DjV6mbXe.js:1)
    at Te (secp256k1-DjV6mbXe.js:1)
    at ke (secp256k1-DjV6mbXe.js:1)
    at <anonymous> (secp256k1-DjV6mbXe.js:1)

What happens

Rolldown produces three relevant chunks:

Chunk Contains
index-*.js (entry) @noble/hashes/sha256, viem core, React, app code
secp256k1-*.js @noble/curves/secp256k1 + abstract curve utilities (Field, weierstrass, hash-to-curve)
various lazy chunks EVMcrispr modules (dynamic imports)

The dependency graph between them:

index-*.js ──static──▶ secp256k1-*.js ──static──▶ index-*.js
     │                                                 ▲
     └──────────dynamic import()───────────────────────┘
  1. index-*.js statically imports {a, i, o, r, t} from secp256k1-*.js (the abstract curve utilities: Field, weierstrass, hash-to-curve, mapToCurveSimpleSWU, and the secp256k1 curve)
  2. secp256k1-*.js statically imports {$, U} from index-*.js (sha256 hash function and hmac)
  3. index-*.js also dynamically import()s secp256k1-*.js (viem's lazy signature recovery)

When the browser evaluates index-*.js, it must first evaluate secp256k1-*.js (static dep). But secp256k1-*.js reads sha256 from index-*.js at module top-level — and index-*.js hasn't finished initializing yet. So sha256 is undefined, and the @noble/curves parameter validator throws.

Reproduction

git clone https://github.com/EVMcrispr/evmcrispr.git
cd evmcrispr
git checkout c67bc95c84cc7afbe7f8e383f3a3815c1bceec2a
bun install
bun run --filter='evmcrispr-terminal' build

Then verify the circular dependency in the output:

# index statically imports from secp256k1
grep -oP 'import\{[^}]*\}from"./secp256k1[^"]*"' apps/evmcrispr-terminal/dist/assets/index-*.js

# secp256k1 statically imports from index
grep -oP 'import\{[^}]*\}from"./index[^"]*"' apps/evmcrispr-terminal/dist/assets/secp256k1-*.js

Serve with npx vite preview inside apps/evmcrispr-terminal and open the browser console to see the error.

Expected behaviour

The bundler should either:

  • Keep modules with top-level cross-package dependencies in the same chunk, or
  • Ensure correct evaluation order so that statically imported bindings are initialised before dependent chunks execute

Workaround

Force both packages into the same chunk:

// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        if (id.includes("@noble/hashes") || id.includes("@noble/curves")) {
          return "noble-crypto";
        }
      },
    },
  },
},

Impact

This affects any project using viem, wagmi, or ethers with Vite 8 when the dependency graph is complex enough that Rolldown's chunk optimizer splits @noble/hashes and @noble/curves into separate chunks with a static circular dependency.

Environment

  • Vite: 8.0.10
  • Rolldown: 1.0.0-rc.17
  • @noble/curves: 1.8.2 (via viem 2.46.3)
  • @noble/hashes: 1.7.2 (via viem 2.46.3)

Related issues

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions