Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AzeroID off-chain resolver implementation #1

Merged
merged 14 commits into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NETWORK=sepolia
REMOTE_GATEWAY=
DEPLOYER_KEY=
SIGNER_ADDR=
INFURA_ID=
ETHERSCAN_API_KEY=
27 changes: 27 additions & 0 deletions packages/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,30 @@ This library facilitates checking signatures over CCIP read responses.

### [OffchainResolver.sol](contracts/OffchainResolver.sol)
This contract implements the offchain resolution system. Set this contract as the resolver for a name, and that name and all its subdomains that are not present in the ENS registry will be resolved via the provided gateway by supported clients.

## Deployment instruction

1. Set the following env variables:

* *REMOTE_GATEWAY*
The target url (default: localhost:8080)

* *DEPLOYER_KEY* (*mandatory)
The private key use to deploy the contract (also the contract owner)

* *SIGNER_KEY* (*mandatory)
The public key which is approved as the trusted signer

* *INFURA_ID*
API key for network provider

* *ETHERSCAN_API_KEY*

* *NETWORK*
The target network (default: sepolia)

2. Run the following command

```bash
./deploy.sh
```
22 changes: 21 additions & 1 deletion packages/contracts/contracts/OffchainResolver.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@ensdomains/ens-contracts/contracts/resolvers/SupportsInterface.sol";
import "./IExtendedResolver.sol";
import "./SignatureVerifier.sol";
Expand All @@ -13,11 +14,12 @@ interface IResolverService {
* Implements an ENS resolver that directs all queries to a CCIP read gateway.
* Callers must implement EIP 3668 and ENSIP 10.
*/
contract OffchainResolver is IExtendedResolver, SupportsInterface {
contract OffchainResolver is Ownable, IExtendedResolver, SupportsInterface {
string public url;
mapping(address=>bool) public signers;

event NewSigners(address[] signers);
event SignersRemoved(address[] signers);
error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData);

constructor(string memory _url, address[] memory _signers) {
Expand All @@ -28,6 +30,24 @@ contract OffchainResolver is IExtendedResolver, SupportsInterface {
emit NewSigners(_signers);
}

function setUrl(string calldata _url) external onlyOwner {
url = _url;
}

function addSigners(address[] calldata _signers) external onlyOwner {
for(uint i = 0; i < _signers.length; i++) {
signers[_signers[i]] = true;
}
emit NewSigners(_signers);
}

function removeSigners(address[] calldata _signers) external onlyOwner {
for(uint i = 0; i < _signers.length; i++) {
signers[_signers[i]] = false;
}
emit SignersRemoved(_signers);
}

function makeSignatureHash(address target, uint64 expires, bytes memory request, bytes memory result) external pure returns(bytes32) {
return SignatureVerifier.makeSignatureHash(target, expires, request, result);
}
Expand Down
11 changes: 11 additions & 0 deletions packages/contracts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -eu

# Can only be overwritten from the .env file, not from the command line!
NETWORK=sepolia

# Load .env
source .env

# Deploy OffchainResolver
npx hardhat --network $NETWORK deploy --tags demo
31 changes: 16 additions & 15 deletions packages/contracts/hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ require('@nomiclabs/hardhat-ethers');
require('@nomiclabs/hardhat-waffle');
require('hardhat-deploy');
require('hardhat-deploy-ethers');
require('dotenv').config();

real_accounts = undefined;
if (process.env.DEPLOYER_KEY && process.env.OWNER_KEY) {
real_accounts = [process.env.OWNER_KEY, process.env.DEPLOYER_KEY];
}
const gatewayurl =
'https://offchain-resolver-example.uc.r.appspot.com/{sender}/{data}.json';

let devgatewayurl = 'http://localhost:8080/{sender}/{data}.json';
if (process.env.REMOTE_GATEWAY) {
devgatewayurl =
`${process.env.REMOTE_GATEWAY}/{sender}/{data}.json`;
if (process.env.DEPLOYER_KEY) {
real_accounts = [process.env.DEPLOYER_KEY];
}
const gatewayurl = process.env.REMOTE_GATEWAY || 'http://localhost:8080/';
/**
* @type import('hardhat/config').HardhatUserConfig
*/
Expand All @@ -26,7 +20,7 @@ module.exports = {
networks: {
hardhat: {
throwOnCallFailures: false,
gatewayurl: devgatewayurl,
gatewayurl,
},
ropsten: {
url: `https://ropsten.infura.io/v3/${process.env.INFURA_ID}`,
Expand All @@ -49,6 +43,13 @@ module.exports = {
accounts: real_accounts,
gatewayurl,
},
sepolia: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_ID}`,
tags: ['test', 'demo'],
chainId: 11155111,
accounts: real_accounts,
gatewayurl,
},
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_ID}`,
tags: ['demo'],
Expand All @@ -61,11 +62,11 @@ module.exports = {
apiKey: process.env.ETHERSCAN_API_KEY,
},
namedAccounts: {
signer: {
default: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
},
deployer: {
default: 1,
default: 0,
},
signer: {
default: process.env.SIGNER_ADDR,
},
},
};
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"@ensdomains/ens-contracts": "^0.0.8",
"@nomiclabs/hardhat-etherscan": "^3.0.0",
"dotenv": "^16.4.5",
"hardhat-deploy-ethers": "^0.3.0-beta.13"
}
}
7 changes: 5 additions & 2 deletions packages/gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ You can run the gateway as a command line tool; in its default configuration it

```
yarn && yarn build
yarn start --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --data test.eth.json
yarn start --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --provider-url <url>
```

`private-key` should be an Ethereum private key that will be used to sign messages. You should configure your resolver contract to expect messages to be signed using the corresponding address.

`data` is the path to the data file; an example file is provided in `test.eth.json`.
`<url>` is the websocket endpoint of the target substrate chain (default: wss://ws.test.azero.dev)

## Customisation
The JSON backend is implemented in [json.ts](src/json.ts), and implements the `Database` interface from [server.ts](src/server.ts). You can replace this with your own database service by implementing the methods provided in that interface. If a record does not exist, you should return the zero value for that type - for example, requests for nonexistent text records should be responded to with the empty string, and requests for nonexistent addresses should be responded to with the all-zero address.
Expand All @@ -25,3 +25,6 @@ const db = JSONDatabase.fromFilename(options.data, parseInt(options.ttl));
const app = makeApp(signer, '/', db);
app.listen(parseInt(options.port));
```

## AZERO-ID implementation
The AzeroId gateway implementation in [azero-id.ts](src/azero-id.ts) reads the state from the AZERO-ID registry contract on AlephZero network. [supported-tlds.json](src/supported-tlds.json) stores the TLDs mapped to their target registry. Update it as per need.
3 changes: 3 additions & 0 deletions packages/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@
"dependencies": {
"@chainlink/ccip-read-server": "^0.2.1",
"@chainlink/ethers-ccip-read-provider": "^0.2.3",
"@ensdomains/address-encoder": "^1.1.2",
"@ensdomains/ens-contracts": "^0.0.8",
"@ensdomains/offchain-resolver-contracts": "^0.2.1",
"@polkadot/api": "^12.3.1",
"@polkadot/api-contract": "^12.3.1",
"commander": "^8.3.0",
"dotenv": "^15.0.0",
"ethers": "^5.7.2"
Expand Down
161 changes: 161 additions & 0 deletions packages/gateway/src/azero-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import abi from './metadata.json';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Database } from './server';
import { ContractPromise } from '@polkadot/api-contract';
import type { WeightV2 } from '@polkadot/types/interfaces';
import { getCoderByCoinType } from "@ensdomains/address-encoder";
import { createDotAddressDecoder } from '@ensdomains/address-encoder/utils'
import { hexlify } from 'ethers/lib/utils';

const AZERO_COIN_TYPE = 643;
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const EMPTY_CONTENT_HASH = '0x';

export interface GasLimit {
refTime: number,
proofSize: number,
}

export class AzeroId implements Database {
ttl: number;
tldToContract: Map<string, ContractPromise>;
maxGasLimit: WeightV2;

constructor(ttl: number, tldToContract: Map<string, ContractPromise>, maxGasLimit: WeightV2) {
this.ttl = ttl;
this.tldToContract = tldToContract;
this.maxGasLimit = maxGasLimit;
}

static async init(ttl: number, providerURL: string, tldToContractAddress: Map<string, string>, gasLimit: GasLimit) {
const wsProvider = new WsProvider(providerURL);
const api = await ApiPromise.create({ provider: wsProvider });

const tldToContract = new Map<string, ContractPromise>();
tldToContractAddress.forEach((addr, tld) => {
tldToContract.set(tld, new ContractPromise(api, abi, addr))
})

const maxGasLimit = api.registry.createType('WeightV2', gasLimit) as WeightV2;

return new AzeroId(
ttl,
tldToContract,
maxGasLimit,
);
}

async addr(name: string, coinType: number) {
coinType = Number(coinType); // convert BigNumber to number
console.log("addr", name, coinType);

let value;
if (coinType == AZERO_COIN_TYPE) {
value = await this.fetchA0ResolverAddress(name);
} else {
let alias = AzeroId.getAlias(""+coinType);
if (alias !== undefined) {
const serviceKey = "address." + alias;
value = await this.fetchRecord(name, serviceKey);
}
if (value === undefined) {
const serviceKey = "address." + coinType;
value = await this.fetchRecord(name, serviceKey);
}
}

if (value === undefined) {
value = coinType == 60? ZERO_ADDRESS:'0x';
} else {
value = AzeroId.encodeAddress(value, coinType);
}

return { addr: value, ttl: this.ttl };
}

async text(name: string, key: string) {
console.log("text", name, key);
const value = await this.fetchRecord(name, key) || '';
return { value, ttl: this.ttl };
}

contenthash(name: string) {
console.log("contenthash", name);
return { contenthash: EMPTY_CONTENT_HASH, ttl: this.ttl };
}

private async fetchRecord(domain: string, key: string) {
let {name, contract} = this.processName(domain);
const resp: any = await contract.query.getRecord(
'',
{
gasLimit: this.maxGasLimit
},
name,
key
);

return resp.output?.toHuman().Ok.Ok;
}

private async fetchA0ResolverAddress(domain: string) {
let {name, contract} = this.processName(domain);
const resp: any = await contract.query.getAddress(
'',
{
gasLimit: this.maxGasLimit
},
name
);

return resp.output?.toHuman().Ok.Ok;
}

private processName(domain: string) {
const labels = domain.split('.');
console.log("Labels:", labels);

const name = labels.shift() || '';
const tld = labels.join('.');
const contract = this.tldToContract.get(tld);

if (contract === undefined) {
throw new Error(`TLD (.${tld}) not supported`);
}

return {name, contract};
}

static getAlias(coinType: string) {
const alias = new Map<string, string>([
['0', 'btc'],
['60', 'eth'],
['354', 'dot'],
['434', 'ksm'],
['501', 'sol'],
]);

return alias.get(coinType);
}

static encodeAddress(addr: string, coinType: number) {
const isEvmCoinType = (c: number) => {
return c == 60 || (c & 0x80000000)!=0
}

if (coinType == AZERO_COIN_TYPE) {
const azeroCoder = createDotAddressDecoder(42);
return hexlify(azeroCoder(addr));
}
if (isEvmCoinType(coinType) && !addr.startsWith('0x')) {
addr = '0x' + addr;
}

try {
const coder = getCoderByCoinType(coinType);
return hexlify(coder.decode(addr));
} catch {
return addr;
}
}
}
Loading
Loading