以太坊源码1-节点发现协议(discv4)

本文结合ethereumJ源码,分析以太坊的节点发现协议(discv4)实现过程。discv4是以太坊用来发现公链P2P网络中的其它节点,组成K桶网络的协议。

ethereumJ从协议上看

  1. discv4协议,用于节点发现 ,使用udp协议,实现节点之间高成功率的穿透,来发现节点,建立连接的信道
  2. rplx协议,节点通信协议,使用tcp协议,为了信息的可靠传输,在已经建好的信道基础上,使用TCP协议传输区块、交易、日志等数据;

discv4涉及的源码包:org.ethereum.net.rlpx.discover

节点发现过程

图片.png

节点发现流程说明:

  1. 本机节点开始运行时,生成本机节点nodeID,即localID,同时打开30303端口,监听节点发送协议网络(使用UDP);
  2. 从配置文件,加载20个引导节点信息,向这些节点循环发送ping报文,在线的引导节点将响应pong报文,将响应的引导节点加入k桶;
  3. 监听udp网络的同时,启动了两个任务:节点发现任务和节点刷新任务;
  4. 节点发现任务30秒循环一次,每次循环跑8遍,每遍以localId为目标节点TargetID,从k桶中获取距离TargetID最接近16个节点,循环向16个节点发送FindNode报文(包含TargetID);
  5. 收到FindNode命令的节点,也以TargetID为目标,从自己的K桶中找出距离最接近的16个节点,回传Neightbour报文;
  6. 本机节点收到Neightbour后,从报文取出新发现的节点,向新节点循环发送Ping报文,并将响应的节点加入k桶;
  7. 节点刷新任务和节点发现任务差不多,不一样的地方是,刷新任务的TargetID不是localId,而是随机生成的nodeId。还有刷新任务刷新速度更频繁,7.2秒循环一次;
  8. 就这样,不断发现和刷新节点,本地节点找到越来越多的邻居节点,组成K桶网络。

K桶

图片.png

上图是一个由K桶组成的路由表。
K桶是一个存储结构,是由k-buckets(k桶)组成的路由表中,因为节点最大距离是256,所以路由表一共有256个k桶,每个桶中最多放k=16个节点。每个以太坊的节点内部都会有一个路由表,用来存储的它发现的网络节点。
存储规则如下:
本地节点通过节点发现协议寻找网络其它节点,每发现一个节点,都会计算该节点与本地节点之间的距离(0-255),根据这个距离将该节点存储于路由表中对应的K桶位置。
如果发现一个新节点,但是这时路由表的K桶满时,会采取最后发现策略,将对应K桶最后发现的节点取出,通过ping命令探测该节点是否在线,如果没有收到响应,新节点将取代它。

节点距离计算
  • 节点Id:每个节点都会有一个nodeId,nodeId是这个节点用ECkey算法生成的密钥对中的公钥,公钥一共65个字节,去掉第一个字节(0x04)的64个字节(512位)就是nodeId。
  • keccak256:是以太坊里的hash256摘要算法。
  • XOR:即异或计算,两个二进制位进行XOR,不等为1,相等为0。
  • 节点之间距离计算
    distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)
    计算n1和n2两个节点的距离,首先对n1和n2的NodeID(512位)用keccak256算法截取成一个256位的摘要,然后对这个两个摘要(256位)进行异或(XOR)计算,节点距离定义为此XOR值的1位最高位的位数。
    例如:
    图片.png

取1的最高位数12,那么这两个节点的距离为14。
因为参与XOR的是256位数字,所有,两个节点的距离是1-256。

注意:这里的节点距离与机器的物理距离无关,这个距离仅仅是逻辑上的一种约定

报文协议

discv4为节点之间互相发现,定义了四种报文协议(命令)

  1. ping:探测命令,用于探测对方节点是否在线。
  2. pong :探测应答命令,用于响应ping报文
  3. findNode:节点查询命令,用于向对方节点请求查找邻居节点
  4. neighbours:节点查询应答命令,用于响应findNode报文,回传找到的邻居节点列表
ping报文

发送

  • 本地节点会向所有新发现的节点发送ping
  • ping发送后,假如15s内没有收到pong响应,将自动重发ping,最多发送三次,三次都没有收到响应,则视为ping失败处理
  • ping失败处理,节点状态会从discovered变为dead、EvictCandidate变为NonActive

处理

  • 接收到ping,则发送pong
pong报文

发送

  • 一旦接收到ping,则发送pong

处理

  • 一旦接收到对方节点发来的pong,则改变对方节点的状态为Alive或者Active
findNode报文

发送

  • 从自己的k桶里面获取最接近目标nodeId的16个节点,并向这些节点发送findNode报文。

处理

  • 接收findNode时,根据收到的targetId,从K桶里面查找最接近targetId的节点,把这些节点封装到Neighbours报文回传。
Neighbours报文

发送

  • 接收到findNode,则发送Neighbours

处理

  • 接收到Neighbours报文,读取报文的节点列表,如果发现是新节点,将加入nodeHandlerMap,节点状态从discovered开始,并发送ping命令。如果节点已经存在,则不处理。

节点生命周期

图片.png
  • discovered:发现状态。引导节点、从持久化文件加载的节点、从邻居节点收到的节点、接收到ping的节点,一开始都处于这个状态
  • alive:在线状态,discovered状态的节点pong响应后,将进入这个状态,但是alive状态的节点还没有加入K桶,需要确认K桶是否有空间。
  • active:活跃状态,active状态的节点正式处于K桶中。alive状态的节点在K桶有空间时可以转入active状态。
  • evictcandidate:候选状态,在K桶满时,active状态的节点如果被新节点PK失败,将进入evictcandidate状态。
  • noactive:alive和active状态的节点,在PK失败时会进入noactive状态。
  • dead:节点在限定时间内没有返回pong信息,则进入dead状态,这是最终状态。

关键类图

图片.png

配置类

配置类——SystemProperties

类路径:org.ethereum.config.SystemProperties
主要方法:

  • getMyKey(),获取EDKey(本机节点的密钥)
  • nodeId(),获取本机节点的nodeId
  • externalIp(),获取本机的公网IP
  • listenPort(),获取监听端口(默认30303)
  • bindIp(),获取绑定本机的IP

处理类

入口类——UDPListener

类路径:org.ethereum.net.rlpx.discover.UDPListener
功能:

  • 使用netty框架,打开30303的端口,启动UDP服务节点,监听节点发现协议。
  • 启动节点发现执行类(DiscoveryExecutor)
  • 加载引导节点,初始化K桶
  • 加载节点管理类(NodeManager)
节点管理类——NodeManager

类路径:org.ethereum.net.rlpx.discover.NodeManager
功能:

  • 节点的全局管理类,所有节点处理Map(nodeHandlerMap)、节点表(NodeTable)、引导节点(bootNodes)、节点持久化类(PeerSource)都在这里面
  • UDPListener监听到的报文(ping、pong、findNode、neighbors)都传到这个类
节点处理类——NodeHandler

类路径:org.ethereum.net.rlpx.discover.NodeHandler
功能:

  • 节点的真实处理类
  • 记录节点的状态,处理节点状态的改变
  • 处理报文(ping、pong、findNode、neighbors)
节点发现执行类——DiscoveryExecutor

类路径:org.ethereum.net.rlpx.discover.DiscoveryExecutor
功能:启动下面两个类

节点发现任务类——DiscoverTask

类路径:org.ethereum.net.rlpx.discover.DiscoverTask
功能:
使用findNode命令来发现邻居节点,30s执行一次,每次循环8遍,每遍都获取k桶最接近localId的16个节点,向这16个节点发送findNode信息(ethereumJ实际用了ALPHA=3个节点)

节点刷新任务类——RefreshTask

类路径:org.ethereum.net.rlpx.discover.RefreshTask
功能:
使用findNode命令刷新K桶,7.2s执行一次,每次循环8遍,和DiscoverTask的区别是,DiscoverTask使用的目标Id是localId,而RefreshTask使用的目标Id是随意Id,获取最接近随机Id的16个节点,向它们发送findNode信息。

节点持久化类——PeerSource

类路径:org.ethereum.db.PeerSource
功能:

  • 节点启动时,从peers文件加载初始化节点
  • 每隔60秒,把所有处理类节点持久化到peers文件

节点相关类

节点表类——NodeTable

类路径:org.ethereum.net.rlpx.discover.table.NodeTable
功能:
这个类就是一个K桶,里面有一个NodeBucket数组,数组大小正好是256,用来存储所有发现的节点(包括本机节点)。

节点桶类——NodeBucket

类路径:org.ethereum.net.rlpx.discover.table.NodeBucket
功能:
这个类代表K桶的一个桶,NodeBucket里面有一个List<NodeEntry>,最多包含16个NodeEntry。

节点实体类——NodeEntry

类路径:org.ethereum.net.rlpx.discover.table.NodeEntry
功能:这个类代表K桶里面的一个节点
属性:

  • ownerId:本地节点的nodeId
  • node:一个节点对象(包含id,ip,port)
  • entryId:节点字符串
  • distance:代表与本机节点的节点距离(值为0-256)
  • modified:更新时间戳,用于getLastSeen()策略,在K桶满时,modified时间最大的节点将被取出。
节点类——Node

类路径:org.ethereum.net.rlpx.Node
功能:Node是比NodeEntry更纯粹意义的节点,NodeEntry代表K桶中的一个节点,而Node仅仅代表一个节点。
属性:

  • nodeId:节点id
  • id:节点ip
  • port:端口号(默认30303)
  • isFakeNodeId:是否虚拟nodeId,当节点没有nodeId时,会生成一个随机nodeId作为虚拟节点Id,这时isFakeNodeId值为true,当节点存在真实nodeId时,这个值为false。

报文类

discv4为四种报文协议定义了统一的报文格式:

packet = packet-header || packet-data
packet-header = mdc || signature || type
packet-data = data

对应的报文类在Message中定义。

报文基类——Message

类路径:org.ethereum.net.rlpx.Message
功能:定义基本的报文属性、编码、解码方法
属性:

  • wire-实际发送的报文就是这个字段

wire = mdc || signature || type || data

  • mdc

mdc = keccak256(signature || type || data)

  • signature

signature = ECDSASignature(keccak256(payload))
payload = type || data

  • data,有四种data(ping、pong、findnode、neighbors)

//PingMessage
data = version || from || to || expiration
//PongMessage
data = to || ping-mac || expiration
//FindNodeMessage
data = target-nodeId || expiration
//NeighborsMessage
data = nodes || expiration
nodes = node || node || node ...
node = host || udpport || tcpport || nodeId

  • type

//PingMessage
type = 1
//PongMessage
type = 2

//FindNodeMessage
type = 3
//NeighborsMessage
type = 4

ping报文类——PingMessage

类路径:org.ethereum.net.rlpx.PingMessage

pong报文类——PongMessage

类路径:org.ethereum.net.rlpx.PongMessage

findNode报文类——FindNodeMessage

类路径:org.ethereum.net.rlpx.FindNodeMessage

neighbors报文类——NeighborsMessage

类路径:org.ethereum.net.rlpx.NeighborsMessage

报文疑问

  1. 所有发送的报文都包含签名,本来认为接收节点收到报文后,会验证签名,但是并不是这样,首先发送报文的节点没有发送节点的公钥(即nodeId),发送节点没有发送公钥(nodeId),那接收节点如何得到呢。这个过程正好是颠倒过来的,接收节点从签名和签名原文反推出公钥,从而得到nodeId,组装Node对象。
  2. 报文里面有expiration字段,值为报文发送时间90分钟后的时间戳,这个字段应该是用来做信息过期处理的,但是ethereumJ里面看不到任何相关的处理代码。
  3. 报文里的mac字段,设置应该是用来作为ping-pong或者findNode-Neighbors匹对的。比如pong报文发送的data里面包含了ping报文的mac,但是很奇怪,在代码实现里,收到pong的节点并没有去校验这个mac值是否和自己发送的ping匹配。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容