3 v4 中心节点固定_死磕以太坊源码分析之p2p节点发现
死磕以太坊源碼分析之p2p節點發現
在閱讀節點發現源碼之前必須要理解kadmilia算法,可以參考:KAD算法詳解。
節點發現概述
節點發現,使本地節點得知其他節點的信息,進而加入到p2p網絡中。
以太坊的節點發現基于類似的kademlia算法,源碼中有兩個版本,v4和v5。v4適用于全節點,通過discover.ListenUDP使用,v5適用于輕節點通過discv5.ListenUDP使用,本文介紹的是v4版本。
節點發現功能主要涉及 Server Table udp 這幾個數據結構,它們有獨自的事件響應循環,節點發現功能便是它們互相協作完成的。其中,每個以太坊客戶端啟動后都會在本地運行一個Server,并將網絡拓撲中相鄰的節點視為Node,而Table是Node的容器,udp則是負責維持底層的連接。這些結構的關系如下圖:
image-20201123210628944p2p服務開啟節點發現
在P2p的server.go 的start方法中:
if?err?:=?srv.setupDiscovery();?err?!=?nil?{??return?err
?}
進入到setupDiscovery中:
//?Discovery?V4?var?unhandled?chan?discover.ReadPacket
?var?sconn?*sharedUDPConn
?if?!srv.NoDiscovery?{
??...
??ntab,?err?:=?discover.ListenUDP(conn,?srv.localnode,?cfg)
??....
?}
discover.ListenUDP方法即開啟了節點發現的功能.
首先解析出監聽地址的UDP端口,根據端口返回與之相連的UDP連接,之后返回連接的本地網絡地址,接著設置最后一個UDP-on-IPv4端口。到此為止節點發現的一些準備工作做好,接下下來開始UDP的監聽:
ntab,?err?:=?discover.ListenUDP(conn,?srv.localnode,?cfg)然后進行UDP 的監聽,下面是監聽的過程:
監聽UDP
//?監聽給定的socket?上的發現的包func?ListenUDP(c?UDPConn,?ln?*enode.LocalNode,?cfg?Config)?(*UDPv4,?error)?{
?return?ListenV4(c,?ln,?cfg)
}
func?ListenV4(c?UDPConn,?ln?*enode.LocalNode,?cfg?Config)?(*UDPv4,?error)?{
?closeCtx,?cancel?:=?context.WithCancel(context.Background())
?t?:=?&UDPv4{
??conn:????????????c,
??priv:????????????cfg.PrivateKey,
??netrestrict:?????cfg.NetRestrict,
??localNode:???????ln,
??db:??????????????ln.Database(),
??gotreply:????????make(chan?reply),
??addReplyMatcher:?make(chan?*replyMatcher),
??closeCtx:????????closeCtx,
??cancelCloseCtx:??cancel,
??log:?????????????cfg.Log,
?}
?if?t.log?==?nil?{
??t.log?=?log.Root()
?}
?tab,?err?:=?newTable(t,?ln.Database(),?cfg.Bootnodes,?t.log)?//?
?if?err?!=?nil?{
??return?nil,?err
?}
?t.tab?=?tab
?go?tab.loop()?//
?t.wg.Add(2)
?go?t.loop()?//
?go?t.readLoop(cfg.Unhandled)?//
?return?t,?nil
}
主要做了以下幾件事:
1.新建路由表
tab,?err?:=?newTable(t,?ln.Database(),?cfg.Bootnodes,?t.log)?新建路由表做了以下幾件事:
- 初始化table對象
- 設置bootnode(setFallbackNodes)
- 節點第一次啟動的時候,節點會與硬編碼在以太坊源碼中的bootnode進行連接,所有的節點加入幾乎都先連接了它。連接上bootnode后,獲取bootnode部分的鄰居節點,然后進行節點發現,獲取更多的活躍的鄰居節點
- nursery 是在 Table 為空并且數據庫中沒有存儲節點時的初始連接節點(上文中的 6 個節點),通過 bootnode 可以發現新的鄰居
- tab.seedRand:使用提供的種子值將生成器初始化為確定性狀態
- loadSeedNodes:加載種子節點;從保留已知節點的數據庫中隨機的抽取30個節點,再加上引導節點列表中的節點,放置入k桶中,如果K桶沒有空間,則假如到替換列表中。
2.測試鄰居節點連通性
首先知道UDP協議是沒有連接的概念的,所以需要不斷的ping 來測試對端節點是否正常,在新建路由表之后,就來到下面的循環,不斷的去做上面的事。
go?tab.loop()定時運行doRefresh、doRevalidate、copyLiveNodes進行刷新K桶。
以太坊的k桶設置:
const?(?alpha???????????=?3??//?Kademlia并發參數,?是系統內一個優化參數,控制每次從K桶最多取出節點個數,ethereum取值3
??
?bucketSize??????=?16?//?K桶大小(可容納節點數)
??
?maxReplacements?=?10?//?每桶更換列表的大小
?hashBits??????????=?len(common.Hash{})?*?8?//每個節點ID長度,32*8=256,?32位16進制
?nBuckets??????????=?hashBits?/?15???????//??K桶個數
??)
首先搞清楚這三個定時器運行的時間:
refreshInterval????=?30?*?time.MinuterevalidateInterval?=?10?*?time.Second
copyNodesInterval??=?30?*?time.Second
doRefresh
doRefresh對隨機目標執行查找以保持K桶已滿。如果表為空(初始引導程序或丟棄的有故障),則插入種子節點。
主要以下幾步:
從數據庫加載隨機節點和引導節點。這應該會產生一些以前見過的節點
tab.loadSeedNodes()將本地節點ID作為目標節點進行查找最近的鄰居節點
tab.net.lookupSelf()func?(t?*UDPv4)?lookupSelf()?[]*enode.Node?{
?return?t.newLookup(t.closeCtx,?encodePubkey(&t.priv.PublicKey)).run()
}
func?(t?*UDPv4)?newLookup(ctx?context.Context,?targetKey?encPubkey)?*lookup?{
?...
??return?t.findnode(n.ID(),?n.addr(),?targetKey)
?})
?return?it
}
向這些節點發起findnode操作查詢離target節點最近的節點列表,將查詢得到的節點進行ping-pong測試,將測試通過的節點落庫保存
經過這個流程后,節點的K桶就能夠比較均勻地將不同網絡節點更新到本地K桶中。
unc?(t?*UDPv4)?findnode(toid?enode.ID,?toaddr?*net.UDPAddr,?target?encPubkey)?([]*node,?error)?{?t.ensureBond(toid,?toaddr)
?nodes?:=?make([]*node,?0,?bucketSize)
?nreceived?:=?0
??//?設置回應回調函數,等待類型為neighborsPacket的鄰近節點包,如果類型對,就執行回調請求
?rm?:=?t.pending(toid,?toaddr.IP,?p_neighborsV4,?func(r?interface{})?(matched?bool,?requestDone?bool)?{
??reply?:=?r.(*neighborsV4)
??for?_,?rn?:=?range?reply.Nodes?{
???nreceived++
??????//?得到一個簡單的node結構
???n,?err?:=?t.nodeFromRPC(toaddr,?rn)
???if?err?!=?nil?{
????t.log.Trace("Invalid?neighbor?node?received",?"ip",?rn.IP,?"addr",?toaddr,?"err",?err)
????continue
???}
???nodes?=?append(nodes,?n)
??}
??return?true,?nreceived?>=?bucketSize
?})
??//上面了一個管道事件,下面開始發送真正的findnode報文,然后進行等待了
?t.send(toaddr,?toid,?&findnodeV4{
??Target:?????target,
??Expiration:?uint64(time.Now().Add(expiration).Unix()),
?})
?return?nodes,?}
查找3個隨機的目標節點
for?i?:=?0;?i?3;?i++?{??tab.net.lookupRandom()
?}
doRevalidate
doRevalidate檢查隨機存儲桶中的最后一個節點是否仍然存在,如果不是,則替換或刪除該節點。
主要以下幾步:
返回隨機的非空K桶中的最后一個節點
last,?bi?:=?tab.nodeToRevalidate()對最后的節點執行Ping操作,然后等待Pong
remoteSeq,?err?:=?tab.net.ping(unwrapNode(last))如果節點ping通了的話,將節點移動到最前面
tab.bumpInBucket(b,?last)沒有收到回復,選擇一個替換節點,或者如果沒有任何替換節點,則刪除該節點
tab.replace(b,?last)copyLiveNodes
copyLiveNodes將表中的節點添加到數據庫,如果節點在表中的時間超過了5分鐘。
這部分代碼比較簡單,就伸展闡述。
if?n.livenessChecks?>?0?&&?now.Sub(n.addedAt)?>=?seedMinTableTime?{????tab.db.UpdateNode(unwrapNode(n))
???}
3.檢測各類信息
go?t.loop()loop循環主要監聽以下幾類消息:
- case
- p :=
- r :=
4. 處理UDP數據包
go?t.readLoop(cfg.Unhandled)主要有以下兩件事:
循環接收其他節點發來的udp消息
nbytes,?from,?err?:=?t.conn.ReadFromUDP(buf)處理接收到的UDP消息
t.handlePacket(from,?buf[:nbytes])接下來對這兩個函數進行進一步的解析。
接收UDP消息
接收UDP消息比較的簡單,就是不斷的從連接中讀取Packet數據,它有以下幾種消息:
ping:用于判斷遠程節點是否在線。
pong:用于回復ping消息的響應。
findnode:查找與給定的目標節點相近的節點。
neighbors:用于回復findnode的響應,與給定的目標節點相近的節點列表
處理UDP消息
主要做了以下幾件事:
數據包解碼
packet,?fromKey,?hash,?err?:=?decodeV4(buf)檢查數據包是否有效,是否可以處理
?packet.preverify(t,?from,?fromID,?fromKey)在校驗這一塊,涉及不同的消息類型不同的校驗,我們來分別對各種消息進行分析。
①:ping
②:pong
③:findNodes
④:neighbors
- 校驗消息是否過期
- 用于回復findnode的響應,校驗回復是否正確
- 校驗消息是否過期
- 校驗節點是否是最近的節點
- 校驗消息是否過期
- 校驗回復是否正確
- 校驗消息是否過期
- 校驗公鑰是否有效
處理packet數據
packet.handle(t,?from,?fromID,?hash)相同的,也會有4種消息,但是我們這邊重點講處理findNodes的消息:
func?(req?*findnodeV4)?handle(t?*UDPv4,?from?*net.UDPAddr,?fromID?enode.ID,?mac?[]byte)?{...
}
我們這里就稍微介紹下如何處理findnode的消息:
func?(req?*findnodeV4)?handle(t?*UDPv4,?from?*net.UDPAddr,?fromID?enode.ID,?mac?[]byte)?{?//?確定最近的節點
?target?:=?enode.ID(crypto.Keccak256Hash(req.Target[:]))
?t.tab.mutex.Lock()
?//最接近的返回表中最接近給定id的n個節點
?closest?:=?t.tab.closest(target,?bucketSize,?true).entries
?t.tab.mutex.Unlock()
?//?以每個數據包最多maxNeighbors的塊的形式發送鄰居,以保持在數據包大小限制以下。
?p?:=?neighborsV4{Expiration:?uint64(time.Now().Add(expiration).Unix())}
?var?sent?bool
?for?_,?n?:=?range?closest?{?//掃描這些最近的節點列表,然后一個包一個包的發送給對方
??if?netutil.CheckRelayIP(from.IP,?n.IP())?==?nil?{
???p.Nodes?=?append(p.Nodes,?nodeToRPC(n))
??}
??if?len(p.Nodes)?==?maxNeighbors?{
???t.send(from,?fromID,?&p)//給對方發送?neighborsPacket?包,里面包含節點列表
???p.Nodes?=?p.Nodes[:0]
???sent?=?true
??}
?}
?if?len(p.Nodes)?>?0?||?!sent?{
??t.send(from,?fromID,?&p)
?}
}
首先先確定最近的節點,再一個包一個包的發給對方,并校驗節點的IP,最后把有效的節點發送給請求方。
涉及的結構體:
UDP
- conn :接口,包括了從UDP中讀取和寫入,關閉UDP連接以及獲取本地地址。
- netrestrict:IP網絡列表
- localNode:本地節點
- tab:路由表
Table
buckets:所有節點都加到這個里面,按照距離
nursery:啟動節點
rand:隨機來源
ips:跟蹤IP,確保IP中最多N個屬于同一網絡范圍
net: UDP 傳輸的接口
- 返回本地節點
- 將enrRequest發送到給定的節點并等待響應
- findnode向給定節點發送一個findnode請求,并等待該節點最多發送了k個鄰居
- 返回查找最近的節點
- 將ping消息發送到給定的節點,然后等待答復
以下是table的結構圖:
image-20201112104254003思維導圖
思維導圖獲取地址
image-20201123211034861關于我
微信號:mindcarver ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 加我探討問題或商務合作
個人博客網站:https://mindcarver.cn/ ? ? ? ? ? ? ? ?最新技術博客
github開源項目:https://github.com/blockchainGuide ? ? ? ? ? ?持續會更新區塊鏈最新技術
QQ群:1057497867
- END -總結
以上是生活随笔為你收集整理的3 v4 中心节点固定_死磕以太坊源码分析之p2p节点发现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vc 只有顶级窗口可以弹出窗口_如何在M
- 下一篇: vba怎么安装_VBA还要学吗?怎么入门