Skip to content

Commit

Permalink
docs: rewrite documentations/improve DX
Browse files Browse the repository at this point in the history
  • Loading branch information
qd-qd committed Aug 8, 2022
1 parent b15a2da commit a5f70c4
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 90 deletions.
170 changes: 114 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,127 +1,185 @@
# ENS Offchain Resolver

This repository contains smart contracts and a node.js gateway server that together allow hosting ENS names offchain using [EIP 3668](https://eips.ethereum.org/EIPS/eip-3668) and [ENSIP 10](https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution).
This repository shows how it is possible to resolve subdomains from an EVM-enabled blockchain based on [EIP 3668](https://eips.ethereum.org/EIPS/eip-3668) and [ENSIP 10](https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution). The goal is to store the second-level domain on the main network while all subdomains attached to it are resolved in another EVM-enabled blockchain.

## Overview

ENS resolution requests to the resolver implemented in this repository are responded to with a directive to query a gateway server for the answer. The gateway server generates and signs a response, which is sent back to the original resolver for decoding and verification. The gateway fetch the from a new registry contract that can be hosted on any network we want to. Full details of this request flow can be found below.
ENS resolution requests to the resolver implemented in this repository are responded to with a directive to query a gateway server for the answer. The gateway server requests the blockchain where the records are stored and signs a response, which is sent back to the original resolver for decoding and verification. Full details of this request flow can be found below.

All of this happens transparently in supported clients (such as ethers.js, or future versions of web3.js which will have this functionality built-in).
All of this happens transparently for end users. Offchain resolution is handled by supported clients such as ethers.js.

## [Gateway Server](packages/gateway)
## [Gateway Server](packages/gateway/readme.md)

The gateway server implements CCIP Read (EIP 3668), and answers requests by looking up the names in a store stored on-chain. Once a record is retrieved, it is signed using a user-provided key to assert its validity, and both record and signature are returned to the caller so they can be provided to the contract that initiated the request.
The gateway server implements the CCIP Read ([EIP 3668](https://eips.ethereum.org/EIPS/eip-3668)) and responds to requests by searching for names stored on-chain. Once a record is retrieved, it is signed using a predefined key to ensure its validity, then the record and the signature are returned to the caller so that they can be provided to the requesting contract, hosted on the mainnet. More details [here](packages/gateway).

## [Contracts](packages/contracts)
## [Client](packages/client/readme.md)

There is two important smart-contrat: The OffchainResolver smart-contract that will be stored on-chain on the L1 and the L2PublicResolver/L2Registry smart-contracts that are stored on-chain on the L2/side-chain we decide to.
The client package simulates a dApp or any scripts that would like to resolve an ENS and fetch additional informations. More details [here](packages/client).

The OffchainResolver smart contract provides a resolver stub that implement CCIP Read (EIP 3668) and ENS wildcard resolution (ENSIP 10). When queried for a name, it directs the client to query the gateway server. When called back with the gateway server response, the resolver verifies the signature was produced by an authorised signer, and returns the response to the client.
## [l1](packages/l1/readme.md)

The L2 contracts replicate the ENS ecosystem on the layer 2 network. That to that, we control how records are stored on-chain and all interaction with them.
This package is a minimalist reproduction of ENS' behaviour on the mainnet. In addition, everything needed to be [EIP 3668](https://eips.ethereum.org/EIPS/eip-3668)/[ENSIP 10](https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution) compliant is implemented there. The offchain resolver is defined on the second-level domain which will allow subdomains to be resolved elsewhere. More details [here](packages/l1).

## Trying it out
## [l2](packages/l2/readme.md)

Start by generating an Ethereum private key; this will be used as a signing key for any messages signed by your gateway service. You can use a variety of tools for this; for instance, this Python snippet will generate one for you:
Last but not least, this package implements an arbitrary way to store subdomain information on the layer2 side. Contracts defined here are requested by the gateway. In this implementation, we deploy a new registry which will store all the subdomains and we push a ENS-compliant PublicResolver that can be used by subdomains owners to store arbitrary data. More details [here](packages/l2).

```
python3 -c "import os; import binascii; print('0x%s' % binascii.hexlify(os.urandom(32)).decode('utf-8'))"
```
## Trying it out

For the rest of this demo we will be using the standard test private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`.
### Init the project

First, install dependencies and build all packages:

```bash
yarn && yarn build
```

Next, create a new `.env.local` file in the `packages/gateway` directory, and add the previously generated private key to the `.env.local` file
Then, run this command to create the .env files you will need

```bash
cp packages/l1/.env.example packages/l1/.env && \
cp packages/gateway/.env.example packages/gateway/.env && \
cp packages/client/.env.example packages/client/.env
```
PRIVATE_KEY=<MY_PRIVATE_KEY>
```

Next, run the gateway:
Now we created the .env files, we'll fill in the right values.

In your terminal, start the l2 package by running

```bash
yarn start:gateway
yarn start:l2
```

You will see output similar to the following:
Looking at the logs, you should find a line like this:

```
Serving on port 8000 with signing address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
```
> Registry address -> 0x5FbDB2315678afecb367f032d93F642f64180aa3
This is the address of the registry deployed on the layer2. Copy this address and paste it as a value for the variable `REGISTRY_ADDRESS` in the [.env file of the gateway package](packages/gateway/.env). The gateway needs it to know which contrat fetch.

Next, edit `contracts/deploy/10_offchain_resolver.js`; replacing the address on line 4 with the one output when you ran the command above. Then, in a new terminal, build and run a test node with an ENS registry and the offchain resolver deployed:
Stop the l2 package. As you can see in the .env file of the gateway, the gateway needs a private key to works. This private key will be used as a signing key for any messages signed by your gateway service. It is important to ensure the data is unaltered.

For the demo purpose, I would suggest you to generate a new private key. You can use a variety of tools for this; for instance, this Python snippet will generate one for you:

```bash
python3 -c "import os; import binascii; print('0x%s' % binascii.hexlify(os.urandom(32)).decode('utf-8'))"
```
cd packages/contracts
npx hardhat node

> Do not send mainnet tokens to any account addresses derived using this private key. Consider this private key as publicly known. If you add any of those accounts to a wallet (eg Metamask), be very careful to avoid sending any mainnet Ether to them: consider naming the account something like "Unsafe" in order to prevent any mistakes.
Once your private key is generated, paste it as a value for the variable `PRIVATE_KEY` in the [.env file of the gateway package](packages/gateway/.env).

We now have all we need to start the gateway package! Start it by running:

```bash
yarn start:gateway
```

You will see output similar to the following:
The gateway is up! In the output of the script, the signer address has been printed. This address is derived from the private key filled in your .env file. We need to push it on-chain on the l1 to check if the received data are from the gateway. Let's copy/paste this address and set it as a value for the `SIGNER` envionment variable in the [.env file of the l1 package](/packages/l1/.env).

Now that the signer address is filled in, stop the gateway and start the l1 package by running:

```bash
yarn start:l1
```
Compilation finished successfully
deploying "ENSRegistry" (tx: 0x8b353610592763c0abd8b06305e9e82c1b14afeecac99b1ce1ee54f5271baa2c)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 1084532 gas
deploying "OffchainResolver" (tx: 0xdb3142c2c4d214b58378a5261859a7f104908a38b4b9911bb75f8f21aa28e896)...: deployed at 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 with 1533637 gas
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:9545/

Accounts
========
Once again, running the l1 printed something interesting. In the log, you'll find a line like this:

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
> Registry address -> 0x5FbDB2315678afecb367f032d93F642f64180aa3
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
This is the address of the registry published on the l1. This is only required for the demo, in production, you will use the official registry deployed by ENS instead of deploying yours. Copy this value and paste it as a value for the variable `REGISTRY_ADDRESS` in the [.env file of the client package](packages/client/.env). The client needs it to know which contrat request.

(truncated for brevity)
```
That's it, everything is configured. Let's up the stack now.

### Run the project

Open 4 different terminal instances at the root of the repository.

Take note of the address to which the L2Registry was deployed (0xe7f1...).
In the first one, up the layer1 network by running:

Open the `.env.local` file in the `packages/gateway` directory and add the address to the `REGISTRY_ADDRESS` variable.
```bash
yarn start:l1

# `yarn start` if you are in the directory of the package
```
REGISTRY_ADDRESS=<MY_L2_REGISTRY_ADDRESS>

In the second one, up the layer2 network by running:

```bash
yarn start:l2

# `yarn start` if you are in the directory of the package
```

In the first tab you opened, kill and restart the gateway server:
In the third one, up the gateway by running:

```bash
yarn start:gateway

# `yarn start` if you are in the directory of the package
```

Now, take note of the address to which the ENSRegistry was deployed (0x5FbDB...).
Finally, in the last instance, you can run the client's script to test the setup. You need to pass an ENS as a parameter of the command to test the flow. First, try to resolve a ENS that is not a subdomain of `mydao.eth` by running:

```bash
yarn start:client aloha.eth
```

Finally, in a third terminal, run the example client to demonstrate resolving a name:
These informations should be printed:

```
yarn start:client --registry 0x5FbDB2315678afecb367f032d93F642f64180aa3 qdqd.mydao.eth
-> L1 informations
l1 offchain resolver address: undefined
eth address: null
```

That means the script works as expected. The script wasn't able to find an owner for the ENS you passed, because it doesn't exist on our local network. This ENS is still free to register.

Now, let's try to resolve a subdomain of the `mydao.eth` that has not been registered on the l2. For example by running:

```bash
yarn start:client aloha.mydao.eth
```

You should see output similar to the following:
These informations should be printed:

```
resolver address 0x8464135c8F25Da09e49BC8782676a84730C318bC
eth address 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
-> L1 informations
l1 offchain resolver address: 0x8464135c8F25Da09e49BC8782676a84730C318bC
eth address: null
```

The address displayed in the output should match the address of the first account generated by hardhat when you run the command (`npx hardhat node`). This is because I use this account to take a the `qdqd.mydao.eth` node as you can see in the file `packages/contracts/deploy/10_offchain_resolver.js`.
This is because the script correctly find the custom mydao's resolver but the subdomain is free in the registry stored in the layer2.

If you try to resolve a mydao subdomain that does not exist yet, you should see the following output:
Let's see what happens if you resolve `qdqd.mydao.eth`, the subdomain registered on the layer2 by the [deployment script](packages/l2/deploy/10_setup_l2.js).

```bash
yarn start:client qdqd.mydao.eth
```
resolver address 0x8464135c8F25Da09e49BC8782676a84730C318bC
eth address null

```
-> L1 informations
l1 offchain resolver address: 0x8464135c8F25Da09e49BC8782676a84730C318bC
eth address: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
This is because the script correctly find the custom mydao's resolver but the subdomain is free in the registry stored in the layer2.
-> Data fetched from the layer2 resolver
eth address: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
btc address: bc1q8fnmuy9cfzmym062a93cuqh2l8l0p46gxy74pg
doge address: DBs4WcRE7eysKwRxHNX88XZVCQ9M6QSUSz
twitter account: qdqd___
content hash: ipfs://QmdTPkMMBWQvL8t7yXogo7jq5pAcWg8J7RkLrDsWZHT82y
avatar: https://metadata.ens.domains/mainnet/avatar/qdqdqd.eth?v=1.0
twitter: qdqd___
github: qd-qd
telegram: qd_qd_qd
email: [email protected]
url: https://ens.domains/
description: smart-contract engineer
notice: This is a custom notice
keywords: solidity,ethereum,developer
company: ledger
```

If you try to resolve a non-mydao domain using the command above, you will see no outputs.
It works! Every data set during the deployement process ([this file](packages/l2/deploy/11_resolver_l2.js)) have been resolve as expected. The script correctly found the custom offchain resolver set in the layer1 for mydao, discovered the subdomain is owned and fetched all the data requested by the script. Note that the address displayed in the output should match the address of the first account printed when you run the command (`yarn start:l2`). This is because we use this account to register the `qdqd.mydao.eth` node as you can see in this file [`packages/contracts/deploy/10_offchain_resolver.js`](packages/l2/deploy/10_setup_l2.js).

## Appendix

Expand Down
4 changes: 2 additions & 2 deletions packages/client/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Address of the registry stored on the L1 (printed when you run `yarn start:l1`)
# If you run this on the mainnet, the address will be `0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e`
# Address of the registry stored on the L1 (automatically printed when you run the l1 package
# REQUIRED
REGISTRY_ADDRESS=
23 changes: 21 additions & 2 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
# ENS offchain resolution client

This package implements a very simple dig-like tool for querying ENS names, with support for offchain resolution. It is intended as a way to test out your code and run the demo, rather than a production-quality tool.

## Usage:
## Notice

Please refer to the [.env.example](/packages/client/.env.example) file to know which environment variables are required.
For the demo, the second-level domain `mydao.eth` is stored and configured on the mainnet and one subdomain of this domain `qdqd` is stored on the layer2. That means you need to request `qdqd.mydao.eth` to see the demo run properly.

## Usage

Make sure dependencies have been previously installed and the package has been built. As this repository uses yarn workspace, you can run these commands from the root of the repository.

```shell
yarn && yarn build
```

Then, start the package

```shell
# From the root of the repository
yarn start:client qdqd.mydao.eth

# From the root of the package
yarn start qdqd.mydao.eth
```

`--provider` can optionally be specified to supply the URL to an Ethereum web3 provider; it defaults to `http://localhost:8001/`.
> the `--provider` flag can optionally be specified to supply the URL to an Ethereum web3 provider; it defaults to `http://localhost:8001/`.
19 changes: 8 additions & 11 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,23 @@ const provider = new ethers.providers.JsonRpcProvider(options.provider, {
provider.resolveName(name),
]);

console.group("\n-> L1 informations")
console.log(`l1 offchain resolver address: ${resolver?.address}`);
console.log(`eth address: ${resolveName}`);
console.groupEnd();

if (resolver) {
if (resolver && resolveName) {
const [

ethAddress,
btcAddress,
dogeAddress,
twitterAccount,
contentHash,
contentHash
] = await Promise.all([
resolver.getAddress(), // ETH
resolver.getAddress(0), // BTC
resolver.getAddress(3), // DOGE
resolver.getText('com.twitter'),
resolver.getContentHash(),
resolver.getAvatar(),
]);

// fetch text records
Expand Down Expand Up @@ -71,16 +73,11 @@ const provider = new ethers.providers.JsonRpcProvider(options.provider, {
resolver.getText("company")
]);

console.group("\n-> L1 informations")
console.log(`l1 offchain resolver address: ${resolver.address}`);
console.log(`name resolved: ${resolveName}`);
console.groupEnd();

console.group("\n-> Data fetched from the layer2 resolver")
console.log(`eth address: ${ethAddress}`);
console.log(`btc address: ${btcAddress}`);
console.log(`doge address: ${dogeAddress}`);
console.log(`twitter account: ${twitterAccount}`);
console.log(`twitter account: ${twitter}`);
console.log(`content hash: ${contentHash}`);
console.log(`avatar: ${avatar}`);
console.log(`twitter: ${twitter}`);
Expand Down
9 changes: 8 additions & 1 deletion packages/gateway/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Private key holds by the gateway to sign data returned by the L2 contrats
# REQUIRED
PRIVATE_KEY=

# Address of the registry stored on the L2
# REQUIRED
REGISTRY_ADDRESS=

# Time-to-live -- Used to set an expiration date to the returned data
# Used to set an expiration date to the returned data
# OPTIONAL -- fallback to 300
TTL=

# Customise server's port
# OPTIONAL -- fallback to 8080
PORT=
25 changes: 20 additions & 5 deletions packages/gateway/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
# ENS Offchain Resolver Gateway
# ENS offchain Resolver Gateway

This package implements a CCIP-read gateway server for ENS offchain resolution. that fetch records from a registry stored on a different network.
The gateway server implements the CCIP Read ([EIP 3668](https://eips.ethereum.org/EIPS/eip-3668)) and responds to requests by searching for names stored on-chain. Once a record is retrieved, it is signed using a predefined key to ensure its validity, and the record and signature are returned to the caller so that they can be provided to the requesting contract, hosted on the mainnet.

## Usage:
It's a simple nodejs server bootstrapped using [`@chainlink/ccip-read-server`](https://www.npmjs.com/package/@chainlink/ethers-ccip-read-provider). This library adds transparent support for EIP 3668.

You can run the gateway as a command line tool
## Notice

```
Please refer to the [.env.example](/packages/gateway/.env.example) file to know which environment variables are required.

## Usage

Make sure dependencies have been previously installed and the package has been built. As this repository uses yarn workspace, you can run these commands from the root of the repository.

```shell
yarn && yarn build
```

Then, start the package

```shell
# From the root of the repository
yarn start:gateway

# From the root of the package
yarn start
```
11 changes: 10 additions & 1 deletion packages/gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ const address = ethers.utils.computeAddress(privateKey);
const signer = new ethers.utils.SigningKey(privateKey);
const app = makeApp(signer, '/');

console.log(`Serving on port ${PORT} with signing address ${address}`);
console.log(`Serving on port ${PORT}`);

// log the address of the signer EOA
// this address must be copied to the .env file of the l1 package (SIGNER)
console.log(
"\n\n\x1b[34m\x1b[1m",
`Signer address -> ${address}`,
"\x1b[0m\n\n"
);

app.listen(parseInt(PORT));

module.exports = app;
1 change: 1 addition & 0 deletions packages/l1/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# The address of the account who signs the response of the gateway
# REQUIRED
SIGNER=
Loading

0 comments on commit a5f70c4

Please sign in to comment.