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 emitsnode
andlabel
,node
should be namedparent
and the computed subnode should be namednode
ordomain
- empty domains aren’t actually deleted from the index, but if a domain is empty the parent’s subdomain count is reduced appropriately. options:
- if historical info not important (still available by blockheight queries), domains should be deleted, and
subdomainCount
computed with a simple count query - if domain existance is necesssary, make
subdomainCount
computed with a where clause to exclude ‘empty’ domains - if filters against subdomainCount are necessary, maybe the current logic works just fine
- if historical info not important (still available by blockheight queries), domains should be deleted, and
Resolver
- the local
Resolver
resource should be keyed byCAIP-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
- the handlers should persist all keys and values emitted by the resolver in
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
- i.e. null byte https://ens.mirror.xyz/9GN77d-MqGvRypm72FcwgxlUnPSuKWhG3rWxddHhRwM
- or other invalid characters (see
isLabelIndexable
)
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