比特币开发指南——P2P网络

96
通若
4.2 2017.11.30 13:46 字数 5968

比特币网络协议允许完整节点为了区块和交易的交换协作维护P2P网络。在把区块和交易转发到其他节点之前,完整节点下载和验证每个区块和交易。文档节点是存储了整个区块链并且能够为其他节点提供历史区块的完整节点。修剪节点是不存储整个区块链的完整节点。许多SPV客户端也使用比特币网络协议连接到完整节点。
共识规则不包含网络,所以比特币程序可能使用替代的网络和协议,例如一些矿工使用的高速区块转发网络和一些提供SPV级别安全的钱包使用的提供交易信息的服务器。

为了提供实际的比特币P2P网络的例子,本章使用比特币核心作为完整节点代表,使用比特币J作为SPV客户端代表。两个程序都是灵活的,所以仅描述了默认的行为。而且为了隐私,例子中实际的IP地址使用RFC5737保留的IP地址替代。

节点发现

第一次启动时,程序不知道任何活跃的完整节点的IP地址。为了发现一些IP地址,他们查询一个或多个DNS名称(叫做DNS种子),硬编码成比特币核心和比特币J。查询的响应应包括一个或多个DNS 主地址记录,该记录具有可能接受新收入连接的完整节点的IP地址。例如,使用Unixdig命令

;; QUESTION SECTION:
;seed.bitcoin.sipa.be.      IN  A

;; ANSWER SECTION:
seed.bitcoin.sipa.be.   60  IN  A  192.0.2.113
seed.bitcoin.sipa.be.   60  IN  A  198.51.100.231
seed.bitcoin.sipa.be.   60  IN  A  203.0.113.183
[...]

DNS种子被比特币社区成员维护:一些成员提供动态的DNS种子服务器,该服务器自动通过扫描网络获取活跃节点的IP地址;其他成员提供需要手动更新的种子,而且更可能为非活跃节点提供IP地址。在两种情况中,如果节点运行在主网默认的8333端口或测试网络默认的18333端口,将被添加到DNS种子中。

DNS种子结果是未被认证的,而且恶意节点操作者或网络中间人攻击者可以只返回被攻击者控制的节点的IP地址,在攻击者自己的网络中隔离程序,并且允许攻击者壮大它的交易和区块。因此,程序不应该依赖唯一的DNS种子。

一旦程序连接到网络中,节点会把网络中具有其他节点的IP地址和端口号的地址信息addr发送给它,提供完全去中心化的节点发现方法。比特币核心在持久的硬盘数据库中保存已知节点的记录,这通常允许它直接连接到后续启动的节点,而不用使用DNS种子。

然而,节点经常离开网络或改变IP地址,所以在成功连接之前,程序可能需要在启动的时候尝试几种不同的连接方式。这会增加连接网络的大量的时间延迟,需要强迫用户在发送交易或检查支付状态之前等待。

为了避免可能的延迟,比特币J总是使用动态DNS种子为被相信当前是活跃状态的种子获取IP地址。比特币核心也尝试在最小化延迟和避免不必要的DNS种子使用之间取得平衡:如果比特币核心进入节点数据库,在回到种子之前,要花费11秒尝试连接到至少它们中的一个;如果连接在规定时间实现,不会查询任何种子。

比特币核心和比特币J也把一个硬编码的IP地址列表和端口号加入到好几十个节点中,这些节点在特定的软件版本被第一次发布时是活跃的。比特币核心也将开始尝试连接到这些节点如果没有DNS种子服务器在60秒内相应查询的话,提供一个自动化回退选项。

作为一个手动的回退选项,比特币核心也提供好几个命令行连接选项,包括从一个特定的节点通过IP地址获取节点列表,或者通过IP地址持久连接到一个特定节点。详情请看-help文本。比特币J也能被编程做相同的事。

比特币播种人,被比特币核心和比特币J使用的好几个节点运行的程序。比特币核心DNS种子正常。被比特币核心和比特币J使用的硬编码的IP地址列表是使用makesedds 脚本生成。

连接节点

通过发送version信息连接到节点,该信息包含你的版本号、区块和当前发送给远程节点的时间。远程节点使用它自己的version信息响应。然后两个节点互相发送verack信息表明连接已经建立。

一旦连接,客户端会给远程节点发送getaddraddr信息获取额外的节点。

为了和节点维持连接,节点默认在激活的30分钟前给节点发送信息。如果90分钟过去了,没有收到任何信息,客户端将假设连接已经关闭。

初始化区块下载

在完整节点验证未确认交易和最新挖出的区块之前,必须下载和验证从区块1到最佳区块链的顶部的所有区块。这就是初始化区块下载(IBD)或初始化同步。

虽然单词“初始化”暗示该方法仅适用一次,但是在大数量区块需要下载的时候,也可使用。例如当先前捕获节点离线很长一段时间。这种情况下,节点可以使用初始化区块下载方法下载自它最后在线时间到现在生产的所有的区块。

比特币核心任何时间都可使用IBD方法,只要最后的区块在本地的最佳区块链上有个区块头部时间超过24h。如果本地的最佳区块链超过144个区块,但是比本地最佳头部链低(也就是,本地区块链过去超过24h),比特币核心0.10.0也将实现IBD。

第一块

比特币核心(直到0.9.3)使用一个简单的初始化区块下载(IBD)方法,我们叫做“第一块”。目标是去从最佳区块链上按序下载区块。


image.png

节点第一次启动,在本地最佳区块链中只有一个区块——硬编码的创世区块(区块0)。该节点选择一个远程节点,叫做同步节点,并发送如下表所示getblocks。信息。

image.png

getblocks信息的header hashes字段中,新节点发送它唯一仅有的区块——创世区块的头部hash。也把stop hash字段设置成0以便请求最大数量的响应。

直到getblocks信息接收的时候,同步节点获取第一个(唯一的一个)头部hash并搜索包含该头部hash的区块所在的最佳区块链。它发现区块0匹配,所以使用从区块1开始的500个区块清单(getblocks信息最大响应)。把这些清单在inv信息中发送,如下图示:

image.png

清单是网络中信息的唯一标识。每个清单包含一个类型字段和对象实例唯一的标识符。对于区块,唯一标识符是一个区块头部的hash。

inv中的区块清单的顺序和出现在区块链中的顺序相同,所以第一个inv信息包含区块1的存储

IBD节点使用接收到的存货从同步节点中请求128个区块在getdata信息中,如下所示:

image.png

因为每个区块头部引用之前区块的头部hash,所以按序请求和发送区块对于第一节点来说很重要。这意味着IBD节点不能完全验证区块直到接收到父区块。那些因为没有接收到父区块所以无法验证有效性的区块叫做孤立区块;下面子章节将详细描述。

区块优先的优点和缺点

第一区块IBD主要优点是简单。主要缺点是IBD节点依赖于单个同步节点进行所有下载。这给我几点启示:

  1. 限速:所有请求都面向同步节点,所以如果同步节点限制了上传带宽,IBD节点将减缓下载速度。注:如果同步节点离线,IBD节点将继续从其他节点下载——但是同一时间仍然只会从一个单独的同步节点下载
  2. 重新下载:同步节点可以给IBD节点发送一个非最佳区块链。IBD节点将无法识别它并非是最佳链,直到初始化区块下载接近完成的时候,强迫IBD节点从另一个节点重新下载。比特币核心在不同的区块高度上放置了几个区块链校验点,帮助IBD节点检测它是一个可选的区块链历史——允许IBD节点在过程早期重新下载。
  3. 磁盘填满攻击:和重新下载紧密相关,如果同步节点发送一个非最佳区块链,链将被存储在磁盘中,浪费空间而且可能以无用的数据填满磁盘。
  4. 高内存使用:不管是故意还是碰巧,同步节点都能发送无序区块,创建无法被验证有效性的孤立区块,直到接收并验证了它们的父区块。孤立区块在等待验证期间一直存储在内存中,这可能会导致高内存使用。
  5. 所有的这些问题都是由在比特币核心0.10.0中使用的头部优先IBD方法部分或全部导致。

下面的表格总结了本子章节提到的信息。

image.png

头部优先

比特币核心0.10.使用一个初始化区块下载(IBD)方法叫做头部优先。目标是下载最佳头部链的头部,尽可能的部分地验证它们,而且并行地下载相应的区块。这解决了旧的区块优先IBD方法的几个问题。

image.png

节点第一次启动时,在本地最佳区块链中仅有一个独立区块——硬编码的创世区块(区块0)。该节点选择一个远程节点,我们叫做同步节点,并发送如下图所示的getheaders信息。

image.png

getheaders信息的header hashes字段中,新节点仅发送它仅有的区块——创世区块的头部hash。也把stop hash字段全部设置成0请求最大响应。

接收到getheaders信息后,同步节点获取第一个头部hash并搜索包含该头部hash的最佳区块链。它发现区块0符合,所以使用从区块1开始的2000(最大响应)个头部回应0。它在headers信息中发送这些头部hash。

image.png

IBD节点可以部分地验证这些区块头部,通过确保所有的字段遵循共识规则,而且根据nBits字段判断头部的hash值低于目标阈值(完全验证需要从相应区块请求所有交易)

IBD节点部分地验证区块头部之后,可以同时做两件事:

  1. 下载更多头部:IBD节点可以给同步节点发送另一个getheaders信息,以请求最佳头部链上的下2000个头部。这些头部可以立刻被验证并重复请求另一批,直到从同步节点接收到的headers信息少于2000个头部,表明没有头部可提供了。自本文起,头部同步可以在200次内完成,或者大约下载32M数据。
    一旦IBD节点从同步节点接受到的headers信息少于2000个头部,给每个节点发送getheaders信息获取他们的最佳头部链。通过比较反馈,可以轻易地判断下载的头部是否属于任意外部节点的最佳头部链。这意味着不诚实的同步节点将很快被发现,甚至校验点都尚未使用(只要IBD节点连接到至少一个诚实节点;比特币核心将继续提供校验点以防没有发现诚实节点)。
  2. 下载区块:IBD节点继续下载头部时,以及头部完成下载之后,IBD节点将请求和下载每个区块。IBD节点可以使用从头部链计算出的头部hash创建请求它所需要的区块的getdata数据。不需要从同步节点请求这些——可以从任意完整节点请求。(虽然不是所有的完整节点都可能存储所有区块)这允许它并行捕获区块而且避免下载速度受限于单独同步节点的上传速度。

为了在多个节点间传播加载,比特币核心一次从一个独立节点仅请求最大16个区块。最多可以连接到8个外部节点,这意味着头部优先比特币核心同时可以最多请求128个区块(和区块优先比特币核心从同步节点请求的最大数量相同)


image.png

比特币核心的头部优先模式使用1024个区块动态窗口最大化下载速度。窗口中最低高度的区块是下一个要被验证的区块。如果区块没有在比特币核心准备验证的时候抵达,比特币核心将等待失速节点2秒钟去发送区块。如果区块仍未抵达,比特币核心将与失速节点断开连接并请求连接其他节点。例如,在上面的图表中,节点A将会被断开,如果没有在2秒内发送区块3。

一旦IBD节点同步到区块链顶端,将接受被后续章节描述的定期区块广播发送的区块。

注:下面的表格总结了本子章节提到的信息。


image.png

区块广播

当一个矿工发现一个新区块时,使用以下方法中的一个把新区块广播给它的节点:

  1. 主动区块推送:矿工发送一个带有新区块的block信息给它的每个完整节点。以这种方式矿工可以理性地绕过标准的中继方法,因为它知道它的节点尚未拥有刚发现的区块。
  2. 标准区块中继:矿工,像一个标准的中继节点行动,发送具有索引新区块的清单的inv信息给它的每个节点(完整节点和SPV)。最常见的响应是:
  • 每个想要通过getdata信息获取区块的区块优先(BF)节点请求完整区块。

  • 每个想要通过getheaders返回区块的区块头部节点包含了最佳头部链中的高度最高的头部的头部hash,而且也可能一些头部进一步返回到最佳头部链允许分叉检测。该信息立刻被一个请求完整区块的getdata信息跟随。通过首先请求头部,头部优先节点可以拒绝孤立区块,如下面子章节描述。

  • 每个想通过getdata获取区块的简化的支付验证(SPV)客户端典型地请求一个merkle区块。
    矿工根据block信息中的区块、headers信息中的一个或多个头部、被0或更多tx信息跟随的merkleblock信息中的与SPV客户端的布鲁姆过滤器相关的merkle区块和交易返回每个请求。

  • 直接的头部公告:一个中继节点通过发送包含所有新区块的头部的headers信息,可以忽略被getheaders跟随的inv信息的往返负载。一个接收这个头部优先节点将部分地验证区块头部,正如在头部优先初始化区块下载期间所做的,如果头部是有效的,使用getdata信息请求完整区块内容。中继节点然后使用blockmerkleblock信息中的完整的或过滤了的区块数据响应getdata数据。通过在连接握手期间发送一个特殊的sendheaders信息,一个头部优先节点或许会标明它更愿接收headers代替inv声明。

区块广播的协议是在BIP 130提出的,而且自从版本0.12开始在比特币核心实现。

默认情况下,比特币核心广播使用了直接头部声明给任意标识了sendheaders的区块,并针对所有尚未使用标准区块的中继使用标准的区块中继。比特币核心将接受发送的使用了上面任意一种方法的区块。

完全节点验证和接受区块,并使用上述描述的标准区块中继方法推荐给其他节点。下面的简明表指出了上面描述的信息的操作(中继、BF、HF和涉及中继节点的SPV、区块优先节点、头部优先节点和一个SPV客户端;任意涉及到使用区块检索方法的节点)


image.png

孤立区块

区块优先节点或许下载孤立区块——该区块的前一个区块头部的hash字段涉及到一个该节点尚未看到的区块头部。换句话说说,孤立区块没有已知的父区块(不像陈旧区块,虽然有已知的父区块,但是不是最佳区块链的一部分)


image.png

当一个区块优先节点下载一个孤立区块,将不会验证它。相反,会给发送该孤立区块的节点发送一个getblocks信息;广播节点将使用包含了下载节点缺失的任意区块的清单信息的inv信息响应。下载节点将使用getdata信息请求这些区块;广播节点将将发送这些带有block信息的区块。下载节点将验证这些区块,一旦之前的孤立区块的父区块已经被验证,它将验证之前的孤立区块。

头部优先节点通过在使用getdata信息请求区块之前,使用getheaders请求区块头部避免这类复杂性。广播节点将发送包含了他认为下载节点需要掌握的最佳头部链的顶部的所有的区块头部(最大2000);每一个头部都将指明父头部,所以当下载节点收到block信息时,区块不会成为孤立区块——所有的父区块都已知(甚至虽然还尚未验证)。如果不管这些的话,block信息中接受到的区块就是孤立区块,头部优先的节点将会立刻将之丢弃。

然而,在主动区块推送方法中,孤立丢弃意味着头部优先节点将忽略矿工发出的孤立区块。

交易广播

为了给节点发送一笔交易,需要发送inv信息。如果接收到getdata响应信息,使用tx发送交易。接收到交易的节点也将以同样的方式转发交易,假定它是个有效的交易。

内存池

完整节点可能会跟踪有资格被加入到下一个区块中的未确认交易。对于那些想要计算一些或全部交易的矿工来说,这是非常基础的,但是对于任意想要跟踪未确认交易的节点来说也是非常有用的,例如为SPV客户端提供未确认交易信息的节点。

因为未确认交易在比特币中没有永久的地位,比特币核心在非持久性内存中存储它们,该内存叫做内存池或成员池。当节点关闭时,内存池也丢失除非交易被钱包存储起来。这意味着从未被计算的未确认交易可能在网络中出现的很慢,因为节点重启或因为清除一些交易,为其他交易创建空间。

被加入到后续成为陈旧区块的区块中的交易或许会被添加到内存池中。如果替换区块加入了他们,这些重新加入的区块可能立刻又被从内存池中移除。这是比特币核心中的情况,从链顶按序移除陈旧区块。因为区块被移除,交易被重新加入到内存池中。所有的陈旧区块都移除之后,替换区块按序加入到链中。因为每个区块都添加,任何确认的交易都会从内存池中移除。

SPV客户端没有内存池因为他们不转发交易。他们不能立刻验证交易是否被加入到区块中,只能花费UTXO,所以他们无法知道哪笔交易有资格被加入到下一个区块中。

行为不端的节点

注意两种广播方式,机制到位惩罚通过发送错误信息消耗带宽和计算力的行为不端的节点。如果节点得到一个禁止分高于banscore=<n>阈值,将被禁止-bantime=<n>定义的一段时间,默认是24h。

警报

在比特币核心0.13.0中移除

比特币早期版本运行开发者和真实的社区成员处理比特币警报,提示用户全网范围严重的问题。该消息系统在比特币0.13.0版本曲线。然而,内部警报,部分检测讲稿和-alertnotify可选功能保留了。

随想录
Web note ad 1