比特币源码分析--深入理解区块链 7.点对点网络:节点选择以及淘汰机制

        在上一篇文章中介绍了P2P节点的连接来源,连接数量限制和连接类型等信息。在此再梳理一遍节点来源:

1.-addnoe:通过在配置文件中设定该参数、或通过RPC命令动态添加节点。该类型的节点在连接时最多可连接8个(添加数量不受限制,下同)。

2. -seednode: 通过在配置文件中设定该参数来添加节点。该类型节点在连接时受收到MAX_OUTBOUND总量限制(11)。

3. -dnsseed: 通过在配置文件中设定该参数表明是否使用DNS种子来获得节点。该参数使用true(1)或false(0)来表示。默认为true。

4. -connect: 通过该参数指定节点。系统连接时没有数量限制。但一旦设定该参数,系统将不会连接到从DNS种子获取的节点,也就是说不会从3来源获取节点。

        上述4个来源中,只有从DNS seed获取的节点才加入到addrman管理器,其它三种均不加入。通过addrman管理节点的目的是为了防止黑客Erebus攻击,阻止黑客通过DNS seed广播恶意的节点地址、或将Bitcoin要连接的节点引向恶意节点。因此一般建议使用来自DNS seed的节点,DNS seed域名内置在代码中,不同的网络有不同的DNS seed,这在上一篇文章已有介绍。 那么其它三种没有使用DNS种子域名获得的节点IP地址是否不安全呢?当然也会存在不安全的因素。但由于这些节点都是人工添加的,因此也可以随时删除,且数量有限。这些节点作为DNS seed的补充,在一些局域网环境或者矿池场景中,人工添加的节点可指向本局域网的节点,从而加速区块数据传输。

        Addrman管理器采用“桶(bucket)”设计,它被设计成1024个桶,每个桶可保存64个节点。同一个组(Group)的地址不允许重复加入,组(group)是根据IP地址所属的AS计算得出(后面会介绍)。在连接时,采用高度随机算法从桶中选取,因此可最大限度避免黑客Erebus攻击。在Addrman管理中对于“桶“维持了一个内存表,系统会定期将桶中的内容转存到磁盘peers.dat文件。Bitcoin客户端在每次启动时都会从DNS种子域名动态获得的多个节点IP地址,这些节点可能是最新的,也可能是旧的。随着时间推移,peers.dat文件中的节点也会越来越多。如何从众多的节点中选择要连接的接点,以及这些节点如何淘汰(逐出)就显得非常重要。

节点如何选择

        Bitcoin建立Outbound类型的连接(即作为客户端)时通过程序(src/net.cpp)中的以下两个线程来完成:

ThreadOpenAddedConnections:

        用于连接通过-addnode参数或RPC命令动态添加的节点。连接数量不超过信标变量semAddnode(信号量nMaxAddnode= 8)限制。

ThreadOpenConnections:

        当通过-connect参数指定了要连接的节点时,该线程只连接-seednode设定的节点和-connect设定的节点。其中连接到-seednode设置的节点数量不超过信标变量semOutbound(m_max_outbound=11, m_max_outbound = m_max_outbound_full_relay(8) + m_max_outbound_block_relay(2) + nMaxFeeler(1))。但连接-connect设置的节点数量没有限制。在这种情况下,不连接放入到Addrman“桶”中的节点,当然也不存在如何从“桶”选择节点的问题,相当于用户只连接自己指定的节点。

        当用户没有设置-connect参数时,该线程只连接-seednode设置的节点和通过解析DNS种子域名获得的节点,并将其放入Addrman管理的“桶”中,这时就存在如何从“桶”中选择的节点问题。在连接节点时,该线程按顺序每从seednode容器选择一个节点,就从“桶”中选择一个节点,当没有指定任何seednode节点时,系统将100%从“桶”中选择。从“桶”选择节点具有高度的随机性,并不是按顺序选择。从“桶“中选择的代码如下:

选择算法:

// https://github.com/bitcoin/bitcoin/blob/master/src/addrman.cpp
std::pair<CAddress, int64_t> AddrManImpl::Select_(bool newOnly) const
{
    AssertLockHeld(cs);

    if (vRandom.empty()) return {};

    if (newOnly && nNew == 0) return {};

    // Use a 50% chance for choosing between tried and new table entries.
    if (!newOnly &&
       (nTried > 0 && (nNew == 0 || insecure_rand.randbool() == 0))) {
        // use a tried node
        double fChanceFactor = 1.0;
        while (1) {
            // Pick a tried bucket, and an initial position in that bucket.
            int nKBucket = insecure_rand.randrange(ADDRMAN_TRIED_BUCKET_COUNT);
            int nKBucketPos = insecure_rand.randrange(ADDRMAN_BUCKET_SIZE);
            // Iterate over the positions of that bucket, starting at the initial one,
            // and looping around.
            int i;
            for (i = 0; i < ADDRMAN_BUCKET_SIZE; ++i) {
                if (vvTried[nKBucket][(nKBucketPos + i) % ADDRMAN_BUCKET_SIZE] != -1) break;
            }
            // If the bucket is entirely empty, start over with a (likely) different one.
            if (i == ADDRMAN_BUCKET_SIZE) continue;
            // Find the entry to return.
            int nId = vvTried[nKBucket][(nKBucketPos + i) % ADDRMAN_BUCKET_SIZE];
            const auto it_found{mapInfo.find(nId)};
            assert(it_found != mapInfo.end());
            const AddrInfo& info{it_found->second};
            // With probability GetChance() * fChanceFactor, return the entry.
            if (insecure_rand.randbits(30) < fChanceFactor * info.GetChance() * (1 << 30)) {
                LogPrint(BCLog::ADDRMAN, "Selected %s from tried\n", info.ToString());
                return {info, info.nLastTry};
            }
            // Otherwise start over with a (likely) different bucket, and increased chance factor.
            fChanceFactor *= 1.2;
        }
    } else {
        // use a new node
        double fChanceFactor = 1.0;
        while (1) {
            // Pick a new bucket, and an initial position in that bucket.
            int nUBucket = insecure_rand.randrange(ADDRMAN_NEW_BUCKET_COUNT);
            int nUBucketPos = insecure_rand.randrange(ADDRMAN_BUCKET_SIZE);
            // Iterate over the positions of that bucket, starting at the initial one,
            // and looping around.
            int i;
            for (i = 0; i < ADDRMAN_BUCKET_SIZE; ++i) {
                if (vvNew[nUBucket][(nUBucketPos + i) % ADDRMAN_BUCKET_SIZE] != -1) break;
            }
            // If the bucket is entirely empty, start over with a (likely) different one.
            if (i == ADDRMAN_BUCKET_SIZE) continue;
            // Find the entry to return.
            int nId = vvNew[nUBucket][(nUBucketPos + i) % ADDRMAN_BUCKET_SIZE];
            const auto it_found{mapInfo.find(nId)};
            assert(it_found != mapInfo.end());
            const AddrInfo& info{it_found->second};
            // With probability GetChance() * fChanceFactor, return the entry.
            if (insecure_rand.randbits(30) < fChanceFactor * info.GetChance() * (1 << 30)) {
                LogPrint(BCLog::ADDRMAN, "Selected %s from new\n", info.ToString());
                return {info, info.nLastTry};
            }
            // Otherwise start over with a (likely) different bucket, and increased chance factor.
            fChanceFactor *= 1.2;
        }
    }
}

        上面的代码展示了从“新桶(new bucket)“和”尝试桶(tried bucket)“中随机选择节点的代码。桶(bucket)是一个二维数组,第一维是表示桶的数量,第二维表示每个桶可存放的节点数。其中桶的数量:

新桶:ADDRMAN_NEW_BUCKET_COUNT=1024

尝试桶:ADDRMAN_TRIED_BUCKET_COUNT= 256

每个桶的大小:ADDRMAN_BUCKET_SIZE = 64

系统使用randrange随机函数获得上不超过上述这些数字大小的随机数来定位桶的位置,并从桶中获得节点信息。

试探性连接FEELER:

        在选择节点连接类型时,有一个类型名叫FEELER,可翻译为试探性连接。试探性连接是用于检查节点是否处于活动状态的短期连接,它可用于逐出前的测试。具体过程是:在ThreadOpenConnections过程中,系统定期随机选取一个节点建立一个试探性连接,如果一个对等节点(peer)被我们考虑从节点管理器中逐出,是因为另外一个对等节点(peer)已映射到尝试列表中的相同的槽内。只有当这个更早的节点离线时才逐出。 在逐出前,先将试探性节点从”新桶(NEW)”移到”尝试桶(TRIED)“列表。以便我们在地址管理器中空出更多的桶来保存新的节点。

ASMAP技术

        如何防止国家级别的ISP或大型的网络服务商可能进行的Erebus攻击,Bitcoin实验性地使用了ASMAP技术。它主要通过计算节点IP地址的分组并确保处于同一个分组的IP地址只能占用一个桶。并在连接时相同的分组也只能连接一个节点。这样对于那些拥有庞大数量的公网IP的大型网络服务商发起Erebus攻击变得不可能。分组(Group)是根据AS计算出来的。在进一步了解ASMAP之前,我们先了解以下几个概念:

RIPE NCC:欧洲IP网络资源协调中心(Réseaux IP Européens Network Coordination Centre,缩写作 RIPE NCC),全球五大区域性互联网注册管理机构之一, 是负责管理欧洲、西亚、前苏联地区Internet资源的区域互联网注册管理机构。

RIS Raw Data:RIS(Routing Information Service)为了帮助网络操作者认识理解网络中的路由,RIPE 提供了 RIS(Routing Information Service)。RIS 利用在世界各地部署的远程路由收集器(Remote Route Collectors,RRCs),收集并存储网络中的路由数据。志愿与 Collector 进行连接的 AS利用 BGP 协议向 Collector 发送 BGP Update 信息,RIS 也会依据相关消息存储以及撤回对应的路由信息。RIS 会免费提供每个收集器收集到的路由表信息,并以 MRT 格式进行存储。每一个 RRC 会存储两类数据:

  • 所有的 BGP 报文:这些文件以 “updates”开头,每 5 分钟创建一次。
  • 完整的 BGP 路由表:这些文件以“bview”开头,每 8 小时创建一次。

bview开头的文件较大(可能超过1G),updates开头的文件较小。

比如RRC00表示编号为00的RRC收集的路由数据,它来自下面的链接地址:

https://www.ripe.net/analyse/internet-measurements/routing-information-service-ris/ris-raw-data

其中的rrc00.ripe.net表明位于阿姆斯特丹 RIPE NCC 的 rrc00.ripe.net 多跳收集器从 1999 年 10 月开始收集来自世界各地同行的更新。

ASN:AS(Autonomous System Number):自治系统编号,指在一个(有时是多个)组织管辖下的所有IP网络和路由器的全体,它们对互联网执行共同的路由策略。也就是说,对于互联网来说,一个AS是一个独立的整体网络。而BGP实现的网络自治也是指各个AS自治。每个AS有自己唯一的编号。

BGP:边界网关协议(Border Gateway Protocol,缩写:BGP)是互联网上一个核心的去中心化自治路由协议。它通过维护IP路由表或“前缀”表来实现自治系统(AS)之间的可达性,属于矢量路由协议。BGP不使用传统的内部网关协议(IGP)的指标,而使用基于路径、网络策略或规则集来决定路由。因此,它更适合被称为矢量性协议,而不是路由协议。 大多数互联网服务提供商必须使用BGP来与其他ISP建立路由连接(尤其是当它们采取多宿主连接时)。因此,即使大多数互联网用户不直接使用它,但是与7号信令系统——即通过PSTN的跨供应商核心响应设置协议相比,BGP仍然是互联网最重要的协议之一。特大型的私有IP网络也可以使用BGP。例如,当需要将若干个大型的OSPF(开放最短路径优先)网络进行合并,而OSPF本身又无法提供这种可扩展性时。使用BGP的另一个原因是其能为多宿主的单个或多个ISP(RFC 1998)网络提供更好的冗余。用户可以通过下面的网站来查询IP地址所属的ASN记忆路由信息。

ASN查询_专业的 IP 地址库_IPIP.NEThttps://tools.ipip.net/as.php

比如我们查询202.96.128.68,它显示的ASN信息是:

AS4134 CHINANET-BACKBONE - No.31,Jin-rong Street, CN

说明该IP所属的ASN是4134,属于中国电信的骨干网。

构建asmap.dat

        asmap.dat是根据RIS Raw data构建的用于Bitcoind对节点IP进行分组的数据文件。RIS Raw data是MRT格式,不能直接用于构建asmap数据文件,必须进行转换,下图描述了这一流程:

下面的命令演示了如何使用相关工具来生成asmap.dat:

// 下面以在centos上的操作为例,说明如何使用asmap工具
// 其中//表示注释, # 表示命令行提示符
// 安装Rust
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# source $HOME/.cargo/env
// 显示rust的版本
# rustc --version
rustc 1.58.1 (db9d1b20b 2022-01-20)

// 安装openssl并指定OPENSSL_LIB_DIR和OPENSSL_INCLUDE_DIR
# yum isntall  openssl-devel
# vim ~/.bashrc
// 添加以下内容:
export OPENSSL_LIB_DIR=/lib64
export OPENSSL_INCLUDE_DIR=/usr/include/openssl

// 使生效
# source ~/.bashrc

// 编译bitcoin-asmap
# mkdir ~/asmap-work
# git clone https://github.com/sipa/bitcoin -b 202004_asmap_tool && cd bitcoin
# ./autogen.sh
# ./configure --disable-shared --without-gui --disable-wallet
# make
# sudo cp src/bitcoin-asmap /usr/bin/.

// 下载asmap-rs工具
# cd ~/asmap-work
# git clone https://github.com/rrybarczyk/asmap-rs && cd asmap-rs 

// 下载RIS raw data (只下载rrc00: https://www.ripe.net/analyse/internet-measurements/routing-information-service-ris/ris-raw-data)
// 下载成功后会在dump目录下生成rrc00-latest-bview.gz,该文件大小可能超过1G.
# cargo run --release download -n 0

// 生成 ASN bottleneck, 它是一串IP地址和对应的ASN编号的字符串,如:
// 193.189.95.0/24 AS34906
// 193.189.96.0/24 AS20850
// 193.189.98.0/23 AS33925
// 193.190.0.0/15 AS2611

// 读取并解压dump目录下的 MRT gz 文件, 解析AS路径, 并保存结果到bottleneck目录。
// 成功后在bottleneck目录脚下生成了 bottleneck.xxxxxxx.txt文件,该文件大小约为25M
# mkdir bottleneck
# cargo run --release find-bottleneck -d dump -o bottleneck

// 文件开始两行的数据如下:
// 120.236.88.0/22 AS9808
// 46.38.51.224/32 AS20764
// 至此我们已经通过RIW raw data生成了asmap所需的数据文件。

// 编码 ASN bottleneck,生成asmap.dat
# bitcoin-asmap encode ~/asmap-work/asmap.dat < bottleneck/bottleneck.*.txt

 至此,我们就可以在bitcoin的参数配置文件文中使用asmap.dat,可以在配置文件添加如下内容:

asmap=/path/to/asmap-work/asmap.dat

解码asmap.dat

        asmap.dat中的所有的数据都是0和1两种形态。系统从asmap.dat读取的数据放在容器(std::vector<bool>)中,当使用时再从容器中将其解码为ASN并根据IP地址转换为Group。在Github中

https://github.com/bitcoin/bitcoin/blob/master/src/util/asmap.cpp

详细说明解码过程。

ASMAP使用:

        在解析DNS种子域名获取节点地址时,将根据asmap计算IP地址的Group,相同的Group将拥有相同的桶号。因此对于防止来自同一个AS的节点将起到过滤作用。在连接节点时,相同Group的节点只允许连接一个,src/net.cpp中的代码说明了这个:

……
// Require outbound connections, other than feelers, to be to distinct network groups
if (!fFeeler && setConnected.count(addr.GetGroup(addrman.GetAsmap()))) {
    break;
}
……

淘汰机制

        启动Bitcoin客户端时从peers.dat加载历史节点,将检查节点是否符合淘汰(逐出)条件,如果符合则将节点从桶中逐出。这些条件是:

  1. 不合理的存活时间,节点的生日(加入到addrman中的时间)超过当前时间10分钟。
  2. 地址存活时间已超过30天。
  3. 尝试了3次但没有一次连接成功。
  4. 距离最近一次连接成功的时间已超过7天, 且最近一周连续失败次数大于等于10次。

相关代码:


https://github.com/bitcoin/bitcoin/blob/master/src/net.cpp

ASMAP相关工具:

https://gist.github.com/cryptagoras/45b2839a1f662c769b94b868ce222d3d

https://github.com/rrybarczyk/asmap-rs

猜你喜欢

转载自blog.csdn.net/dragon_trooquant/article/details/122967983