LinkedResolver Proof-of-Concept

I’d like to describe a new design I’ve been experimenting with, that combines a bunch of ideas together.

  • blue tree represents ENS registry tree on L1.
    • eg. raffy.eth ⇆ [root] → eth → raffy
    • recall, the nodes (namehash) in this tree have data (owner, resolver)
  • green circles represent a new concept called a Namespace
    • a namespace is exactly like the ENS registry except:
      • there is (1) owner for the entire tree
      • there is (1) resolver for the entire tree
      • nodes of the tree are relative to the root of the tree
        • eg. ice.cold.raffy.eth is "ice.cold" under "raffy.eth"
        • eg. 123.abc.chonk.eth is "123" under "abc.chonk.eth"
    • each namespace has an identifier (ns)
  • purple circles represent a single-level of namespace dynamism
    • the namespace of {label}.chonk.eth is dependent on label
      • eg. abc.chonk.eth → ns = 4
      • eg. def.chonk.eth → ns = 3
  • red lines are links from node to namespace

  • Conceptually, a namespace stores your data.
  • This protocol links a name to a namespace.
  • Multiple names can link to the same namespace (aliasing)

Protocol Contracts

  • Namespace.sol + INamespace.sol — a registry of namespaces
  • LinkedResolver.sol — source chain resolver contract that links a basename (eg. raffy.eth)
    1. Link directly to a Namespace
      • setLink("raffy.eth", uint256(ns))
      • raffy.eth → [root] of ns = 1
      • a.b.c.raffy.eth → "a.b.c" of ns = 1
    2. Link to a contract, that links to a Namespace
      • setLink("chonk.eth", address(contract))
      • chonk.eth → ask contract for ns of label
      • {label}.chonk.eth → [root] of ns = contract(label)
      • a.b.c.{label}.chonk.eth → "a.b.c" of ns = contract(label)

One of Many Examples

Rental.sol — a vanilla OZ ERC-721 NFT that is link-aware

Rental implements LinkConfig.sol mixin which implements 2 functions:

  1. _setNamespaceProgram(bytes)
  2. _setBasenameNamespace(ns)

The Namespace Program defines how a contract translates a label into a namespace. The program takes (label: string, now: uint256) as input and expects (ns: uint256) as output.

The Basename Namespace is the namespace used when the basename is resolved, eg. chonk.eth in the above example. The small green circle (ns = 2) corresponds to the basename namespace.
image

Rental Program Explained

Rental uses the following program:
image

This program is:
image

Example: resolve("123.abc.chonk.eth", addr(60))

  1. Basename = chonk.eth → LinkedResolver
  2. Link = Rental (contract address)
  3. Read program from Rental
    • uses slot: keccak("namespace.program")
  4. Execute program with inputs: (block.timestamp, "abc")
    • token = keccak("abc")
    • exp = _datas[token].exp
    • if (block.timestamp < exp) exit(1) // expired
    • return _datas[token].ns
  5. Find "123" in that namespace
  6. Read addr(60)

Description of Deployment

  • Protocol on L1: deploy LinkedResolver
  • Developer on L2: deploy Rental
  • User on L2: Rental.mint("abc") → ns => 4
  • User on L2: Namespace.setRecord(ns=4, path="", key="addr(60)", value="0x...")
  • Developer on L1: register "chonk.eth"
  • Developer on L1: LinkedResolver.setLink("chonk.eth", Rental)
  • Anyone on L1: resolve("abc.chonk.eth", addr(60))

Actual Demo

Sepolia (testnet) → Base (mainnet)

The key idea is that the contract and the namespace program can be anything.

  • It can be a ERC-721, ERC-1155, or something custom.
  • It could link from label to labelhash to namespace (Rental example)
  • It could link from label to integer to namespace (eg. 10K collection)
  • It could link from label to “claimed name” using a separate claiming mechanism that associates “claimed name” with namespace, where only the owner of a 1
    • I have Good Morning Cafe #331
    • As owner of 331, I can claim "raffy" (eg. 331 → raffy)
    • As owner of 331, I can set namespace to X
    • raffy.[gmcafe.eth] → ns = X
  • It could link to something unrelated to a token, like another contract, maybe a directory of ERC-20s?
  • It could associate ENS names to an existing NFT

The namespace program can also execute logic. In the Rental example, the rental mechanism is solely defined on L2 yet enforced on L1.

  • any rental logic you want
  • any pricing logic you want
  • any minting logic you want
  • you could make a name resolve randomly
  • you could make a name resolve different based on time
  • anything!

Advanced Features

Any feature added to the LinkedResolver is shared by all users of this protocol.

The LinkedResolver implements the following new features:

  1. EVM Fallback Address — coinType = 0x80000000 is considered the universal EVM address. If this address is set and you query a nonexistant EVM coinType like addr(60) or addr(8453), it will automatically fallback to the universal address.

  2. LastModifiedResolver — given any record like addr(60), text(avatar), contenthash() you can query lastMod(record) and get the timestamp when that record was last changed.

  3. Consolidated Storage Keys — all records are translated into universal key format. This massively simplifies the complexity of LinkedResolver and Namespace contracts as all lookups are just bytes → bytes.

  4. Hashed Storage — regardless of how large of data you store in the Namespace, the value can be efficiently proven crosschain because it first proves the hash of the value, and then is supplied the value unproven (instead of storage proofs), and then the hash is verified.

  5. Gasless Expiration — if the namespace program returns an invalid namespace, the name doesn’t resolve

Simplest example: Link an ENS name to a Namespace

  1. Get an ENS name on Sepolia, unwrap it (no NameWrapper support yet), and set the resolver to LinkedResolver (0x084462610A20eFbDB686b30fe587ecA0E234D2EF)
  2. create() a Namespace on Base
  3. setLink() on LinkedResolver to your namespace, formatted as uint256 in hex
    • ns = 10 → 0x000000000000000000000000000000000000000000000000000000000000000A
  4. Resolve your name!

Note: The LinkedResolver demo on Sepolia is using a TrustedVerifier that talks to my server, which is connected to Base. My server is signing the stateRoots such that modifications on Base are visible within a minute. The LinkedResolver can be deployed with any verifier w/o modification. A production deployment for Base would rely on Base’s Rollup (via OPFaultVerifier) and have a finalization delay (minimum of 1.75 days). Other rollups have different finalization periods, some significantly shorter than optimistic fault proofs.

image

1 Like

This looks interesting, but it seems to be very similar to ENS v2, yet built on v1. Are you proposing it as an alternative to the current v2 design? Or for something else?

1 Like

It was mostly just exploring ideas:

  • Separating names and namespaces (possibly this is confusing terminology) is interesting as it makes data portable and reusable. Linking multiple identities to the same namespace seems useful.

  • I also think it matches an user’s journey in ENS: start with an offchain name → get a subdomain → get an .eth. This shouldn’t require 3 data migrations.

  • By putting all user data in Namespace storage, a single editor app can manage all that data, even though the names linking to them can be tokenized (or not) using various methods across unrelated projects.

  • Fully-owned namespaces make management easy: since the resolver and owner are known at every level of the tree, the owner can just write anywhere (eg. create “a.b.c.d” w/o first creating the ancestors) and edit multiple nodes at the same time. If they want to delete their entire tree, they just create a new namespace and link it.

  • “Link to Namespace” and “Link to Contract that Links to Namespace” I think covers 99%+ of use-cases, which are: I have a name, but I don’t want to pay mainnet gas to set records, or I have a community, and I want to give them ENS. I’m not sure how many names need tokenized at multiple levels. This solution currently only supports 1 level (although more is possible.)

  • Being able to add features on the L1 LinkedResolver and having all users of the protocol benefit seems very interesting as long as they don’t impact existing user records. Last Modified Resolver is an example of a feature I always wanted.

  • The Namespace Program idea was exploring what Unruggable Gateways can do beyond the basics. Although somewhat weird, I think the underlying idea is pretty cool: trustlessly lifting code from L2 to L1 and executing it.

My main objective was: I wanted to make something that was maximally easy for a developer to integrate ENS. With this approach, you’d simply deploy an L2 contract for your collection/community, acquire an L1 name, and link to it. No server. No resolver. No ENS setters/getters.

The TeamNick NFT is a good case study. Currently, that is a custom crosschain ERC-721 that only supports 1 level of subdomains with avatar and address records and requires a custom manager. With this design, any vanilla NFT contract with a small program (like _setNamespaceProgram(hex"5a010646483c...") would be a direct replacement, while also supporting any deep subdomains, any record type, and all of the new Linked Resolver features.

4 Likes

Sounds good! I’d love to see this approach applied to v2. Many of the ideas are already embedded in the design, but others could be used to make the whole thing simpler for users.

3 Likes

I’m assuming that additional colour will be added to the current state of the v2 architecture/design at frensday (which I can then feedback to @raffy / he can watch on the livestream)?

Having played with some ideas relating to this, it is extremely flexible and this:

is particularly cool.

Noting the last commit to GitHub - ensdomains/enschain was a few months ago is this general architecture still the same. What would be the best way to apply/contribute real world code to v2 for this, and more generally?

2 Likes

Although we’re still iterating on the design, the interfaces should be fairly stable now - though we can’t guarantee no changes at all - so you can base any work off them fairly safely.

1 Like