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

GeoENS pull request - initial revision #2390

Merged
merged 11 commits into from Sep 13, 2020
316 changes: 316 additions & 0 deletions EIPS/eip-2390.md
@@ -0,0 +1,316 @@
---
eip: 2390
title: Geo-ENS
author: James Choncholas (@james-choncholas)
discussions-to: https://github.com/ethereum/EIPs/issues/2959
status: Draft
type: Standards Track
category: ERC
created: 2019-11-15
requires: 137, 165, 1062, 1185
---

## Simple Summary
GeoENS brings geographic split horizon capabilities to ENS. It's GeoDNS for ENS!

## Abstract
This EIP specifies an ENS resolver interface for geographically split horizon DNS.
Geographic split horizon DNS returns resource records that are specific to an end
user's location.
This technique is commonly used by CDNs to direct traffic to content caches nearest users.
Geographic split horizon resolution is primarily geared towards ENS
resolvers storing DNS resource records [EIP-1185](./eip-1185.md), although the technique could be
used on other interfaces like IPFS content hash storage [EIP-1062](./eip-1062.md).

## Motivation
There are many use cases for traditional GeoDNS systems, like Amazon's Route53,
in the centralized web.
These use cases include proximity-based load balancing and serving content
specific to the geographic location of the query.
Unfortunately the ENS specification does not provide a mechanism for
geo-specific resolution.
ENS can respond to queries with IP addresses (as described in [EIP-1185](./eip-1185.md))
however there is no way to respond to geo-specific queries.
This EIP proposes a standard to give the ENS system geo-proximal awareness
to serve a similar purpose as GeoDNS.

GeoENS can do more than DNS-based solutions.
In addition to geographic split horizon DNS, GeoENS can be used for the following:
- Locating digital resources (like smart contracts) that represent physical objects in the real world.
- Smart contract managing access to a physical object associated with a specific location.
- ENS + IPFS web hosting (as described in [EIP-1062](./eip-1062.md)) with content translated to the native language of the query source.
- Tokenizing objects with a physical location.

Because of the decentralized nature of ENS, geo-specific resolution is different than traditional GeoDNS.
GeoDNS works as follows. DNS queries are identified by their source IP address.
This IP is looked up in a database like [GeoIP2](https://www.maxmind.com/en/geoip2-services-and-databases)
from MaxMind which maps the IP address to a location.
This method of locating the source of a query is error prone and unreliable.
If the GeoIP database is out of date, queried locations can be vastly different than their true location.
GeoENS does not rely on a database because the user includes a location in their query.

It follows that queries can be made by users for any location, not just their location.
Traditional DNS will only return the resource assigned to a query's provenance.
GeoENS does not correlate a query's provinance with a location, allowing the
entire globe to be queried from a single location.

An additional shortcoming of traditional DNS is the fact that there is no way to return a list of servers in a certain proximity.
This is paramount for uses cases that require discovering the resource with the lowest latency.
GeoENS allows a list of resources, like IP addresses, to be gathered within a specific location.
Then a client to determine themselves which resource has the lowest latency.

Lastly, publicly facing GeoDNS services do not give fine granularity control
over geographic regions for GeoDNS queries.
Cloud based DNS services like [Amazon's Route 53](https://aws.amazon.com/route53/)
only allow specifying geographic regions at the granularity of a State in
the United States.
GeoENS on the other hand gives 8 characters of geohash resolution which
corresponds to +-20 meter accuracy.

## Specification
This EIP proposes a new interface to ENS resolvers such that geo-spacial information
can be recorded and retrieved from the blockchain.
The interface changes are described below for "address resolvers" described in EIP137
however the idea applies to any record described in EIP1185 and EIP1062, namely DNS
james-choncholas marked this conversation as resolved.
Show resolved Hide resolved
Resolvers, Text Resolvers, ABI Resolvers, etc.

### What is a geohash?
A [Geohash](https://en.m.wikipedia.org/wiki/Geohash#Algorithm_and_example)
is an interleaving of latitude and longitude bits, whose
length determines it's precision.
Geohashes are typically encoded in base 32 characters.

### function setGeoAddr(bytes32 node, string calldata geohash, address addr) external authorised(node)
james-choncholas marked this conversation as resolved.
Show resolved Hide resolved
Sets a resource (contract address, IP, ABI, TEXT, etc.) by node and geohash.
Geohashes must be unique per address and are exactly 8 characters long.
This leads to an accuracy of +-20 meters.
Write default initialized resource value, `address(0)`, to remove a resource from the resolver.

### function geoAddr(bytes32 node, string calldata geohash) external view returns (address[] memory ret)
Query the resolver contract for a specific node and location.
All resources (contract addresses, IP addresses, ABIs, TEXT records, etc.) matching
the node and prefix geohash provided are returned.
This permits querying by exact geohash of 8 characters to return the content at that location,
or querying by geographic bounding box described by a geohash of less than 8 character precision.

Any type of geohash can be used including [Z-order](https://en.wikipedia.org/wiki/Z-order_curve)
[Hilbert](https://en.wikipedia.org/wiki/Hilbert_curve) or the more accurate
[S2 Geometry](https://s2geometry.io/devguide/s2cell_hierarchy.html) library
from Google.
There are also ways to search the geographic data using geohashes without
always ending up with a rectangular query region.
[Searching circular shaped regions](https://github.com/ashwin711/proximityhash) is
slightly more complex as it requires multiple queries.

## Rationale
The proposed implementation uses a sparse [Quadtree](https://dl.acm.org/doi/10.1007/BF00288933) trie as an index for
resource records as it has low storage overhead and good search performance.
The leaf nodes of the tree store resource records while non-leaves represent one geohash character.
Each node in the tree at depth d corresponds to a geohash of precision d.
The tree has depth 8 because the maximum precision of a geohash is 8 characters.
The tree has fanout 32 because the radix of a geohash character is 32.
The path to get to a leaf node always has depth 8 and the leaf contains the content (like IP address)
of the geohash represented by the path to the leaf.
The tree is sparse as 71% of the Earth's surface is covered by water.
The tree facilitates common traversal algorithms (DFS, BFS) to return
lists of resource records within a geographic bounding box.

## Backwards Compatibility
This EIP does not introduce issues with backwards compatibility.

## Test Cases
See https://github.com/james-choncholas/resolvers/blob/master/test/TestPublicResolver.js

## Implementation
This address resolver, written in Solidity, implements the specifications outlined above.
The same idea presented here can be applied to other resolver interfaces as specified in EIP137.
Note that geohashes are passed and stored using 64 bit unsigned integers.
Using integers instead of strings for geohashes is more performant, especially in the `geomap` mapping.
For comparison purposes, see https://github.com/james-choncholas/geoens/tree/master/contracts/StringOwnedGeoENSResolver.sol for the inefficient string implementation.


```solidity
pragma solidity ^0.5.0;

import "../ResolverBase.sol";

contract GeoENSResolver is ResolverBase {
bytes4 constant ERC2390 = 0x8fbcc5ce;
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, bytes8 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 can't be larger than 32
return b;
}

function geoAddr(bytes32 node, bytes8 geohash, uint8 precision) external view returns (address[] memory ret) {
bytes32(node); // single node georesolver ignores node
assert(precision <= geohash.length);

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(uint8 i=0; i < precision; i++) {

uint8 c = chartobase32(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 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 chilins
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, bytes8 geohash, address addr) external authorised(node) {
bytes32(node); // single node georesolver ignores node

// 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 < geohash.length; i++) {

uint8 c = chartobase32(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);
}
}
```

## Security Considerations
This contract has similar functionality to ENS Resolvers - refer there for security considerations.
Additionally, this contract has a dimension of data privacy.
Users query via the geoAddr function specifying a geohash of less than 8 characters
which defines the query region.
Users who run light clients leak the query region to their connected full-nodes.
Users who rely on nodes run by third parties (like Infura) will also leak
the query region.
Users who run their own full node or have access to a trusted full node do
not leak any location data.

Given the way most location services work, the query region is likely to contain
the user's actual location.
The difference between API access, light, and full nodes has always had
an impact on privacy but now the impact is underscored by the involvement
of coarse granularity user location.



Comment on lines +313 to +314
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

For repository wide consistency.

## Copyright
james-choncholas marked this conversation as resolved.
Show resolved Hide resolved
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).