Skip to content

ENSNode V2 Notes

Goals of ENSNode V2

The original architecture of the ENS Subgraph assumes a single-chain, on-chain namespace. The protocol and ecosystem has since evolved: the ENS namespace now exists across multiple chains and across many possible off-chain datasources. The primary goal of ENSNode v2 is to become a unified multichain & off-chain ENS indexer, capable of representing the full state of the protocol at a given time.

Many of the ENS datasources are dynamic in nature and capable of changing over time. For example, the normalization algorithm may change to produce newly normalized labels, additional labels may be healed over time, and the state of off-chain databases may change incredibly frequently. A new protocol-centric architecture & data structure

Additionally, the original schema proposed in the ENS Subgraph isn’t 1:1 with the protocol itself, increasing confusion, and will likely need to be updated in the future for and changes made in ENS Protocol V2. ENSNode v2 of course aims to perfectly support ENS v2 and Namechain as they release.

Proposed Architecture

┌──────────────────────────────────────────┐
│ API Server │
└──────┬──────────────┬────────────────┬───┘
│ │ │
┌──────────▼─┐ ┌────────▼───┐ ┌────────▼────────┐
│ Ponder │ │ ENSRainbow │ │ CCIP Read │
└──────────┬─┘ └────────┬───┘ └──────────┬──────┘
│ │ │
┌──────▼──────────────▼──────────────────▼─┐
│ Postgres (Isolated Schemas) │
└──────────────────────────────────────────┘

At the top level, an API server that stitches the various datasources together at request-time, likely with lots of semantic caching involved. Likely HTTP-JSON based, perhaps GraphQL-based (but GraphQL comes with so many downsides, should be seriously evaluated). Large opportunity for ENS protocol semantic endpoints (i.e. every well-known query from ensjs and ens-app-v3 could/should be a dedicated endpoint given how specific and important that functionality seems to be).

Each datasource persists its data to postgres, either a separate postgres instance or a separate schema in a shared postgres. No native joins are used to power queries, as that likely gets a little too far into the weeds of syncing ponder state with other tables in the same db between re-indexes, etc.

Ponder Indexer (currently ENSIndexer)

ponder multichain-indexes all on-chain state and shoves it into the postgres database. our ensnode api server talks to ponder’s postgres tables via shared drizzle schema. api server maintains some internal state to persist which ponder schema is ‘active’ and then talks to that specific schema.

Indexer Schema & Logic Improvements

the ‘empty’ domains should be handled more accurately, depending on how important serving empty domains is for people.

  • Domain#subdomainCount could/should be a computed property by count(children of parent)
    • removes need to recursively update parent records during domain delete
    • removes need to increment during domain creation
    • new impl likely needs to exclude ‘empty’ domains (see registry notes for context)

domain createdAt should not update on re-registration, should be original createdAt

various resources use both null and zeroAddress to indicate emptiness, this is horrible and creates numerous checks like this where they check for !== NULL && !== zeroAddress

wrappedOwnerId should not be materialized onto domain, should just be resolved through wrappedDomain.owner

Registry

  • in Registry:NewOwner, the event emits node and label, node should be named parent and the computed subnode should be named node or domain
  • empty domains aren’t actually deleted from the index, but if a domain is empty the parent’s subdomain count is reduced appropriately. options:
    1. if historical info not important (still available by blockheight queries), domains should be deleted, and subdomainCount computed with a simple count query
    2. if domain existance is necesssary, make subdomainCount computed with a where clause to exclude ‘empty’ domains
    3. if filters against subdomainCount are necessary, maybe the current logic works just fine

Resolver

  • the local Resolver resource should be keyed by CAIP-10 ID, not pairwise ala subgraph, to match on-chain datamodel
    • the handlers should persist all keys and values emitted by the resolver in Records
    • the Record model stores (node, key, value) and is keyed by (resolverId, node, key)
    • on active resolver change, simply point the domain’s resolverId to the resolver’s address
    • any domain’s records are computed through the current resolverId and querying

any resolver that implements the CCIP Read standard will have to have its records implemented at the API layer which can stitch the indexed data with realtime offchain data via CCIP Reads. if we don’t want to implement the CCIP Read proxy as part of this unified api, the api should know if a Resolver defers to CCIP and communicate that effectively in the response so that clients can do it themselves.

in the subgraph implementation, resolver handlers must upsert resolvers because people can set records etc for a node that has not (yet) specified this resolver as active, meaning the create in Registry:NewResolver has yet to fire.

resolvers should be keyed by (chainId, address) and manage a mapping of records for a node, to be more protocol-centric. coinTypes and texts keys & values should be fully indexed (if possible — intentionally ignored in the subgraph because of some historical reason…)

Yes, when it comes to all forms of key -> value pairs that comprise resolver records, the ENS Subgraph only indexes the keys and not the values. The motivation for this comes from a concern that some apps might improperly decide to use the ENS Subgraph as a source of truth for resolver record values, rather than ENS protocol standards for how resolver record values should be dynamically looked up. A naive implementation that considers the ENS Subgraph as a source of truth for these can cause a lot of problems.

Registrar

the subgraph implements all of the BaseRegistrar, EthRegistrarController, and EthRegistrarControllerOld logic together

ENSRainbow

ENSRainbow would be extended to handle all possible label healing data sources and provide a labelhash lookup api to the ensnode api service. In order to track on-chain datasources of healed labels (namely NameWrapper wraps [hah]), this service will likely run its own separate ponder indexer to track those events. It should also serve the existing legacy rainbow tables (ens_names.sql) along with tracking any other additional sources of healed labels.

It should store all known labels, normalized, healed, or otherwise, and then over time (cron? on-demand?) attempt to normalize any yet-normalized labels and add them to the set of healed labels. This ensures that changes in unicode and the normalization algorithm are represented in real-time.

It should also filter out any labels that are not indexable

In theory (and likely in practice) any normalized name will never become invalid, so once added to the set of healed labels it can live there forever.

ENSIP Ideas

  • unable to automatically identify subname registries via onchain event, CCIP standard dosn’t include any info about data source, so we’ll need to encode manually for now
  • ENSIP - shared interface for subdomain registrars
  • ENSIP — standard for how a resolver on L1 can (optionally) emit an event specifying contract on an L2 that it proxies records from
    • optional, in the popular case of L2-managed subnames
    • removes centralized dependency on the CCIP Gateway
    • flaky test experience with .cb.id name gateway
    • also helps indexer discovery