blockful-io / external-resolver

This project aims to scale the Ethereum Name Service (ENS) by consolidating existing patterns and proofs of concept into a unified and production-ready codebase.
MIT License
14 stars 3 forks source link
arbitrum ens ethereum solidity typeorm viem

External Resolver

Overview

The Ethereum Name Service (ENS) has revolutionized the way we interact with the blockchain by replacing complex addresses with human-readable domain names like "myname.eth". However, ENS faces scalability and cost challenges that hinder its widespread adoption. The External Resolver project offers an innovative solution to overcome these obstacles by combining established patterns such as ERC-3668, EIP-5559, ENSIP-10, and ENSIP-16.

At its core, a "resolver" is a crucial component of ENS that translates human-readable domain names into relevant blockchain information, such as wallet addresses, public keys, and custom records. The "resolution" process is fundamental for making domain names usable in decentralized applications (dApps) and wallets.

The External Resolver takes the concept of resolution further by allowing ENS data to be stored and managed off-chain. This drastically reduces transaction costs, improves network scalability, and enables more advanced features like larger and more complex data records.

This project not only makes ENS more efficient and cost-effective but also opens up a world of possibilities for developers and users, expanding the potential of ENS as a foundational infrastructure for Web3. By providing a comprehensive reference implementation for off-chain storage and management, the External Resolver empowers the community to innovate and build upon the ENS ecosystem.

Objectives

Deployments

Mainnet

Contract Network Address
DatabaseResolver Ethereum 0xBF3F57862717099319285c1E2664Cd583f35E333

Sepolia

Contract Network Address
DatabaseResolver Ethereum 0xc1D4903Eba794035d2D81D210325b57a95C8a007
ArbitrumVerifier Ethereum 0x8fc4a214705e3c40032e99f867d964c012bf8efb
L1Resolver Ethereum 0xF0c1d78C73B2fCBF17e1c4DbBBD9df30a9556BB8
ENSRegistry Arbitrum 0x8d55e297c37993ebbd2e7a8d7688f7e5b35f1b50
ReverseRegistrar Arbitrum 0xb3c9ff08671bbadddd0436cc46fbfa005c8da0a7
BaseRegistrarImplementation Arbitrum 0x2C6a113C513fa0fd404abcCE3aC8a4BE16ccb651
NameWrapper Arbitrum 0xff4f34ac12a84de527cf9e24856fc8d7c42cc379
ETHRegistrarController Arbitrum 0x263c644d8f5d4bdb44cfab020491ec6fc4ca5271
SubdomainController Arbitrum 0x41eede073217084a30f6f3bc2c546bda1f08b5ca
PublicResolver Arbitrum 0x0a33f065c9c8f0F5c56BB84b1593631725F0f3af

Components

The External Resolver consists of three main components, each of them is a self-contained project with its own set of files and logic, ensuring seamless integration and collaboration between them. This modular architecture allows for flexibility and customization, making the External Resolver a versatile solution for various use cases.

Gateway

The Gateway serves as the bridge between the blockchain and external data sources. It follows the EIP-3668 specification to fetch data from off-chain storage and relays it back to the client. The Gateway ensures secure and efficient communication between the different components of the system.

Contracts

The smart contracts are the backbone of the External Resolver. They include the L1 Resolver, which redirects requests to external resolvers, the L2 Resolver Contract, which handles the actual resolution of domain names on Layer 2 networks and more. These contracts are designed to be modular and adaptable, allowing for deployment on various EVM-compatible chains.

Database Resolver

L1 Resolver

A smart contract that redirects requests to specified external contract deployed to any EVM compatible protocol.

L2 Resolver

An L2 contract capable of resolving ENS domains to corresponding addresses and fetching additional information fully compatible with the ENS' Public Resolver but responsible for authentication.

Client

The client acts as the interface between the user and the Blockchain. It handles requests for domain resolution and interacts with the Gateway to retrieve the necessary information.

Sample interaction with the Database Resolver:

try {
    await client.simulateContract({
      functionName: 'register',
      abi: dbAbi,
      args: [toHex(name), 300],
      account: signer.address,
      address: resolverAddr,
    })
  } catch (err) {
    const data = getRevertErrorData(err)
    if (data?.errorName === 'StorageHandledByOffChainDatabase') {
      const [domain, url, message] = data.args as [
        DomainData,
        string,
        MessageData,
      ]
      await handleDBStorage({ domain, url, message, signer })
    } else {
      console.error('writing failed: ', { err })
    }
}

Sample interaction with the Layer 1 Resolver:

try {
    await client.simulateContract({
      functionName: 'setText',
      abi: l1Abi,
      args: [toHex(packetToBytes(name)), 'com.twitter', '@blockful'],
      address: resolverAddr,
    })
  } catch (err) {
    const data = getRevertErrorData(err)
    if (data?.errorName === 'StorageHandledByL2') {
      const [chainId, contractAddress] = data.args as [bigint, `0x${string}`]

      await handleL2Storage({
        chainId,
        l2Url: providerL2,
        args: {
          functionName: 'setText',
          abi: l2Abi,
          args: [namehash(name), 'com.twitter', '@blockful'],
          address: contractAddress,
          account: signer,
        },
      })
    } else if (data) {
      console.error('error setting text: ', data.errorName)
    } else {
      console.error('error setting text: ', { err })
    }
}

Usage

To run the External Resolver project in its entirety, you'll need to complete the installation process. Since we provide an off-chain resolver solution, it's essential to set up both the database and the Arbitrum Layer 2 environment. This will enable you to run comprehensive end-to-end tests and verify the functionality of the entire project.

Prerequisites

Setup

  1. Clone this repository to your local machine.
  2. Copy the env.example file to .env in the root directory.
  3. Install dependencies: npm install
  4. Build the contracts: npm run build

Database Setup

  1. Run a local PostgreSQL instance (no initial data is inserted):

    docker-compose up db -d
  2. Deploy the contracts locally:

    npm run contracts dev:db
  3. Start the gateway:

    npm run gateway dev:db
  4. Write properties to a given domain:

      npm run client start:write:db
  5. Request domain properties through the client:

    npm run client read
Migrations

This repository relies on migrations to manage the database schema. To create a new migration, run the following command:

npm run gateway migration:create --name=<migration_name>

To apply the migration, run the following command:

npm run migration:generate -- -n <migration_name>

Layer 2 Setup

  1. Deploy the contracts to the local Arbitrum node (follow the Arbitrum's local node setup tutorial):

    npm run contracts dev:arb:l2
  2. Gather the contract address from the terminal and add it here so the L1 domain gets resolved by the L2 contract you just deployed.

  3. Start the gateway:

    npm run gateway dev:arb
  4. Request domain properties through the client:

    npm run client start

Deployment

Gateway

Ensure you have the Railway CLI installed.

  1. Install the Railway CLI:

    npm i -g @railway/cli
  2. Log in to your Railway account:

    railway login
  3. Link the repo to the project:

    railway link
  4. Deploy the Gateway:

    railway up

Contracts

  1. npm run contracts deploy:db -- --rpc-url <RPC_URL>

Architecture

High-Level Overview

Database

Database Architecture

Layer 2

Layer 2 Architecture

Flowchart overview

Database

Domain Register and data writing:

  1. Find the resolver associated with the given domain through the Universal Resolver
  2. Call the register function on the resolver
  3. Client receive a StorageHandledByDB revert with the arguments required to call the gateway
  4. Sign the request with the given arguments using the EIP-712
  5. Call the gateway on endpoint /{sender}/{data}.json as specified by the EIP-3668
  6. Gateway validates the signer and create a new entry on the database for this domain

domain register and data writing

Reading domain properties:

  1. Call the resolver function on the Universal Resolver passing the reading method in an encoded format as argument
  2. Client receive the OffchainLookup revert with the required arguments to call the gateway
  3. Client calls the gateway on endpoint /{sender}/{data}.json as specified by the EIP-3668
  4. Gateway reads the data and sign it using it's own private key which as previously marked as authorized on the Database Resolver
  5. Client calls the callback function with the gateway signed response and extra data from the Database Resolver
  6. The Database Resolver contract validates the signature came from an authorized source and decode de data
  7. Data is returned to the client

reading domain properties

Layer 2

Domain Register:

  1. Find the resolver associated with the given domain through the Universal Resolver
  2. Call the register function on the resolver passing the address of the Layer 2 resolver that will be managing the properties of a given domain
  3. Client calls setOwner on the L1 Resolver
  4. Client receive a StorageHandledByL2 revert with the arguments required to call the gateway
  5. Client calls the L2 Resolver with the returned arguments

domain register

Conclusion

This project aims to significantly enhance the scalability and usability of the Ethereum Name Service through the development of a comprehensive reference codebase. By combining existing patterns and best practices, we aim to lower costs for users and drive increased adoption within the industry. We welcome collaboration and feedback from the community as we progress towards our goals.

Contributing

We welcome contributions from the community to improve this project. To contribute, please follow these guidelines:

  1. Fork the repository and create a new branch for your feature or bug fix.
  2. Make your changes and ensure they follow the project's coding conventions.
  3. Test your changes locally to ensure they work as expected.
  4. Create a pull request with a detailed description of your changes.

License

This project is licensed under the MIT License.

Acknowledgements

Special thanks to the Ethereum Name Service (ENS) community for their contributions and support.