diff --git a/EIPS/eip-2390.md b/EIPS/eip-2390.md new file mode 100644 index 00000000000000..832efb82c8a5f9 --- /dev/null +++ b/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 +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) +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 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. + + + +## Copyright +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).