2020-11-24

博客园Logo
首页
新闻
博问
专区
闪存
班级

代码改变世界
搜索
注册
登录
返回主页
mindcarver
博客园
首页
新随笔
联系
订阅
管理
死磕以太坊源码分析之p2p节点发现

死磕以太坊源码分析之p2p节点发现
在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解。

节点发现概述

节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中。

以太坊的节点发现基于类似的kademlia算法,源码中有两个版本,v4和v5。v4适用于全节点,通过discover.ListenUDP使用,v5适用于轻节点通过discv5.ListenUDP使用,本文介绍的是v4版本。

节点发现功能主要涉及 Server Table udp 这几个数据结构,它们有独自的事件响应循环,节点发现功能便是它们互相协作完成的。其中,每个以太坊客户端启动后都会在本地运行一个Server,并将网络拓扑中相邻的节点视为Node,而Table是Node的容器,udp则是负责维持底层的连接。这些结构的关系如下图:

image-20201123210628944

p2p服务开启节点发现

在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.Minute
revalidateInterval = 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, <-rm.errc
}
查找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 <-t.closeCtx.Done():检测是否停止
p := <-t.addReplyMatcher:检测是否有添加新的待处理消息
r := <-t.gotreply:检测是否接收到其他节点的回复消息
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

参考文档

http://mindcarver.cn/ ⭐️⭐️⭐️⭐️
https://github.com/blockchainGuide/ ⭐️⭐️⭐️⭐️
https://www.cnblogs.com/xiaolincoding/p/12571184.html
http://qjpcpu.github.io/blog/2018/01/29/shen-ru-ethereumyuan-ma-p2pmo-kuai-ji-chu-jie-gou/
https://www.jianshu.com/p/b232c870dcd2
https://bbs.huaweicloud.com/blogs/113684
https://www.jianshu.com/p/94d02a41a146
分类: 以太坊源码分析
标签: 节点发现, p2p网络, P2P, Kademlia, 区块链, 以太坊, 以太坊源码, findnode, 路由表
好文要顶 关注我 收藏该文    
mindcarver
关注 - 8
粉丝 - 7
+加关注
0 0
« 上一篇: 死磕以太坊源码分析之Kademlia算法
posted @ 2020-11-23 21:25  mindcarver  阅读(51)  评论(0)  编辑  收藏
刷新评论刷新页面返回顶部
登录后才能发表评论,立即 登录 或 注册, 访问 网站首页
博客园派送云上免费午餐,AWS注册立享12个月免费套餐
【推荐】News: 大型组态、工控、仿真、CADGIS 50万行VC++源码免费下载
【推荐】从零开始的RPG游戏制作教程,来《魔兽争霸III》共同成长
【推荐】了不起的开发者,挡不住的华为,园子里的品牌专区
【推荐】未知数的距离,毫秒间的传递,声网与你实时互动
【福利】AWS携手博客园为开发者送免费套餐与抵扣券
【推荐】 阿里云折扣价格返场,错过再等一年

相关博文:
· SpingSecurity+SpringSession死磕
· 死磕英语之一
· 【杭研大咖说】王新勇:死磕云原生的“懒人”
· 工坊第二天
· 工坊第五天
» 更多推荐...
AWS免费套餐
最新 IT 新闻: 
· 整整10年了!这就是微信的第一段代码 
· Google Assistant现在可以用来控制智能灯具 
· 微软投资2700万欧元在爱尔兰开设工程中心 设立200个新岗位 
· 开发人员已进行175小时《赛博朋克2077》 仍未打完 
· 谷歌Stadia云游戏即将在iPhone上推出公测版本 
» 更多新闻...
公告
学习圈
公众号:区块链技术栈,区块链从业者必备公众号
个人微信号:mindcarver,2T区块链学习资源分享
github地址:https://github.com/blockchainGuide/,区块链学科技术开源项目
昵称: mindcarver 
园龄: 510个月 
粉丝: 7 
关注: 8
+加关注
<	202011>
日	一	二	三	四	五	六
1	2	3	4	5	6	7
8	9	10	11	12	13	14
15	16	17	18	19	20	21
22	23	24	25	26	27	28
29	30	1	2	3	4	5
6	7	8	9	10	11	12
搜索

 找找看

 谷歌搜索
最新随笔
1.死磕以太坊源码分析之p2p节点发现
2.死磕以太坊源码分析之Kademlia算法
3.以太坊源码分析之P2P网络启动
4.以太坊blockchain源码分析
5.以太坊交易池源码解析
6.以太坊启动过程源码解析
7.以太坊启动参数详解
8.istanbul算法详解
9.详解私钥、密码、keystore和助记词
10.什么是以太坊
我的标签
以太坊(9)
区块链(8)
P2P(3)
p2p网络(2)
节点发现(2)
Kademlia(2)
以太坊启动(2)
以太坊源码(2)
以太坊源码分析(1)
原型模式(1)
更多
积分与排名
积分 -	7806
排名 -	95327
随笔分类 (19)
区块链(1)
设计模式专栏(9)
以太坊基础理论(3)
以太坊源码分析(6)
随笔档案 (20)
202011(3)
202010(7)
20194(1)
201812(9)
最新评论
1. Re:java 深克隆(深拷贝)与浅克隆(拷贝)详解
重写clone方法实现深拷贝的测试程序中,并没有体现出实现了深拷贝,两次输出的对象不同,而且没有发生比较。建议修改为更有对比性的测试用例
--RYUJUNG
2. Re:java 深克隆(深拷贝)与浅克隆(拷贝)详解
@ w3xue希望原文转载,不要更改任何内容,并且注明出处。谢谢配合...
--codecarver
3. Re:java 深克隆(深拷贝)与浅克隆(拷贝)详解
@ w3xue这是什么情况?...
--codecarver
4. Re:设计模式7大原则
@ 雪人,不爱雪主要博客园对markdown的拍版支持不如csdn,整体内容看上去不舒服...
--走向脚下的前方
5. Re:设计模式7大原则
写的很清晰
--雪人,不爱雪
阅读排行榜
1. java 深克隆(深拷贝)与浅克隆(拷贝)详解(3633)
2. 设计模式之构造者模式(1082)
3. 设计模式7大原则(462)
4. 设计模式之原型模式(255)
5. 史上最全的并发编程学习目录(249)
评论排行榜
1. java 深克隆(深拷贝)与浅克隆(拷贝)详解(3)
2. 设计模式7大原则(2)
推荐排行榜
1. 设计模式7大原则(6)
2. 设计模式之构造者模式(2)
3. 以太坊启动过程源码解析(1)
4. 设计模式之原型模式(1)
5. java 深克隆(深拷贝)与浅克隆(拷贝)详解(1)
Copyright © 2020 mindcarver 
Powered by .NET 5.0.0 on Kubernetes
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值