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, although the technique could be
used on other interfaces like IPFS content hash storage EIP-1062.
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)
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) 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
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
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
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.
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-orderHilbert or the more accurate
S2 Geometry 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 is
slightly more complex as it requires multiple queries.
Rationale
The proposed implementation uses a sparse Quadtree 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.
pragmasolidity^0.5.0;import"../ResolverBase.sol";contractGeoENSResolverisResolverBase{bytes4constantERC2390=0x8fbcc5ce;uintconstantMAX_ADDR_RETURNS=64;uintconstantTREE_VISITATION_QUEUESZ=64;uint8constantASCII_0=48;uint8constantASCII_9=57;uint8constantASCII_a=97;uint8constantASCII_b=98;uint8constantASCII_i=105;uint8constantASCII_l=108;uint8constantASCII_o=111;uint8constantASCII_z=122;structNode{addressdata;// 0 if not leaf
uint256parent;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[])privategeomap;eventGeoENSRecordChanged(bytes32indexednode,bytes8geohash,addressaddr);// only 5 bits of ret value are used
functionchartobase32(bytec)pureinternalreturns(uint8b){uint8ascii=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
returnb;}functiongeoAddr(bytes32node,bytes8geohash,uint8precision)externalviewreturns(address[]memoryret){bytes32(node);// single node georesolver ignores node
assert(precision<=geohash.length);ret=newaddress[](MAX_ADDR_RETURNS);if(geomap[node].length==0){returnret;}uintret_i=0;// walk into the geomap data structure
uintpointer=0;// not actual pointer but index into geomap
for(uint8i=0;i<precision;i++){uint8c=chartobase32(geohash[i]);uintnext=geomap[node][pointer].children[c];if(next==0){// nothing found for this geohash.
// return early.
returnret;}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[]memoryindexes_to_visit=newuint[](TREE_VISITATION_QUEUESZ);indexes_to_visit[0]=pointer;uintfront_i=0;uintback_i=1;while(front_i!=back_i){Nodememorycur_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(uinti=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;}}returnret;}// when setting, geohash must be precise to 8 digits.
functionsetGeoAddr(bytes32node,bytes8geohash,addressaddr)externalauthorised(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:newuint256[](32)}));}// walk into the geomap data structure
uintpointer=0;// not actual pointer but index into geomap
for(uinti=0;i<geohash.length;i++){uint8c=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:newuint256[](32)}));geomap[node][pointer].children[c]=geomap[node].length-1;}pointer=geomap[node][pointer].children[c];}Nodestoragecur_node=geomap[node][pointer];// storage = get reference
cur_node.data=addr;emitGeoENSRecordChanged(node,geohash,addr);}functionsupportsInterface(bytes4interfaceID)publicpurereturns(bool){returninterfaceID==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.