|
1 | 1 | --- |
2 | | -description: Allows metadata to be queried on EIP-3668 enabled names |
| 2 | +description: Provides a discovery mechanism for metadata for names resolved via EIP-3668 |
3 | 3 | contributors: |
4 | 4 | - jefflau |
5 | | - - makoto |
| 5 | + - matoken.eth |
6 | 6 | ensip: |
7 | 7 | status: draft |
8 | 8 | created: 2022-09-22 |
| 9 | + updated: 2025-11-03 |
9 | 10 | ignoredRules: ["heading:implementation", "heading:open-items"] |
10 | 11 | --- |
11 | 12 |
|
12 | | -# ENSIP-16: Offchain Metadata |
| 13 | +# ENSIP-16: Metadata Event Discovery |
13 | 14 |
|
14 | 15 | ## Abstract |
15 | 16 |
|
16 | | -This ENSIP specifies APIs for querying metadata directly on the resolver for EIP-3668 (CCIP Read: Secure offchain data retrieval) enabled names. EIP-3668 will power many of the domains in the future, however since the retrieval mechanism uses wildcard + offchain resolver, there is no standardised way to retrieve important metadata information such as the owner (who can change the records), or which L2/offchain database the records are stored on. |
| 17 | +This ENSIP specifies APIs for querying metadata directly on the resolver for EIP-3668 (CCIP Read: Secure offchain data retrieval) enabled names. EIP-3668 will power many domains in the future, however since the retrieval mechanism uses wildcard + offchain resolver, there is no standardised way to retrieve important metadata information such as which L2/offchain database the records are stored on and where JSON RPC endpoint is to find event log information. |
17 | 18 |
|
18 | 19 | ## Motivation |
19 | 20 |
|
20 | | -With EIP-3668 subdomains already starting to see wide adoption, it is important that there is a way for frontend interfaces to get important metadata to allow a smooth user experience. For instance a UI needs to be able to check if the currently connected user has the right to update an EIP-3668 name. |
| 21 | +With EIP-3668 subdomains already starting to see wide adoption, it is important that there is a standardised way to discover and access metadata about offchain names. |
21 | 22 |
|
22 | | -This ENSIP addresses this by adding a way of important metadata to be gathered on the offchain resolver, which would likely revert and be also resolved offchain, however there is an option for it to be also left onchain if there value was static and wouldn't need to be changed often. |
| 23 | +This ENSIP allows third-party indexing services to listen to the `MetadataChanged` event to automatically discover and index metadata from different chains and smart contracts, without relying on centralised RPC endpoint repositories. |
23 | 24 |
|
24 | 25 | ## Specification |
25 | 26 |
|
26 | | -The metadata should include 2 different types of info |
27 | | - |
28 | | -- Offchain data storage location related info: `graphqlUrl` includes the URL to fetch the metadata. |
29 | | - |
30 | | -- Ownership related info: `owner`, `isApprovedForAll` defines who can own or update the given record. |
31 | | - |
32 | | -### Context |
33 | | - |
34 | | -An optional field "context" is introduced by utilizing an arbitrary bytes string to define the namespace to which a record belongs. |
35 | | - |
36 | | -For example, this "context" can refer to the address of the entity that has set a particular record. By associating records with specific addresses, users can confidently manage their records in a trustless manner on Layer 2 without direct access to the ENS Registry contract on the Ethereum mainnet. Please refer to [ENS-Bedrock-Resolver](https://github.com/corpus-io/ENS-Bedrock-Resolver#context) for the reference integration |
37 | | - |
38 | | -### Dynamic Metadata |
39 | | - |
40 | | -Metadata serves a crucial role in providing valuable insights about a node owner and their specific resolver. In certain scenarios, resolvers may choose to adopt diverse approaches to resolve data based on the node. An example of this would be handling subdomains of a particular node differently. For instance, we could resolve "optimism.foo.eth" using a contract on optimism and "gnosis.foo.eth" using a contract on gnosis. |
41 | | -By passing the name through metadata, we empower the resolution process, enabling CcipResolve flows to become remarkably flexible and scalable. This level of adaptability ensures that our system can accommodate a wide array of use cases, making it more user-friendly and accommodating for a diverse range of scenarios. |
42 | | - |
43 | | -## Implementation |
44 | | - |
45 | | -### L1 |
| 27 | +Compliant resolvers MUST implement the following interface: |
46 | 28 |
|
47 | 29 | ```solidity |
48 | 30 | // To be included in |
49 | 31 | // https://github.com/ensdomains/ens-contracts/blob/staging/contracts/resolvers/Resolver.sol |
50 | | -interface IOffChainResolver { |
51 | | - /** @dev Returns the owner of the resolver on L2 |
52 | | - * @param node |
53 | | - * @return owner in bytes32 instead of address to cater for non EVM based owner information |
54 | | - */ |
55 | | - owner(bytes32 node) returns (bytes owner); |
56 | | -
|
57 | | - // optional. |
58 | | - // this returns data via l2 with EIP-3668 so that non EVM chains can also return information of which address can update the record |
59 | | - // The same function name exists on L2 where delegate returns address instead of bytes |
60 | | - function isApprovedFor(bytes context, bytes32 node, bytes delegate) returns (bool); |
| 32 | +interface IOffchainResolverMetadataProvider { |
61 | 33 |
|
62 | | - /** @dev Returns the metadata of the resolver on L2 |
63 | | - * @return graphqlUrl url of graphql endpoint that provides additional information about the offchain name and its subdomains |
| 34 | + /** |
| 35 | + * @dev Returns metadata for discovering the location of offchain name data |
| 36 | + * @param name DNS-encoded name to query |
| 37 | + * @return rpcURLs The JSON RPC endpoint for querying offchain data (optional, may be empty array) |
| 38 | + * @return chainId The chain ID where the data is stored (format for non-EVM systems to be determined) |
| 39 | + * @return baseRegistry The base registry address on the target chain that emits events (optional, may be zero address) |
64 | 40 | */ |
65 | 41 | function metadata(bytes calldata name) |
66 | 42 | external |
67 | 43 | view |
68 | | - returns (string memory) |
69 | | - { |
70 | | - return (graphqlUrl); |
71 | | - } |
| 44 | + returns ( |
| 45 | + string[] memory rpcURLs, |
| 46 | + uint256 chainId, |
| 47 | + address baseRegistry |
| 48 | + ); |
72 | 49 |
|
73 | | - // Optional. If context is dynamic, the event won't be emitted. |
74 | 50 | event MetadataChanged( |
75 | | - string name, |
76 | | - string graphqlUrl, |
| 51 | + bytes name, // DNS-encoded name |
| 52 | + string[] rpcURLs, // JSON RPC endpoint (optional, may be empty array) |
| 53 | + uint256 chainId, // Chain identifier (format for non-EVM systems to be determined) |
| 54 | + address baseRegistry // Base registry address (optional, may be zero address) |
77 | 55 | ); |
78 | 56 | } |
79 | 57 | ``` |
80 | 58 |
|
81 | | -### L2 (EVM compatible chain only) |
82 | | - |
83 | | -```solidity |
84 | | -// To be included in the contract returned by `metadata` function `storageLocation` |
85 | | -interface IL2Resolver { |
86 | | - /** |
87 | | - * @dev Check to see if the delegate has been approved by the context for the node. |
88 | | - * |
89 | | - * @param context = an arbitrary bytes string to define the namespace to which a record belongs such as the name owner. |
90 | | - * @param node |
91 | | - * @param delegate = an address that is allowed to update record under context |
92 | | - */ |
93 | | - function isApprovedFor(bytes context,bytes32 node,address delegate) returns (bool); |
| 59 | +**Requirements:** |
| 60 | +- `name` must be provided to call `metadata` function. When indexing `MetadataChanged` event, indexers MUST apply the `name` to all names that have the suffixes of the `name`. |
| 61 | +- `chainId` must be provided. For EVM-compatible chains, `chainId` SHOULD match the chain's EIP-155 identifier. A chainId of 0 means a non-EVM chain or off-chain database. |
| 62 | +- `rpcURLs` is optional and may be an empty array. |
| 63 | +- `baseRegistry` is optional and may be the zero address. If a non-zero address is specified, events will be emitted from this address. The interface of this contract is yet to be determined. |
94 | 64 |
|
95 | | - event Approved( |
96 | | - bytes context, |
97 | | - bytes32 indexed node, |
98 | | - address indexed delegate, |
99 | | - bool indexed approved |
100 | | - ); |
101 | | -} |
102 | | -``` |
| 65 | +### metadata function |
103 | 66 |
|
104 | | -```javascript |
105 | | -const node = namehash('ccipreadsub.example.eth') |
106 | | -const resolver = await ens.resolver(node) |
107 | | -const owner = await resolver.owner(node) |
108 | | -// 0x123... |
109 | | -const dataLocation = await.resolver.graphqlUrl() |
110 | | -// { |
111 | | -// url: 'http://example.com/ens/graphql', |
112 | | -// } |
113 | | -``` |
| 67 | +The metadata function allows resolvers to dynamically provide information about where offchain data for a given name can be queried. This function returns the same information as would be emitted in a MetadataChanged event: the JSON RPC endpoint URLs, the chain ID where the data resides, and the base registry address on the target chain. |
114 | 68 |
|
115 | | -#### GraphQL schema |
| 69 | +### MetadataChanged Event |
116 | 70 |
|
117 | | -[GraphQL](https://graphql.org) is a query language for APIs and a runtime for fulfilling those queries with onchain event data. You can use the hosted/decentralised indexing service such as [The Graph](https://thegraph.com), [Goldsky](https://docs.goldsky.com/introduction), [QuickNode](https://marketplace.quicknode.com/add-on/subgraph-hosting) or host your own using The Graph, or [ponder](https://ponder.sh) |
| 71 | +The MetadataChanged event emits the same information the `metadata` function returns so that indexers can subscribe to the event as the details change rather than periodically querying the function. |
118 | 72 |
|
119 | | -#### L1 |
| 73 | +**Key Terminology:** |
| 74 | +- **registry** = A contract that manages name ownership and hierarchical relationships for a set of subnames. The base registry manages top-level domains (also known as root registry within the v2 contract), while subregistries manage names under a specific parent |
| 75 | +- **subregistry** = A registry contract that manages subnames under a parent name. Linked from a parent registry via SubregistryUpdate events |
| 76 | +- **registry id/tokenId** = The unique identifier for a name NFT, derived from the labelhash and version id that increments every time the permission of the name changes effectively invalidating any permissions or approvals tied to the old token ID. |
| 77 | +- **EAC(Enhanced Access Control)** = a general-purpose access control base class. `resource` is a unique key to manage the resource which changes every time `tokenId` changes. For more detail, read [the namechain README](https://github.com/ensdomains/namechain/tree/main/contracts#access-control). EAC may not be the only way to manage access control and other events may be added in future. |
| 78 | +- **node** = In v1, node is a unique identifier of the name derived by a `namehash` logic. In ENS v2, `node` is a placeholder node for compatibility with standard resolver behavior and always set to 0. The full ENS name attached to the resolver needs to be reconstructed by traversing registry hierarchy set by `SubregistryUpdate`. |
120 | 79 |
|
121 | | -`Metadata` is an optional schema that indexes `MetadataChanged` event. |
| 80 | +### rpcUrls Events |
122 | 81 |
|
123 | | -```graphql |
| 82 | +`rpcUrls` url endpoint emits the following jsonrpc events when chainId is not `0`. When chainId is `0` it may emit custom events yet to be determined. |
124 | 83 |
|
125 | | -type Domain @entity{ |
126 | | - id |
127 | | - metadata: Metadata |
128 | | -} |
| 84 | +#### Registry Events |
129 | 85 |
|
130 | | -type Metadata @entity { |
131 | | - "l1 resolver address" |
132 | | - id: ID! |
133 | | - "Name of the Chain" |
134 | | - name: String |
135 | | - "url of the graphql endpoint" |
136 | | - graphqlUrl: String |
137 | | -} |
| 86 | +```solidity |
| 87 | +// Emitted when a new subname is registered. |
| 88 | +// A subname without expiration should set type(uint256).max |
| 89 | +// Context can attach any arbitrary data, such as resource id to keep track of EAC |
| 90 | +event NameRegistered( |
| 91 | + uint256 indexed tokenId, |
| 92 | + string label, |
| 93 | + uint64 expiration, |
| 94 | + address registeredBy, |
| 95 | + uint256 context |
| 96 | +); |
| 97 | +
|
| 98 | +// Emitted when a new token id is generated |
| 99 | +event TokenRegenerated( |
| 100 | + uint256 oldTokenId, |
| 101 | + uint256 newTokenId, |
| 102 | + uint256 context |
| 103 | +); |
| 104 | +
|
| 105 | +// Standard ERC1155 transfer event for name ownership changes |
| 106 | +event TransferSingle( |
| 107 | + address indexed operator, |
| 108 | + address indexed from, |
| 109 | + address indexed to, |
| 110 | + uint256 id, |
| 111 | + uint256 value // must always be 1 |
| 112 | +); |
| 113 | +
|
| 114 | +// Standard ERC1155 transfer event for multiple name ownership changes |
| 115 | +event TransferBatch( |
| 116 | + address indexed operator, |
| 117 | + address indexed from, |
| 118 | + address indexed to, |
| 119 | + uint256[] ids, |
| 120 | + uint256[] values |
| 121 | +); |
| 122 | +
|
| 123 | +// Emitted when a name is renewed |
| 124 | +
|
| 125 | +event ExpiryUpdated( |
| 126 | + uint256 indexed tokenId, |
| 127 | + uint64 newExpiration |
| 128 | +); |
| 129 | +
|
| 130 | +// Emitted when subregistry is updated |
| 131 | +event SubregistryUpdated( |
| 132 | + uint256 indexed id, |
| 133 | + address subregistry |
| 134 | +); |
| 135 | +
|
| 136 | +// Emitted when resolver is updated |
| 137 | +event ResolverUpdated( |
| 138 | + uint256 indexed id, |
| 139 | + address resolver |
| 140 | +); |
138 | 141 |
|
139 | 142 | ``` |
140 | 143 |
|
141 | | -#### L2 |
142 | | - |
143 | | -L2 graphql URL is discoverable via `metadata` function `graphqlUrl` field. |
144 | | -Because the canonical ownership of the name exists on L1, some L2/offchain storage may choose to allow multiple entities to update the same node namespaced by `context`. When querying the domain data, the query should be filtered by `context` that is returned by `metadata`function `context` field |
145 | | - |
146 | | -```graphql |
147 | | -type Domain { |
148 | | - id: ID! # concatenation of context and namehash delimited by `-` |
149 | | - context: Bytes |
150 | | - name: String |
151 | | - namehash: Bytes |
152 | | - labelName: String |
153 | | - labelhash: Bytes |
154 | | - resolvedAddress: Bytes |
155 | | - parent: Domain |
156 | | - subdomains: [Domain] |
157 | | - subdomainCount: Int! |
158 | | - resolver: Resolver! |
159 | | - expiryDate: BigInt |
160 | | -} |
| 144 | +#### Resolver Events |
161 | 145 |
|
162 | | -type Resolver @entity { |
163 | | - id: ID! # concatenation of node, resolver address and context delimited by `-` |
164 | | - node: Bytes |
165 | | - context: Bytes |
166 | | - address: Bytes |
167 | | - domain: Domain |
168 | | - addr: Bytes |
169 | | - contentHash: Bytes |
170 | | - texts: [String!] |
171 | | - coinTypes: [BigInt!] |
172 | | -} |
| 146 | +```solidity |
| 147 | +event AddressChanged( |
| 148 | + bytes32 indexed node, |
| 149 | + uint256 coinType, |
| 150 | + bytes newAddress |
| 151 | +); |
| 152 | +
|
| 153 | +event AddrChanged( |
| 154 | + bytes32 indexed node, |
| 155 | + address a |
| 156 | +); |
| 157 | +
|
| 158 | +event TextChanged( |
| 159 | + bytes32 indexed node, |
| 160 | + string indexed indexedKey, |
| 161 | + string key, |
| 162 | + string value |
| 163 | +); |
| 164 | +
|
| 165 | +event ContenthashChanged( |
| 166 | + bytes32 indexed node, |
| 167 | + bytes hash |
| 168 | +); |
| 169 | +
|
| 170 | +event NameChanged( |
| 171 | + bytes32 indexed node, |
| 172 | + string name) |
| 173 | +; |
173 | 174 | ``` |
174 | 175 |
|
175 | | -## Backwards Compatibility |
| 176 | +## Rationale |
| 177 | + |
| 178 | +This ENSIP addresses a key limitation of EIP-3668: while it enables offchain data retrieval, it provides no standardized way for third parties to discover where that data lives or how to index it. |
| 179 | + |
| 180 | +By providing metadata at the resolver level, this ENSIP enables: |
176 | 181 |
|
177 | | -None |
| 182 | +1. **Automatic indexer discovery** - Indexers can discover new L2/offchain data sources by listening to events, without requiring manual configuration or centralized registries of RPC endpoints. |
178 | 183 |
|
179 | | -## Open Items |
| 184 | +2. **Flexible data sources** - The optional nature of `rpcURLs` and `baseRegistry` accommodates different deployment patterns: from fully decentralized L2 registries that emit events, to custom databases that may emit APIs yet to be determined that are more suitable to index non-EVM chains or offchain database names. |
180 | 185 |
|
181 | | -- Should `owner` and `isApprovedForAll` be within graphql or should be own metadata function? |
| 186 | +3. **Future extensibility** - By requiring `chainId` but leaving the non-EVM format undefined, this ENSIP establishes the pattern while remaining open to future non-EVM integrations. |
182 | 187 |
|
183 | 188 | ## Copyright |
184 | 189 |
|
185 | | -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
| 190 | +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
0 commit comments