美文网首页P2P和以太坊P2P网络技术
以太坊源码1-节点发现协议(discv4)

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

作者: jection | 来源:发表于2018-10-18 16:47 被阅读0次

    本文结合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匹配。

    相关文章

      网友评论

        本文标题:以太坊源码1-节点发现协议(discv4)

        本文链接:https://www.haomeiwen.com/subject/fwerzftx.html