以太坊是一种区块链的实现。在以太坊网络中,众多的节点彼此连接,构成了以太坊网络:以太坊节点软件提供两个核心功能:数据存储、合约代码执行。在每个以太坊全节点中,都保存有完整的区块链数据。以太坊不仅将交易数据保存在链上,编译后的合约代码同样也保存在链上。以太坊全节点中,同时还提供了一个虚拟机来执行合约代码。
以太坊源码分析--p2p节点发现
节点发现功能主要涉及ServerTableudp这几个数据结构,它们有独自的事件响应循环,节点发现功能便是它们互相协作完成的。其中,每个以太坊客户端启动后都会在本地运行一个Server,并将网络拓扑中相邻的节点视为Node,而Table是Node的容器,udp则是负责维持底层的连接。下面重点描述它们中重要的字段和事件循环处理的关键部分。
PrivateKey-本节点的私钥,用于与其他节点建立时的握手协商
Protocols-支持的所有上层协议
StaticNodes-预设的静态Peer,,节点启动时会首先去向它们发起连接,建立邻居关系
newTransport-下层传输层实现,定义握手过程中的数据加密解密方式,默认的传输层实现是用newRLPX()创建的rlpx,这不是本文的重点
ntab-典型实现是Table,所有peer以Node的形式存放在Table
ourHandshake-与其他节点建立连接时的握手信息,包含本地节点的版本号以及支持的上层协议
addpeer-连接握手完成后,连接过程通过这个通道通知Server
Server的监听循环,启动底层监听socket,ETH链开发唯:MrsFu123 当收到连接请求时,Accept后调用setupConn()开始连接建立过程
Server的主要事件处理和功能实现循环
Node唯一表示网络上的一个节点
IP-IP地址
UDP/TCP-连接使用的UDP/TCP端口号,
ID-以太坊网络中唯一标识一个节点,本质上是一个椭圆曲线公钥(PublicKey),与Server的PrivateKey对应。一个节点的IP地址不一定是固定的,但ID是唯一的。
sha-用于节点间的距离计算
Table主要用来管理与本节点与其他节点的连接的建立更新删除
bucket-所有peer按与本节点的距离远近放在不同的桶(bucket)中,详见之后的节点维护
refreshReq-更新Table请求通道
Table的主要事件循环,主要负责控制refresh和revalidate过程。
refresh.C-定时(30s)启动Peer刷新过程的定时器
refreshReq-接收其他线程投递到Table的刷新Peer连接的通知,当收到该通知时启动更新,详见之后的更新邻居关系
revalidate.C-定时重新检查以连接节点的有效性的定时器,详见之后的探活检测
udp负责节点间通信的底层消息控制,是Table运行的Kademlia协议的底层组件
conn-底层监听端口的连接
addpending-udp用来接收pending的channel。使用场景为:当我们向其他节点发送数据包后(packet)后可能会期待收到它的回复,pending用来记录一次这种还没有到来的回复。举个例子,当我们发送ping包时,总是期待对方回复pong包。这时就可以将构造一个pending结构,其中包含期待接收的pong包的信息以及对应的callback函数,将这个pengding投递到udp的这个channel。udp在收到匹配的pong后,执行预设的callback。
gotreply-udp用来接收其他节点回复的通道,配合上面的addpending,收到回复后,遍历已有的pending链表,看是否有匹配的pending。
Table-和Server中的ntab是同一个Table
udp的处理循环,负责控制消息的向上递交和收发控制
udp的底层接受数据包循环,负责接收其他节点的packet
以太坊使用Kademlia分布式路由存储协议来进行网络拓扑维护,了解该协议建议先阅读易懂分布式。更权威的资料可以查看wiki。总的来说该协议:
源码中由Table结构保存所有bucket,bucket结构如下
节点可以在entries和replacements互相转化,一个entries节点如果Validate失败,那么它会被原本将一个原本在replacements数组的节点替换。
有效性检测就是利用ping消息进行探活操作。Table.loop()启动了一个定时器(0~10s),定期随机选择一个bucket,向其entries中末尾的节点发送ping消息,如果对方回应了pong,则探活成功。
Table.loop()会定期(定时器超时)或不定期(收到refreshReq)地进行更新邻居关系(发现新邻居),两者都调用doRefresh()方法,该方法对在网络上查找离自身和三个随机节点最近的若干个节点。
Table的lookup()方法用来实现节点查找目标节点,它的实现就是Kademlia协议,通过节点间的接力,一步一步接近目标。
当一个节点启动后,它会首先向配置的静态节点发起连接,发起连接的过程称为Dial,源码中通过创建dialTask跟踪这个过程
dialTask表示一次向其他节点主动发起连接的任务
在Server启动时,会调用newDialState()根据预配置的StaticNodes初始化一批dialTask,并在Server.run()方法中,启动这些这些任务。
Dial过程需要知道目标节点(dest)的IP地址,如果不知道的话,就要先使用recolve()解析出目标的IP地址,怎么解析?就是先要用借助Kademlia协议在网络中查找目标节点。
当得到目标节点的IP后,下一步便是建立连接,这是通过dialTask.dial()建立连接
连接建立的握手过程分为两个阶段,在在SetupConn()中实现
第一阶段为ECDH密钥建立:
第二阶段为协议握手,互相交换支持的上层协议
如果两次握手都通过,dialTask将向Server的addpeer通道发送peer的信息