P2P网络 - 比特币开发指南

P2P网络 - 比特币开发指南

原文链接: https://bitcoin.org/en/developer-guide#operating-modes

翻译: terryc007

版本:1.0


比特币开发指南

1. 区块链

2. 交易

3.合约

4.钱包

5.支付处理

6.工作模式

7.P2P网络

8.挖矿


比特币网络协议允许全节点一起协作维持p2p网络,实现区块,交易数据的交换。全节点下载,并验证每个个区块,交易,然后在转发给其他节点。档案节点是一种全节点,它会存储各个区块链,并为其他节点提供历史区块服务。裁剪节点是一种不会保存这个区块链的全节点。很多SPV客户端也使用比特币网络协议来连接全节点。

因为共识规则不涉及到网络,因此比特币程序可以选择不同的网络,不同的协议,比如一些矿工使用高速区块转发网络,一些提供SPV级别安全的钱包,使用专业的交易信息服务器。

为提高一个可实操的比特币P2P网络例子,这个章节使用比特币内核作为典型的全节点,BitcoinJ作为典型SPV客户端。这两个程序都是灵活的,因此这里只讨论默认他们的行为。同时,考虑到隐私,下面例子中的ip地址已经被替换成RFC5737预留的IP地址。

节点发现

当程序第一启动时,它并不知道任何活跃节点的ip地址。为了发现一些全节点的ip地址,他们会查询硬编码在比特币内核或BitCoinJ中的,一个或多个DNS域名,在返回的结果中应该包含一个或多个DNS A记录,里面有一些可接受新连接的全节点的ip地址。 比如,使用Unix命令 dig:

;;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地址;其他的提供一些静态DNS种子,这需要手动更新,不过他们很有可能提供不活跃节点的ip地址。不管是动态的,还是静态的DNS种子,如果节点在主网上运行在端口号8333,或在测试网络运行在端口号18333,就会被加入到DNS种子。

DNS种子结果没有被授权,一个恶意的DNS种子运营者或网络中间人攻击者能返回仅被攻击者控制的节点的ip地址,在攻击者自己的网络中,孤立节点,并给他们假的交易,区块数据。因为这个原因,程序不应该只依赖一个DNS种子。

一但程序连接上比特币网络,它的节点就可以开始发送,带有网络中其他节点IP地址,端口号的addr消息给其他节点。这个提供了一个完整的去中心化节点发现方法。比特币内核会在本地数据库中保存已知节点的信息,通常,等下一次程序启动时,它就不需要使用DNS种子,直接可以跟这些节点连接即可。

然而,节点通常会离开网络或者改变ip地址,这样程序在启动时,在需要多次尝试才有可能连接到比特币网络。这了会增加连接到比特币网络的延迟时间,使得用户在发送交易或检查支付状态前,不得不等待一段时间。

为避免这种延迟,BitcoinJ总是使用动态DNS种子,来获取那些被确定为活跃节点的IP地址。比特币处内核也尝试在降低延迟,避免使用不必要的DNS节点中权衡。如果比特币内核在它的节点数据库中有记录,它就会用11秒时间去连接至少其中一个节点,失败后,才使用DNS节点获取ip地址;如果在11秒内成功建立连接,则不在向DNS种子查询。

比特币内核跟BitcoinJ在其他特定版本的第一版本发布时,他们在代码里面都硬编码了一些节点当时活跃节点的IP地址跟端口号。比特币内核内置了一个自动回调选项,当没有DNS种子服务器在60秒内回应查询,比特币内核会开始尝试跟这些节点连接。

作为一个手段回调选项,比特币内核也提供了好几个命令行连接选项,包括从一个指定节点,通过其IP地址获取一个节点列表,或直接跟一个指定节点,通过IP地址建立持久连接。通过-help获取命令行详情。BitcoinJ也可以通过编程实现这样的功能。

资源: Bitcoin种子, 这个程序管理了好几个比特币内核,BitcoinJ都有用到的DNS种子。比特币内核DNS种子政策。比特币内核,BitcoinJ里硬编码的节点IP地址是使用makeseeds script生成的。

连接到节点

通过给远程节点,发送version 消息跟他节点建立连接,消息里面包括软件版本号,区块,当前时间。远程节点也返回一个version消息。然后,他们再给其他节点发送verack消息,表示他们已经建立连接。

一但建立连接,客户端就能给远程节点发送getaddraddr消息获取其他节点信息。

客户端为维持跟节点的连接,在其离线前30分钟,它会给其他节点发送一个消息。如果节点在90分钟内,没有返回消息,那么这个客户端就认为连接已经关闭。

初始化区块下载

在全节点验证非确认交易,最近挖出的区块前,它必须下载,验证最佳区块链上所有的区块(从创世区块到最顶部的区块)。这叫做初始化区块下载(IBD)或者叫初始化同步。

虽然“初始化” 意味着这个方法只会使用一次,但可以在任何时候,在下载大量区块的时候,用到它,比如当一个之前连接过的节点下线了很长一段时间。这种情况,节点能使用IBD方法去下载从它最近上线以来,所有的挖出的区块。

在任何时候,只要比特币内核本地最佳区块链上,它最新的区块的区块头时间,以及超过了24小时,它就会使用IBD方法获取新的区块。如果本地最佳区块头链,比本地最佳区块链多出144个区块(这也就意味着,本地最佳区块链已经有24小时没有更新了),比特币内核0.10.0也会使用IBD方法。

区块优先

比特币内核(一直到0.9.3版本为止)使用简单的初始化区块下载(IBD)方法,我们称之为区块优先。它的目标是从最佳区块链按序下载区块。

节点第一启动时,在它本地最佳区块链只有一个区块 — 硬编码的创世区块(区块0)。节点了会选择一个远程节点(也叫同步节点),并给它发送一个getblocks消息,如下图所示:

getblocks消息头部哈希域,这个新节点发这个区块仅有的头哈希 - 创世区块(6fe2...0000 内部字节序)。同时它把停止哈希域全设置为0以获取最大返回结果。

同步节点一但收到这个getblocks消息,它使用第一个哈希头去它本地最佳区块链搜索带有这个哈希头的区块。如果找到block 0与之匹配,它就从区块1开始,返回500个区块清单(getblocks消息返回的最大个数)。它使用inv消息来发送这些区块清单。 如下面所示:

存货清单是一些在比特币网络上独一无二的身份标识信息。每个存货包含一个类型,一个对象实例的唯一标识。对于区块而言,这个唯一标识是区块头的哈希值。

inv消息中区块存货信息的顺序跟其在区块链中的是一样的,因此第一个inv消息包含了区块1到区块501存货信息。(比如,如上面所示,区块1的哈希值是4860...0000)

IBD节点使用收到存货清单,通过getdata消息,从同步节点获取128个区块。如下图所示:

对于区块优先的节点而言,按序请求,发送区块是非常重要的,因为每个区块头会引用它前面的区块头。这就意味IBD节点,必须在父区块还没有接收完之前,是不能完全的验证区块的。之所以不能验证区块,是因为那些没有收到父区块的区块是孤块;下小节会详细地介绍到。

一但收到getdata消息,同步节点就会把请求的区块返回给IBD节点。每个区块被序列化成区块格式,并发送各自的block消息。发送的第一个block消息(block1),如下图所示。

IBD节点下载每个区块,并验证,然后获取下一个还未请求过的区块,并维持一个128个区块的下载队列。当它获取完存货清单中所有的区块后,它就发送另外一个getblocks消息给同步节点,以获取最多500个区块的存货清单。第二个getblocks消息包含多个区块头哈希,如下图所示:

同步节点一但收到第二个getblocks消息,它就按照收到区块头哈希的顺序,挨个在它本地最佳的区块链中寻找匹配的区块。如果它找到一个匹配的区块,它会从该区块的下个区块开始,返回500个区块存货清单。 如果没有找到一个匹配的哈希(除了那个截止哈希外),它会假定这个两个节点只有block0是一样的,因此它会发送一个从block1开始的inv消息。

通过这样的重复查找,允许同步节点发送有用的存货清单,即使IDB节点本地的区块链是从同步节点的本地区块链分叉而来的。IBD节点上的区块离区块链顶部区块越近,分叉检测就变的越有用。 [?]

当IBD节点收到第二个inv消息后,它会使用getdata消息请求这些区块。同步节点会给IBD节点返回block消息。然后IBD节点会使用getblocks消息,继续请求更多的存货清单。 不断的重复这个过程直到IBD节点同步完整个区块链。到这后,IBD节点通过普通的区块广播(后续小节会讲到)来接受新的区块。

区块优先的优缺点

区块优先主要优点在于简单。其主要缺点在于只依赖于一个同步节点来下载区块数据。这会带来几个影响:

  • 速度受限: 因为所有的请求都指向一个同步节点,这样如果同步节点的上传带宽有限,那么IBD节点的下载速度就很慢。注意:如果同步节点离线,比特币内核会从另外一个同步节点下载— 但是它仍然一次只从一个同步节点下载。

  • 下载重起:同步节点可能给IBD节点发送非最佳(但是其他的都有效)区块链。IBD节点是无法验证它到底是不是最佳的区块链,直到初始化区块下载接近完成时才可以。这会强制IBD节点重新从另外一个节点下载区块链。开发者在比特币内核中,在多个不同区块高度,加了好几个区块链检测点,来帮组IBD节点监测它是否在下载一条非最佳区块链。 这可以让IBD节点尽早的重启下载。

  • 硬盘充满攻击:这个跟下载重起关系很大,如果同步节点发送了一个非最佳区块链,这条链会存储在硬盘上,浪费磁盘空间,可能会使磁盘上充满无用的数据。

  • 高内存消耗:不管是故意的,还是意外,同步节点可能无序地发送区块,这会导致一些孤块,只有收到并验证了其父块后,才能验证这些孤块。孤块在等待验证期间会一直存在内存中,这会消耗大量内存。

在比特币内核0.10.0中,所有的这些问题在头部优先IBD方法中,部分或全部的得以解决。

资源: 下面的的表格总结了这节提到的消息。点击消息栏的链接可以查看对于消息的参考页面。

消息 getblocks inv getdata block
From→To IBD→Sync Sync→IBD IBD→Sync Sync→IBD
内容 一个/多个头哈希 最多500个区块存货(唯一id) 一个/多个区块存货 一个 序列化区块

区块头优先

比特币内核0.10.0使用区块头优先IBD的初始化区块下载方法。其目标是先下载最佳区块链头,部分验证,然后并行下载相应的区块。这解决了几个区块优先IBD方法中的问题。

节点第一次启动时,它本地最佳区块链只要一个区块 - 硬编码的创世区块(block0)。 它会选择一个远程节点,这里我们称之为同步节点,然后给同步节点发送getheaders消息。如下图所示:

getheaders消息的区块头哈希域,新节点只发送了它本地仅有的区块头哈希 - 创世区块哈希( 6fe2…0000 内部字节序)。同时会截止哈希域全设为0,以获取最多哈希值。

同步节点一但收到getheaders消息,它会取出第一个区块头哈希,然后用它在本地最佳区块链中搜索区块。如果block0匹配,同步节点就会从block1开始,返回2000个区块头哈希。它以headers消息的方式,来发送这些区块头。如下图所示:

IBD节点可以部分验证区块头,它是通过确保区块头所有字段遵循共识规则,同时区块头的哈希值要低于nBits字段的目标阀值来实现。(要完整验证区块,仍需要获得该区块所有交易后才可以)

当IBD节点部分验证完区块头之后,它可以并行做两件事情:

  1. 下载更多的区块头: IBD节点可以发送另外一个getheaders消息到同步节点,以获取最佳区块链上,下一批2000个区块头。这些区块头可以被立即验证,同时不断的重复批量发送请求,直到从同步节点收到的headers消息中所包含的头少于2000个,这表示已没有更多的区块头。在撰写本文时,少于200个来回,就可以完成整个区块头同步,大约需要下载32MB数据。

    一但IBD节点收到一个少于2000个区块头的headers消息,它就给它所有外连的节点发送一个getheaders消息,看看他们最佳区块链的情况。通过对比它们返回的消息,它很容易通过它外联的节点,判断它所下载的区块头是不是属于最佳区块链上的。这就意味一个不诚实的节点很快会被发现,即使不用检测点(只要IBD节点连接到至少一个诚实节点,如果找不到诚实节点,比特币内核会继续提供检查点)。

  2. 下载区块: 当IBD继续下载区块头时,以及完成区块头下载后,IBD节点会请求并下载每个区块。IBD节点通过区块头链中区块的哈希,来创建getdata消息。而在区块优先中,getdata中的区块头哈希需要通过inv消息中的区块存货清单来提供。但在区块头先中,就不必从同步节点获取区块, 它可以从其他任何全节点获取。(虽然并不是所有的全节点存储所有的区块。) 这就运行它能够并行获取区块,同时避免下载速度受限于单个同步节点的带宽速度。

为从更多的节点加载数据,比特币内核一次最多从单个节点获取16个区块,最多8个外向连接。这就意味采用区块头优先的比特币内核, 在IBD阶段,同时最多可以同时发起128个区块请求。(跟采用区块优先的比特币内核最大请求数是一样的)

比特币内核采用的区块头优先模式,采用1024个区块为一移动下载时间窗口,以最大化下载速度。在下载时间窗口中最低区块,是下一个即将被验证的区块。 如果轮到区块验证了,但区块还未下载完成,比特币内核会至少会等2秒,等待区块从失速节点下载完成,如果还没有下载完成,比特币内核会断开跟失速节点的连接,并尝试给另外一个节点连接。比如,如上图所示,如果在2秒后,节点A不能发送区块3,那么节点A会被断开连接。

一但IBD节点同步完整个区块链,他会接收来那些通过正常区块广播而来的区块,这个会在后面的小节会讲到。

资源: 下面的的表格总结了这节提到的消息。点击消息栏的链接可以查看对于消息的参考页面。

消息 getheaders headers getdata block
发送→接 IBD→Sync Sync→IBD IBDMany ManyIBD
内容 一个/多个区块头哈希 最大2000个区块头哈希 一个/多个从哈希头派生出来的区块存货清单 一个序列化区块

区块广播

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

  • 主动推送区块:矿工给它的每个一个全节点发送一个带有新区块的block消息。在这种情况,矿工不采用标准的转发方法,因为它知道跟它连接的节点没有一个正好发现这个区块。

  • 标准转发区块: 矿工扮演一个标准的转发节点,给它每一个节点(全节点,SPV),通过发送带有新区块存货订单的inv消息。 通常节点会有以下返回:

    1. 每个区块优先(BF)节点,想要从全节点通过getdata消息中获取区块信息。

    2. 每个区块头优先(HF)节点,想要从全节点通过getheaders消息获取区块,消息中应包括其最佳区块链上,最高区块头的哈希头,以及可能带有一些,用于检测分叉的,在最佳区块链上的后续区块头。然后紧接着发送一个getdata消息去请一个完整的区块。通过先请求区块头,一个区块头优先的节点可能会拒绝孤块,这会在下面小节会讲到。

    3. 每个SPV客户端,通常想通过getdata消息获取默克尔区块。

      矿工根据每个请求相应的给他们返回消息。 通过block消息发送区块,通过headers消息发送一个或多个区块头,在0/多个tx消息后,通过merkleblock消息,发送与SPV客户端bloom过滤器相应的默克尔区块,交易。

      1. 直接公告区块头:中转节点可跳过getheadersinv消息之间来回切换的方式,获取新区块信息,直接立即发送一个包含完整新区块头的headers消息。HF(区块头优先)节点收到这个消息后,当它在区块头优先IBD阶段时,它会部分验证区块头,如果区块头是有限的,它就会通过getdata消息,来请求整个区块内容。 中转节点会给getdata请求,相应的以blockmerkleblock消息返回完整的,或过滤后的区块数据。HF节点在握手连接时,可以通过发送一个特殊的sendheaders消息来发出它更喜欢接收headers消息,而非inv消息的信号。

        这个区块广播协议已经在BIP130被提议,自从比特币内核0.12后,都实现了这个协议。

在默认情况下,比特币内核使用直接广播区块的方式给那些发送了sendheaders信号的节点广播区块,而对其他节点使用标准区块转发方式。比特币内核接受以上所有方式转发的区块。

全节点验证收到的区块,然后使用标准区块转发方式转发给它的节点。下面精简表格,重点罗列了这个过程中用到的消息(Relay, BH, HF, SPV 分别对应 转发节点,区块优先节点,区块头优先节点,SPV客户端;Any - 表示一个使用任何获取区块方法的节点)

消息 inv getdata getheaders headers
From→To Relay→Any BF→Relay HF→Relay Relay→HF
内容 新区块清单 新区块清单 在HF节点最佳区块头链(BHC)上,一个/多个区块头哈希 最多2000个区块头,把HF节点的BHC跟转发节点的BHC连接起来
消息 block merkleblock tx
From→To Relay→BF/HF Relay→SPV Relay→SPV
内容 新序列化区块 对新区块修改后的默克尔区块 来自新区块,跟bloom过滤器匹配的序列化的交易

孤块

区块优先节点可能会下载孤块。所谓的孤块就是指之前区块头哈希字段,所指向的区块还未看到。也就是说,孤块没有可知的父区块(跟陈腐区块不一样,它们有父区块,但是它们不属于最近区块链)。

当区块优先节点下载了一个孤块,节点不会立刻去验证它,而是给发送孤块的节点(广播节点)发送一个getblocks消息,这个广播节点会返回一个带有区块清单的inv消息,这个清单里面是节点丢失区块的信息(最多500条);下载节点使用getdata消息请求这些区块;然后广播节点会以block消息形式发送这些区块。然后下载节点会验证这些区块,一但之前孤块的父区块下载完成,并被验证,下载节点就会验证之前的孤块。

区块头优先节点为避免复杂,它在使用getdata消息请求一个区块前,经常先使用getheaders消息请求区块头。 广播节点会给下载节点发送一个headers消息,这个消息里面包含了它认为的,下载节点要达到最佳区块链顶部,所需要的所有区块头(做多2000条);每个区块头都会指向它的父区块,因此当下载节点收到block消息时,该区块不应该是一个孤块 — 因为它所有的父块哈希都已经知道(即使他们还未被验证)。如果在block消息中,收到的区块是孤块,那么区块头优先节点会立即丢弃它。

然而,丢弃孤块意味着,区块头优先节点会忽略掉矿工以主动推送方法发出的孤块。

交易广播

要发送一个交易到另外一个节点,需要发送一个inv消息。如果节点收到一个getdata消息,就会使用tx发送这个交易。节点收到这个交易后,如果是一个有效的交易,也会以同样的方式转发交易。

内存池

全节点可以跟踪那些可以打包进下一区块的未确认交易。这对于那些挖取这些或全部交易的矿工来说,是非常有必要的,但是它对于任何想要跟踪未被确认交易的节点来说,也是很有用,比如为SPV节点提供未确认交易信息服务的节点。

因为在比特币中,未确认交易没有一种持久状态,比特币内核会把他们保存在内存里,也叫做内存池。当一个节点关掉后,除了那些保存到钱包的交易外,其他在内存池中的交易全部会丢失。这就意味着,但节点重起后,那些没有被挖出的未确认交易会慢慢的从网络中消息掉,或因为内存不足时,把其中的一些未确认交易从内存池中删除掉。

那些没有被打包进去区块的交易会变成陈腐区块,它们可能会被重新加到内存池中。如果替代区块已包含这些交易,这些从新加入到内存池的交易会立即被删除掉。在比特币内核中,这种情况就是,从最佳区块链最顶部开始(最高区块),把链上的陈腐区块一个个删掉。当每个区块被删除时,它的交易会重新加到内存池中。删掉完陈腐区块后,在区块链顶部,逐个加上替代区块。当添加完一个区块后,区块中确认的交易就会从内存中删除掉。

SPV客户端因为不需要转发交易,所以他们就没有内存池。他们不能独立的验证一个交易是否包含在一个区块里面,同时SPV客户端只能花UTXOs,所以他们不知道哪个交易是合格的,是可以打包到下一个区块的。

作弊节点

要注意的是,对于区块,交易这两种广播,系统有一个机制来惩罚那些作弊节点。 他们会通过发送错误信息来占用带宽,计算资源。如果一个节点的banscore值大于-banscore=<n> 设置的阀值,它就会被禁止-bantime=<n>中所设置的时长,这个默认是86400秒(24小时)。

警告

在早期的比特币内核版本,是允许开发者,可信的社区成员给用户发布比特币警告,以通知用户比特币网络出现严重的问题。这个消息系统在比特币内核0.13.0就已经作废掉;然而,内部警告,分叉检测警告,-alertnofity功能还保留着。


声明:

文中带有[?]的地方,表示我对此翻译明显感觉不太对的,后续会不断修正。

有些地方可能会翻译的不好,不地道,甚至错误,如果有发现,还请留言,指出,以便我好修正,谢谢!

推荐阅读更多精彩内容