diff --git a/docs/ens-v2.md b/docs/ens-v2.md index 1560aa3..b8b41b9 100644 --- a/docs/ens-v2.md +++ b/docs/ens-v2.md @@ -38,18 +38,25 @@ fails closed (`ENSv2 is not deployed on mainnet`). ### Discovery anchor -The only configured address is the Universal Resolver proxy -`0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe` (stable across ENS redeploys). -Everything else is read at runtime: `ROOT_REGISTRY()` → `getSubregistry("eth")` -→ ETHRegistry → … Implementation addresses (VerifiableFactory, UserRegistry, -PermissionedResolver) come from `ensV2ConfigFor` defaults and are **overridable** -because ENS redeploys Sepolia periodically. +The only configured discovery address is a `UniversalResolverV2`; everything +else is read at runtime: `ROOT_REGISTRY()` → `getSubregistry("eth")` → ETHRegistry +→ … The default is the `UniversalResolverV2` of one specific Sepolia deployment +(`0x4a1817d13e9cf196f471725176355c1234b63c70`, root `0x8115…4354`), **not** the +public `UpgradableUniversalResolverProxy` `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe`: +ENS repoints that proxy between Sepolia redeploys (observed twice on 2026-09-14/15), +which moves `ROOT_REGISTRY` and orphans every name registered on the previous +stack — including `enspack.eth`. Pinning keeps discovery, the implementation +addresses and the ETHRegistrar (used by fork tests) mutually consistent. Set +`ENSPACK_ENSV2_UNIVERSAL_RESOLVER` to the proxy to follow ENS, and update all +five overrides together when moving to a new deployment. Names on a pinned +deployment resolve through enspack tooling and services; the ENS app only shows +whatever deployment the proxy currently points at. ### Deployment-specific overrides | Env | Default (docs.ens.domains Sepolia table) | |-----|-------------------------------------------| -| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe` | +| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | `0x4a1817d13e9cf196f471725176355c1234b63c70` (deployment's UniversalResolverV2; proxy `0xeEeE…EeEe` follows ENS) | | `ENSPACK_ENSV2_VERIFIABLE_FACTORY` | `0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef` | | `ENSPACK_ENSV2_USER_REGISTRY_IMPL` | `0x624a25d67b59d587752ebec8dded8827dae52050` | | `ENSPACK_ENSV2_PERMISSIONED_RESOLVER_IMPL` | `0x9eae5c2730a7dd16bdd1dee6421a1b91e3b0365e` | diff --git a/packages/cli/README.md b/packages/cli/README.md index 11e3fdc..b9c9827 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -83,7 +83,7 @@ webseed; see `packages/cli/test/get.e2e.test.ts`. | `ETH_RPC_URL` | Mainnet (and Anvil fork) JSON-RPC | | `SEPOLIA_RPC_URL` | Sepolia JSON-RPC | | `ENSPACK_ENS_VERSION` | `v1` or `v2`. Overrides `--chain` default (Sepolia → v2, mainnet → v1) | -| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | ENSv2 Universal Resolver (default `0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe`) | +| `ENSPACK_ENSV2_UNIVERSAL_RESOLVER` | ENSv2 UniversalResolverV2 (default `0x4a1817d13e9cf196f471725176355c1234b63c70`, a pinned Sepolia deployment; see `docs/ens-v2.md`) | | `ENSPACK_ENSV2_VERIFIABLE_FACTORY` | VerifiableFactory (Sepolia redeploys; override if ENS docs change) | | `ENSPACK_ENSV2_USER_REGISTRY_IMPL` | UserRegistry implementation | | `ENSPACK_ENSV2_PERMISSIONED_RESOLVER_IMPL` | PermissionedResolver implementation | diff --git a/packages/cli/src/defaults.ts b/packages/cli/src/defaults.ts index 2a49565..40cbf8a 100644 --- a/packages/cli/src/defaults.ts +++ b/packages/cli/src/defaults.ts @@ -44,8 +44,11 @@ function parseGateways(env: NodeJS.ProcessEnv): string[] { return [...extras, ...DEFAULT_GATEWAYS]; } -function isPrivateKey(value: string): value is `0x${string}` { - return /^0x[0-9a-fA-F]{64}$/.test(value); +/** Secret stores often strip the `0x`; accept both forms, never log the value. */ +function normalizePrivateKey(value: string): `0x${string}` | null { + const trimmed = value.trim(); + const hex = trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.slice(2) : trimmed; + return /^[0-9a-fA-F]{64}$/.test(hex) ? `0x${hex}` : null; } /** @@ -61,10 +64,11 @@ function publisherFromEnv(env: NodeJS.ProcessEnv): CliDeps["publisherFactory"] { }, }; } - if (!isPrivateKey(key)) { + const normalized = normalizePrivateKey(key); + if (normalized === null) { throw new EnspackError("PUBLISH", "ENSPACK_PUBLISHER_KEY must be a 32-byte hex private key"); } - const account = privateKeyToAccount(key); + const account = privateKeyToAccount(normalized); const viemChain = chain === "sepolia" ? sepolia : mainnet; const client = publicClientFor(chain, rpcUrl); const wallet = createWalletClient({ @@ -102,13 +106,14 @@ function operatorKeyFromEnv(env: NodeJS.ProcessEnv): `0x${string}` { "ENSPACK_OPERATOR_KEY is not set (falls back to ENSPACK_PUBLISHER_KEY)", ); } - if (!isPrivateKey(key)) { + const normalized = normalizePrivateKey(key); + if (normalized === null) { throw new EnspackError( "PUBLISH", "ENSPACK_OPERATOR_KEY (or ENSPACK_PUBLISHER_KEY) must be a 32-byte hex private key", ); } - return key; + return normalized; } function ensSetupFromEnv(env: NodeJS.ProcessEnv): NonNullable { diff --git a/packages/cli/test/defaults.test.ts b/packages/cli/test/defaults.test.ts index eabeef1..990e48b 100644 --- a/packages/cli/test/defaults.test.ts +++ b/packages/cli/test/defaults.test.ts @@ -46,7 +46,7 @@ describe("ensOptsFor (WP-17)", () => { it("selects v2 on sepolia by default", () => { const opts = ensOptsFor("sepolia", {}); expect(opts.ensVersion).toBe("v2"); - expect(opts.ensV2?.universalResolver).toBe("0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe"); + expect(opts.ensV2?.universalResolver).toBe("0x4A1817d13E9cF196f471725176355C1234b63C70"); }); it("selects v1 on mainnet by default", () => { diff --git a/packages/core/src/ens/v2/config.ts b/packages/core/src/ens/v2/config.ts index ac513c0..c2a5619 100644 --- a/packages/core/src/ens/v2/config.ts +++ b/packages/core/src/ens/v2/config.ts @@ -14,8 +14,15 @@ export interface EnsV2Config { ethRegistrar: `0x${string}`; } +/** + * One coherent Sepolia deployment (docs.ens.domains table, 2026-05-25 stack). The public + * `UpgradableUniversalResolverProxy` (0xeEeE…EeEe) is repointed by ENS between redeploys, which + * silently moves ROOT_REGISTRY and orphans everything registered on the previous stack; pinning + * this deployment's own UniversalResolverV2 keeps discovery, impls and ETHRegistrar consistent. + * Set ENSPACK_ENSV2_UNIVERSAL_RESOLVER=0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe to follow ENS. + */ const SEPOLIA_DEFAULTS = { - universalResolver: "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe", + universalResolver: "0x4a1817d13e9cf196f471725176355c1234b63c70", verifiableFactory: "0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef", userRegistryImpl: "0x624a25d67b59d587752ebec8dded8827dae52050", permissionedResolverImpl: "0x9eae5c2730a7dd16bdd1dee6421a1b91e3b0365e", diff --git a/packages/core/test/ens-v2/config.test.ts b/packages/core/test/ens-v2/config.test.ts index 66c8d26..e5fbbe8 100644 --- a/packages/core/test/ens-v2/config.test.ts +++ b/packages/core/test/ens-v2/config.test.ts @@ -3,7 +3,7 @@ import { labelhash } from "viem/ens"; import { describe, expect, it } from "vitest"; import { EnspackError, ensV2ConfigFor, ensVersionFor, labelId } from "../../src/index.js"; -const UR = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe"; +const UR = "0x4a1817d13e9cf196f471725176355c1234b63c70"; const FACTORY = "0x10dc6333cdfe1fcef624c6e0a8221b91804cd7ef"; const ALT = "0x0000000000000000000000000000000000000001"; diff --git a/services/registrar/src/server.ts b/services/registrar/src/server.ts index 59f5273..dc19423 100644 --- a/services/registrar/src/server.ts +++ b/services/registrar/src/server.ts @@ -24,11 +24,12 @@ function requireEnv(name: string): string { } function operatorAccount() { - const key = env("ENSPACK_OPERATOR_KEY"); - if (key === undefined || !/^0x[0-9a-fA-F]{64}$/.test(key)) { - throw new Error("ENSPACK_OPERATOR_KEY must be a 0x-prefixed 32-byte hex private key"); + const raw = env("ENSPACK_OPERATOR_KEY")?.trim(); + const hex = raw === undefined ? undefined : raw.replace(/^0[xX]/, ""); + if (hex === undefined || !/^[0-9a-fA-F]{64}$/.test(hex)) { + throw new Error("ENSPACK_OPERATOR_KEY must be a 32-byte hex private key"); } - return privateKeyToAccount(key as Hex); + return privateKeyToAccount(`0x${hex}` as Hex); } function chainName(): "mainnet" | "sepolia" { diff --git a/services/seed/test/health.test.ts b/services/seed/test/health.test.ts index 9507a10..05a965d 100644 --- a/services/seed/test/health.test.ts +++ b/services/seed/test/health.test.ts @@ -45,7 +45,7 @@ describe("seedEnsOpts", () => { it("defaults sepolia to v2 and honors ENSPACK_ENS_VERSION", () => { expect(seedEnsOpts("sepolia", {}).ensVersion).toBe("v2"); expect(seedEnsOpts("sepolia", {}).ensV2?.universalResolver).toBe( - "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe", + "0x4A1817d13E9cF196f471725176355C1234b63C70", ); expect(seedEnsOpts("sepolia", { ENSPACK_ENS_VERSION: "v1" }).ensVersion).toBe("v1"); expect(seedEnsOpts("mainnet", {}).ensVersion).toBe("v1");