Skip to content

feat: materialize canonicality in the index, expose in omnigraph#2061

Merged
shrugs merged 28 commits intomainfrom
feat/materialize-canonicality
May 8, 2026
Merged

feat: materialize canonicality in the index, expose in omnigraph#2061
shrugs merged 28 commits intomainfrom
feat/materialize-canonicality

Conversation

@shrugs
Copy link
Copy Markdown
Member

@shrugs shrugs commented May 5, 2026

Reviewer Focus (Read This First)

  • canonicality reconciliation in canonicality-db-helpers.ts. the canonical edge between Registry R and parent Domain D is NOT materialized in a parallel table; it is derived on demand by checking that the unidirectional pointers agree (R.canonicalDomainId = D.idD.subregistryId = R.id). reconcile runs at every pointer write and only fires the cascade when Registry.canonical actually flips.
  • the cascade has two paths: in-memory PK update when Registry.__hasChildren = false (the dominant case for fresh ENSv1 virtual registries on first wire-up), or a single recursive-CTE batch UPDATE that walks the canonical subgraph by agreement check and flips Registry.canonical + Domain.canonical on every visited row. the SQL path goes through context.ensDb.sql which forces a Ponder cache flush + invalidate; we accept that cost because it's bounded to flips on Registries that actually have descendants.
  • ENSv2Registry.ParentUpdated is emitted from the child registry and may arrive before the parent's LabelRegistered/SubregistryUpdated. the helpers blindly write the unidirectional pointers and reconcile only on actual flag flips, so order doesn't matter — worth a second read.
  • handleBridgedResolverChange reads the previous resolver from the Domain-Resolver Relation, so it must run before Protocol Acceleration's NewResolver/ResolverUpdated handlers overwrite the DRR row. ordering enforced in apps/ensindexer/ponder/src/register-handlers.ts. two implied invariants are documented in the helper: 1:1 originating-Domain-per-bridged-Registry, and bridged target Registry indexed before its bridged-resolver event fires.

Problem & Motivation

  • canonicality was computed at query time via recursive CTEs starting from getRootRegistryIds(). every canonical filter, every name walk, every canonical-path lookup paid the traversal cost.
  • the old registryCanonicalDomain table was a last-write-wins shim with no edge auth on the writer side; the API layer compensated with edge-auth joins everywhere it was consumed. brittle.
  • TODO(canonical-names) markers and a user-facing disclaimer in the enskit example flagged this as a known stopgap. moving canonicality into the index unblocks accurate Domain.canonical/Registry.canonical exposure on the omnigraph schema.
  • conceptual reframing: canonicality is about nameability, not addressability or resolvability. both ENSv1 and ENSv2 Root Registries are canonical, so ENSv1 Domains remain canonical even after ENSv2 is deployed (they continue to surface a Domain.name and Domain.path). Forward Resolution still prefers the ENSv2 namegraph when both exist; that preference is encoded in the resolver, not in the canonicality flags.
  • v1 and v2 share a single canonicality definition: a Registry is canonical iff it can be traced back to a Root via canonical edges. only the namespace's Root Registries are seeded canonical at creation; everything else earns canonicality through the natural reconcile flow. concretely this means a v1 Domain whose Bridged Resolver claims its sub-registry leaves its mainnet v1 children non-canonical (e.g. mainnet bridge.linea.eth is non-canonical because mainnet linea.eth.subregistryId points at the bridged Linea Registry, not at the mainnet v1 virtual registry; only the Linea-side bridge.linea.eth is the resolution-visible canonical record).

What Changed (Concrete)

  1. schema: add Registry.canonical, Registry.canonicalDomainId, Registry.__hasChildren, Domain.canonical. drop the old registryCanonicalDomain table. the canonical edge is not materialized in a parallel table — Registry.canonicalDomainId (Registry → Domain) and Domain.subregistryId (Domain → Registry) are written blindly when their onchain events fire; the bidirectional canonical edge is derived on demand by checking agreement.
  2. new apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts:
    • ensureDomainInRegistry: inherit parent Registry's canonical flag onto the new Domain, and flip the parent's __hasChildren sentinel from false→true on the first child (idempotent thereafter, monotonic).
    • handleRegistryCanonicalDomainUpdated: writes the unidirectional Registry.canonicalDomainId and reconciles canonicality.
    • handleSubregistryUpdated: writes the unidirectional Domain.subregistryId and reconciles canonicality on both prev and next subregistries.
    • handleBridgedResolverChange: detaches/attaches both halves of the bridged canonical edge; documents the two implied invariants (1:1 + event ordering) and the fix sketch if either is ever violated.
    • reconcileRegistryCanonicality: recompute Registry.canonical from the agreement check (root registries short-circuit to true); calls cascadeCanonicality only when the flag actually flips.
    • cascadeCanonicality: in-memory PK update when Registry.__hasChildren = false; otherwise a single recursive-CTE batch UPDATE that walks the canonical subgraph by agreement check.
  3. new apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts: ensureRegistry upsert that seeds canonical = true only for the namespace's Root Registries (ENSv1 root + ENSv2 root when defined). all other Registries default to false and earn canonicality through reconcile.
  4. new isRootRegistryId(namespace, registryId) predicate in @ensnode/ensnode-sdk. removes getRootRegistryIds (plural) — root membership is now a predicate, not an enumeration.
  5. ENSv1 NewOwner switches to ensureRegistry + ensureDomainInRegistry; for non-TLDs also writes the parent's subregistryId and calls handleRegistryCanonicalDomainUpdated for the synthesized ENSv1VirtualRegistry. ENSv1 NewResolver now invokes handleBridgedResolverChange (event signature gains resolver).
  6. ENSv2 LabelRegistered/LabelReserved switch to ensureRegistry + ensureDomainInRegistry. ENSv2 SubregistryUpdated calls handleSubregistryUpdated. new ENSv2 ParentUpdated handler — child registry claims its canonical parent via handleRegistryCanonicalDomainUpdated (parent address interpreted same-chain). new ENSv2 ResolverUpdated handler invokes handleBridgedResolverChange.
  7. omnigraph getCanonicalPath: short-circuits on domain.canonical = false, walks upward via the agreement check at each hop (registries.canonical_domain_id = domains.id AND domains.subregistry_id = registries.id).
  8. omnigraph find-domains: delete canonical-registries-cte.ts. filterByCanonical is now WHERE base.canonical = TRUE. base-domain-set derives parentId via the same two-join-with-agreement-predicate pattern. filterByName walks via the agreement check at each hop in its recursive CTE.
  9. omnigraph getDomainByInterpretedName: walks raw domain.subregistry_id edges (not the canonical edge) — the comment at the recursive CTE makes this explicit. addressability via alias paths is intentional; canonical name/path comes from the canonical-edge walk in getCanonicalPath. BridgedResolver recursion branch retained; ENSv1Resolver/ENSv2Resolver fallback branches retained.
  10. graphql: add canonical: Boolean! on Domain and Registry interfaces (and all implementing types). Resolver.bridged return type changed from AccountId to the bridged target Registry (interface). regenerated schema.graphql and introspection.ts.
  11. BridgedResolverTarget in @ensnode/ensnode-sdk/internal simplified: { registry: AccountId, shadow: boolean }{ registryId: RegistryId, ... }.
  12. update ENSv2 Registry ABI to latest (drop Approval event, rename anonymous tokenId output).
  13. enskit SearchView: drop the canonical-derivation disclaimer.

Design & Planning

  • Planning artifacts: none formal. the stopgap was inline-documented via TODO(canonical-names) markers and the enskit disclaimer. materializing canonicality into the index was the obvious next step once the bidirectional invariant could be maintained at index time; collapsing the parallel materialization tables (domainCanonicalSubregistry, registryDomains) came later, after measuring the per-NewOwner array-rewrite cost on .eth virtual-registry scale.
  • Key conceptual decision: ENSv1 stays canonical post-ENSv2-deployment. canonicality models the namegraph each Domain belongs to, not which graph Forward Resolution prefers. this keeps Domain.name/Domain.path populated for v1-only entities (e.g. addr.reverse, the v1 root's children) and lets resolution preference live in the resolver, where it belongs.
  • Second design decision: derive the bidirectional canonical edge on demand rather than materialize it in a parallel table. eliminates the per-NewOwner registryDomains.domainIds array rewrite (the steady-state mainnet bottleneck) and the parallel domainCanonicalSubregistry upsert/delete dance. cascade pays a Ponder cache flush only when Registry.canonical actually flips AND the registry has descendants — Registry.__hasChildren short-circuits to in-memory PK update for the dominant fresh-virtual-registry case.
  • Third design decision: v1 and v2 share a single canonicality definition. the earlier "v1 is canonical by axiom, seed all v1 registries canonical=true" shortcut was wrong — it incorrectly canonicalized v1 children of a v1 Domain whose Bridged Resolver had claimed the canonical sub-registry elsewhere. only roots are seeded; everything else flows through the same reconcile path as v2.
  • Reviewed / approved by: n/a — soliciting review now.

Self-Review

  • Bugs caught:
    • handleRegistryCanonicalDomainUpdated initially didn't dislodge a prior occupant on the new domain — added the dislodge path in reconciliation.
    • handleBridgedResolverChange initially read the prior target from a parallel canonical-edge table, which forced a complicated recovery when bridging detaches. switched to reading the prior resolver from the Domain-Resolver Relation (the natural source of truth), and ordered this helper before Protocol Acceleration's DRR-overwriting handlers in register-handlers.ts.
    • the v1-canonical-by-axiom shortcut (seeding all v1 registries canonical=true) was wrong: it incorrectly canonicalized v1 children of a v1 Domain whose Bridged Resolver had claimed the canonical sub-registry elsewhere (e.g. mainnet bridge.linea.eth). fixed by only seeding root registries and letting the natural reconcile flow handle v1 the same way as v2.
  • Logic simplified: dropped the parallel domainCanonicalSubregistry and registryDomains tables entirely. getCanonicalPath and filterByName lost their materialized-edge JOINs (the agreement check is two joins inline). base-domain-set lost its parallel-table left-join. v1-specific seeding logic in ensureRegistry collapsed to "is this a root?".
  • Naming: setRegistryCanonicalDomainhandleRegistryCanonicalDomainUpdated for symmetry with handleSubregistryUpdated/handleBridgedResolverChange. consolidated materializeAndCascade + updateRegistryCanonicalityreconcileRegistryCanonicality + cascadeCanonicality (one decides whether to flip, the other applies the flip).
  • Dead code removed: getRootRegistryIds, canonical-registries-cte.ts, the parentDomain alias in the old query, the old registryCanonicalDomain / domainCanonicalSubregistry / registryDomains tables, materializeAndCascade, updateRegistryCanonicality, two TODO(canonical-names) blocks in ENSv2Registry.

Cross-Codebase Alignment

  • Search terms used: registryCanonicalDomain, getRootRegistryIds, canonical_domain, canonicalSubregistry, domainCanonicalSubregistry, registryDomains, isBridgedResolver, materializeAndCascade, updateRegistryCanonicality.
  • Reviewed but unchanged: ensapi find-domains order/limit layers (not affected), ensadmin (no consumers of removed helpers), ponder-subgraph (no canonicality references), ensapi resolution forward path (only the isBridgedResolver call site).
  • Deferred alignment: none identified.

Downstream & Consumer Impact

  • omnigraph schema (additive): Domain.canonical: Boolean!, Registry.canonical: Boolean!.
  • omnigraph schema (breaking): Resolver.bridged return type changed from AccountId to the bridged target Registry (interface). consumers selecting bridged { ... } now get the Registry shape with rich navigation; consumers reading bridged as AccountId need to update their selection.
  • @ensnode/ensnode-sdk (breaking): getRootRegistryIds removed in favor of the isRootRegistryId(namespace, registryId) predicate.
  • @ensnode/ensnode-sdk/internal (breaking): BridgedResolverTarget shape — { registry, shadow }{ registryId, ... }.
  • behavior change worth flagging: ENSv1 Domains now report canonical = true (previously false once ENSv2 was deployed). consumers that filtered on canonical: true to reach the v2 namegraph specifically will need to filter on __typename instead, or scope by Registry. Forward Resolution preference is unchanged.
  • Docs updated: inline schema descriptions and code comments. no docs site changes.
  • ensindexer schema changes — re-index required (implicit per ponder semantics).

Testing Evidence

  • New / updated integration tests:
    • Registry.canonical asserts both v1 and v2 Root Registries are canonical.
    • Domain.canonical asserts ENSv1-only addr.reverse is canonical (lookup by id; nameability holds even when resolution prefers v2).
    • Query.domains > sees .eth domain expects v1 'eth' to surface name: "eth" (v1 root is canonical, so the canonical-name walk produces a non-null name for its children).
    • Domain.path > returns the canonical path for a Domain looked up via a non-canonical aliasdomain(by: { name: "wallet.linked.parent.eth" }) resolves to the wallet Domain via raw subregistry edges (addressability), and its path walks the canonical edge by agreement check through wallet.sub1.sub2.parent.eth → ... → eth (nameability). this is the test that locks in the addressability/nameability split.
  • Known gaps: per-package unit tests are not the convention for ensindexer behavior; cascade correctness is exercised end-to-end by the integration suite. devnet fixture lacks a known non-canonical Domain to assert exclusion against canonical: true; left as it.todo.
  • What reviewers have to reason about manually: ordering edge cases for ENSv2 ParentUpdated vs SubregistryUpdated and LabelRegistered. handlers are structured to be order-insensitive (write unidirectional pointers blindly, reconcile only on actual flag flips), but worth a second read.

Scope Reductions

  • Follow-ups: extend isBridgedResolver for ENSv2 bridges when defined (detach is wired and works today; attach is currently unreachable via the ENSv2 ResolverUpdated path). consider runtime enforcement of the 1:1 originating-Domain-per-bridged-Registry invariant in handleBridgedResolverChange (currently documented, not enforced). drop the Resolver.bridged AccountId-shaped read path entirely once consumers migrate.
  • Why they were deferred: keep this PR focused on the materialization + reconcile + cascade design.

Risk Analysis

  • Risk areas: the cascade SQL forces a Ponder cache flush + invalidate; flush frequency is bounded to "Registry has children AND its canonical flag actually flips" — i.e. bridged-resolver attach/detach and ENSv2 reparenting on already-populated subtrees. fresh first-wire-ups stay in-memory via __hasChildren = false (the dominant v1 NewOwner case). ordering of ENSv2 ParentUpdated vs LabelRegistered/SubregistryUpdated — handlers are order-insensitive but the reconcile-on-pointer-write pattern is the load-bearing piece. ordering of handleBridgedResolverChange vs Protocol Acceleration's DRR overwrite — enforced in register-handlers.ts. assumption that the canonical namegraph is a tree (each registry has at most one canonical parent domain) — enforced by the bidirectional agreement check; if a cycle is ever introduced the recursive CTE's UNION (not UNION ALL) prunes duplicates and termination is preserved.
  • Mitigations or rollback options: rollback = revert + re-index. handleBridgedResolverChange invariants (1:1 + event ordering) are documented inline with a fix sketch if either is ever violated.
  • Named owner if this causes problems: @shrugs.

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

Copilot AI review requested due to automatic review settings May 5, 2026 22:17
@shrugs shrugs requested a review from a team as a code owner May 5, 2026 22:17
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: e77f7fc

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

This PR includes changesets to release 24 packages
Name Type
ensapi Major
@ensnode/ensnode-sdk Major
ensadmin Major
ensindexer Major
ensrainbow Major
fallback-ensapi Major
@namehash/ens-referrals Major
@ensnode/ensdb-sdk Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@ensnode/integration-test-env Major
@namehash/namehash-ui Major
@docs/ensnode Major
@docs/ensrainbow Major
enssdk Major
enscli Major
enskit Major
ensskills Major
@ensnode/datasources Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@ensnode/ensindexer-perf-testing Major
@ensnode/enskit-react-example 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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment May 8, 2026 5:34pm
ensnode-enskit-react-example Ready Ready Preview, Comment May 8, 2026 5:34pm
ensnode.io Ready Ready Preview, Comment May 8, 2026 5:34pm
ensrainbow.io Ready Ready Preview, Comment May 8, 2026 5:34pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reworks canonical-namegraph storage and traversal: adds canonical flags and pointers to schema, removes the canonical-registries CTE, introduces indexer DB helpers to maintain canonical invariants (including bridged-resolver wiring), and updates Omnigraph queries, forward-resolution, and GraphQL schema to be canonical-aware.

Changes

Canonicality: schema, indexer helpers, and indexer wiring

Layer / File(s) Summary
Schema / public shape
packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts, packages/ensnode-sdk/src/shared/root-registry.ts, packages/datasources/src/ens-test-env.ts, .changeset/*
Adds registry.canonicalDomainId, registry.canonical: boolean, domain.canonical: boolean; updates root-registry utilities and test env; changeset documents new GraphQL fields.
DB helpers / core impl
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
New helpers: ensureDomainInRegistry, handleRegistryCanonicalDomainUpdated, handleSubregistryUpdated, handleBridgedResolverChange, reconcileRegistryCanonicality, cascadeCanonicality.
Registry helper
apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts
New ensureRegistry(...) that idempotently inserts registry rows and seeds canonical for namespace root registries.
Indexer wiring / handlers
apps/ensindexer/src/plugins/ensv2/handlers/*, apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
Handlers changed to call ensureRegistry/ensureDomainInRegistry and canonicality helpers; ENSv2 ParentUpdated listener added; ENSv1 handlers call handleBridgedResolverChange on resolver updates.
Plugin ordering / migration
apps/ensindexer/ponder/src/register-handlers.ts, apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts
Added NodeMigration handler and reordered guarded plugin registration so NodeMigration runs before ENSv2/ProtocolAcceleration.
Tests / fixtures / docs
apps/ensapi/src/test/integration/devnet-names.ts, .changeset/*, AGENTS.md
Devnet fixtures and integration tests updated to assert canonical semantics; changeset added for omnigraph canonical fields; AGENTS note updated.

Omnigraph queries, forward-resolution, and GraphQL surface

Layer / File(s) Summary
Data shape / projections
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts
domainsBase() now includes canonical and derives parentId via registry↔domain canonical agreement.
Remove CTE / filter simplification
apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts, apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts
Deleted canonical-registries CTE module; filter-by-canonical now filters on base.canonical == true.
Traversal rewrite
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts, apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts, apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
Recursive CTEs rewritten to ascend via registry.canonical_domain_id and domain.subregistry_id; getCanonicalPath checks domain.canonical early and enforces MAX_SUPPORTED_NAME_DEPTH; interpreted-name lookup uses forward/disjoint walks returning depth-ordered rows.
Forward-resolution / bridged resolver
apps/ensapi/src/lib/resolution/forward-resolution.ts, packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts
isBridgedResolver return shape changed to include registryId; forward-resolution accelerate path short-circuits to _resolveForward with the bridged registry.
GraphQL exposure
apps/ensapi/src/omnigraph-api/schema/domain.ts, apps/ensapi/src/omnigraph-api/schema/registry.ts, apps/ensapi/src/omnigraph-api/schema/resolver.ts
Added Domain.canonical and Registry.canonical boolean fields; Resolver.bridged updated to return RegistryInterfaceRef via bridged?.registryId.
Constants / guards
apps/ensapi/src/omnigraph-api/lib/constants.ts
Added MAX_SUPPORTED_NAME_DEPTH = 16 to bound traversal depth.
Tests / UI wording
apps/ensapi/src/omnigraph-api/schema/*.test.ts, examples/enskit-react-example/src/SearchView.tsx
Integration tests updated for canonical semantics; SearchView warning replaced with statement that only canonical domains are rendered.

Sequence Diagram(s)

sequenceDiagram
  participant Indexer as Indexer
  participant DB as ENS DB
  participant ENSSDK as ENS SDK (isBridgedResolver)
  participant ENSApi as ENSApi / Omnigraph

  Note over Indexer,DB: Indexer processes on-chain events
  Indexer->>DB: ensureRegistry(registryId, ...)
  Indexer->>DB: ensureDomainInRegistry(registryId, domainId)
  Indexer->>ENSSDK: isBridgedResolver(namespace, resolver)
  ENSSDK-->>Indexer: BridgedResolverTarget | null
  alt bridged target found
    Indexer->>DB: handleBridgedResolverChange(...)\n(set domain.subregistry, set registry.canonicalDomainId)
  end
  Note over DB,ENSApi: DB stores canonical flags and pointers
  ENSApi->>DB: query domainsBase / forwardWalkDisjointNamegraph
  DB-->>ENSApi: canonical-aware path rows
  ENSApi->>ENSSDK: forward-resolution via bridged registry (registryId/registry)
  ENSSDK-->>ENSApi: resolution result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • namehash/ensnode#1576 — Overlaps on canonical-name/path resolution logic and prior canonical-edge approaches.
  • namehash/ensnode#1983 — Related bridged-resolver callsite changes and forward-resolution coordination.
  • namehash/ensnode#1654 — Overlaps on canonical-registries CTE vs boolean canonical model tradeoffs.

"I hopped through fields and tables bright,
Canonicals stitched by moonlit night,
Bridges mended, paths made true,
Registry roots now pointing through.
— a small rabbit, delighted 🐇"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: materialize canonicality in the index, expose in omnigraph' clearly and concisely describes the main change: materializing canonical-name edges in the index and exposing them in the omnigraph schema.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description is comprehensive and exceeds template requirements, providing detailed context on changes, rationale, design decisions, testing, and risk analysis.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/materialize-canonicality

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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 moves ENS “canonicality” from query-time recursive traversal into index-time materialization, then exposes the resulting canonical flags and canonical parent/child edges through the Omnigraph GraphQL schema. This reduces query complexity/cost (no canonical registries CTE / edge-auth joins) and enables first-class Domain.canonical / Registry.canonical semantics.

Changes:

  • Materializes canonicality in the indexed DB (Registry.canonical/canonicalDomainId, Domain.canonical/canonicalSubregistryId) plus a per-registry child list (registryDomains) to support cascading canonicality updates.
  • Updates ENSv1/ENSv2 indexer handlers (incl. new ENSv2 ParentUpdated + ResolverUpdated) to maintain the bidirectional canonical edge invariant and bridged-resolver attachments.
  • Simplifies Omnigraph canonical path + domain search/name-walk logic to use the materialized edges/flags; adds canonical fields to GraphQL types and regenerates SDK schema artifacts.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/enssdk/src/omnigraph/generated/schema.graphql Adds canonical: Boolean to Domain/Registry interfaces and implementing types.
packages/enssdk/src/omnigraph/generated/introspection.ts Regenerates introspection to include new canonical fields.
packages/ensnode-sdk/src/shared/root-registry.ts Removes getRootRegistryIds; clarifies getRootRegistryId as canonical root.
packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts Changes bridged-resolver detection to return a richer discriminated union incl. registry id + metadata; requires originatingNode.
packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts Adds canonicality columns + introduces registryDomains table; removes registryCanonicalDomain.
packages/datasources/src/abis/ensv2/Registry.ts Updates ENSv2 Registry ABI (drops Approval event; renames an output).
examples/enskit-react-example/src/SearchView.tsx Updates example search UI copy and query to render only canonical domains.
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Ensures registries/domains are tracked in registryDomains; adds ParentUpdated and ResolverUpdated canonicality/bridge wiring.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Switches to ensureRegistry + canonicality helpers; wires bridged-resolver changes from ENSv1 NewResolver.
apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts Adds ensureRegistry helper that seeds root registry as canonical.
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Introduces core canonicality maintenance helpers (bidirectional edge + cascading flips + bridge attach/detach).
apps/ensapi/src/omnigraph-api/schema/resolver.ts Updates Resolver.bridgesTo to new isBridgedResolver signature (uses sentinel node).
apps/ensapi/src/omnigraph-api/schema/registry.ts Exposes Registry.canonical field.
apps/ensapi/src/omnigraph-api/schema/domain.ts Exposes Domain.canonical field.
apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts Updates test commentary to reflect new canonicality mechanics.
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts Removes query-time bridged-resolver recursion; walks canonical namegraph using materialized edges/flags.
apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Simplifies canonical path traversal to materialized edges + short-circuit on domain.canonical.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts Updates upward traversal to use registry.canonicalDomainId instead of removed table.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts Filters via domain.canonical instead of canonical registries CTE.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts Derives parentId from registry.canonicalDomainId and includes canonical in the base shape.
apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts Deletes no-longer-needed canonical registries recursive CTE.
apps/ensapi/src/lib/resolution/forward-resolution.ts Updates forward-resolution recursion to new isBridgedResolver signature/projection.
Comments suppressed due to low confidence (1)

packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts:290

  • Domain.canonical is now a frequently-used filter predicate (e.g. filterByCanonical becomes WHERE base.canonical = TRUE, and the enskit example queries canonical: true), but the schema doesn’t add an index/partial index on domains.canonical. On large datasets this can turn canonical-only queries into full scans.

Consider adding a partial index like (canonical) WHERE canonical = true (or a composite index that matches your common query patterns) to keep canonical filtering performant.

    // Whether this Domain is part of the canonical namegraph. Mirrors the parent Registry's flag.
    canonical: t.boolean().notNull().default(false),

    // The Subregistry of this Domain that participates in the canonical namegraph (i.e. the
    // Registry whose `canonicalDomainId` points back to this Domain). May differ from
    // `subregistryId` when a Bridged Resolver attaches a different Registry under this Domain.
    canonicalSubregistryId: t.text().$type<RegistryId>(),

    // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin
  }),
  (t) => ({
    byType: index().on(t.type),
    byRegistry: index().on(t.registryId),
    bySubregistry: index().on(t.subregistryId).where(sql`${t.subregistryId} IS NOT NULL`),
    byOwner: index().on(t.ownerId),
    byLabelHash: index().on(t.labelHash),
  }),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR materializes Registry.canonical and Domain.canonical flags into the index, replacing the old query-time recursive CTE approach and the registryCanonicalDomain shim table. The bidirectional canonical edge is derived on demand from two agreeing unidirectional pointers (Registry.canonicalDomainIdDomain.subregistryId), with a cascadeCanonicality recursive-CTE batch UPDATE that walks and flips entire canonical subgraphs when a registry's flag changes.

  • Schema: adds Registry.canonical, Registry.canonicalDomainId, Registry.__hasChildren, Domain.canonical; drops registryCanonicalDomain / domainCanonicalSubregistry / registryDomains tables; breaking omnigraph change: Resolver.bridged return type changes from AccountId to Registry interface.
  • Indexer: new canonicality-db-helpers.ts and registry-db-helpers.ts implement the reconcile + cascade flow; ENSv1 NewOwner, ENSv2 LabelRegistered/SubregistryUpdated/ParentUpdated/ResolverUpdated all route through these helpers; handler registration order is explicitly documented (NodeMigration → ENSv2 → ProtocolAcceleration).
  • Query layer: getCanonicalPath, filterByCanonical, filterByName, base-domain-set all rewritten to use the materialized flag and inline two-join agreement predicate, removing the old CTE-based canonical registry traversal.

Confidence Score: 3/5

Two confirmed defects affect production bridged namegraphs (Basenames, Lineanames) and could corrupt canonicality silently or crash the indexer.

The cascadeCanonicality SQL logic and query-layer rewrites are well-designed and internally consistent. However, two defects in the indexer write path are confirmed: (1) every non-TLD ENSv1 NewOwner unconditionally overwrites domain.subregistryId with the L1 virtual registry, silently destroying any bridged canonical edge already set by handleBridgedResolverChange; (2) handleBridgedResolverChange calls handleRegistryCanonicalDomainUpdated which throws if the target virtual registry row does not exist, and for real Basenames/Lineanames deployments the mainnet NewResolver event precedes any L2 NewOwner, so this invariant is violated in practice and would crash the indexer during a fresh index run.

apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts (unconditional subregistryId overwrite) and apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts (missing registry-existence guard before handleRegistryCanonicalDomainUpdated).

Important Files Changed

Filename Overview
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts New core module: five helpers to maintain Registry.canonical / Domain.canonical. reconcileRegistryCanonicality + cascadeCanonicality logic is sound, but handleBridgedResolverChange will crash when the bridged target VR hasn't been indexed yet.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Refactored to use new canonicality helpers; the non-TLD NewOwner path unconditionally overwrites domain.subregistryId with the L1 virtual registry, silently destroying a previously-set bridged canonical edge.
apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts New ensureRegistry helper seeds canonical=true only for root registries via isRootRegistryId; onConflictDoNothing correctly preserves canonicality from earlier reconciles on re-registration.
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Added ParentUpdated and ResolverUpdated handlers; ParentUpdated calls handleRegistryCanonicalDomainUpdated which is order-insensitive. Clean integration with new canonicality helpers.
packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts Refactored from a flat function to a cached config list. New BridgedResolverConfig shape adds originDomainId and targetRegistryId, enabling edge-auth in handleBridgedResolverChange.
packages/ensnode-sdk/src/shared/root-registry.ts Replaces getRootRegistryIds (enum) with isRootRegistryId (predicate); drops Basenames/Lineanames VRs as roots, consistent with the new only-true-roots-are-seeded-canonical design.
apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Simplified to short-circuit on domain.canonical = false, then walk upward via bidirectional agreement check. Post-CTE rows guard restored after earlier review.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts Derives parentId inline via two-join bidirectional agreement check, eliminating the old registryCanonicalDomain parallel-table join. Adds canonical column for downstream filterByCanonical.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts Recursive CTE now traverses canonical edges via bidirectional agreement predicate. The UNION ALL in the recursive step correctly terminates (depth-bounded by pathLength).
apps/ensindexer/ponder/src/register-handlers.ts Enforces NodeMigration → ENSv2 → ProtocolAcceleration registration order; the comment clearly documents the invariants. Extraction of node-migration.ts makes the ordering explicit.
packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts Adds canonical, canonicalDomainId, __hasChildren on Registry and canonical on Domain; drops registryCanonicalDomain table. Schema is consistent with the new materialized-flag design.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["ENSv1/ENSv2 on-chain event"] --> B{Event type}
    B -->|"NewOwner (non-TLD)"| C["handleSubregistryUpdated\n(parentDomain → L1 VR)"]
    B -->|"SubregistryUpdated"| D["handleSubregistryUpdated\n(domain → next subregistry)"]
    B -->|"ParentUpdated"| E["handleRegistryCanonicalDomainUpdated\n(child registry → parent domain)"]
    B -->|"NewResolver / ResolverUpdated"| F["handleBridgedResolverChange\n(detach prev / attach next bridged)"]
    C --> G["reconcileRegistryCanonicality"]
    D --> G
    E --> G
    F --> G
    G --> H{"flag flipped?"}
    H -->|No| I["no-op"]
    H -->|"Yes: __hasChildren=false"| J["in-memory PK update\n(ENSv1 fast-path)"]
    H -->|"Yes: __hasChildren=true"| K["cascadeCanonicality\nrecursive-CTE batch UPDATE\n(flush Ponder cache)"]
    K --> L["Registry.canonical + Domain.canonical\nupdated for entire subtree"]
Loading

Reviews (14): Last reviewed commit: "fix: remove unnecessary changeset" | Re-trigger Greptile

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Comment thread apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts (1)

130-148: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent fall-through when the deepest resolver is an ENSv2Resolver.

The // TODO: ENSv2Resolver branch on Line 144 means that when deepestResolver is set but is not the ENSv1 fallback resolver, control silently flows into the return exact ? deepest.domainId : null; line. For a non-exact match with an ENSv2Resolver in the middle of the path, callers will get null with no log or error to attribute the miss — making this hard to debug post-merge. Consider at least a logger.debug({ deepestResolver }) or a typed error to aid triage until the ENSv2Resolver hop is implemented.

Want me to open a tracking issue for the ENSv2Resolver hop and add a debug log here in the meantime?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts` around
lines 130 - 148, The deepestResolver branch currently falls through silently for
ENSv2 resolvers; update the TODO: ENSv2Resolver branch to log the resolver
before returning so non-exact misses are debuggable — e.g., call logger.debug({
deepestResolver, path, depth }) (or logger.warn) when deepestResolver exists and
isn't equal to ENSv1Resolver (found via maybeGetDatasourceContract) before the
final return; keep the existing ENSv1Resolver fallback logic
(resolveCanonicalDomainId/getENSv1RootRegistryId) and then return exact ?
deepest.domainId : null as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ensapi/src/omnigraph-api/schema/domain.ts`:
- Around line 109-113: The Domain.canonical and Registry.canonical GraphQL field
definitions are missing explicit nullable declarations and default to nullable:
true; update the t.field calls for the Domain.canonical resolver (symbol:
canonical in apps/ensapi/src/omnigraph-api/schema/domain.ts) and the
Registry.canonical resolver (symbol: canonical in
apps/ensapi/src/omnigraph-api/schema/registry.ts) to include nullable: false so
the schema reflects the database not-null boolean semantics and matches file
conventions.

In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 173-190: handleBridgedResolverChange currently treats
domain.canonicalSubregistryId (prev) as bridge-owned and unconditionally calls
setRegistryCanonicalDomain(prev, null) when a bridged resolver disappears;
instead only detach a canonicalSubregistryId that was created by the bridge for
this originatingNode. Modify handleBridgedResolverChange to determine the set of
bridge-derived registry IDs for the originatingNode (use isBridgedResolver logic
or the same provenance mechanism that creates those registries) and only call
setRegistryCanonicalDomain(context, prev, null) when prev is non-null and
matches one of those bridge-derived IDs; do not clear canonicalSubregistryId for
registries that are normal (non-bridge) parent/child links. Use function
names/vars: handleBridgedResolverChange, canonicalSubregistryId,
setRegistryCanonicalDomain, isBridgedResolver, originatingNode, prev to locate
and implement this conditional check or tracking of bridge provenance.

In `@packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts`:
- Around line 616-619: The registry_domains schema uses a hot-path text[] column
(registryDomains.domainIds) causing full-row rewrites on
ensureDomainInRegistry/removeDomainFromRegistry and expensive reads in
canonicality-db-helpers.ts; replace this with a normalized onchainTable that
stores one row per (registryId, domainId) with a composite primary key, or if
immediate migration is not possible open a follow-up to implement that
normalized table and add instrumentation around cascade write latency and read
hotspots (measure operations in ensureDomainInRegistry/removeDomainFromRegistry
and the cascade walks in canonicality-db-helpers.ts) so we can detect
regressions early.

---

Outside diff comments:
In `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts`:
- Around line 130-148: The deepestResolver branch currently falls through
silently for ENSv2 resolvers; update the TODO: ENSv2Resolver branch to log the
resolver before returning so non-exact misses are debuggable — e.g., call
logger.debug({ deepestResolver, path, depth }) (or logger.warn) when
deepestResolver exists and isn't equal to ENSv1Resolver (found via
maybeGetDatasourceContract) before the final return; keep the existing
ENSv1Resolver fallback logic (resolveCanonicalDomainId/getENSv1RootRegistryId)
and then return exact ? deepest.domainId : null as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: efd22769-1097-48d0-b83a-3042c36ab0d9

📥 Commits

Reviewing files that changed from the base of the PR and between ca39fbe and 3b8eeca.

⛔ Files ignored due to path filters (2)
  • packages/enssdk/src/omnigraph/generated/introspection.ts is excluded by !**/generated/**
  • packages/enssdk/src/omnigraph/generated/schema.graphql is excluded by !**/generated/**
📒 Files selected for processing (20)
  • apps/ensapi/src/lib/resolution/forward-resolution.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.ts
  • apps/ensapi/src/omnigraph-api/schema/resolver.ts
  • apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
  • apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
  • examples/enskit-react-example/src/SearchView.tsx
  • packages/datasources/src/abis/ensv2/Registry.ts
  • packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts
  • packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts
  • packages/ensnode-sdk/src/shared/root-registry.ts
💤 Files with no reviewable changes (2)
  • apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts

Comment thread apps/ensapi/src/omnigraph-api/schema/domain.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Comment thread packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts Outdated
Add integration tests covering Domain.canonical and Registry.canonical:
canonical=true for v2-rooted entities, canonical=false for ENSv1 addr.reverse
and the v1 root registry (which is non-canonical in ens-test-env where the
v2 root is the namespace's canonical root). Add changeset for the omnigraph
schema additions.
vercel[bot]

This comment was marked as resolved.

- Reorder handler registration: NodeMigration -> ENSv2 -> ProtocolAcceleration.
  Lets handleBridgedResolverChange read the previous Domain-Resolver Relation
  before ProtocolAcceleration overwrites it, removing the brittle
  canonicalSubregistryId-as-provenance hack that detached normal canonical
  edges on non-bridged resolver updates.
- Extract node migration into its own handlers/node-migration.ts so it can be
  registered ahead of both plugins.
- handleBridgedResolverChange now takes the registry AccountId and reads prev
  via DRR PK lookup; isBridgedResolver determines bridge provenance.
- Drop unused removeDomainFromRegistry.
- ensureDomainInRegistry throws when registry row is missing (invariant).
- updateRegistryCanonicality: read child once, add MAX_CASCADE_DEPTH=16 guard.
- getCanonicalPath: throw on impossible zero-row result for defense in depth.
- Domain.canonical and Registry.canonical are nullable: false on the omnigraph
  schema (matches notNull DB columns). Regenerate schema.graphql + introspection.
- ParentUpdated: add TODO comment about whether it should also be a domain event.
@shrugs
Copy link
Copy Markdown
Member Author

shrugs commented May 6, 2026

@greptile review

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 25 out of 27 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/ensindexer/ponder/src/register-handlers.ts Outdated
Comment thread .changeset/canonical-fields-omnigraph.md Outdated
Comment thread apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts (1)

374-380: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ParentUpdated event still not linked to domain history.

The TODO at line 377 acknowledges this gap, and the event row is created at line 378 but never connected to either the child or the parent domain history via ensureDomainEvent. If you intend to associate ParentUpdated with the child domain (the registry that emitted it), the most natural place is thisRegistry's root domain (or the domain whose canonical parent just changed). Otherwise, please convert the TODO into an explicit decision in code so reviewers don't continue flagging it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts` around
lines 374 - 380, The ParentUpdated event is created via ensureEvent but not
linked to domain history; either attach it to the affected domain or make the
TODO an explicit decision. Fix by calling ensureDomainEvent for the domain whose
canonical parent changed (use the child/domain identifier from the ParentUpdated
payload, e.g. event.node or the registry root for thisRegistry) after
ensureEvent so the event is recorded in domain history; alternatively replace
the TODO with a clear comment/flag that documents the intentional choice not to
link ParentUpdated. Ensure you reference ParentUpdated, ensureEvent and
ensureDomainEvent (and thisRegistry or event.node as the domain identifier) when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/canonical-fields-omnigraph.md:
- Line 5: The changeset text incorrectly states that Domain.canonical and
Registry.canonical are nullable; update the release note in .changeset to
reflect that both fields are non-nullable (GraphQL type Boolean!), e.g., change
wording to state they are exposed as non-nullable Boolean! fields and clearly
indicate they always return a boolean value, referencing the symbols
Domain.canonical and Registry.canonical so readers know which schema fields were
changed.

In `@apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts`:
- Around line 38-50: The recursive CTE "upward" currently stops at MAX_DEPTH
(16) and can silently return a truncated canonical path; modify the logic around
the "upward" CTE and the final path/name assembly to detect when recursion hit
MAX_DEPTH (i.e., any row with upward.depth >= MAX_DEPTH) and surface an error
instead of returning partial results. Concretely, either add a SQL guard after
the recursion (e.g., an existence check on upward where depth >= MAX_DEPTH that
causes the query to fail) or perform an explicit check in the calling function
(the get-canonical-path flow) after the query and throw a clear error when
MAX_DEPTH was reached; adjust both the upward CTE and the later assembly used
around the same block (also apply to the similar block at the 59-64 region) so
deep ENS names don't get silently truncated.

In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 24-29: The MAX_CASCADE_DEPTH constant is set too low and will
falsely abort valid ENS trees because updateRegistryCanonicality() recurses once
per canonical subregistry hop; replace this brittle depth cap with explicit
cycle detection (track visited registry/node identifiers in
updateRegistryCanonicality and bail with a clear error if a repeated id is
encountered) or, if a cap must remain, raise MAX_CASCADE_DEPTH to the protocol's
actual maximum nesting and document it; update any other uses (e.g., the second
occurrence at the other location) to use the new cycle-detection logic or the
revised constant.
- Around line 41-48: The current read-modify-write (existing/domainIds) can lose
concurrent updates; replace it with a single atomic upsert that appends the new
domainId at the DB level instead of reading the full row first. Modify the
insert on ensIndexerSchema.registryDomains via context.ensDb to use
onConflictDoUpdate that sets domainIds =
dedup(array_cat(registry_domains.domainIds, ARRAY[<domainId>]::text[])) (or the
equivalent array_cat/array_append + DISTINCT expression your SQL driver
supports) so the DB merges the new domainId into the existing array atomically;
reference the same registryId conflict key and avoid the prior existing
check/merge in canonicality-db-helpers.ts (remove the existing find/if block and
the client-side domainIds merge). Ensure the expression uses EXCLUDED or the
table column names your query builder exposes to perform the concat+distinct in
the UPDATE clause.
- Around line 41-58: The code inserts/updates registryDomains before confirming
the parent registry exists, which can leave an orphaned child-list on error;
change the order in the function that handles registry/domain linking (the block
using registryId, domainId, context.ensDb and collections
ensIndexerSchema.registryDomains, ensIndexerSchema.registry,
ensIndexerSchema.domain) so you first fetch/validate the parent registry (await
context.ensDb.find(ensIndexerSchema.registry, { id: registryId })) and throw if
missing, and only after that perform the registryDomains insert/update and the
domain update (onConflictDoUpdate for registryDomains and .update(...).set({
canonical: reg.canonical })) to prevent creating the child list when the parent
is absent.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts`:
- Around line 389-407: The handler in ENSv2Registry uses `tokenId` typed as
`bigint` and force-casts it with `as never`; update the event arg typing to use
the branded TokenId (e.g. EventWithArgs<{ tokenId: TokenId; resolver:
NormalizedAddress }>) and remove the `as never` casts when calling makeStorageId
and interpretTokenIdAsNode so you pass the TokenId directly; ensure the TokenId
type is imported/available and keep the rest of the logic calling
handleBridgedResolverChange(context, registry, domainId, originatingNode,
resolver) unchanged.

---

Duplicate comments:
In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts`:
- Around line 374-380: The ParentUpdated event is created via ensureEvent but
not linked to domain history; either attach it to the affected domain or make
the TODO an explicit decision. Fix by calling ensureDomainEvent for the domain
whose canonical parent changed (use the child/domain identifier from the
ParentUpdated payload, e.g. event.node or the registry root for thisRegistry)
after ensureEvent so the event is recorded in domain history; alternatively
replace the TODO with a clear comment/flag that documents the intentional choice
not to link ParentUpdated. Ensure you reference ParentUpdated, ensureEvent and
ensureDomainEvent (and thisRegistry or event.node as the domain identifier) when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 809c939b-d98f-4942-a4ae-f4edaba89cc8

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8eeca and 1426de2.

⛔ Files ignored due to path filters (2)
  • packages/enssdk/src/omnigraph/generated/introspection.ts is excluded by !**/generated/**
  • packages/enssdk/src/omnigraph/generated/schema.graphql is excluded by !**/generated/**
📒 Files selected for processing (12)
  • .changeset/canonical-fields-omnigraph.md
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.ts
  • apps/ensindexer/ponder/src/register-handlers.ts
  • apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
  • apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts

Comment thread .changeset/canonical-fields-omnigraph.md Outdated
Comment thread apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
The wallet Registry's ParentUpdated claims sub1.sub2.parent.eth as its
canonical parent; linked.parent.eth.subregistry was re-pointed to the same
Registry without a corresponding ParentUpdated, so wallet.linked.parent.eth
is now a non-canonical alias that does not resolve via the canonical-name
walk. Update Domain.path tests and DEVNET_NAMES to reflect this.

Also: v1 'eth' Domain has a null canonical name in ens-test-env (v2 root is
the namespace's canonical root), so update Query.domains > sees .eth domain
to assert name: null on the v1 entity. Account.domains expected list flips
wallet.linked.parent.eth → wallet.sub1.sub2.parent.eth.
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 31 out of 33 changed files in this pull request and generated 5 comments.

Comment thread apps/ensapi/src/omnigraph-api/schema/resolver.ts
Comment thread .changeset/canonical-fields-omnigraph.md Outdated
Comment thread packages/ensnode-sdk/src/shared/root-registry.ts
Comment thread packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts
- ENSv2 ParentUpdated handler: note `parent` is interpreted same-chain;
  cross-chain parentage goes through Bridged Resolvers, not ParentUpdated
- handleBridgedResolverChange: document the two implied invariants —
  1:1 originating-Domain-per-bridged-Registry, and bridged target indexed
  before the Bridged Resolver event fires (with the fix sketch if a future
  Bridged Resolver violates it)
- Add changeset for the breaking Resolver.bridged GraphQL type change
  (AccountId scalar → Registry interface)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- getRootRegistryIds removed (replaced by isRootRegistryId predicate)
- BridgedResolverTarget shape: shadow → registryId (in /internal export)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 33 out of 35 changed files in this pull request and generated 4 comments.

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread .changeset/resolver-bridged-registry-type.md Outdated
Comment thread packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts
every nameable Domain is canonical by definition, so the canonical filter
on Query.domains was redundant. Query.domains now always scopes to
Canonical Domains. consumers reading Domain.canonical directly are
unaffected; consumers relying on canonical:false to surface non-canonical
Domains via this query should switch to Account.domains / Registry.domains
or read Domain.canonical directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 36 out of 38 changed files in this pull request and generated 6 comments.

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Outdated
Comment thread docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
shrugs and others added 2 commits May 8, 2026 11:55
- handleRegistryCanonicalDomainUpdated: docstring now matches the
  single-reconcile body; invariant tag matches the function name
- Resolver.bridged changeset: lead with the migration (scalar → Registry
  interface) instead of the post-change shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- canonicality-db-helpers: typo "canoniality"; grammar "may not yet" → "need not exist yet"
- get-canonical-path: typo "Canonial"
- ensdb/database-schemas docs: align with current canonicality model (boolean
  flags + on-demand agreement check, no parallel materialization tables)
- isBridgedResolver: memoize per-namespace BridgedResolverConfig[] — sits on
  every NewResolver/ResolverUpdated event during indexing and every
  forward-resolution hop, configs are pure function of namespace

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 36 out of 38 changed files in this pull request and generated 2 comments.

Comment thread .changeset/ensnode-sdk-root-registry-bridged-target.md Outdated
shrugs and others added 2 commits May 8, 2026 12:11
inline the cache check at entry + cache write at exit instead of splitting
build/cached-accessor across two functions. one name for callers, one place
to edit when adding a new bridge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request May 8, 2026
Comment on lines 102 to 108
const parentDomainId = makeENSv1DomainId(registry, parentNode);
// route through handleSubregistryUpdated so any prior subregistry edge (e.g. a bridged
// attachment) is properly reconciled instead of orphaned by a blind overwrite.
await handleSubregistryUpdated(context, parentDomainId, parentRegistryId);

// set the parent Domain's subregistry to said registry
await context.ensDb
.update(ensIndexerSchema.domain, { id: parentDomainId })
.set({ subregistryId: parentRegistryId });

// ensure Canonical Domain reference
await context.ensDb
.insert(ensIndexerSchema.registryCanonicalDomain)
.values({ registryId: parentRegistryId, domainId: parentDomainId })
.onConflictDoNothing();
await handleRegistryCanonicalDomainUpdated(context, parentRegistryId, parentDomainId);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 ENSv1 NewOwner unconditionally destroys bridged subregistryId

Every non-TLD NewOwner calls handleSubregistryUpdated(context, parentDomainId, parentRegistryId) where parentRegistryId is the L1 virtual registry. handleSubregistryUpdated writes the new value unconditionally (after a prevId === nextId early-exit that won't fire because the bridged L2 registry and the L1 VR are different IDs).

Concrete failure: bridge.linea.eth (or any other ENSv1 subname under linea.eth) gets a NewOwner event on mainnet. handleSubregistryUpdated sets linea_eth_domain.subregistryId = L1_VR_for_linea_eth, overwriting the previously bridged pointer to the Linea L2 registry. reconcileRegistryCanonicality is then called for both the Linea L2 registry (prev) and the L1 VR (next). Because linea_eth_domain.subregistryId now agrees with the L1 VR, the L1 VR becomes canonical and the entire Linea namegraph loses canonicality silently. The comment // route through handleSubregistryUpdated so any prior subregistry edge (e.g. a bridged attachment) is properly reconciled describes canonicality reconciliation, not preservation of the bridged pointer — the bridged pointer is lost.

Comment on lines +220 to +228
// if the next resolver is a Bridged Resolver, we need to update the Domain's Subregistry
if (nextBridged) {
// update the domain's indicated subregistry
await handleSubregistryUpdated(context, domainId, nextBridged.targetRegistryId);

// only update the Registry's Canonical Domain iff this is the correct originating Domain
if (nextBridged.originDomainId === domainId) {
await handleRegistryCanonicalDomainUpdated(context, nextBridged.targetRegistryId, domainId);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 handleBridgedResolverChange crashes when bridged target registry doesn't yet exist

handleRegistryCanonicalDomainUpdated (line 227) throws Invariant(...): Registry does not yet exist if the target registry row is absent. The target VR (makeENSv1VirtualRegistryId(basenamesRegistry, base_eth_node)) is created only when the first ENSv1 NewOwner on Base fires — i.e., when the first Basenames subname is indexed. The mainnet NewResolver(base.eth, BasenamesL1Resolver) event predates Basenames' launch: in real-world timestamp order it is processed by Ponder before any Base NewOwner events exist, meaning the VR row is absent and the indexer crashes here. The code comment in this function acknowledges the invariant but treats it as a property of the specific resolvers rather than verifying it holds.

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 36 out of 38 changed files in this pull request and generated 1 comment.

}

const rows = await walkCanonicalNamegraph(registryId, path);
// walk the disjoint namegraph by indicated by `registryId` through `path`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants