DFINITY區(qū)塊鏈
這一篇將重點(diǎn)對(duì)區(qū)塊鏈網(wǎng)絡(luò)底層的對(duì)等網(wǎng)絡(luò)模型做一個(gè)簡單介紹和代碼分析。
作者:季宙棟肾砂、叢宏雷
核心關(guān)鍵詞:P2P對(duì)等網(wǎng)絡(luò)盲镶、Kademlia、DHT
本章的源代碼地址:
https://github.com/dfinity/go-dfinity-p2p
一舱权、DFINITY網(wǎng)絡(luò)簡介
在DFINITY網(wǎng)絡(luò)中,所有客戶端組成一個(gè)去中心化的p2p網(wǎng)絡(luò)(完全對(duì)等網(wǎng)絡(luò)仑嗅,沒有主節(jié)點(diǎn))宴倍,上層應(yīng)用通過客戶端的Send接口向P2P網(wǎng)絡(luò)中發(fā)送消息,通過Receive接口接收其他客戶端向網(wǎng)絡(luò)發(fā)送的消息仓技。
DFINITY的P2P網(wǎng)絡(luò)基于Protocol Labs的libp2p構(gòu)建鸵贬,也就是S/Kademlia網(wǎng)絡(luò)。S/Kademlia網(wǎng)絡(luò)對(duì)Kademlia網(wǎng)絡(luò)在安全性方面做了增強(qiáng)脖捻,其網(wǎng)絡(luò)架構(gòu)和應(yīng)用接口都和Kademlia網(wǎng)絡(luò)相同阔逼。與Kademlia相同,在整個(gè)P2P網(wǎng)絡(luò)架構(gòu)中郭变,所有DFINITY客戶端都被當(dāng)作一個(gè)二叉樹的葉子節(jié)點(diǎn)颜价,通過客戶端ID的最短唯一前綴表示了其在整個(gè)二叉樹的位置。通過節(jié)點(diǎn)ID與數(shù)據(jù)的哈希值直接對(duì)應(yīng)诉濒,在相應(yīng)的節(jié)點(diǎn)保存數(shù)據(jù)的元信息周伦。當(dāng)我們?cè)诰W(wǎng)絡(luò)中搜索數(shù)據(jù)(即通常搜索存儲(chǔ)文件哈希值或者關(guān)鍵詞的節(jié)點(diǎn))的時(shí)候,Kademlia算法需要知道這些數(shù)據(jù)的哈希值未荒,然后在分布式網(wǎng)絡(luò)中開始搜索专挪。每一步都會(huì)找到離目標(biāo)更接近的一些節(jié)點(diǎn),當(dāng)有節(jié)點(diǎn)直接返回搜索的值片排,或者再也找不到與鍵更近的節(jié)點(diǎn)ID的時(shí)候停止搜索寨腔。Kademlia網(wǎng)絡(luò)的搜索和其他分布式哈希表類似,在一個(gè)包含n個(gè)節(jié)點(diǎn)的網(wǎng)絡(luò)中率寡,完成一次搜索的復(fù)雜度為O(log(n))迫卢。
去中心化的S/Kademlia網(wǎng)絡(luò)的更大優(yōu)勢(shì)就是它顯著增強(qiáng)了抵御拒絕服務(wù)攻擊的能力。即使網(wǎng)絡(luò)中的一整批節(jié)點(diǎn)遭受攻擊冶共,也不會(huì)對(duì)網(wǎng)絡(luò)的可用性造成很大的影響乾蛤,通過網(wǎng)絡(luò)的自組織能力每界,可以很快繞過這些被攻擊的節(jié)點(diǎn)完成網(wǎng)絡(luò)重建,恢復(fù)網(wǎng)絡(luò)的可用性家卖。
二眨层、P2P客戶端簡介
下面對(duì)DFINITY的P2P客戶端進(jìn)行詳細(xì)的分析。DFINITY P2P客戶端的系統(tǒng)架構(gòu)如下圖所示:
首先介紹P2P客戶端的啟動(dòng)和加入網(wǎng)絡(luò)的流程上荡,然后分別介紹其中的各個(gè)服務(wù)趴樱。
首先看看P2P客戶端的配置,主要配置參數(shù)如下:
ListenIP: 節(jié)點(diǎn)的服務(wù)IPListenPort: 節(jié)點(diǎn)的服務(wù)端口KBucketSize: Kademlia的K桶的大小SampleSize: 在Kademlia網(wǎng)絡(luò)進(jìn)行采樣更新時(shí)酪捡,每次采樣數(shù)目SeedNodes: Kademlia網(wǎng)絡(luò)的種子節(jié)點(diǎn)列表StreamstoreCapacity: 每個(gè)節(jié)點(diǎn)維護(hù)與其他客戶端的鏈接數(shù)目ArtifactCacheSize: 每個(gè)節(jié)點(diǎn)的數(shù)據(jù)緩存的大小
1)客戶端的初始化與啟動(dòng)
客戶端節(jié)點(diǎn)的啟動(dòng)首先需要從本地加載配置叁征,具體流程如下:
1. 根據(jù)配置構(gòu)建本地的數(shù)據(jù)緩存;
2. 生成自己的私鑰和公鑰逛薇,并依據(jù)公鑰生成自己的節(jié)點(diǎn)ID航揉;
3. 初始化網(wǎng)絡(luò)節(jié)點(diǎn)信息緩存和連接池。網(wǎng)絡(luò)節(jié)點(diǎn)信息緩存負(fù)責(zé)緩存網(wǎng)絡(luò)中所有其他節(jié)點(diǎn)的信息金刁,連接池管理著本客戶端與所有其他節(jié)點(diǎn)的連接。
4. 初始化Kademlia路由表议薪,即KBucket(K桶)
5. 將當(dāng)前節(jié)點(diǎn)的ID加入到路由表中
6. 初始化witnesses緩存尤蛮,此緩存負(fù)責(zé)紀(jì)錄網(wǎng)絡(luò)中哪些節(jié)點(diǎn)也可能緩存了本地?cái)?shù)據(jù)緩存中的數(shù)據(jù)
完成以上初始化后,客戶端即可啟動(dòng)斯议,加入到P2P網(wǎng)絡(luò)中产捞。
2)加入P2P網(wǎng)絡(luò)
客戶端完成本地初始化后,首先鏈接到配置的種子節(jié)點(diǎn)哼御,從種子節(jié)點(diǎn)開始坯临,逐漸構(gòu)建自己在網(wǎng)絡(luò)中的鏈接關(guān)系。具體流程如下:
1. 構(gòu)建swarm network恋昼,開始監(jiān)聽Listener IP
2. 啟動(dòng)pair服務(wù)看靠,pair服務(wù)負(fù)責(zé)處理來自網(wǎng)絡(luò)中其他節(jié)點(diǎn)的pair請(qǐng)求
3. 啟動(dòng)ping服務(wù),ping服務(wù)負(fù)責(zé)處理節(jié)點(diǎn)間的心跳消息
4. 啟動(dòng)sample服務(wù)液肌,sample服務(wù)負(fù)責(zé)處理sample請(qǐng)求
5. 向初始化時(shí)配置的所有種子節(jié)點(diǎn)發(fā)送hello請(qǐng)求挟炬,如果得到種子節(jié)點(diǎn)的hello回復(fù),則將此種子節(jié)點(diǎn)加入到KBucket路由表中
6. 啟動(dòng)節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)嗦哆,節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)將周期性發(fā)送sample請(qǐng)求谤祖,更新自己的網(wǎng)絡(luò)節(jié)點(diǎn)緩存
7. 啟動(dòng)節(jié)點(diǎn)數(shù)據(jù)同步的Stream服務(wù),Stream服務(wù)通過連接池維護(hù)本地節(jié)點(diǎn)與近鄰節(jié)點(diǎn)間鏈接老速,當(dāng)節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)更新發(fā)現(xiàn)更新的近鄰節(jié)點(diǎn)粥喜,Stream服務(wù)也將自動(dòng)與新的近鄰節(jié)點(diǎn)建立連接
8. 最后啟動(dòng)廣播服務(wù),至此客戶端成功加入到P2P網(wǎng)絡(luò)中橘券,開始處理其他應(yīng)用的Send調(diào)用额湘。
在與種子節(jié)點(diǎn)建立連接后卿吐,種子節(jié)點(diǎn)將會(huì)把它知道的該節(jié)點(diǎn)的近鄰節(jié)點(diǎn)列表發(fā)送給該節(jié)點(diǎn),然后該節(jié)點(diǎn)基于自身的節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)持續(xù)更新自己的近鄰節(jié)點(diǎn)列表缩挑,直至搜索到網(wǎng)絡(luò)中的近鄰節(jié)點(diǎn)但两。
3)Ping服務(wù)
ping服務(wù)主要負(fù)責(zé)P2P網(wǎng)絡(luò)中的心跳消息的處理,在P2P網(wǎng)絡(luò)中供置,心跳消息有兩個(gè)用途:
1. 在一個(gè)節(jié)點(diǎn)與另一個(gè)節(jié)點(diǎn)建立連接關(guān)系時(shí)谨湘,通過ping服務(wù)做網(wǎng)絡(luò)驗(yàn)證,并紀(jì)錄鏈接TTL等信息
2. 每個(gè)節(jié)點(diǎn)也周期性從節(jié)點(diǎn)信息緩存中隨機(jī)選擇若干節(jié)點(diǎn)芥丧,通過ping服務(wù)驗(yàn)證相互的連通性紧阔,從而及早發(fā)現(xiàn)網(wǎng)絡(luò)節(jié)點(diǎn)的變化
ping服務(wù)對(duì)于其他節(jié)點(diǎn)發(fā)出的ping請(qǐng)求作出處理,處理流程如下:
1. 讀取對(duì)方發(fā)送的ping數(shù)據(jù)
2. 將對(duì)方的ping數(shù)據(jù)返回給發(fā)送方
3. 更新請(qǐng)求節(jié)點(diǎn)在KBucket路由表的位置
4)節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)
由于P2P網(wǎng)絡(luò)中隨時(shí)可能有新的節(jié)點(diǎn)加入续担,也可能有節(jié)點(diǎn)下線或退出擅耽,要維持整個(gè)網(wǎng)絡(luò)的可用性,P2P網(wǎng)絡(luò)節(jié)點(diǎn)需要周期性對(duì)網(wǎng)絡(luò)進(jìn)行探測物遇,與其他節(jié)點(diǎn)溝通乖仇,更新網(wǎng)絡(luò)中相鄰節(jié)點(diǎn)的活動(dòng)狀態(tài)⊙耍客戶端后臺(tái)的節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)就是負(fù)責(zé)此項(xiàng)任務(wù)乃沙。
客戶端后臺(tái)的節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)工作流程如下:
1. 收集當(dāng)前KBucket路由表中紀(jì)錄的所有節(jié)點(diǎn)
2. 將其隨機(jī)排列組合,從中隨機(jī)選擇M個(gè)節(jié)點(diǎn)(M為初始化配置參數(shù))
3. 分別向這M個(gè)隨機(jī)節(jié)點(diǎn)發(fā)送Sample請(qǐng)求
4. 在這M個(gè)隨機(jī)節(jié)點(diǎn)的Sample回復(fù)中诗舰,將包含網(wǎng)絡(luò)中一些其他節(jié)點(diǎn)的信息
5. 客戶端將分別解析這些sample回復(fù)中的其他節(jié)點(diǎn)的信息警儒,將其加入到自己的節(jié)點(diǎn)信息緩存中,并首先設(shè)置其TTL為TempAddrTTL
6. 然后客戶端將對(duì)新發(fā)現(xiàn)的節(jié)點(diǎn)發(fā)出ping請(qǐng)求眶根,如果ping成功蜀铲,則更改TTL為ProviderAddrTTL,并將此節(jié)點(diǎn)加入到KBucket路由表中
7. 如果Sample請(qǐng)求失敗属百,將此節(jié)點(diǎn)從節(jié)點(diǎn)緩存和KBucket路由表中
從上面流程可以看出记劝,節(jié)點(diǎn)發(fā)現(xiàn)服務(wù)基于Sample請(qǐng)求構(gòu)建每個(gè)節(jié)點(diǎn)的網(wǎng)絡(luò)狀態(tài)。
5)Sample服務(wù)
Sample服務(wù)诸老,也就是采樣服務(wù)隆夯,負(fù)責(zé)處理來自其他節(jié)點(diǎn)的Sample請(qǐng)求。服務(wù)請(qǐng)求節(jié)點(diǎn)對(duì)自己所掌握的網(wǎng)絡(luò)節(jié)點(diǎn)信息進(jìn)行采樣别伏,發(fā)送請(qǐng)求蹄衷;服務(wù)回復(fù)節(jié)點(diǎn)同樣按照自己所掌握的網(wǎng)絡(luò)節(jié)點(diǎn)信息,對(duì)請(qǐng)求節(jié)點(diǎn)的近鄰節(jié)點(diǎn)進(jìn)行采樣厘肮,構(gòu)造Sample 回復(fù)愧口,發(fā)送回服務(wù)請(qǐng)求節(jié)點(diǎn)。Kademlia網(wǎng)絡(luò)通過此服務(wù)加速節(jié)點(diǎn)的查詢类茂。
在收到其他節(jié)點(diǎn)的Sample請(qǐng)求時(shí)耍属,客戶端需要做如下處理:
1. 收集當(dāng)前KBucket路由表中紀(jì)錄的所有節(jié)點(diǎn)
2. 對(duì)所有節(jié)點(diǎn)托嚣,按照與請(qǐng)求節(jié)點(diǎn)的Kademlia距離(即共同前綴距離)進(jìn)行排序
3. 對(duì)排序結(jié)果按照指數(shù)分布進(jìn)行隨機(jī)采樣,挑選出M個(gè)節(jié)點(diǎn)
4. 將此M個(gè)節(jié)點(diǎn)的信息構(gòu)建一個(gè)Sample回復(fù)
5. 將Sample回復(fù)發(fā)送給請(qǐng)求節(jié)點(diǎn)
6. 更新請(qǐng)求節(jié)點(diǎn)在KBucket路由表的位置
6)Stream服務(wù)
客戶端的stream服務(wù)負(fù)責(zé)管理維護(hù)節(jié)點(diǎn)的連接池厚骗,與節(jié)點(diǎn)最近鄰的N個(gè)節(jié)點(diǎn)保持連接關(guān)系(N為初始化時(shí)配置的連接池容量)示启。當(dāng)客戶端發(fā)現(xiàn)與近鄰節(jié)點(diǎn)的連接錯(cuò)誤時(shí),連接池中的連接將被自動(dòng)清除领舰,客戶端也將自動(dòng)更新與近鄰節(jié)點(diǎn)建立連接夫嗓,并更新連接池。具體處理流程如下:
1. 首先檢查連接池是否已滿冲秽,如果已滿則不需要處理舍咖。否則需要向連接池中添加新的連接
2. 收集當(dāng)前KBucket路由表中紀(jì)錄的所有節(jié)點(diǎn)
3. 以當(dāng)前節(jié)點(diǎn)為網(wǎng)絡(luò)中心節(jié)點(diǎn),將所有節(jié)點(diǎn)分為左右兩部分
4. 將兩部分節(jié)點(diǎn)分別按照與當(dāng)前節(jié)點(diǎn)的Kademlia距離進(jìn)行排序锉桑,排序后各選取與當(dāng)前節(jié)點(diǎn)最近鄰的N/2個(gè)節(jié)點(diǎn)
5. 檢查這些節(jié)點(diǎn)排霉,如果連接池中沒有與此節(jié)點(diǎn)的連接,向該節(jié)點(diǎn)發(fā)送pair請(qǐng)求民轴,建立連接
6. 建立pair連接后攻柠,將為此鏈接啟動(dòng)pair服務(wù),完成節(jié)點(diǎn)間數(shù)據(jù)的互通后裸。
7)Pair服務(wù)
客戶端的pair服務(wù)負(fù)責(zé)處理網(wǎng)絡(luò)中的其他節(jié)點(diǎn)的pair請(qǐng)求辙诞,與其他節(jié)點(diǎn)建立pair關(guān)系。建立了pair關(guān)系的節(jié)點(diǎn)間轻抱,將自動(dòng)廣播自己收到的數(shù)據(jù)請(qǐng)求。具體處理流程如下:
1. 收集當(dāng)前KBucket中所有節(jié)點(diǎn)
2. 計(jì)算離自己最近鄰的K個(gè)節(jié)點(diǎn)
3. 檢查發(fā)出pair請(qǐng)求的節(jié)點(diǎn)是否為最近鄰的K個(gè)節(jié)點(diǎn)中
4. 如果不在其中旦部,拒絕此pair請(qǐng)求
5. 否則祈搜,將此鏈接加入到自己的鏈接池中,同時(shí)為此鏈接啟動(dòng)服務(wù)routine
8)連接池的服務(wù)routine
此routine負(fù)責(zé)從連接池中連接上接受其他節(jié)點(diǎn)發(fā)送過來的數(shù)據(jù)士八,并將此數(shù)據(jù)添加到自己節(jié)點(diǎn)的數(shù)據(jù)緩存中容燕,同時(shí)維護(hù)每個(gè)數(shù)據(jù)的witnesses。在做數(shù)據(jù)廣播的時(shí)候婚度,此witnesses用于判斷對(duì)應(yīng)節(jié)點(diǎn)是否已經(jīng)擁有了此數(shù)據(jù)的備份蘸秘,如果已經(jīng)有了,則不需要將數(shù)據(jù)廣播到對(duì)應(yīng)節(jié)點(diǎn)蝗茁。具體routine的處理流程如下:
1. 讀取stream醋虏,分別讀取數(shù)據(jù)的checksum,datasize等數(shù)據(jù)元信息
2. 如果當(dāng)前節(jié)點(diǎn)已經(jīng)緩存了此數(shù)據(jù)哮翘,直接向發(fā)送方回復(fù)
3. 繼續(xù)接收完整數(shù)據(jù)颈嚼,驗(yàn)證數(shù)據(jù)的完整性
4. 將數(shù)據(jù)的發(fā)送方加入到此數(shù)據(jù)的witnesses列表中
5. 檢查當(dāng)前數(shù)據(jù)緩存是否已經(jīng)緩存了此數(shù)據(jù),如果沒有阻课,將其加入到數(shù)據(jù)緩存中
6. 如果當(dāng)前節(jié)點(diǎn)第一次收到此數(shù)據(jù),將此份數(shù)據(jù)通知上層應(yīng)用
9)廣播服務(wù)
客戶端的廣播服務(wù)負(fù)責(zé)接收上層應(yīng)用的數(shù)據(jù)發(fā)送請(qǐng)求限煞,將其發(fā)送到P2P網(wǎng)絡(luò)中。具體處理流程如下:
1. 計(jì)算數(shù)據(jù)的checksum等數(shù)據(jù)元信息
2. 對(duì)當(dāng)前節(jié)點(diǎn)鏈接池中的所有鏈接
a) 以checksum為索引署驻,檢查witnesses列表奋献,檢查對(duì)應(yīng)節(jié)點(diǎn)是否依舊緩存了此數(shù)據(jù)
b) 如果沒有緩存此數(shù)據(jù),發(fā)送數(shù)據(jù)元信息到對(duì)應(yīng)節(jié)點(diǎn)
3. 將數(shù)據(jù)切分為chunk秽荞,將每個(gè)chunk抚官,依次發(fā)送給所有近鄰節(jié)點(diǎn)
4. 如果數(shù)據(jù)發(fā)送過程中發(fā)現(xiàn)網(wǎng)絡(luò)錯(cuò)誤扬跋,關(guān)閉與對(duì)應(yīng)節(jié)點(diǎn)的鏈接,并將其從鏈接池中刪除凌节。
10)去中心化全網(wǎng)數(shù)據(jù)轉(zhuǎn)發(fā)應(yīng)用示例
從上面可以看到廣播服務(wù)只是將數(shù)據(jù)廣播到本地節(jié)點(diǎn)的近鄰節(jié)點(diǎn)中钦听,要實(shí)現(xiàn)數(shù)據(jù)的全網(wǎng)廣播可以通過上層應(yīng)用實(shí)現(xiàn)倍奢。具體實(shí)現(xiàn)如下:
1. 創(chuàng)建一個(gè)routine,持續(xù)從P2P客戶端接收數(shù)據(jù)
2. 對(duì)于每個(gè)接收到的數(shù)據(jù)卒煞,再通過P2P客戶端發(fā)送到網(wǎng)絡(luò)中畔裕。這樣通過近鄰節(jié)點(diǎn)間接力傳播的方式,完成數(shù)據(jù)的全網(wǎng)廣播扮饶。