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.
- Enhance Scalability: Improve ENS scalability for broader adoption.
- Cost-Effectiveness: Lower costs for ENS users.
- Increase Usability: Make ENS more user-friendly and accessible.
- Reference implementation: Create a reference on how to implement off-chain storage and management.
Contract | Network | Address |
---|---|---|
DatabaseResolver | Ethereum | 0xBF3F57862717099319285c1E2664Cd583f35E333 |
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 |
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.
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.
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.
A smart contract that redirects requests to specified external contract deployed to any EVM compatible protocol.
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.
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 })
}
}
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.
- Foundry
- Run local node by calling
anvil
- Run local node by calling
- Clone this repository to your local machine.
- Copy the
env.example
file to.env
in the root directory. - Install dependencies:
npm install
- Build the contracts:
npm run build
-
Run a local PostgreSQL instance (no initial data is inserted):
docker-compose up db -d
-
Deploy the contracts locally:
npm run contracts dev:db
-
Start the gateway:
npm run gateway dev:db
-
Write properties to a given domain:
npm run client start:write:db
-
Request domain properties through the client:
npm run client read
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>
-
Deploy the contracts to the local Arbitrum node (follow the Arbitrum's local node setup tutorial):
npm run contracts dev:arb:l2
-
Gather the contract address from the terminal and add it here so the L1 domain gets resolved by the L2 contract you just deployed.
-
Start the gateway:
npm run gateway dev:arb
-
Request domain properties through the client:
npm run client start
Ensure you have the Railway CLI installed.
-
Install the Railway CLI:
npm i -g @railway/cli
-
Log in to your Railway account:
railway login
-
Link the repo to the project:
railway link
-
Deploy the Gateway:
railway up
npm run contracts deploy:db -- --rpc-url <RPC_URL>
Domain Register and data writing:
- Find the resolver associated with the given domain through the Universal Resolver
- Call the
register
function on the resolver - Client receive a
StorageHandledByDB
revert with the arguments required to call the gateway - Sign the request with the given arguments using the EIP-712
- Call the gateway on endpoint
/{sender}/{data}.json
as specified by the EIP-3668 - Gateway validates the signer and create a new entry on the database for this domain
Reading domain properties:
- Call the
resolver
function on the Universal Resolver passing the reading method in an encoded format as argument - Client receive the
OffchainLookup
revert with the required arguments to call the gateway - Client calls the gateway on endpoint
/{sender}/{data}.json
as specified by the EIP-3668 - Gateway reads the data and sign it using it's own private key which as previously marked as authorized on the Database Resolver
- Client calls the callback function with the gateway signed response and extra data from the Database Resolver
- The Database Resolver contract validates the signature came from an authorized source and decode de data
- Data is returned to the client
Domain Register:
- Find the resolver associated with the given domain through the Universal Resolver
- 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 - Client calls
setOwner
on the L1 Resolver - Client receive a
StorageHandledByL2
revert with the arguments required to call the gateway - Client calls the L2 Resolver with the returned arguments
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.
We welcome contributions from the community to improve this project. To contribute, please follow these guidelines:
- Fork the repository and create a new branch for your feature or bug fix.
- Make your changes and ensure they follow the project's coding conventions.
- Test your changes locally to ensure they work as expected.
- Create a pull request with a detailed description of your changes.
This project is licensed under the MIT License.
Special thanks to the Ethereum Name Service (ENS) community for their contributions and support.