Skip to content

Commit

Permalink
Merge pull request #1 from scio-labs/feat/azero-offchain-resolver
Browse files Browse the repository at this point in the history
AzeroID off-chain resolver implementation
  • Loading branch information
realnimish authored Sep 8, 2024
2 parents 099b7e9 + 118c9d5 commit 260466a
Show file tree
Hide file tree
Showing 14 changed files with 6,508 additions and 27 deletions.
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

0 comments on commit 260466a

Please sign in to comment.