Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ENSv2 Plugin #356

Draft
wants to merge 15 commits into
base: feat/ens-v2-support
Choose a base branch
from
Draft

Conversation

shrugs
Copy link
Contributor

@shrugs shrugs commented Mar 7, 2025

This PR proposes an ENSv2-native indexing schema and handler logic. I recommend reading the raw files rather than the diff, and while its current implementation is logically separate from the ENSNode v1 implementation in this repo, I plan to prototype a merge of the two to demonstrate data backwards-compatibility.

The key architectural decision is to represent the ENS namespace as a tree of Registries and Domains. A Registry represents an on-chain contract, and a Domain represents the on-chain token managed by the ERC1155 Registry contracts. This accurately represents the state of the ENS namespace and enables dynamic tree reorganizations, either from a Registry creating and deleting subnames or a Domain updating its Subregistry, effectively replacing the entire subtree in a single transaction.

In short: Registries manage many Domains. Each Domain may have one (sub)Registry, which may manage many Domains, ...etc.

Autogenerated GraphQL is not exactly the most ergonomic api for this schema, so i'm proposing some custom api routes to generate friendlier and ENSjs-specific endpoints.

Relevant Files

  • packages/ponder-schema/src/ponder.schema.ts, see the ENSv2 section at the end for full schema
  • apps/ensindexer/src/plugins/ens-v2/handlers/shared/Registry.ts — Registry handlers (both Root and ETH)
  • apps/ensindexer/src/plugins/ens-v2/handlers/RegistryDatastore.ts — Datastore handlers
  • apps/ensnode-api/src/v1.ts — demonstration of custom API routes, deployed here

Prototype Deployment

You can play with a prototype deployment of this PR.

Custom API

The custom API service is deployed to this url https://ensv2-ensnode-prototype-api-alpha.up.railway.app

Autogenerated API

The indexer serves its autogenerated api here: https://ensv2-ensnode-prototype-alpha.up.railway.app/ponder showing it indexing the root and .eth registries as well as the eth and test.eth domains.

query Registries {
  v2_registrys {
    items {
      id
      isSubregistryOfDomain: domain {
        name
      }
      managesDomains: domains {
        totalCount
        items {
          name
          node
        }
      }
    }
  }
}

Registry

A Registry represents an ENS Registry contract that manages a set of Domains.

  • id: CAIP-10 formatted contract address
  • domains: Registry manages many domains

Domain

Represents a single name in the ENS namespace (e.g. "vitalik.eth").

Properties:

  • tokenId: The ERC1155 token ID (masked labelHash)
  • label: Human-readable label text (if known) (e.g. "vitalik")
  • name: Full materialized name (e.g. "vitalik.eth")
  • node: Namehash of the materialized name name (e.g. namehash("vitalik.eth"))
  • uri: Optional token URI
  • owner: Address of the label owner
  • subregistry / subregistryFlags: subregistry & flags
  • resolver / resolverFlags: resolver & flags

Relationships:

  • registry: Parent registry that manages this domain

Resolver

Represents a resolver contract that stores records for nodes.

  • id: CAIP-10 formatted contract address

ResolverRecords

Stores resolution records for a specific node within a resolver contract.

  • addresses: Cryptocurrency addresses associated with this name
  • ... etc

ResolverRecordsAddress

Stores address records for a given node in a resolver.

Properties:

  • coinType: SLIP-44 coin type
  • address: the address value

Notes

Tree Invariant Enforcement

I've implemented first-write-wins for subregistry-domain relationships, to avoid cyclical references in the tree. I think that this invariant should be enforced at contract-level if possible, but have implemented it at indexer-level for now in order to demonstrate. To summarize: registries already included in the namespace tree should not be able to be re-used, since it introduces the possibility of creating a graph, not a tree structure, making upward traversal to the root registry impossible.

for example, the ENSv2 deployment on sepolia has created a cycle in the name graph by setting the subregistry of test.eth to the same address as the EthRegistry (0xFd8562F0B884b5f8d137ff50D25fc26b34868172).

https://sepolia.etherscan.io/tx/0x1508d0b1a2843efbcb4b19d0ba8741c521e6e867d444f2624d77a063b102646a#eventlog

If this invariant isn't enforced in the Datastore, then the spec for how indexers should manage this behavior should be well-defined.

Registry Address Announcement

Currently in ENSv2 a Registry contract is any contract that implements ERC1155 and NewSubname(). Because registries may (and are expected to) emit events prior to announcing themselves on the Datastore (via SubregistryUpdate), an indexer must index every ERC1155 and NewSubname event on the ENSv2 chain in order to ensure that when a registry is linked to a label/token, its state is already known. This may be quite taxing on processing/storage requirements for ENSv2 indexers. Indexers could filter all of these events by contracts that advertise an IRegistry supported interface, but this still requires RPC calls & processing for every single ERC1155/NewSubname event on Namechain.

One solution is to include in the protocol that Registry contracts must announce themselves at constructor-time via a singleton contract (perhaps the Datastore). Then indexers like ENSNode can use the factory pattern to watch that event (say, AnnounceRegistry(indexed address registry) or RegisterRegistry(...)) in order to exclusively index the subset of contracts that advertise themselves for use within ENSv2.

Event Order Guarantees

While not necessary (and this proposed indexing logic is designed without them), event-order guarantees mandated by the protocol could make indexer implementations much simpler to reason about. Currently events may arrive in any order, greatly increasing the number of states an indexer must be expected to handle. If there were a guarantee that an event (like NewSubname) was emitted first and once per subname, indexing logic could be simpler and more precise.

For example, right now each handler must assume that it may be the first event received regarding a given entity (Registry, Label, Resolver, etc) and therefore must use upsert semantics to handle each case. If there were an event guaranteed to come first, the indexer could use create and then exclusively update the relevant resource in future events, resulting in a simpler-to-reason-about control flow.

That said, the current pattern that assumes events can arrive in any order is the most resilient, if more complicated to reason about.

Non .eth Registry Migrations

How do other registries migrate to ENSv2, if at all? Is the .eth registry migration still a full transition to ENSv2 as proposed previously?

TokenId Masking

How should indexers handle the case that a Registry mints two subnames with different labelHashes (and therefore tokenIds) that, after masking the lower 32 bits, collide? This results in two tokens in the Registry contract but a single shared entry in the Datastore. If the liklihood of a collision is low enough (presumably it is, even with 32 bits missing), this behavior can be considered undefined, but indexers should still have a plan to at least not crash at runtime if such an event were ever to occur.

In this proposal I suggest always masking the incoming tokenIds and keying domains by masked id, such that minting a colliding tokenId on a Registry contract just results in an update to the existing Domain entity, rather than the minting of another. This way subregistry/resolver updates can be trivially associated with the correct domain, rather than resulting in a many domains own one subregistry relationship.

Disconnected Subtrees

Right now we have a guarantee that the only Registries tracked by the indexer are part of the canonical ENS namespace, since we're hardcoding the Root and ETH Registries. In the future, however, indexers will need to index events from any IRegistry contract, which may or may not actually be in the tree at any given time. This means name resolution must occur at request-time, as whether a given Domain can be traced to the Root regsitry (and is therefore 'in the tree') can change block-to-block. This is true for lookups as well, which require resolving each label's subregistry — it is not trivial for the indexer alone to re-materialize things like a Domain's name and node (which depend on its location in the tree) at event-time, as the node may not be in the tree at all or be moved to a different subtree entirely in a single transaction.

Domains that don't connect back to the RootRegistry could be considered to not exist, which seems simplest, but it may be desirable to use ENSNode to query the state of Regsitries & Domains & their Resolvers etc before a given Registry is added to the canonical tree.

We could indicate 'validity' of a name within the canonical tree via an isValid or isCanonical type boolean on each Domain response.

This deserves further consideration.

@shrugs shrugs self-assigned this Mar 7, 2025
Copy link

vercel bot commented Mar 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
admin.ensnode.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 13, 2025 6:20pm
ensadmin-next ❌ Failed (Inspect) Mar 13, 2025 6:20pm
ensnode.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 13, 2025 6:20pm
ensrainbow.io ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 13, 2025 6:20pm

// the last element is the node and it exists in the tree
return {
path: rows,
domain: await db.query.v2_domain.findFirst({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

perhaps race condition where this domain is no longer valid in the tree if the indexed state changed between these two queries — would be good to place them in a transaction or something idk, to ensure atomicity

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.

None yet

1 participant