DFINITY区块链
这一篇将重点对区块链网络底层的对等网络模型做一个简单介绍和代码分析。
作者:季宙栋、丛宏雷
核心关键词:P2P对等网络、Kademlia、DHT
本章的源代码地址:
https://github.com/dfinity/go-dfinity-p2p
一、DFINITY网络简介
在DFINITY网络中,所有客户端组成一个去中心化的p2p网络(完全对等网络,没有主节点),上层应用通过客户端的Send接口向P2P网络中发送消息,通过Receive接口接收其他客户端向网络发送的消息。
DFINITY的P2P网络基于Protocol Labs的libp2p构建,也就是S/Kademlia网络。S/Kademlia网络对Kademlia网络在安全性方面做了增强,其网络架构和应用接口都和Kademlia网络相同。与Kademlia相同,在整个P2P网络架构中,所有DFINITY客户端都被当作一个二叉树的叶子节点,通过客户端ID的最短唯一前缀表示了其在整个二叉树的位置。通过节点ID与数据的哈希值直接对应,在相应的节点保存数据的元信息。当我们在网络中搜索数据(即通常搜索存储文件哈希值或者关键词的节点)的时候,Kademlia算法需要知道这些数据的哈希值,然后在分布式网络中开始搜索。每一步都会找到离目标更接近的一些节点,当有节点直接返回搜索的值,或者再也找不到与键更近的节点ID的时候停止搜索。Kademlia网络的搜索和其他分布式哈希表类似,在一个包含n个节点的网络中,完成一次搜索的复杂度为O(log(n))。
去中心化的S/Kademlia网络的更大优势就是它显著增强了抵御拒绝服务攻击的能力。即使网络中的一整批节点遭受攻击,也不会对网络的可用性造成很大的影响,通过网络的自组织能力,可以很快绕过这些被攻击的节点完成网络重建,恢复网络的可用性。
二、P2P客户端简介
下面对DFINITY的P2P客户端进行详细的分析。DFINITY P2P客户端的系统架构如下图所示:
首先介绍P2P客户端的启动和加入网络的流程,然后分别介绍其中的各个服务。
首先看看P2P客户端的配置,主要配置参数如下:
ListenIP: 节点的服务IPListenPort: 节点的服务端口KBucketSize: Kademlia的K桶的大小SampleSize: 在Kademlia网络进行采样更新时,每次采样数目SeedNodes: Kademlia网络的种子节点列表StreamstoreCapacity: 每个节点维护与其他客户端的链接数目ArtifactCacheSize: 每个节点的数据缓存的大小
1)客户端的初始化与启动
客户端节点的启动首先需要从本地加载配置,具体流程如下:
1. 根据配置构建本地的数据缓存;
2. 生成自己的私钥和公钥,并依据公钥生成自己的节点ID;
3. 初始化网络节点信息缓存和连接池。网络节点信息缓存负责缓存网络中所有其他节点的信息,连接池管理着本客户端与所有其他节点的连接。
4. 初始化Kademlia路由表,即KBucket(K桶)
5. 将当前节点的ID加入到路由表中
6. 初始化witnesses缓存,此缓存负责纪录网络中哪些节点也可能缓存了本地数据缓存中的数据
完成以上初始化后,客户端即可启动,加入到P2P网络中。
2)加入P2P网络
客户端完成本地初始化后,首先链接到配置的种子节点,从种子节点开始,逐渐构建自己在网络中的链接关系。具体流程如下:
1. 构建swarm network,开始监听Listener IP
2. 启动pair服务,pair服务负责处理来自网络中其他节点的pair请求
3. 启动ping服务,ping服务负责处理节点间的心跳消息
4. 启动sample服务,sample服务负责处理sample请求
5. 向初始化时配置的所有种子节点发送hello请求,如果得到种子节点的hello回复,则将此种子节点加入到KBucket路由表中
6. 启动节点发现服务,节点发现服务将周期性发送sample请求,更新自己的网络节点缓存
7. 启动节点数据同步的Stream服务,Stream服务通过连接池维护本地节点与近邻节点间链接,当节点发现服务更新发现更新的近邻节点,Stream服务也将自动与新的近邻节点建立连接
8. 最后启动广播服务,至此客户端成功加入到P2P网络中,开始处理其他应用的Send调用。
在与种子节点建立连接后,种子节点将会把它知道的该节点的近邻节点列表发送给该节点,然后该节点基于自身的节点发现服务持续更新自己的近邻节点列表,直至搜索到网络中的近邻节点。
3)Ping服务
ping服务主要负责P2P网络中的心跳消息的处理,在P2P网络中,心跳消息有两个用途:
1. 在一个节点与另一个节点建立连接关系时,通过ping服务做网络验证,并纪录链接TTL等信息
2. 每个节点也周期性从节点信息缓存中随机选择若干节点,通过ping服务验证相互的连通性,从而及早发现网络节点的变化
ping服务对于其他节点发出的ping请求作出处理,处理流程如下:
1. 读取对方发送的ping数据
2. 将对方的ping数据返回给发送方
3. 更新请求节点在KBucket路由表的位置
4)节点发现服务
由于P2P网络中随时可能有新的节点加入,也可能有节点下线或退出,要维持整个网络的可用性,P2P网络节点需要周期性对网络进行探测,与其他节点沟通,更新网络中相邻节点的活动状态。客户端后台的节点发现服务就是负责此项任务。
客户端后台的节点发现服务工作流程如下:
1. 收集当前KBucket路由表中纪录的所有节点
2. 将其随机排列组合,从中随机选择M个节点(M为初始化配置参数)
3. 分别向这M个随机节点发送Sample请求
4. 在这M个随机节点的Sample回复中,将包含网络中一些其他节点的信息
5. 客户端将分别解析这些sample回复中的其他节点的信息,将其加入到自己的节点信息缓存中,并首先设置其TTL为TempAddrTTL
6. 然后客户端将对新发现的节点发出ping请求,如果ping成功,则更改TTL为ProviderAddrTTL,并将此节点加入到KBucket路由表中
7. 如果Sample请求失败,将此节点从节点缓存和KBucket路由表中
从上面流程可以看出,节点发现服务基于Sample请求构建每个节点的网络状态。
5)Sample服务
Sample服务,也就是采样服务,负责处理来自其他节点的Sample请求。服务请求节点对自己所掌握的网络节点信息进行采样,发送请求;服务回复节点同样按照自己所掌握的网络节点信息,对请求节点的近邻节点进行采样,构造Sample 回复,发送回服务请求节点。Kademlia网络通过此服务加速节点的查询。
在收到其他节点的Sample请求时,客户端需要做如下处理:
1. 收集当前KBucket路由表中纪录的所有节点
2. 对所有节点,按照与请求节点的Kademlia距离(即共同前缀距离)进行排序
3. 对排序结果按照指数分布进行随机采样,挑选出M个节点
4. 将此M个节点的信息构建一个Sample回复
5. 将Sample回复发送给请求节点
6. 更新请求节点在KBucket路由表的位置
6)Stream服务
客户端的stream服务负责管理维护节点的连接池,与节点最近邻的N个节点保持连接关系(N为初始化时配置的连接池容量)。当客户端发现与近邻节点的连接错误时,连接池中的连接将被自动清除,客户端也将自动更新与近邻节点建立连接,并更新连接池。具体处理流程如下:
1. 首先检查连接池是否已满,如果已满则不需要处理。否则需要向连接池中添加新的连接
2. 收集当前KBucket路由表中纪录的所有节点
3. 以当前节点为网络中心节点,将所有节点分为左右两部分
4. 将两部分节点分别按照与当前节点的Kademlia距离进行排序,排序后各选取与当前节点最近邻的N/2个节点
5. 检查这些节点,如果连接池中没有与此节点的连接,向该节点发送pair请求,建立连接
6. 建立pair连接后,将为此链接启动pair服务,完成节点间数据的互通。
7)Pair服务
客户端的pair服务负责处理网络中的其他节点的pair请求,与其他节点建立pair关系。建立了pair关系的节点间,将自动广播自己收到的数据请求。具体处理流程如下:
1. 收集当前KBucket中所有节点
2. 计算离自己最近邻的K个节点
3. 检查发出pair请求的节点是否为最近邻的K个节点中
4. 如果不在其中,拒绝此pair请求
5. 否则,将此链接加入到自己的链接池中,同时为此链接启动服务routine
8)连接池的服务routine
此routine负责从连接池中连接上接受其他节点发送过来的数据,并将此数据添加到自己节点的数据缓存中,同时维护每个数据的witnesses。在做数据广播的时候,此witnesses用于判断对应节点是否已经拥有了此数据的备份,如果已经有了,则不需要将数据广播到对应节点。具体routine的处理流程如下:
1. 读取stream,分别读取数据的checksum,datasize等数据元信息
2. 如果当前节点已经缓存了此数据,直接向发送方回复
3. 继续接收完整数据,验证数据的完整性
4. 将数据的发送方加入到此数据的witnesses列表中
5. 检查当前数据缓存是否已经缓存了此数据,如果没有,将其加入到数据缓存中
6. 如果当前节点第一次收到此数据,将此份数据通知上层应用
9)广播服务
客户端的广播服务负责接收上层应用的数据发送请求,将其发送到P2P网络中。具体处理流程如下:
1. 计算数据的checksum等数据元信息
2. 对当前节点链接池中的所有链接
a) 以checksum为索引,检查witnesses列表,检查对应节点是否依旧缓存了此数据
b) 如果没有缓存此数据,发送数据元信息到对应节点
3. 将数据切分为chunk,将每个chunk,依次发送给所有近邻节点
4. 如果数据发送过程中发现网络错误,关闭与对应节点的链接,并将其从链接池中删除。
10)去中心化全网数据转发应用示例
从上面可以看到广播服务只是将数据广播到本地节点的近邻节点中,要实现数据的全网广播可以通过上层应用实现。具体实现如下:
1. 创建一个routine,持续从P2P客户端接收数据
2. 对于每个接收到的数据,再通过P2P客户端发送到网络中。这样通过近邻节点间接力传播的方式,完成数据的全网广播。