-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: feat/ens-v2-support
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
// the last element is the node and it exists in the tree | ||
return { | ||
path: rows, | ||
domain: await db.query.v2_domain.findFirst({ |
There was a problem hiding this comment.
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
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 schemaapps/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 handlersapps/ensnode-api/src/v1.ts
— demonstration of custom API routes, deployed herePrototype 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
andtest.eth
domains.Registry
A Registry represents an ENS Registry contract that manages a set of Domains.
id
: CAIP-10 formatted contract addressdomains
: Registry manages many domainsDomain
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 URIowner
: Address of the label ownersubregistry
/subregistryFlags
: subregistry & flagsresolver
/resolverFlags
: resolver & flagsRelationships:
registry
: Parent registry that manages this domainResolver
Represents a resolver contract that stores records for
node
s.id
: CAIP-10 formatted contract addressResolverRecords
Stores resolution records for a specific
node
within a resolver contract.addresses
: Cryptocurrency addresses associated with this nameResolverRecordsAddress
Stores address records for a given
node
in aresolver
.Properties:
coinType
: SLIP-44 coin typeaddress
: the address valueNotes
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 (viaSubregistryUpdate
), an indexer must index every ERC1155 andNewSubname
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 anIRegistry
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)
orRegisterRegistry(...)
) 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 exclusivelyupdate
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'sname
andnode
(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
orisCanonical
type boolean on eachDomain
response.This deserves further consideration.