Results of the first ENS Layer 2 meeting

The first ENS Layer 2 meeting, coordinated by @makoto, was held today. Participants included people from several L2 solutions, ENS community and team members, and wallet developers.

A recording of the session is available here: https://www.youtube.com/watch?v=vloI0VT8DXE.

There was some discussion of both the possibility of hosting ENS on its own rollup, and means to encode and resolve L2-specific addresses using ENS, but both of these were deemed offtopic and a subject for future discussion.

The main topic of discussion was how to support ENS name resolution for layer 2 platforms, specifically:

  • Committing names to Layer 2 solutions so that they and their subdomains can be managed without needing to perform L1 transactions.
  • Resolving L2-based names without the client doing the resolution needing to be aware of the details of the L2 implementation a given name is managed by.

Discussion revolved around the starting point given by Vitalik in this post on Ethereum Magicians, which can be summarised as:

  • Names that are to be managed on an L2 have ownership transferred to a contract responsible for interfacing between ENS and that L2.
  • Attesters are participants who lock a bond in the above contract, and sign messages asserting what a name resolves to.
  • If an attester makes a false attestation, another party can make a challenge; doing so requires the attester to prove the resolution of the name (at the time the transaction was signed) onchain, or lose their deposit.

This concept was explored in some more detail at the first ENS workshop, with refinements being suggested, including reversing the burden of proof - requiring the challenger to prove the attester wrong, which shortens the time period between an attester producing a false signature and their losing their deposit, thus reducing the threat window and hence the size of the deposit required.

Participants generally agreed that this was a good direction to explore, with several refinements being suggested. The most notable of those was by Harry Kalodner, from Offchain Labs, who suggested an alternate approach, along these lines:

  • Each L2 ENS interface contract maintains a merkle root of the resolution status of all the names it is responsible for. This merkle tree is structured according to a standard that is identical for all L2 implementations.
  • Changes to any name managed by this contract on the L2 result in updates to this merkle root on L1 - though these updates can be delayed and batched.
  • Instead of producing a signature backed up by a bond, attesters can simply provide witness data that allows the client to resolve the name themselves by checking it against the relevant merkle root.
  • Faster updates can be supported by having attesters submit a bond and generate signatures asserting what a future merkle root’s value will be; unlike Vitalik’s original proposal, anyone in posession of an invalid proof can use it to invalidate the attester’s deposit, without needing to understand the details of the particular L2 in question.

I (Nick Johnson) observed that while on the surface use of a structure like this reduces flexibility for resolution by requiring it to only be static lookups, this can be remediated by making the data committed to a tuple of (resolver address, call data). To resolve, clients (locally) call the resolver address with the call data, then in the same context, call the resolver to perform their initial resolution request. The typical usage of this would be for the call data to be a function call that configures the resolver to resolve that particular name, but this is application-specific, and clients need not understand the data being sent.

Participants wrapped up by agreeing to form a working group to further explore ENS-on-L2 and begin designing a standard, with future calls as needed.

Having thought more about Harry’s excellent idea, I’d like to propose a further refinement to it.

Its chief disadvantage over earlier proposals is that it requires an update on L1 each time any of the managed names are updated on L2. These can be batched, but the effectiveness of this depends on the number of names managed and the update rate, and it still requires an additional expense for ENS integration on an L2.

However, L2s generally already have a merkle root or some other equivalent proof on L1, which commits to the entire state of the L2 in question. We cannot easily use this directly, as the format is likely to be different for each L2 implementation. Instead, we could require each interface contract to expose a resolveName(bytes32 namehash, bytes witness) function. Attesters thus produce an L2-specific witness blob which they provide to the client; the client then calls the resolveName function on L1, providing the name to be resolved and the blob. Internally, this function understands the proof encoding for the L2 in question, and can decode and verify it, then perform the resolution for the client.

This lets us do away with ENS-specific state roots, while still avoiding the need for bonded validators, except in the case that users want to resolve names more quickly than the L2 solution’s update rate on L1 - which can be implemented as a second phase if desired.

  • Each L2 ENS interface contract maintains a merkle root of the resolution status of all the names it is responsible for. This merkle tree is structured according to a standard that is identical for all L2 implementations.

So I thought about this. My main concern with it is basically that it would require all L2s to fit into the same standard in a fairly deep way, that often doesn’t naturally work well with how the L2s work. For example, a ZK rollup may want to use some special-purpose arithmetic-friendly hash function for their Merkle tree, or even abandon Merkling outright and use a Kate commitment, etc. Forcing them into a (realistically sha3-based) Merkle tree would mean that that one part of their system would either require vastly more effort to SNARK-prove than the rest of the system, or have lower security guarantees than the rest of the system.

We cannot easily use this directly, as the format is likely to be different for each L2 implementation. Instead, we could require each interface contract to expose a resolveName(bytes32 namehash, bytes witness)

I do like this. It is definitely a good way to generalize beyond just Merkle trees; allow the rollup to specify its own witness format and verification function, and solves the problem above. And it could be generalized to not just names but also general state reading.

One challenge is that it does not generalize well to static calls that actually do require non-trivial state reading. Though I suppose you could do something clever where in a zk rollup the witness verification would just verify a ZKP of the execution, and in an optimistic rollup the “witness” would just need to include a signature from an attester with a locked balance! I guess the goal would be that ideally in the OR you would want the sequencers themselves (or other full nodes that already have deposits for whatever duties in the OR) to be providing responses to the static calls.

So this does seem like it leads in an interesting direction! Basically instead of “enshrining” the security-deposit-based mechanism, we allow L2s themselves to define a way to execute a static call given a particular state root (or in a plasma chain block index or whatever); that way could just be direct witness verification, or it could be verifying an off-chain signed message that can be challenged if false. The only thing that is “enshrined” would be the API that you would call into an L2 to verify a static-call with some witness.

Just for the record, here is the list of the participants

ENS team

  • Nick
  • Jeff
  • Makoto

L2

  • Zac Williamson (Aztec)
  • Joe Andrews (Aztec)
  • Karl Floersch (Optimism)
  • Harry Kalodner (Offchain labs)

Wallet

  • Shane Fontaine (Authereum)
  • Chris Whinfrey (Authereum)
  • Peter Kim (Coinbase)
  • Hadrian Croubois (iex.ec, ENSLogin)
  • Richard Moor (ethers.js, Firefly $5 HW wallet)

So, here’s a sketch of how it could work:

Supporting contracts expose a function getProxyURL(bytes4 functionId) view returns (bytes4 callbackId, string[] urls). Using it is a three-step process:

  1. Call getProxyURL with the function ID of a function they wish to call against the contract that requires external information. The contract returns one or more URLs to which the call should be made, along with the function ID of a callback function that can be used to process the data returned at that URL.
  2. Make an HTTP request to one of the URLs returned by getProxyURL, supplying the encoded function call. The request returns an opaque blob of data which must be prefixed with the callback function ID supplied by the contract in step 1.
  3. Call the original contract with the data returned in step 2. The return value of this call is the final result of the function call.

An example implementation of this for ENS would constitute a custom resolver contract that implements getProxyURL. The resolver understands all the function IDs normally supported by that resolver (Eg, getAddr, getText, etc). The return value is the URL of a proxy server that implements this standard, and the functionID of processProxyResponse(bytes data, bytes witness).

The proxy server is configured to understand the same set of functions, and resolves them by making the query against the L2 platform, returning an encoded call to processProxyResponse with the result of calling the function, and witness data necessary to verify the result against the state root stored in the L2’s interface contract on L1.

ENS resolution is thus modified to operate as follows:

  1. Hash the name using namehash
  2. Look up the resolver on the ENS registry. If the resolver does not exist, remove the first label from the name and go to step 1.
  3. If the resolver exists, first check if getProxyURL returns an address for the desired resolution function (eg getAddr). If it does, follow the process above. Otherwise, resolve the record as normal by calling the resolution function directly.

Notably, this standard isn’t just generic to all L2s - it can be used for other use-cases entirely. For example, a merkelized token contract could use it to allow people to fetch balances etc. Wallets or API providers could implement this natively, so such calls look just the same as a regular eth_call to the application.

Can we somehow incorporate a way to specify/fetch tokenURI of ERC721 standard? Current ENS as NFT is a bit limiting due to the lack of metadata. If this transition allows people to add metadata into their ENS names, that could potentially increase the utility of ENS as NFT a lot.

This feature should integrate at the Ethereum network layer, rather than relying on external networks.
getProxyURL should instead be getProxyPeerIds.
Two L2 networks can then communicate over libp2p.
@vbuterin

It might also make sense to consider some geo-spacial standard by which clients may choose to connect to nearer peers.

Not really; we can’t change the existing registrar contract, and this would require changing it too.

I don’t think we should let ideology outweigh practicality. HTTP is a well established standard that’s suitable for this use-case, and not all L2s are going to use libp2p - which isn’t really designed for a connect/query/disconnect workflow in any case.

However, the field is for a URI; I’m very happy to see people specifying bindings for protocols other than HTTP.

The argument that HTTP is established is compelling.
My mind has been changed.

1 Like