Skip to content

Commit

Permalink
Initial implementation of GeoENS resolver - EIP 2390
Browse files Browse the repository at this point in the history
GeoENS brings geographic split horizon capabilities to ENS.
See more at EIP 2390 ethereum/EIPs#2390
  • Loading branch information
james-choncholas committed May 8, 2020
1 parent 2788959 commit d3a5f09
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 2 deletions.
4 changes: 3 additions & 1 deletion contracts/OwnedResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "./profiles/ABIResolver.sol";
import "./profiles/AddrResolver.sol";
import "./profiles/ContentHashResolver.sol";
import "./profiles/DNSResolver.sol";
import "./profiles/GeoENSResolver.sol";
import "./profiles/InterfaceResolver.sol";
import "./profiles/NameResolver.sol";
import "./profiles/PubkeyResolver.sol";
Expand All @@ -13,7 +15,7 @@ import "./profiles/TextResolver.sol";
* A simple resolver anyone can use; only allows the owner of a node to set its
* address.
*/
contract OwnedResolver is Ownable, ABIResolver, AddrResolver, ContentHashResolver, InterfaceResolver, NameResolver, PubkeyResolver, TextResolver {
contract OwnedResolver is Ownable, ABIResolver, AddrResolver, ContentHashResolver, DNSResolver, GeoENSResolver, InterfaceResolver, NameResolver, PubkeyResolver, TextResolver {
function isAuthorised(bytes32 node) internal view returns(bool) {
return msg.sender == owner();
}
Expand Down
3 changes: 2 additions & 1 deletion contracts/PublicResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "./profiles/ABIResolver.sol";
import "./profiles/AddrResolver.sol";
import "./profiles/ContentHashResolver.sol";
import "./profiles/DNSResolver.sol";
import "./profiles/GeoENSResolver.sol";
import "./profiles/InterfaceResolver.sol";
import "./profiles/NameResolver.sol";
import "./profiles/PubkeyResolver.sol";
Expand All @@ -15,7 +16,7 @@ import "./profiles/TextResolver.sol";
* A simple resolver anyone can use; only allows the owner of a node to set its
* address.
*/
contract PublicResolver is ABIResolver, AddrResolver, ContentHashResolver, DNSResolver, InterfaceResolver, NameResolver, PubkeyResolver, TextResolver {
contract PublicResolver is ABIResolver, AddrResolver, ContentHashResolver, DNSResolver, GeoENSResolver, InterfaceResolver, NameResolver, PubkeyResolver, TextResolver {
ENS ens;

/**
Expand Down
162 changes: 162 additions & 0 deletions contracts/profiles/GeoENSResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
pragma solidity ^0.5.0;

import "../ResolverBase.sol";

contract GeoENSResolver is ResolverBase {
bytes4 constant ERC2390 = 0xa263115e;
uint constant MAX_ADDR_RETURNS = 64;
uint constant TREE_VISITATION_QUEUESZ = 64;
uint8 constant ASCII_0 = 48;
uint8 constant ASCII_9 = 57;
uint8 constant ASCII_a = 97;
uint8 constant ASCII_b = 98;
uint8 constant ASCII_i = 105;
uint8 constant ASCII_l = 108;
uint8 constant ASCII_o = 111;
uint8 constant ASCII_z = 122;

struct Node {
address data; // 0 if not leaf
uint256 parent;
uint256[] children; // always length 32
}

// A geohash is 8, base-32 characters.
// A geomap is stored as tree of fan-out 32 (because
// geohash is base 32) and height 8 (because geohash
// length is 8 characters)
mapping(bytes32=>Node[]) private geomap;

event GeoENSRecordChanged(bytes32 indexed node, string geohash, address addr);

// only 5 bits of ret value are used
function chartobase32(byte c) pure internal returns (uint8 b) {
uint8 ascii = uint8(c);
require( (ascii >= ASCII_0 && ascii <= ASCII_9) ||
(ascii > ASCII_a && ascii <= ASCII_z));
require(ascii != ASCII_a);
require(ascii != ASCII_i);
require(ascii != ASCII_l);
require(ascii != ASCII_o);

if (ascii <= (ASCII_0 + 9)) {
b = ascii - ASCII_0;

} else {
// base32 b = 10
// ascii 'b' = 0x60
// note base32 skips the letter 'a'
b = ascii - ASCII_b + 10;

// base32 also skips the following letters
if (ascii > ASCII_i)
b --;
if (ascii > ASCII_l)
b --;
if (ascii > ASCII_o)
b --;
}
require(b < 32); // base 32 cant be larger than 32
return b;
}

function geoAddr(bytes32 node, string calldata geohash) external view returns (address[] memory ret) {
bytes32(node); // single node georesolver ignores node
require(bytes(geohash).length < 9); // 8 characters = +-1.9 meter resolution

ret = new address[](MAX_ADDR_RETURNS);
if (geomap[node].length == 0) { return ret; }
uint ret_i = 0;

// walk into the geomap data structure
uint pointer = 0; // not actual pointer but index into geomap
for(uint i=0; i < bytes(geohash).length; i++) {

uint8 c = chartobase32(bytes(geohash)[i]);
uint next = geomap[node][pointer].children[c];
if (next == 0) {
// nothing found for this geohash.
// return early.
return ret;
} else {
pointer = next;
}
}

// pointer is now node representing the resolution of the query geohash.
// DFS until all addresses found or ret[] is full.
// Do not use recursion because this is a blockchain...
uint[] memory indexes_to_visit = new uint[](TREE_VISITATION_QUEUESZ);
indexes_to_visit[0] = pointer;
uint front_i = 0;
uint back_i = 1;

while(front_i != back_i) {
Node memory cur_node = geomap[node][indexes_to_visit[front_i]];
front_i ++;

// if not a leaf node...
if (cur_node.data == address(0)) {
// visit all the chilin's
for(uint i=0; i<cur_node.children.length; i++) {
// only visit valid children
if (cur_node.children[i] != 0) {
assert(back_i < TREE_VISITATION_QUEUESZ);
indexes_to_visit[back_i] = cur_node.children[i];
back_i ++;

}
}
} else {
ret[ret_i] = cur_node.data;
ret_i ++;
if (ret_i > MAX_ADDR_RETURNS) break;
}
}

return ret;
}

// when setting, geohash must be precise to 8 digits.
function setGeoAddr(bytes32 node, string calldata geohash, address addr) external authorised(node) {
bytes32(node); // single node georesolver ignores node
require(bytes(geohash).length == 8); // 8 characters = +-1.9 meter resolution

// create root node if not yet created
if (geomap[node].length == 0) {
geomap[node].push( Node({
data: address(0),
parent: 0,
children: new uint256[](32)
}));
}

// walk into the geomap data structure
uint pointer = 0; // not actual pointer but index into geomap
for(uint i=0; i < bytes(geohash).length; i++) {

uint8 c = chartobase32(bytes(geohash)[i]);

if (geomap[node][pointer].children[c] == 0) {
// nothing found for this geohash.
// we need to create a path to the leaf
geomap[node].push( Node({
data: address(0),
parent: pointer,
children: new uint256[](32)
}));
geomap[node][pointer].children[c] = geomap[node].length - 1;
}
pointer = geomap[node][pointer].children[c];
}

Node storage cur_node = geomap[node][pointer]; // storage = get reference
cur_node.data = addr;

emit GeoENSRecordChanged(node, geohash, addr);
}

function supportsInterface(bytes4 interfaceID) public pure returns (bool) {
return interfaceID == ERC2390 || super.supportsInterface(interfaceID);
}
}
70 changes: 70 additions & 0 deletions test/TestPublicResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,76 @@ contract('PublicResolver', function (accounts) {
assert.equal(web3.eth.abi.decodeParameters(['string'], results[1])[0], "https://ethereum.org/");
});
});

describe('geoens', async () => {
var geo1 = 'ezs42bcd';
var geo2 = 'ezs42bdd';
let differentNode = namehash.hash('yeth');

it("should directly resolve a simple geohash query", async () => {
await resolver.setGeoAddr(node, geo1, accounts[1], {from: accounts[0]});

a = await resolver.geoAddr(node, geo1);
assert.equal(a[0], accounts[1], "Did not correctly resolve address on direct query");
assert.equal(a[1], 0, "Did not correctly resolve address on direct query");
});


it("should not resolve a non-existant geohash query", async () => {
await resolver.setGeoAddr(node, geo1, accounts[1], {from: accounts[0]});

a = await resolver.geoAddr(node, geo2);
assert.equal(a[0], 0, "Resolved a geohash which was never set in the contract");

a = await resolver.geoAddr(differentNode, geo1);
assert.equal(a[0], 0, "Resolved a domain which was never set in the contract");
});


it("should resolve only one geohash on direct query", async () => {
await resolver.setGeoAddr(node, geo1, accounts[1], {from: accounts[0]});
await resolver.setGeoAddr(node, geo2, accounts[2], {from: accounts[0]});
await ens.setSubnodeOwner('0x0', sha3('yeth'), accounts[0], {from: accounts[0]});
await resolver.setGeoAddr(differentNode, geo2, accounts[3], {from: accounts[0]});

a = await resolver.geoAddr(node, geo1);
assert.equal(a[0], accounts[1], "Did not correctly resolve address on direct query");
assert.equal(a[1], 0, "Did not correctly resolve address on direct query");

a = await resolver.geoAddr(node, geo2);
assert.equal(a[0], accounts[2], "Did not correctly resolve address on direct query");
assert.equal(a[1], 0, "Did not correctly resolve address on direct query");
});


it("should resolve only one geohash on indirect query", async () => {
await resolver.setGeoAddr(node, geo1, accounts[1], {from: accounts[0]});
await resolver.setGeoAddr(node, geo2, accounts[2], {from: accounts[0]});
await ens.setSubnodeOwner('0x0', sha3('yeth'), accounts[0], {from: accounts[0]});
await resolver.setGeoAddr(differentNode, geo2, accounts[3], {from: accounts[0]});

a = await resolver.geoAddr(node, 'ezs42bc');
assert.equal(a[0], accounts[1], "Did not correctly resolve address on indirect query");
assert.equal(a[1], 0, "Returned geohash that doesn't match query");

a = await resolver.geoAddr(node, 'ezs42bd');
assert.equal(a[0], accounts[2], "Did not correctly resolve address on indirect query");
assert.equal(a[1], 0, "Returned geohash that doesn't match query");
});


it("should resolve multiple geohashes on range query for a specific node", async () => {
await resolver.setGeoAddr(node, geo1, accounts[1], {from: accounts[0]});
await resolver.setGeoAddr(node, geo2, accounts[2], {from: accounts[0]});
await ens.setSubnodeOwner('0x0', sha3('yeth'), accounts[0], {from: accounts[0]});
await resolver.setGeoAddr(differentNode, geo2, accounts[3], {from: accounts[0]});

a = await resolver.geoAddr(node, 'ezs42b');
assert.equal(a[0], accounts[1], "Did not correctly resolve address on indirect query");
assert.equal(a[1], accounts[2], "Returned geohash that doesn't match query");
assert.equal(a[2], 0, "Returned geohash that doesn't match query");
});
});
});

function dnsName(name) {
Expand Down

0 comments on commit d3a5f09

Please sign in to comment.