1. 摘要
區(qū)塊鏈節(jié)點的網(wǎng)絡(luò)模塊主要負責節(jié)點之間的點對點(P2P)的通信,具有管理節(jié)點榴都、節(jié)點間的數(shù)據(jù)收發(fā)等功能待锈。本文介紹P2P點對點網(wǎng)絡(luò)傳輸?shù)脑硪约耙蕴痪W(wǎng)絡(luò)節(jié)點發(fā)現(xiàn)和傳輸?shù)脑怼?/p>
2. 區(qū)塊鏈P2P網(wǎng)絡(luò)原理
2.1 區(qū)塊鏈其他節(jié)點如何加入到現(xiàn)有P2P網(wǎng)絡(luò)中
在區(qū)塊鏈的全網(wǎng)節(jié)點中存在一類叫“種子節(jié)點”的節(jié)點(以下簡稱種子節(jié)點),該類節(jié)點是網(wǎng)絡(luò)中的最早出現(xiàn)的節(jié)點嘴高,負責對其他節(jié)點的分享工作竿音。
在區(qū)塊鏈的安裝程序中會初始化配置種子節(jié)點的信息,包括節(jié)點的IP地址和端口號拴驮,所有新加入的節(jié)點在安裝區(qū)塊鏈程序并啟動時會與初始化的種子節(jié)點建立連接春瞬,將自身的IP地址和端口號告知種子節(jié)點,種子節(jié)點也將現(xiàn)有P2P網(wǎng)絡(luò)中的其他節(jié)點信息告知新節(jié)點套啤,并將新加入的節(jié)點告知其他節(jié)點宽气。
新節(jié)點收到其他節(jié)點信息后,與這些現(xiàn)有節(jié)點建立連接潜沦。自此输硝,新加入的節(jié)點就加入到P2P網(wǎng)絡(luò)中谭梗。
2.2 網(wǎng)絡(luò)模塊如何與種子節(jié)點溝通的澈吨?
2.3 新節(jié)點的網(wǎng)絡(luò)模塊如何與其他兄弟節(jié)點溝通呢吴汪?
2.4 全網(wǎng)時間同步工作
區(qū)塊鏈的業(yè)務(wù)與時間戳密不可分,因此必須保證各個節(jié)點的時間是同步的喇闸,通過計算本地時間與網(wǎng)絡(luò)時間的時間偏差(以下稱為網(wǎng)絡(luò)時間偏移量)袄琳,利用該網(wǎng)絡(luò)時間偏移量對每個節(jié)點的本地時間進行修正來保證全網(wǎng)時間的一致性。在參與區(qū)塊鏈的業(yè)務(wù)時燃乍,本地節(jié)點的時間計算方式為本地系統(tǒng)時間加上網(wǎng)絡(luò)時間偏移量即得到當前節(jié)點系統(tǒng)時間唆樊。
每個節(jié)點從公共的NTP服務(wù)器獲取格林威治標準時間(代號GMT時間),計算本地時間與GMT時間的網(wǎng)絡(luò)時間偏移量刻蟹,利用該時間偏移量參數(shù)調(diào)整本地節(jié)點的時間逗旁,并且每隔2分鐘就重新計算時間偏移量;如果計算時間偏移量的時間消耗較大(大于3秒)時要立即重新計算舆瘪。
網(wǎng)絡(luò)時間偏移量的計算方法如下:每個節(jié)點從至少三個公共的NTP服務(wù)器獲取網(wǎng)絡(luò)時間片效,然后分別減去本地時間得到網(wǎng)絡(luò)時間偏移量參數(shù),為了防止網(wǎng)絡(luò)震動導致個別網(wǎng)絡(luò)時間的偏差較大而影響最終時間偏移量的計算英古,要排除掉偏移量中相互之間偏差較大的偏移量參數(shù)(如大于500ms)淀衣,最后取這些偏移量參數(shù)的算術(shù)平均值,即為本地節(jié)點網(wǎng)絡(luò)時間偏移量召调。
如果從公共的NTP服務(wù)器無法獲取到網(wǎng)絡(luò)時間膨桥,則向全網(wǎng)其他節(jié)點獲取時間偏移量蛮浑,然后取獲取到的時間偏移量參數(shù)的算術(shù)平均值,即為本地節(jié)點網(wǎng)絡(luò)時間偏移量只嚣。
2.5 節(jié)點間消息轉(zhuǎn)發(fā)
為了提高處理能力沮稚,與節(jié)點之間的交互可以采取異步消息的方式,當與其他節(jié)點進行異步消息交互時册舞,將消息放入消息轉(zhuǎn)發(fā)隊列中蕴掏,在后臺完成將消息轉(zhuǎn)發(fā)給其他目的節(jié)點的消息轉(zhuǎn)發(fā)工作。
不斷地監(jiān)聽每個節(jié)點的消息轉(zhuǎn)發(fā)隊列环础,查看是否有待發(fā)送的消息囚似,如果存在則將待發(fā)送的消息發(fā)送給該節(jié)點剩拢。為了防止因節(jié)點待轉(zhuǎn)發(fā)的消息過多而引起其他節(jié)點的消息堵塞线得,限制每個節(jié)點每輪最多只轉(zhuǎn)發(fā)10條消息,發(fā)完后等待下一輪徐伐。
2.6 節(jié)點內(nèi)部模塊之間消息轉(zhuǎn)發(fā)
區(qū)塊鏈節(jié)點中分為網(wǎng)絡(luò)模塊贯钩、賬本模塊、賬戶模塊办素、區(qū)塊模塊角雷、交易模塊、合約模塊等11個模塊性穿,各個模塊都各司其職勺三,負責處理各類消息。當網(wǎng)絡(luò)模塊收到其他節(jié)點發(fā)送過來的消息后需曾,根據(jù)消息名稱轉(zhuǎn)交其他不同的模塊進行處理吗坚。模塊劃分如下所示:
網(wǎng)絡(luò)模塊只負責處理網(wǎng)絡(luò)通信協(xié)議部分的消息,包括版本消息呆万、確認消息商源、地址消息、獲取地址消息谋减、獲取時間消息牡彻、響應(yīng)時間消息、心跳消息等與網(wǎng)絡(luò)相關(guān)的消息名稱出爹,當網(wǎng)絡(luò)模塊收到這些消息名稱的消息之后庄吼,由該模塊的相應(yīng)處理器進行業(yè)務(wù)處理。
當網(wǎng)絡(luò)模塊收到其他消息后严就,首先根據(jù)消息名稱查找能處理該消息的模塊名稱霸褒,然后將此消息封裝成內(nèi)部命名的消息格式,再通過websocket通信方式轉(zhuǎn)發(fā)給處理模塊盈蛮;但是如果與該模塊之間的通道被占滿废菱,則就將該消息放入消息轉(zhuǎn)發(fā)列表中技矮,由網(wǎng)絡(luò)模塊在后臺進行消息轉(zhuǎn)發(fā),接收模塊在接收到此消息后由消息處理器進行處理殊轴。
3. 以太坊P2P原理及實現(xiàn)
以太坊的p2p網(wǎng)絡(luò)主要有兩部分構(gòu)成:節(jié)點之間互相連接用于傳輸數(shù)據(jù)的tcp網(wǎng)絡(luò)涉及的算法有Gossip 協(xié)議算法衰倦;節(jié)點之間互相廣播用于節(jié)點發(fā)現(xiàn)的udp網(wǎng)絡(luò),涉及的算法有kademlia DHT算法旁理。
3.1 分布式哈希表DHT(Kademlia算法)介紹
以太坊網(wǎng)絡(luò)的節(jié)點發(fā)現(xiàn)使用了結(jié)構(gòu)化的kad算法樊零,而上層的節(jié)點廣播是以無結(jié)構(gòu)的形式進行。先了解下Kademlia算法的原理孽文。
Kademlia算法是一種分布式存儲及路由的算法驻襟。什么是分布式存儲?試想一下芋哭,一所1000人的學校沉衣,現(xiàn)在學校突然決定拆掉圖書館(不設(shè)立中心化的服務(wù)器),將圖書館里所有的書都分發(fā)到每位學生手上(所有的文件分散存儲在各個節(jié)點上)减牺。即是所有的學生豌习,共同組成了一個分布式的圖書館。
在這種場景下拔疚,有幾個關(guān)鍵的問題需要回答肥隆。
1)關(guān)鍵問題
- 每個同學手上都分配哪些書。即如何分配存儲內(nèi)容到各個節(jié)點稚失,新增/刪除內(nèi)容如何處理栋艳。
- 當你需要找到一本書,譬如《分布式算法》的時候句各,如何知道哪位同學手上有《分布式算法》(對1000個人挨個問一遍吸占,“你有沒有《分布式算法》?”诫钓,顯然是個不經(jīng)濟的做法)旬昭,又如何聯(lián)系上這位同學。即一個節(jié)點如果想獲取某個特定的文件菌湃,如何找到存儲文件的節(jié)點/地址/路徑问拘。
如何尋找需要的書籍?
接下來惧所,讓我們來看看Kademlia算法如何巧妙地解決這些問題骤坐。
2)節(jié)點的要素
首先我們來看看每個同學(節(jié)點)都有哪些屬性:
- 學號(Node ID,2進制下愈,160位)
- 手機號碼(節(jié)點的IP地址及端口)
每個同學會維護以下內(nèi)容:
- 從圖書館分發(fā)下來的書本(被分配到需要存儲的內(nèi)容)纽绍,每本書當然都有書名和書本內(nèi)容(內(nèi)容以<key, value>對的形式存儲,可以理解為文件名和文件內(nèi)容)势似;
- 一個通訊錄拌夏,包含一小部分其他同學的學號和手機號僧著,通訊錄按學號分層(一個路由表,稱為“k-bucket”障簿,按Node ID分層盹愚,記錄有限個數(shù)的其他節(jié)點的ID和IP地址及端口)。
根據(jù)上面那個類比站故,可以看看這個表格:
概念對比
(Hash的概念解釋皆怕,可參見百度百科-哈希算法)
關(guān)于為什么不是每個同學都有全量通訊錄(每個節(jié)點都維護全量路由信息):其一,分布式系統(tǒng)中節(jié)點的進入和退出是相當頻繁的西篓,每次有變動時都全網(wǎng)廣播通訊錄更新愈腾,通訊量會很大;其二岂津,一旦任意一個同學被壞人綁架了(節(jié)點被黑客攻破)虱黄,則壞人馬上就擁有了所有人的手機號碼,這并不安全寸爆。
3)文件的存儲及查找
原來收藏在圖書館里礁鲁,按索引號碼得整整齊齊的書盐欺,以一種什么樣的方式分發(fā)到同學們手里呢赁豆?大致的原則,包括:1)書本能夠比較均衡地分布在同學們的手里冗美,不會出現(xiàn)部分同學手里書特別多魔种、而大部分同學連一本書都沒有的情況;2)同學想找一本特定的書的時候粉洼,能夠一種相對簡單的索引方式找到這本書节预。
Kademlia作了下面這種安排:
假設(shè)《分布式算法》這本書的書名的hash值是 00010000,那么這本書就會被要求存在學號為00010000的同學手上属韧。(這要求hash算法的值域與node ID的值域一致安拟。Kademlia的Node ID是160位2進制。這里的示例對Node ID進行了簡略)
但還得考慮到會有同學缺勤宵喂。萬一00010000今天沒來上學(節(jié)點沒有上線或徹底退出網(wǎng)絡(luò))糠赦,那《分布式算法》這本書豈不是誰都拿不到了?那算法要求這本書不能只存在一個同學手上锅棕,而是被要求同時存儲在學號最接近00010000的k位同學手上拙泽,即00010001、00010010裸燎、00010011…等同學手上都會有這本書顾瞻。
同樣地,當你需要找《分布式算法》這本書時德绿,將書名hash一下荷荤,得到 00010000退渗,這個便是索書號,你就知道該找哪(幾)位同學了蕴纳。剩下的問題氓辣,就是找到這(幾)位同學的手機號。
4)節(jié)點的異或距離
由于你手上只有一部分同學的通訊錄袱蚓,你很可能并沒有00010000的手機號(IP地址)钞啸。那如何聯(lián)系上目標同學呢?
一個可行的思路就是在你的通訊錄里找到一位擁有目標同學的聯(lián)系方式的同學喇潘。前面提到体斩,每位同學手上的通訊錄都是按距離分層的。算法的設(shè)計是颖低,如果一個同學離你越近絮吵,你手上的通訊錄里存有ta的手機號碼的概率越大。而算法的核心的思路就可以是:當你知道目標同學Z與你之間的距離忱屑,你可以在你的通訊錄上先找到一個你認為與同學Z最相近的同學B蹬敲,請同學B再進一步去查找同學Z的手機號。
上文提到的距離莺戒,是學號(Node ID)之間的異或距離(XOR distance)伴嗡。異或是針對yes/no或者二進制的運算.
異或的運算法則為:0⊕0=0,1⊕0=1从铲,0⊕1=1瘪校,1⊕1=0(同為0,異為1)
百度百科-異或
舉2個例子:
01010000與01010010距離(即是2個ID的異或值)為00000010(換算為十進制即為2)名段;
01000000與00000001距離為01000001(換算為十進制即為26+1阱扬,即65);
如此類推伸辟。
那通訊錄是如何按距離分層呢麻惶?下面的示例會告訴你,按異或距離分層信夫,基本上可以理解為按位數(shù)分層窃蹋。設(shè)想以下情景:
以0000110為基礎(chǔ)節(jié)點,如果一個節(jié)點的ID忙迁,前面所有位數(shù)都與它相同脐彩,只有最后1位不同,這樣的節(jié)點只有1個——0000111姊扔,與基礎(chǔ)節(jié)點的異或值為0000001惠奸,即距離為1;對于0000110而言恰梢,這樣的節(jié)點歸為“k-bucket 1”佛南;
如果一個節(jié)點的ID梗掰,前面所有位數(shù)相同,從倒數(shù)第2位開始不同嗅回,這樣的節(jié)點只有2個:0000101及穗、0000100,與基礎(chǔ)節(jié)點的異或值為0000011和0000010绵载,即距離范圍為3和2埂陆;對于0000110而言,這樣的節(jié)點歸為“k-bucket 2”娃豹;
……
如果一個節(jié)點的ID焚虱,前面所有位數(shù)相同,從倒數(shù)第i位開始不同懂版,這樣的節(jié)點只有2(i-1)個鹃栽,與基礎(chǔ)節(jié)點的距離范圍為[2(i-1), 2i);對于0000110而言躯畴,這樣的節(jié)點歸為“k-bucket i”民鼓;
對上面描述的另一種理解方式:如果將整個網(wǎng)絡(luò)的節(jié)點梳理為一個按節(jié)點ID排列的二叉樹,樹最末端的每個葉子便是一個節(jié)點蓬抄,則下圖就比較直觀的展現(xiàn)出丰嘉,節(jié)點之間的距離的關(guān)系。
k-bucket示意圖:右下角的黑色實心圓倡鲸,為基礎(chǔ)節(jié)點(按wiki百科的配圖修改)
回到我們的類比供嚎。每個同學只維護一部分的通訊錄黄娘,這個通訊錄按照距離分層(可以理解為按學號與自己的學號從第幾位開始不同而分層)峭状,即k-bucket1, k-bucket 2, k-bucket 3…雖然每個k-bucket中實際存在的同學人數(shù)逐漸增多,但每個同學在它自己的每個k-bucket中只記錄k位同學的手機號(k個節(jié)點的地址與端口逼争,這里的k是一個可調(diào)節(jié)的常量參數(shù))优床。
由于學號(節(jié)點的ID)有160位,所以每個同學的通訊錄中共分160層(節(jié)點共有160個k-bucket)誓焦。整個網(wǎng)絡(luò)最多可以容納2^160個同學(節(jié)點)胆敞,但是每個同學(節(jié)點)最多只維護160 * k 行通訊錄(其他節(jié)點的地址與端口)。
5)節(jié)點定位
我們現(xiàn)在來闡述一個完整的索書流程杂伟。
A同學(學號00000110)想找《分布式算法》移层,A首先需要計算書名的哈希值,hash(《分布式算法》) = 00010000赫粥。那么A就知道ta需要找到00010000號同學(命名為Z同學)或?qū)W號與Z鄰近的同學观话。
Z的學號00010000與自己的異或距離為 00010110,距離范圍在[24, 25)越平,所以這個Z同學可能在k-bucket 5中(或者說频蛔,Z同學的學號與A同學的學號從第5位開始不同灵迫,所以Z同學可能在k-bucket 5中)。
然后A同學看看自己的k-bucket 5有沒有Z同學:
- 如果有晦溪,那就直接聯(lián)系Z同學要書瀑粥;
- 如果沒有,在k-bucket 5里隨便找一個B同學(注意任意B同學三圆,它的學號第5位肯定與Z相同狞换,即它與Z同學的距離會小于24,相當于比Z舟肉、A之間的距離縮短了一半以上)哀澈,請求B同學在它自己的通訊錄里按同樣的查找方式找一下Z同學:
-- 如果B知道Z同學,那就把Z同學的手機號(IP Address)告訴A度气;
-- 如果B也不知道Z同學割按,那B按同樣的搜索方法,可以在自己的通訊錄里找到一個離Z更近的C同學(Z磷籍、C之間距離小于23)适荣,把C同學推薦給A;A同學請求C同學進行下一步查找院领。
Kademlia的這種查詢機制弛矛,有點像是將一張紙不斷地對折來收縮搜索范圍,保證對于任意n個學生比然,最多只需要查詢log2(n)次丈氓,即可找到獲得目標同學的聯(lián)系方式(即在對于任意一個有[2(n?1), 2n)個節(jié)點的網(wǎng)絡(luò),最多只需要n步搜索即可找到目標節(jié)點)强法。
以上便是Kademlia算法的基本原理万俗。以下再簡要介紹協(xié)議中的技術(shù)細節(jié)。
6)算法的三個參數(shù):keyspace饮怯,k和α
- keyspace
-- 即ID有多少位
-- 決定每個節(jié)點的通訊錄有幾層 - k
-- 每個一層k-bucket里裝k個node的信息闰歪,即<node ID, IP Adress, port>
-- 每次查找node時,返回k個node的信息
-- 對于某個特定的data蓖墅,離其key最近的k個節(jié)點被會要求存儲這個data - α
-- 每次向其他node請求查找某個node時库倘,會向α個node發(fā)出請求
7)節(jié)點的指令
Kademlia算法中,每個節(jié)點只有4個指令
- PING
-- 測試一個節(jié)點是否在線 - STORE
-- 要求一個節(jié)點存儲一份數(shù)據(jù) - FIND_NODE
-- 根據(jù)節(jié)點ID查找一個節(jié)點 - FIND_VALUE
-- 根據(jù)KEY查找一個數(shù)據(jù)论矾,實則上跟FIND_NODE非常類似
8) k-bucket的維護及更新機制
- 每個bucket里的節(jié)點都按最后一次接觸的時間倒序排列
- 每次執(zhí)行四個指令中的任意一個都會觸發(fā)更新
- 當一個節(jié)點與自己接觸時教翩,檢查它是否在K-bucket中
-- 如果在,那么將它挪到k-bucket列表的最底(最新)
-- 如果不在贪壳,PING一下列表最上面(最舊)的一個節(jié)點
-- a) 如果PING通了饱亿,將舊節(jié)點挪到列表最底,并丟棄新節(jié)點
-- b) 如果PING不通,刪除舊節(jié)點路捧,并將新節(jié)點加入列表
該機制保證了任意節(jié)點加入和離開都不影響整體網(wǎng)絡(luò)关霸。
9)總結(jié)
Kademlia是分布式哈希表(Distributed Hash Table, DHT)的一種。而DHT是一類去中心化的分布式系統(tǒng)杰扫。在這類系統(tǒng)中队寇,每個節(jié)點(node)分別維護一部分的存儲內(nèi)容以及其他節(jié)點的路由/地址,使得網(wǎng)絡(luò)中任何參與者(即節(jié)點)發(fā)生變更(進入/退出)時章姓,對整個網(wǎng)絡(luò)造成的影響最小佳遣。DHT可以用于構(gòu)建更復雜的應(yīng)用,包括分布式文件系統(tǒng)凡伊、點對點技術(shù)文件分享系統(tǒng)零渐、合作的網(wǎng)頁高速緩存、域名系統(tǒng)以及實時通信等系忙。
Kademlia算法在2002年由Petar Maymounkov 和 David Mazières 所設(shè)計诵盼,以異或距離來對哈希表進行分層是其特點。Kademlia后來被eMule银还、BitTorrent等P2P軟件采用作為底層算法风宁。Kademlia可以作為信息安全技術(shù)的奠基之一。
Kademlia的優(yōu)點在于:
- 對于任意一個有[ 2(n?1) ,2??)個節(jié)點的網(wǎng)絡(luò)蛹疯,最多只需要n步搜索即可找到目標節(jié)點戒财;
- K-bucket的更新機制一定程度上保持了網(wǎng)絡(luò)的活性和安全性。
3.2 節(jié)點發(fā)現(xiàn)的udp網(wǎng)絡(luò)
(1)P2P整體結(jié)構(gòu)
在以太坊中捺弦,節(jié)點之間數(shù)據(jù)的傳輸是通過tcp來完成的饮寞。但是節(jié)點如何找到可以進行數(shù)據(jù)傳輸?shù)墓?jié)點?這就涉及到P2P的核心部分節(jié)點發(fā)現(xiàn)了列吼。每個節(jié)點會維護一個table幽崩,table中會存儲可供連接的其他節(jié)點的地址冈欢。這個table通過基于udp的節(jié)點發(fā)現(xiàn)機制來保持更新和可用歉铝。當一個節(jié)點需要更多的TCP連接用于數(shù)據(jù)傳輸時,它就會從這個table中獲取待連接的地址并建立TCP連接凑耻。
與節(jié)點建立連接的流程大致如下圖:
上圖中,第一和第二步每隔一段時間就會重復一次柠贤。目的是保持當前節(jié)點的table所存儲的url的可用性香浩。當節(jié)點需要尋找新的節(jié)點用于tcp數(shù)據(jù)傳輸時,就從一直在更新維護的table中取出一定數(shù)量的url進行連接即可臼勉。
為了防止節(jié)點之間的 tcp 互聯(lián)形成信息孤島邻吭,每個以太坊節(jié)點間的連接都分為兩種類型:bound_in 和 bound_out。bound_in 指本節(jié)點接收到別人發(fā)來的請求后建立的 tcp 連接宴霸,bound_out 指本節(jié)點主動發(fā)起的 tcp 連接囱晴。假設(shè)一個節(jié)點最多只能與6個節(jié)點互聯(lián)膏蚓,那么在默認的設(shè)置中,節(jié)點最多只能主動和 4 個節(jié)點連接畸写,剩余的必須留著給其它節(jié)點接入用驮瞧。對于 bound_in 連接的數(shù)量則不做限制。
(2)UDP部分
前文提到過枯芬,以太坊會維護一個table結(jié)構(gòu)用于存儲發(fā)現(xiàn)的節(jié)點url论笔,當需要連接新的節(jié)點時會從table中獲取一定數(shù)量的url進行tcp連接。在這個過程中千所,table的更新和維護將會在獨立的goroutine中進行狂魔。這部分涉及的代碼主要在 p2p/discvV5 目錄下的 udp.go, net.go, table.go 這三個文件中。下面我們看看udp這部分的主要邏輯結(jié)構(gòu):
readloop()
readloop()方法在 p2p/discvV5/udp.go 文件中淫痰,用來監(jiān)聽所有收到的udp消息最楷。通過 ReadFromUDP() 接收收到的消息,然后在 handlePacket() 方法內(nèi)將消息序列化后傳遞給后續(xù)的消息處理程序待错。
sendPacket()
sendPacket() 用于將消息通過 udp 發(fā)送給目標地址管嬉。
UDP msg type
所有通過 udp 發(fā)出的消息都分為兩種類型:ping 和 pong。ping 作為通信的發(fā)起類型朗鸠,pong 作為通信的應(yīng)答類型蚯撩。一個完整的通訊應(yīng)該是:
- Node A 向 Node B 發(fā)送 ping 類型的 udp 消息。
- Node B 收到此消息后進行消息處理烛占。
- Node B 向 Node A 發(fā)送 pong 類型的消息作為之前收到的 ping 消息的應(yīng)答胎挎。
loop()
loop() 方法在 p2p/discvV5/net.go 文件中,是整個 p2p 部分的核心方法忆家。它控制了節(jié)點發(fā)現(xiàn)機制的大部分邏輯內(nèi)容犹菇,由一個大的 select 進行各種 case 的監(jiān)控。主體代碼大致如下:
nodeState
nodeState 表示了一個url對應(yīng)的狀態(tài)芽卿。在 net.go 的 init() 方法中定義了各種 nodeState:
nodeState主要是為了記錄新地址的可用狀態(tài)揭芍。當節(jié)點接觸新的 url 時,此 url 會首先被定義為 unknown 狀態(tài)卸例,然后進入后續(xù) ping pong 驗證階段称杨。驗證流程過完以后 就會被定義為 known 狀態(tài)然后保存到 table 中。
enter() 和 handle()
nodeState 結(jié)構(gòu)下定義了兩個方法 enter() 和 handle()筷转。
- enter 是 nodeState 的入口方法姑原,當一個 url 從 nodeState1 轉(zhuǎn)到 nodeState2 時,它就會調(diào)用 nodeState2 的 enter() 方法呜舒。
- handle 是 nodeState 的處理方法锭汛,當節(jié)點收到某個 url 的 udp 消息時,會調(diào)用此 url 當前 nodeState 的 handle() 方法。一般 handle() 方法會針對不同的 udp 消息類型進行不同的邏輯處理:
handle() 方法在處理后會返回一個 nodeState唤殴,意思是我當前的 nodeState 在處理了這個 udp 消息后下一步將要轉(zhuǎn)換的新的 state般婆。比如在上面的代碼中,verifywait 狀態(tài)下的 url 在收到 pong 類型的 udp 消息后就會轉(zhuǎn)換成 known 狀態(tài)朵逝。
(3)新節(jié)點加入流程
新節(jié)點的加入分兩種:
1蔚袍,獲取到新的 url 后主動向?qū)Ψ桨l(fā)起 udp 請求,在完成一系列驗證后將此 url 加入到 table 中廉侧。
2页响,收到陌生 url 地址發(fā)來的 udp 請求,在互相做完驗證后加入到 table 中段誊。
這兩種情況是相對應(yīng)的闰蚕,NodeA 獲取到 NodeB 的 url 后向 NodeB 發(fā)起 udp 請求(情況1),NodeB 收到來自 NodeA 的陌生 url 后做應(yīng)答(情況2)连舍。雙方驗證完成后互相將對方的 url 加入到table中没陡。整個流程步驟如下:
1,作為驗證發(fā)起方索赏,NodeA 在收到 url 后會將這個 url 的 nodeState 設(shè)置為 verifyinit 并通過 verifyinit 的 enter() 方法向 nodeB 的 url 發(fā)起 ping 消息盼玄。
2,NodeB 收到此 ping 消息后先獲取此 url 的 nodeState潜腻,發(fā)現(xiàn)未找到于是將其設(shè)置為 unknown 然后調(diào)用 unknown 的 handle() 方法埃儿。
3,在 handle() 方法中融涣,NodeB 會先向 NodeA 發(fā)送一個 pong 消息表示對收到的 ping 的應(yīng)答桥氏。然后再向 NodeA 發(fā)送一個新的 ping 消息等待 NodeA的應(yīng)答吹菱。同時將 NodeA 的 state 設(shè)置為 verifywait们衙。
4瞬逊,NodeA 收到 NodeB 發(fā)來的 pong 消息后調(diào)用 verifyinit 狀態(tài)的 handle() 然后將 NodeB 設(shè)置為 remoteverifywait 狀態(tài)。remoteverifywait 的意思就是 NodeB 在我這個節(jié)點(NodeA)這里已經(jīng)經(jīng)過 ping pong 驗證通過了忽你,但是我還沒在 NodeB 那邊經(jīng)過 ping pong 驗證幼东。
5,將 NodeB 的 state 轉(zhuǎn)換為 remoteverifywait 后會調(diào)用 remoteverifywait 的 enter() 方法科雳。此方法開啟一個定時器根蟹,定時器到點后向本節(jié)點發(fā)送一個 timeout 消息。
6炸渡,處理完 pong 消息后娜亿,NodeA 又收到了來自 NodeB的 ping 消息,此時調(diào)用 remoteverifywait 的 handle() 向 NodeB 發(fā)送一個 pong 應(yīng)答蚌堵。
7,NodeB 收到 NodeA 的 pong 應(yīng)答后調(diào)用 verifywait 的 handle() 方法,此方法會將 NodeA 的 state 設(shè)置為 known吼畏。在 state 轉(zhuǎn)換后調(diào)用新 state 也就是 known state 的 enter() 方法督赤。known 的 enter() 中會將 NodeA url 加入到 NodeB 的 table 中。
8泻蚊,Step5 中 NodeA 的定時器到點并向 NodeA 自己發(fā)送 timeout 消息躲舌。此消息會調(diào)用 remoteverifywait 的 handle() 然后將 NodeB 的狀態(tài)設(shè)置為 known。和 Step7 中一樣性雄,更改狀態(tài)為 known 后 NodeB 的 url 會加入到 NodeA 的 table 中没卸。
p.s. 以上步驟結(jié)合源碼會更清晰
(4)lookup節(jié)點發(fā)現(xiàn)
上文中,NodeA 收到了 NodeB 的 url 從而發(fā)起兩者間的聯(lián)系秒旋。那么 NodeA 是如何得到這個 url 的呢约计?答案是通過 lookup()方法來發(fā)現(xiàn)這個 url。
lookup() 方法源碼在 p2p/discvV5/net.go 文件中迁筛。此方法在需要獲取新的 url 或者需要刷新 table 時被調(diào)用煤蚌。
lookup() 需要輸入一個 target 作為目標,然后尋找和 target 最近的 n 個節(jié)點细卧。target 是由一個節(jié)點的 NodeID 經(jīng)過哈希計算得來的尉桩。在以太坊中,每次生成新的target 都會虛構(gòu)一個節(jié)點 url 然后 hash 計算得到 target贪庙。lookup() 方法的基本思路是先從本地 table 獲取和 target 最近的一部分鄰居節(jié)點蜘犁,然后再獲取這些鄰居節(jié)點的 table 中和 target 最近的部分節(jié)點。最后從得到的節(jié)點中選距離 target 最近的 n 個節(jié)點 url 作為新的 table 內(nèi)容止邮。如果這些 url 中有部分是之前未接觸的这橙,則程序會走上文提到的新節(jié)點加入流程來建立關(guān)系。
這里提到的距離最近不是指物理距離农尖,而是數(shù)理上的最近析恋。程序會計算節(jié)點 NodeID 經(jīng)過 hash 計算后和 target 的差異大小來判斷數(shù)理上的距離大小,具體可以參考 closest() 方法盛卡。
(5)結(jié)論
以太坊的P2P大致就是上述的實現(xiàn)方式助隧。總的來說就是通過 udp 發(fā)現(xiàn)可供使用的節(jié)點 url 并將這些 url 維護在本地的 table 中滑沧,通過 tcp 和其他節(jié)點進行連接并進行數(shù)據(jù)傳輸并村,當需要新的節(jié)點時從 table 中獲取未接觸過的 url 建立新的 tcp 連接。
3.3 GOSSIP協(xié)議
以太坊節(jié)點通信采用GOSSIP協(xié)議滓技。
gossip 協(xié)議(gossip protocol)又稱 epidemic 協(xié)議(epidemic protocol)哩牍,是基于流行病傳播方式的節(jié)點或者進程之間信息交換的協(xié)議,在分布式系統(tǒng)中被廣泛使用令漂,比如我們可以使用 gossip 協(xié)議來確保網(wǎng)絡(luò)中所有節(jié)點的數(shù)據(jù)一樣膝昆。gossip protocol 最初是由施樂公司帕洛阿爾托研究中心(Palo Alto Research Center)的研究員艾倫·德默斯(Alan Demers)于1987年創(chuàng)造的丸边。
從 gossip 單詞就可以看到,其中文意思是八卦荚孵、流言等意思妹窖,我們可以想象下緋聞的傳播(或者流行病的傳播);gossip 協(xié)議的工作原理就類似于這個收叶。gossip 協(xié)議利用一種隨機的方式將信息傳播到整個網(wǎng)絡(luò)中骄呼,并在一定時間內(nèi)使得系統(tǒng)內(nèi)的所有節(jié)點數(shù)據(jù)一致。Gossip 其實是一種去中心化思路的分布式協(xié)議判没,解決狀態(tài)在集群中的傳播和狀態(tài)一致性的保證兩個問題蜓萄。
我們通過一個具體的實例來深入體會一下 Gossip 傳播的完整過程
為了表述清楚,我們先做一些前提設(shè)定
(1)Gossip 是周期性的散播消息澄峰,把周期限定為 1 秒
(2)被感染節(jié)點隨機選擇 k 個鄰接節(jié)點(fan-out)散播消息嫉沽,這里把 fan-out 設(shè)置為 3,每次最多往 3 個節(jié)點散播摊阀。
(3)每次散播消息都選擇尚未發(fā)送過的節(jié)點進行散播
(4)收到消息的節(jié)點不再往發(fā)送節(jié)點散播耻蛇,比如 A -> B,那么 B 進行散播的時候胞此,不再發(fā)給 A臣咖。
這里一共有 16 個節(jié)點,節(jié)點 1 為初始被感染節(jié)點漱牵,通過 Gossip 過程夺蛇,最終所有節(jié)點都被感染:
在以太坊,超級賬本 Fabric 和 Cassandra 數(shù)據(jù)庫中酣胀,Gossip 協(xié)議在其信息同步中都發(fā)揮了重要作用刁赦。
(1)Gossip協(xié)議類型
傳播協(xié)議/謠言協(xié)議(Dissemination Protocols / Rumor-Mongering Protocols):
通過網(wǎng)絡(luò)中的泛洪代理來工作,節(jié)點收到廣播的數(shù)據(jù)后直接轉(zhuǎn)發(fā)給所有的鄰居節(jié)點闻镶;此方式可以提高網(wǎng)絡(luò)的健壯性甚脉,但是容易造成廣播風暴。
謠言傳播铆农,廣泛地散播謠言牺氨,它指的是當一個節(jié)點有了新數(shù)據(jù)后,這個節(jié)點變成活躍狀態(tài)墩剖,并周期性地聯(lián)系其他節(jié)點向其發(fā)送新數(shù)據(jù)猴凹,直到所有的節(jié)點都存儲了該新數(shù)據(jù):
從圖中你可以看到,節(jié)點 A 向節(jié)點 B岭皂、D 發(fā)送新數(shù)據(jù)郊霎,節(jié)點 B 收到新數(shù)據(jù)后,變成活躍節(jié)點爷绘,然后節(jié)點 B 向節(jié)點 C书劝、D 發(fā)送新數(shù)據(jù)进倍。其實,謠言傳播非常具有傳染性庄撮,它適合動態(tài)變化的分布式系統(tǒng)背捌。
反熵協(xié)議(Anti-Entropy Protocols):用于修復復制數(shù)據(jù)毙籽,通過比較復制和協(xié)調(diào)差異進行操作洞斯;Hyperledger Fabric中的數(shù)據(jù)同步就是使用此方式實現(xiàn)。
計算聚合的協(xié)議(Protocols that Compute Aggregates):通過對網(wǎng)絡(luò)中節(jié)點的信息進行采樣坑赡,并將這些值組合起來得到系統(tǒng)范圍內(nèi)的值烙如,從而計算出網(wǎng)絡(luò)范圍內(nèi)的集合 ;之后將建立一種全面的信息流模式毅否。
(2)Gossip數(shù)據(jù)傳輸(Gossip Messaging)
Gossip的兩個節(jié)點A亚铁、B之間有三種交互模式:
1、Push 模式:B節(jié)點將數(shù)據(jù)(key螟加,value徘溢,version)及對應(yīng)的版本號推送給 A 節(jié)點,A 節(jié)點更新 B 節(jié)點中比自己新的數(shù)據(jù)捆探。在 Push 模式中然爆,發(fā)起信息交換的節(jié)點隨機選取節(jié)點并向其發(fā)送自己的信息,一般擁有新信息的節(jié)點才會作為發(fā)起節(jié)點黍图。
假設(shè)存在 n 個節(jié)點,采用 Push 方式助被,在數(shù)據(jù)傳播的初期,已收到消息的節(jié)點數(shù)目呈指數(shù)增長揩环,直到有一半節(jié)點收到該消息。此后蹦渣,未收到消息的節(jié)點集合將會在每一輪以一個常數(shù)因子進行收縮锄奢。當消息傳播過程結(jié)束時該常數(shù)因子為灰伟,因為在一輪中沒有收到消息的節(jié)點的占栏账。因此,收縮過程將會通過輪直到所有的節(jié)點均收到消息男杈,Push 模式在每一輪中發(fā)送的消息數(shù)量為先蒋。
2借笙、Pull 模式:A 僅將數(shù)據(jù) key君编、version 推送給 B,B 將本地比 A 新的數(shù)據(jù)(key蚓胸,value挣饥,version)推送給 A,A 更新本地沛膳。
在 Pull 模式中扔枫,發(fā)起數(shù)據(jù)交換的節(jié)點隨機選擇節(jié)點并獲取所選節(jié)點的數(shù)據(jù),一般無新數(shù)據(jù)的節(jié)點才會作為發(fā)起節(jié)點锹安。Pull 方式與 Push 方式相反短荐,在數(shù)據(jù)傳播的初期,收到消息的節(jié)點的數(shù)量增長緩慢叹哭;當已有一半節(jié)點收到消息之后忍宋,每輪過后未收到消息的節(jié)點占比將呈平方收縮。
這是因為在一輪開始時假設(shè)有個未收到消息的節(jié)點风罩,每一個節(jié)點將有的概率收到消息糠排,因此節(jié)點保持未收到消息狀態(tài)的概率為,這一輪結(jié)束后未收到消息的節(jié)點數(shù)量為泊交。因此乳讥,未收到消息的節(jié)點數(shù)量的收縮過程僅需要輪柱查,該過程中每一輪傳遞的消息數(shù)量為。
3云石、Push-Pull 模式:在 Pull 的基礎(chǔ)上唉工,A 再將本地比 B 新的數(shù)據(jù)推送給 B,然后 B 再更新本地數(shù)據(jù)汹忠。也就是在 Pull 之后淋硝,A 再對比自己掌握的信息,更新 B 手中掌握的信息宽菜。
Push-Pull 模式是結(jié)合了具備可預測性的 Push 機制和具備平方收縮特性的 Pull 機制谣膳,在這種模式下,消息雙向發(fā)送铅乡。發(fā)起信息交換的節(jié)點先初始化一個時間計數(shù)器來代表信息的生命值继谚,初始計數(shù)為 0。
信息的生命值會隨著每次傳播而遞增阵幸。發(fā)起信息交換的節(jié)點隨機選取節(jié)點并向其發(fā)送自己的消息花履,同時從所選取的節(jié)點上獲取該節(jié)點的消息,這個過程將持續(xù)進行直到消息的生命值超過挚赊。
假設(shè)存在 n 個節(jié)點诡壁,在全連通網(wǎng)絡(luò)中采用 Push-Pull 的方式,那么完成 Gossip 過程需要輪和次數(shù)據(jù)交換荠割。
- 想象 A妹卿、B 是老同學,畢業(yè)后工作在同一家公司蔑鹦,朝夕相處夺克, A 想要了解 B 的信息,B 就只需要把 A 不知道的事情告訴他举反,類似于“推”模式懊直。
- 倘若只是工作在同一棟樓,兩人掌握的信息依然有一定的交集火鼻,A 與 B 在簡單的交流后室囊,B 就可以告訴 A 所不知道的信息,就類似于“拉”模式魁索。
- 如果兩個人畢業(yè)后在不同的國家融撞,在信息內(nèi)容上基本沒有交集,這時候就需要將自己知道的全部信息告訴彼此粗蔚,也就類似于“推-拉”模式尝偎。
3.4 交易廣播和區(qū)塊廣播的TCP部分
eth/handle.go中的ProtocolManager管理節(jié)點之間通信。節(jié)點與節(jié)點之間的通信,也就是區(qū)塊和交易的廣播或同步致扯。
這里先介紹廣播肤寝。提及廣播,要先說一個有趣的協(xié)議:gossip抖僵,對鲤看,就是流言蜚語。如果有關(guān)于明星的八卦或是負面新聞耍群,不用多長時間义桂,可能滿大街的人們就都知道了。廣播就類似于流言蜚語的傳播蹈垢,一傳十慷吊,十傳百的擴散出去,最后整個網(wǎng)絡(luò)都知曉了曹抬。
以下是ProtocolManager實現(xiàn)區(qū)塊和交易的廣播的流程圖:
接下來會一步一步介紹溉瓶。
0.索引
01.廣播和同步的啟動
02.區(qū)塊廣播
03.區(qū)塊廣播相關(guān)源碼
04.交易廣播以及源碼
05.異步發(fā)送區(qū)塊和交易的說明
06.總結(jié)
1.廣播和同步的啟動
區(qū)塊和交易的廣播與同步由ProtocolManager
協(xié)議管理控制。啟動方法為Start
沐祷。
func (pm *ProtocolManager) Start(maxPeers int) {
pm.maxPeers = maxPeers
// 廣播交易
pm.txsCh = make(chan core.NewTxsEvent, txChanSize)
pm.txsSub = pm.txpool.SubscribeNewTxsEvent(pm.txsCh)
go pm.txBroadcastLoop()
// 廣播區(qū)塊
pm.minedBlockSub = pm.eventMux.Subscribe(core.NewMinedBlockEvent{})
go pm.minedBroadcastLoop()
// 開始同步
go pm.syncer()
go pm.txsyncLoop()
}
開啟了4個協(xié)程:
- 1.創(chuàng)建了新交易事件的通道嚷闭,然后開始
pm.txBroadcastLoop()
廣播交易。 - 2.創(chuàng)建了新區(qū)塊事件的通道赖临,然后開始
pm.minedBroadcastLoop()
廣播區(qū)塊。 - 3.同步區(qū)塊灾锯,同步的過程在
eth/sync.go
里兢榨,下一次介紹。 - 4.同步交易顺饮。
先從區(qū)塊廣播開始吵聪。
2.區(qū)塊廣播
區(qū)塊廣播指的是礦工挖出新的區(qū)塊后,將新區(qū)塊告知并發(fā)給p2p網(wǎng)絡(luò)中的所有節(jié)點兼雄。這里涉及到兩個廣播過程:
- 1.礦工廣播新區(qū)塊
- 2.其他的中繼節(jié)點廣播新區(qū)快
如下圖:
第一輪:
黃色的節(jié)點表示礦工吟逝,礦工挖到區(qū)塊后,接下來要將區(qū)塊廣播出去赦肋,也就是發(fā)送給相鄰的節(jié)點块攒,這里相鄰的節(jié)點有5個,兩個紅色的節(jié)點和三個藍色的節(jié)點佃乘。紅色的節(jié)點表示收到區(qū)塊的節(jié)點囱井,藍色的節(jié)點表示收到區(qū)塊哈希的節(jié)點。
這里紅色的節(jié)點是有一定數(shù)量要求的趣避。取的是庞呕,要廣播的節(jié)點數(shù)量的平方根。要廣播5個節(jié)點,5取平方根再取整為2個住练。也就是說礦工向這兩個紅色節(jié)點直接發(fā)送了區(qū)塊地啰,然后向剩余的節(jié)點發(fā)送了區(qū)塊哈希。
第二輪:
接收到區(qū)塊哈希的藍色節(jié)點向發(fā)來區(qū)塊哈希的節(jié)點(也就是礦工)請求下載區(qū)塊讲逛,下載完區(qū)塊后亏吝,就跟接收到區(qū)塊的紅色節(jié)點一樣,向它的相鄰節(jié)點發(fā)送區(qū)塊和區(qū)塊哈希妆绞,如第一輪的過程顺呕。
這其中會有一種情況產(chǎn)生,如果提前接收到了未來的區(qū)塊括饶,比如說株茶,區(qū)塊A->區(qū)塊B->區(qū)塊C,需要的是區(qū)塊B图焰,但是接收到了區(qū)塊C的情況启盛,這時候會將區(qū)塊哈希進行廣播。
第n輪:
同第二輪技羔,直到整個網(wǎng)絡(luò)都知曉廣播的區(qū)塊僵闯。
(注意:下面的源碼介紹的是以礦工挖出區(qū)塊后的第一次廣播,也就是進行第一輪操作藤滥。交易廣播亦是如此鳖粟。)
3.區(qū)塊廣播相關(guān)源碼
首先是區(qū)塊廣播循環(huán)minedBroadcastLoop()
。
func (pm *ProtocolManager) minedBroadcastLoop() {
// 自動停止拙绊,如果退訂了通道向图。
for obj := range pm.minedBlockSub.Chan() {
if ev, ok := obj.Data.(core.NewMinedBlockEvent); ok {
// 廣播區(qū)塊。
pm.BroadcastBlock(ev.Block, true) // 首先廣播區(qū)塊标沪。
pm.BroadcastBlock(ev.Block, false) // 然后只廣播區(qū)塊哈希榄攀。
}
}
}
區(qū)塊廣播循環(huán)minedBroadcastLoop()
開啟了之后,會一直讀取區(qū)塊事件的通道金句,也就是如果有新的區(qū)塊事件產(chǎn)生檩赢,就能立即知曉。
然后進行區(qū)塊廣播违寞。調(diào)用了兩次pm.BroadcastBlock
方法贞瞒。第一次標志位為true
,給部分節(jié)點廣播區(qū)塊坞靶。第二次標志位為false
憔狞,只廣播區(qū)塊哈希。
關(guān)于pm.BroadcastBlock
方法彰阴。
根據(jù)propagate
標志位的不同設(shè)置瘾敢,對應(yīng)不同的區(qū)塊廣播方式。
func (pm *ProtocolManager) BroadcastBlock(block *types.Block, propagate bool)
-
1.先獲取新的區(qū)塊的哈希
hash
,和本地節(jié)點的相鄰節(jié)點中簇抵,未知這個區(qū)塊的節(jié)點列表peers
庆杜。hash := block.Hash() peers := pm.peers.PeersWithoutBlock(hash)
-
2.如果
propagate
字段為true
。廣播區(qū)塊碟摆,節(jié)點的數(shù)量為peers
的長度的平方根晃财。transferLen := int(math.Sqrt(float64(len(peers)))) transfer := peers[:transferLen] for _, peer := range transfer { peer.AsyncSendNewBlock(block, td) }
-
3.如果
propagate
字段為false
。廣播區(qū)塊哈希典蜕,節(jié)點的數(shù)量為peers
剩余的節(jié)點數(shù)量断盛。(即沒有收到區(qū)塊的節(jié)點。)for _, peer := range peers { peer.AsyncSendNewBlockHash(block) }
異步發(fā)送區(qū)塊或區(qū)塊哈希
(代碼在eth/peer.go
里)
發(fā)送區(qū)塊愉舔。
在遠程節(jié)點的廣播隊列里加入了區(qū)塊事件钢猛,如果遠程節(jié)點的廣播隊列 queuedProps
滿了,則無法收到轩缤。然后標注該遠程節(jié)點已知該區(qū)塊命迈。
func (p *peer) AsyncSendNewBlock(block *types.Block, td *big.Int) {
select {
case p.queuedProps <- &propEvent{block: block, td: td}:
p.knownBlocks.Add(block.Hash())
default:
p.Log().Debug("Dropping block propagation", "number", block.NumberU64(), "hash", block.Hash())
}
}
發(fā)送區(qū)塊哈希。
與發(fā)送區(qū)塊類似火的。在遠程節(jié)點的區(qū)塊哈希通知隊列里加入了區(qū)塊事件壶愤。然后標注該遠程節(jié)點已知該區(qū)塊。
func (p *peer) AsyncSendNewBlockHash(block *types.Block) {
select {
case p.queuedAnns <- block:
p.knownBlocks.Add(block.Hash())
default:
p.Log().Debug("Dropping block announcement", "number", block.NumberU64(), "hash", block.Hash())
}
}
4.交易廣播以及源碼
由于交易的數(shù)量比較多馏鹤,所以每次廣播的是一批交易征椒。交易的廣播也相對比較簡單,一批新的交易湃累,直接傳給相鄰節(jié)點即可陕靠。
交易廣播循環(huán) txBroadcastLoop()
循環(huán)讀取交易事件通道,如果接收到新的一批交易脱茉,則廣播出去。
func (pm *ProtocolManager) txBroadcastLoop() {
for {
select {
case event := <-pm.txsCh:
pm.BroadcastTxs(event.Txs)
case <-pm.txsSub.Err():
return
}
}
}
pm.BroadcastTxs 方法
廣播交易垄开,由于是一批交易琴许,所以要先知道相鄰的節(jié)點缺少這一批交易里的哪一些交易。定義了交易集合的映射溉躲,即遠程節(jié)點對應(yīng)該遠程節(jié)點缺少的交易列表榜田。然后發(fā)送交易。
func (pm *ProtocolManager) BroadcastTxs(txs types.Transactions) {
var txset = make(map[*peer]types.Transactions)
// 廣播給無該交易的節(jié)點
for _, tx := range txs {
peers := pm.peers.PeersWithoutTx(tx.Hash())
for _, peer := range peers {
txset[peer] = append(txset[peer], tx)
}
log.Trace("Broadcast transaction", "hash", tx.Hash(), "recipients", len(peers))
}
// 發(fā)送交易
for peer, txs := range txset {
peer.AsyncSendTransactions(txs)
}
}
異步發(fā)送交易
在遠程節(jié)點的交易隊列里加入了交易事件锻梳,如果遠程節(jié)點的交易隊列 queuedTxs
滿了箭券,則無法收到。然后標注該遠程節(jié)點已知該交易疑枯。(一批交易辩块。)
func (p *peer) AsyncSendTransactions(txs []*types.Transaction) {
select {
case p.queuedTxs <- txs:
for _, tx := range txs {
p.knownTxs.Add(tx.Hash())
}
default:
p.Log().Debug("Dropping transaction propagation", "count", len(txs))
}
}
5.異步發(fā)送區(qū)塊和交易的說明
在進行區(qū)塊廣播和交易廣播的時候,都是采用異步發(fā)送的形式,每個遠程節(jié)點都設(shè)置了三個廣播的通道废亭,queuedProps
国章,區(qū)塊通道,緩存為4個區(qū)塊豆村;queuedAnns
液兽,區(qū)塊哈希通道,緩存為4個區(qū)塊哈希掌动;queuedTxs
交易通道四啰,緩存為128個交易。
需要進行區(qū)塊或交易的廣播的時候粗恢,將區(qū)塊或交易放入遠程節(jié)點相應(yīng)的通道中柑晒。
遠程節(jié)點讀取通道內(nèi)容
(代碼在eth/peer.go
里)
在上層網(wǎng)絡(luò)的peerSet
中加入新的遠程節(jié)點,也就是Register
注冊節(jié)點的時候适滓,會開一個單獨的協(xié)程敦迄,啟動遠程節(jié)點的廣播方法,即go p.broadcast()
凭迹。
func (ps *peerSet) Register(p *peer) error {
...
ps.peers[p.id] = p
go p.broadcast()
return nil
}
go p.broadcast()
方法罚屋,是一個異步讀取循環(huán),每次從交易通道嗅绸,或區(qū)塊通道脾猛,或區(qū)塊哈希通道中讀取內(nèi)容,然后執(zhí)行對應(yīng)的發(fā)送方法鱼鸠。
func (p *peer) broadcast() {
for {
select {
case txs := <-p.queuedTxs:
if err := p.SendTransactions(txs); err != nil {return}
...
case prop := <-p.queuedProps:
if err := p.SendNewBlock(prop.block, prop.td); err != nil {return}
...
case block := <-p.queuedAnns:
if err := p.SendNewBlockHashes([]common.Hash{block.Hash()}, []uint64{block.NumberU64()}); err != nil {return}
...
case <-p.term:
return
}
}
}
這里以發(fā)送區(qū)塊為例猛拴,先標注該遠程節(jié)點已知該區(qū)塊,然后調(diào)用p2p.Send
方法蚀狰,將區(qū)塊發(fā)送給遠程的節(jié)點愉昆。
func (p *peer) SendNewBlock(block *types.Block, td *big.Int) error {
p.knownBlocks.Add(block.Hash())
return p2p.Send(p.rw, NewBlockMsg, []interface{}{block, td})
}
然后是Send
方法,將區(qū)塊數(shù)據(jù)進行rlp編碼后置入r
中麻蹋,size
為rlp編碼后的數(shù)據(jù)長度跛溉。調(diào)用w.WriteMsg
方法將要發(fā)送的數(shù)據(jù)寫入w
通道。
func Send(w MsgWriter, msgcode uint64, data interface{}) error {
size, r, err := rlp.EncodeToReader(data)
...
return w.WriteMsg(Msg{Code: msgcode, Size: uint32(size), Payload: r})
}
6.總結(jié)
- 1.區(qū)塊和交易的廣播是一種gossip的傳播方式扮授,每個節(jié)點都向相鄰的節(jié)點傳播芳室,最后蔓延開去,整個p2p網(wǎng)絡(luò)就都知曉了廣播的消息刹勃。
- 2.區(qū)塊廣播有兩個內(nèi)容堪侯,分別為區(qū)塊和區(qū)塊哈希的廣播。
- 3.交易廣播的對象是一批交易荔仁。
4. 參考
(1)解讀區(qū)塊鏈中P2P網(wǎng)絡(luò)結(jié)構(gòu)工作機制
https://blog.csdn.net/qq_43721475/article/details/104662564
(2)硬核干貨 | 區(qū)塊鏈的基石:以太坊的 P2P 網(wǎng)絡(luò)實現(xiàn)
https://blog.csdn.net/weixin_42934313/article/details/84590384
(3)以太坊源碼(01):P2P網(wǎng)絡(luò)及節(jié)點發(fā)現(xiàn)機制
https://www.cnblogs.com/blockchain/p/7943962.html
(4)以太坊源碼解讀(9)以太坊的P2P模塊解析——底層網(wǎng)絡(luò)構(gòu)建和啟動
https://blog.csdn.net/lj900911/article/details/84027202
(5)分布式哈希表DHT(Kademlia算法)——通俗易懂
https://blog.csdn.net/qq_26720653/article/details/106496916
(6)[以太坊源碼分析][p2p網(wǎng)絡(luò)01]:什么是以太坊p2p網(wǎng)絡(luò)
http://www.reibang.com/p/83ddaede545f
(7)[以太坊源碼分析][p2p網(wǎng)絡(luò)02]:啟動底層網(wǎng)絡(luò)以及監(jiān)聽TCP連接
http://www.reibang.com/p/dbb8c4fc5a11
(8)[以太坊源碼分析][p2p網(wǎng)絡(luò)04]:基于UDP的節(jié)點發(fā)現(xiàn)
http://www.reibang.com/p/b232c870dcd2
(9)[以太坊源碼分析][p2p網(wǎng)絡(luò)05]:底層節(jié)點如何與上層節(jié)點聯(lián)系
http://www.reibang.com/p/3db90d1b045e
(10)[以太坊源碼分析][p2p網(wǎng)絡(luò)06]:交易廣播和區(qū)塊廣播
http://www.reibang.com/p/76d2a28468ce
(11)[以太坊源碼分析][p2p網(wǎng)絡(luò)07]:同步區(qū)塊和交易
http://www.reibang.com/p/c0580a4b3d28
(12)分布式原理:一文了解 Gossip 協(xié)議
https://blog.csdn.net/dongli0/article/details/98848710
(13)Gossip協(xié)議:“八卦版”區(qū)塊鏈通信協(xié)議
https://www.codercto.com/a/43380.html