网络¶
概述¶
P2P是一种分布式网络,网络的参与者共享他们所拥有的一部分硬件资源,比如处理能力、存储能力、网络连接能力、打印机等,这些共享的资源需要由网络提供服务和内容,能被其它对等节点直接访问而无需经过中间实体。在此网络中的参与者既是服务和内容提供者,又是服务和内容获取者。
区别于传统的Client/Server中央服务器结构,P2P网络中的每个节点的地位都是对等的。每个节点在充当客户端的同时,也可以作为服务端给其它节点提供服务,极大地提高了资源的利用率。
区块链网络¶
P2P 是区块链结构中的网络层,网络层的主要目的是实现区块链网络中节点之间的信息传播、验证和交流。区块链网络本质上是一个P2P网络,每个节点既能接受信息也能产生信息。节点之间通过维护一个共同的区块链数据来保持通信。
P2P网络作为区块链的基础,为区块链带来如下优点:
- 防止单点攻击
- 高容错性
- 较好的兼容性与可扩展性
TRON网络¶
TRON架构图如下:
P2P网络作为TRON的最底层模块,直接决定了整个区块链网络的稳定性。网络模块按照功能可以划分成以下四部分:
下面将分别介绍这四个功能部分。
节点发现¶
节点发现是区块链节点接入区块链网络的第一步。区块链网络是一种结构化的P2P网络。结构化网络会将所有节点按照某种结构有序的组织起来,比如形成一个环状网络或者树状的网络。 结构化网络普遍基于DHT(Distributed Hash Table,分布式哈希表) 算法实现。具体的实现方案有 Chord、Pastry、CAN、Kademlia 等算法。TRON 网络采用 Kademlia 算法。
Kademlia 算法¶
Kademlia 是分布式散列表(DHT,Distributed Hash Table)的一种实现,是去中心化 P2P 网络中最核心的一种路由寻址技术,可以在无中心服务器的情况下,在网络中快速找到目标节点。
关于算法的详细介绍请参考 Kademlia。
TRON 实现¶
TRON 实现的 Kademlia 算法,要点如下:
- 节点ID:随机产生的512bit ID
- 节点距离:节点距离通过两个节点的ID异或运算得到,公式为:
节点距离 = 256 - 节点ID异或结果的前导0的个数
,如果计算结果为负数,距离等于0。 - k桶:即节点路由表。根据节点距离的远近,将远端节点划分到不同的桶中,与本节点距离相同的远端节点被记录在相同的桶中,每个桶最多容纳16个节点。根据节点距离的计算公式就可以看出,TRON 实现的 Kademlia 算法一共维护256个桶。
TRON 节点发现协议包括以下四种UDP消息:
DISCOVER_PING
- 用于探测⼀个节点是否在线DISCOVER_PONG
- 用于响应DISCOVER_PING
消息DISCOVER_FIND_NODE
- 用于查找与⽬标节点距离最近的其它节点DISCOVER_NEIGHBORS
- 用于响应DISCOVER_FIND_NODE
消息,会返回一个或者多个节点,最多16个
初始化K桶¶
节点启动后,会读取节点配置文件中配置的种子节点,以及数据库中记录的对等节点,然后向它们发送 DISCOVER_PING
消息,如果收到回复 DISCOVER_PONG
,在K桶没满的情况下,将相应节点写入K桶;如果相应桶已经达到了16个节点,则向桶中最早的节点发起挑战,挑战成功,将被挑战节点删除,将挑战节点加入K桶。K桶初始化完成后,进行节点发现流程。
发送DISCOVER_FIND_NODE消息,发现更多节点¶
节点发现服务会开启两个节点发现定时任务(DiscoverTask
和RefreshTask
),周期执行节点发现过程来更新k桶。
-
DiscoverTask
是发现更多距离自己近的节点,每30s执行一次,执行流程如下图所示: -
RefreshTask
是通过随机节点ID来扩充本地k桶,即发现距离随机节点ID更近的节点,每7.2s执行一次,执行流程如下图所示:
DiscoverTask
和RefreshTask
中使用的节点发现算法,在一次调用中会执行8轮,每轮向K桶中距离目标节点 ID 最近的3个节点发送 DISCOVER_FIND_NODE
消息,并等待回复。
接收到neighbors消息,更新K桶¶
当本节点接收到远端节点回复的 DISCOVER_NEIGHBORS
消息后,会依次向收到的邻居节点发送 DISCOVER_PING
消息,接下来如果收到了回复消息DISCOVER_PONG
,则判断相应的K桶是否装满,如果K桶未满,则将新节点加入K桶,如果K桶满了,则向其中的某个节点发起挑战,若挑战成功(向被挑战的节点发送DISCOVER_PING
消息,如果未能收到其回复DISCOVER_PONG
,则为挑战成功,否则挑战失败),则将旧节点从K桶中删除,将新节点加入K桶。
节点周期的执行节点发现任务,不断的更新K桶,构建自己的节点路由表。接下来就是节点间建立连接的过程了。
节点连接¶
在了解节点间如何建立TCP连接之前,您需要首先了解peer节点类型。
peer节点管理¶
节点需要对peer节点进行管理分类,以进行高效稳定的节点连接。远端节点可以分为如下几类:
- Active nodes:通过配置文件指定,系统启动后,会主动与其建立连接的节点。如果连接建立失败,则在每个TCP连接定时任务中都会重新尝试与其建立连接。
- Passive nodes:通过配置文件指定,被动接受连接的节点。
- Trust nodes:通过配置文件指定,Active nodes和Passive nodes都是trust nodes。当收到trust node的连接请求时,会跳过其它的条件检查,直接接受请求。
- badNodes:当收到异常的协议报文时,会将节点加入到badNodes,生效时长1个小时,当收到 badNodes 的连接请求,会直接拒绝请求
- recentlyDisconnectedNodes:当断开某条连接后,会把节点加入到recentlyDisconnectedNodes,有效时长30s,当收到 recentlyDisconnectedNodes 的连接请求,会直接拒绝请求
建立节点间TCP连接¶
在节点启动后,会创建一个建立节点间TCP连接的定时任务poolLoopExecutor
,用于选择节点,并与之建立连接。建立TCP连接定时任务,工作过程如下:
TCP连接主要分为两步:首先,确定要与之建立连接的节点的列表;列表中需要包含active nodes中还没有成功建立连接的节点,然后计算还需要建立连接的数量,从邻居发现节点列表里面根据 节点过滤策略 过滤出满足要求的节点,再根据 节点打分策略 对节点打分排序,将最高的相应数量的节点加入到请求列表中。最后,与请求列表中的节点建立TCP连接。
节点过滤策略¶
建立节点连接时,需过滤掉如下几类节点,并判断节点自己的连接数是否达到了最大连接。
- 自身节点
- recentlyDisconnectedNodes列表中的节点
- badNodes列表中的节点
- 已经建立了连接的节点
- 与该节点ip建立的连接数已经达到了上限值maxConnectionsWithSameIp的节点
但是对于可信节点,会忽略掉部分过滤策略,始终与其建立连接。
节点打分策略¶
节点分数用于确定建立连接的优先级,分数越高,优先级越高。打分维度包括:
- 丢包率:丢包率越低,说明数据通信质量越好。分数与丢包率成反比,最高分为100,最低为0
- 网络延迟:网络延时越小,说明网络质量越好。分数与平均网络延时成反比,最高分为20,最低为0
- TCP流量:TCP流量越大,说明通信比较活跃。分数与TCP流量成正比,最高分为20,最低为0
- 断开连接次数:断开连接次数越少,说明节点越稳定。断开连接分数为负数,且与断开连接次数为正比,值为断开连接次数的10倍
- Handshake:曾经handshake成功过的节点,表示有相同的区块链信息,因此优先选择和他们建立连接。Handshake成功次数大于0时,Handshake得分为20,否则得分为0
- 处罚状态:处于处罚状态的节点,分数为0,不参与其它维度打分,包含以下几种情况:
- 节点断开连接时间不到60s
- 节点在badNodes列表中
- 区块链信息不一致
计算节点分数时,首先判断节点是否为处罚状态,如果是,则分数计为0,否则,节点分数为各个维度的得分之和。
握手¶
TCP 连接建立成功后,主动发起TCP连接请求的节点,会向邻居节点发送握手消息 P2P_HELLO
,目的为了确认节点间的链路信息是否一致,以及是否需要发起区块同步流程。
当邻居节点收到 P2P_HELLO
后,会与本地信息做比较,比如检查p2p version、创世块信息是否一致,若一致,还需要检查固化快,以及判断是否是重复的连接、恶意节点等。若所有的检查条件都通过,则会回复P2P_HELLO
消息,然后进行区块同步或广播;否则,断开连接。
信道保活¶
信道保活是通过 P2P_PING
、P2P_PONG
TCP报文来完成的。当节点与邻居节点建立了TCP连接并握手成功后,节点会为连接开启一个线程 pingTask
定期发送 P2P_PING
消息以维护该TCP连接,每10s调度一次。如果在超时时间内未收到节点回复的 P2P_PONG
消息,则断开连接。
区块同步¶
在与对方节点完成握手后,如果发现对方节点的区块链比本地的区块链要长,根据最长链原则,会触发区块同步的处理流程syncService.startSync
。同步过程中的报文交互如下图:
节点A向对方节点B发送 SYNC_BLOCK_CHAIN
消息,以宣告本地链的摘要信息。对方节点B收到后,计算出节点A缺失的区块清单,并将缺失区块的id列表通过 BLOCK_CHAIN_INVENTORY
消息发送给节点A,一次最多携带2000个区块id。
节点A收到 BLOCK_CHAIN_INVENTORY
消息后,取出缺失区块id,并通过异步的方式向节点B发送 FETCH_INV_DATA
消息以请求缺少的区块,一次最多请求100个区块。如果还有需要同步的区块(即 BLOCK_CHAIN_INVENTORY
报文中的remain_num大于0),会触发新一轮的区块同步流程。
节点B收到节点A的 FETCH_INV_DATA
报文后,通过 BLOCK
消息将区块发送给节点A。节点A收到 BLOCK
报文后,异步处理该区块。
链摘要及缺失区块清单¶
下面根据几个不同的区块同步场景示例,说明链摘要和节点缺少的区块清单的生成。
- 链摘要:有序的区块blockID列表,包括:最高固化块、最高非固化块,以及之间二分法对应的区块
- 缺失区块清单:邻居节点根据链摘要与自己链进行比较,确定对方缺失的区块清单,返回一组连续的区块blockID以及剩余的块数
普通场景¶
本地头块高度为1018,固化块高度为1000,两个节点刚建立连接,所以共同块高度为0,通过二分方式获取到节点A的本地链摘要为:1000、1010、1015、1017、1018。
节点B收到节点A的链摘要后,结合本地链可以生产节点A缺少的区块清单为:1018、1019、1020、1021。之后节点A根据缺失区块清单,请求同步区块1019、1020、1021。
切链场景¶
本地主链头块高度为1018,固化块高度为1000,两个节点刚建立连接,所以共同块高度为0,通过二分方式获取到节点A的本地链摘要为:1000、1010、1015、1017、1018。
节点B收到节点A的链摘要后,发现本地主链跟节点A的主链不在同一条链上,对比节点A的链摘要,找到共同块高度为1015,则认为节点A缺少的区块清单为:1015、1016'、1017'、1018',1019'。之后节点A根据缺失区块清单,请求同步区块1018',1019'。
另一种切链场景,本地主链头块高度为1018,固化块高度为1000,共同块为1017‘,位于fork链上,通过二分方式获取到节点A的本地链摘要为:1000、1009、1014、1016'、1017'。
节点B收到节点A的链摘要后,结合本地链可以生产节点A缺少的区块清单为:1017'、1018'、1019'。之后节点A根据缺失区块清单,请求同步区块1018',1019'。
区块和交易广播¶
当超级代表节点生产出新的区块,或者全节点接收到用户发起的新交易时,会发起交易&区块广播流程。当节点接收到新区块或者新交易时,会转发相应的区块或交易,转发与广播的流程一样。报文交互如下图所示:
其中涉及到的消息类型包括:
INVENTORY
- 广播清单:区块或者交易id列表FETCH_INV_DATA
- 请求需要获取的清单数据:区块或者交易id列表BLOCK
- 区块数据TRXS
- 交易数据
节点A将待广播交易或区块通过 INVENTORY
清单消息发送到节点B。节点B收到 INVENTORY
清单消息后,需要检查对方节点的状态,如果可以接收该消息,则将清单中的区块/交易放入待获取队列 invToFetch
中。如果是区块清单,还会立即触发"获取区块&交易任务",来向节点A发送 FETCH_INV_DATA
消息获取区块&交易。
节点A收到 FETCH_INV_DATA
消息后,会检查是否发送过清单消息给对方,如果发送过,则根据清单数据,向节点B发送交易或者区块消息。节点B收到交易或者区块消息后,处理消息,并触发转发流程。
总结¶
本文介绍了TRON最底层模块-P2P网络相关的实现细节,包括节点发现、节点连接、区块同步、区块和交易广播流程,希望通过阅读本文能帮助开发者进一步了解和开发java-tron网络相关模块。