Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-2390: Geo-ENS

Authors James Choncholas (@james-choncholas)
Created 2019-11-15
Discussion Link https://github.com/ethereum/EIPs/issues/2959
Requires EIP-137, EIP-165, EIP-1062, EIP-1185

简述

GeoENS 为 ENS 带来了地理位置分割域名的能力。它是 ENS 的 GeoDNS !

摘要

此 EIP 为地理位置分割域名 DNS 指定了一个 ENS 解析器接口。 地理位置分割域名 DNS 返回特定于最终用户位置的资源记录。 CDN 通常使用此技术将流量定向到离用户最近的内容缓存。 地理位置分割域名解析主要面向存储 DNS 资源记录的 ENS 解析器 EIP-1185,尽管该技术可以 在其他接口上使用,例如 IPFS 内容哈希存储 EIP-1062

动机

在中心化网络中,传统的 GeoDNS 系统(如 Amazon 的 Route53)有许多用例。 这些用例包括基于邻近性的负载平衡和提供 特定于查询地理位置的内容。 不幸的是,ENS 规范没有提供 geo-specific 解析机制。 ENS 可以使用 IP 地址响应查询(如 EIP-1185 中所述) 但无法响应 geo-specific 的查询。 此 EIP 提出了一个标准,使 ENS 系统具有地理位置感知能力 以达到与 GeoDNS 类似的目的。

GeoENS 可以做比基于 DNS 的解决方案更多的事情。 除了地理位置分割域名 DNS 之外,GeoENS 还可以用于以下目的:

  • 定位代表现实世界中物理对象的数字资源(如智能合约)。
  • 智能合约管理对与特定位置关联的物理对象的访问。
  • ENS + IPFS 网络托管(如 EIP-1062 中所述),内容已翻译为查询源的母语。
  • 对具有物理位置的对象进行 Token 化。

由于 ENS 的去中心化性质,geo-specific 解析与传统的 GeoDNS 不同。 GeoDNS 的工作方式如下。DNS 查询通过其源 IP 地址进行识别。 此 IP 在诸如 MaxMind 的 GeoIP2 之类的数据库中查找, 该数据库将 IP 地址映射到一个位置。 这种定位查询源的方法容易出错且不可靠。 如果 GeoIP 数据库已过期,则查询到的位置可能与其真实位置大相径庭。 GeoENS 不依赖数据库,因为用户在其查询中包含一个位置。

因此,用户可以查询任何位置,而不仅仅是他们自己的位置。 传统的 DNS 将仅返回分配给查询来源的资源。 GeoENS 不会将查询的来源与位置相关联,从而允许从单个位置查询整个地球。

传统 DNS 的另一个缺点是无法返回特定邻近范围内的服务器列表。 这对于需要发现具有最低延迟的资源的用例至关重要。 GeoENS 允许在特定位置内收集资源列表,如 IP 地址。 然后,客户端可以自行确定哪个资源具有最低的延迟。

最后,面向公众的 GeoDNS 服务无法对 GeoDNS 查询的地理区域进行精细控制。 基于云的 DNS 服务,如 Amazon 的 Route 53 仅允许以美国的一个州为粒度指定地理区域。 另一方面,GeoENS 提供 8 个字符的 geohash 解析, 对应于 +-20 米的精度。

规范

此 EIP 提出了一个用于 ENS 解析器的新接口,以便可以将地理空间信息 记录并从区块链中检索。 下面介绍了 EIP137 中描述的“地址解析器”的接口更改, 但该想法适用于 EIP1185 和 EIP1062 中描述的任何记录,即 DNS 解析器,文本解析器,ABI 解析器等。

什么是 geohash?

Geohash 是纬度和经度位的交错,其 长度决定了它的精度。 Geohash 通常以 base 32 字符编码。

function setGeoAddr(bytes32 node, string calldata geohash, address addr) external authorised(node)

按节点和 geohash 设置资源(合约地址,IP,ABI,TEXT 等)。 每个地址的 Geohash 必须是唯一的,并且长度恰好为 8 个字符。 这导致 +-20 米的精度。 写入默认初始化的资源值 address(0),以从解析器中删除资源。

function geoAddr(bytes32 node, string calldata geohash) external view returns (address[] memory ret)

查询解析器合约以获取特定节点和位置。 返回所有匹配节点和前缀 geohash 的资源(合约地址,IP 地址,ABI,TEXT 记录等)。 这允许按 8 个字符的精确 geohash 查询以返回该位置的内容, 或按小于 8 个字符精度的 geohash 描述的地理边界框进行查询。

可以使用任何类型的 geohash,包括 Z 阶 Hilbert 或更准确的 Google 的 S2 Geometry 库。 还有一些方法可以使用 geohash 搜索地理数据,而无需 总是以矩形查询区域结束。 搜索圆形区域 是 稍微复杂一些,因为它需要多个查询。

理由

所提出的实现使用稀疏的四叉树 trie 作为资源记录的索引,因为它具有低存储开销和良好的搜索性能。 树的叶节点存储资源记录,而非叶节点表示一个 geohash 字符。 深度为 d 的树中的每个节点对应于精度为 d 的 geohash。 树的深度为 8,因为 geohash 的最大精度为 8 个字符。 树的扇出为 32,因为 geohash 字符的基数为 32。 到达叶节点的路径始终具有深度 8,并且叶包含由到达叶的路径表示的 geohash 的内容(如 IP 地址)。 该树是稀疏的,因为地球表面有 71% 被水覆盖。 该树有助于常见的遍历算法(DFS,BFS)以返回 地理边界框内的资源记录列表。

向后兼容性

此 EIP 不会引入与向后兼容性有关的问题。

测试用例

请参见 https://github.com/james-choncholas/resolvers/blob/master/test/TestPublicResolver.js

实现

这个用 Solidity 编写的地址解析器实现了上面概述的规范。 这里提出的相同想法可以应用于 EIP137 中指定的其他解析器接口。 请注意,geohash 使用 64 位无符号整数传递和存储。 将整数而不是字符串用于 geohash 的性能更高,尤其是在 geomap 映射中。 为了进行比较,请参见 https://github.com/james-choncholas/geoens/tree/master/contracts/StringOwnedGeoENSResolver.sol,以获取效率低下的字符串实现。

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)
    // geohash 为 8 个 base-32 字符。
    // geomap 存储为扇出为 32 的树(因为
    // geohash 为 base 32)和高度为 8(因为 geohash
    // 长度为 8 个字符)
    mapping(bytes32=>Node[]) private geomap;

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

    // only 5 bits of ret value are used
    // 仅使用 ret 值的 5 位
    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'
            // base32 b = 10
            // ascii 'b' = 0x60
            // 注意 base32 跳过字母 'a'
            b = ascii - ASCII_b + 10;

            // base32 also skips the following letters
            // base32 也跳过以下字母
            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
        // 走进 geomap 数据结构
        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.
                // 没有找到此 geohash 的任何内容。
                // 提前返回。
                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...
        // 指针现在是表示查询 geohash 的解析的节点。
        // DFS 直到找到所有地址或 ret[] 已满。
        // 不要使用递归,因为区块链...
        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.
    // 设置时,geohash 必须精确到 8 位数字。
    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
        // 走进 geomap 数据结构
        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
                // 没有找到此 geohash 的任何内容。
                // 我们需要创建到叶子的路径
                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);
    }
}

安全注意事项

此合约具有与 ENS 解析器类似的功能 - 有关安全注意事项,请参阅此处。 此外,此合约还具有数据隐私维度。 用户通过 geoAddr 函数进行查询,指定一个小于 8 个字符的 geohash, 该 geohash 定义了查询区域。 运行轻客户端的用户会将其查询区域泄漏到其连接的完整节点。 依赖第三方运行的节点(如 Infura)的用户也会泄漏 查询区域。 运行自己的完整节点或有权访问受信任的完整节点的用户不会 泄漏任何位置数据。

鉴于大多数位置服务的工作方式,查询区域可能包含 用户的实际位置。 API 访问,轻节点和完整节点之间的差异一直具有 对隐私的影响,但是现在,由于涉及粗粒度的用户位置,因此该影响得到了强调。

版权

通过 CC0 放弃版权及相关权利。

Citation

Please cite this document as:

James Choncholas (@james-choncholas), "ERC-2390: Geo-ENS [DRAFT]," Ethereum Improvement Proposals, no. 2390, November 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2390.