该协议使用一个类似Kademlia的分布式哈希表(DHT),用于存储有关以太坊节点的信息。选择Kademlia结构是因为它是一种有效地组织分布式节点索引并具有低直径拓扑结构的方式。
在该协议中,节点通过将节点信息存储在具有特定距离度量的桶中,以一种分布式的方式维护网络拓扑。Kademlia结构提供了一种高效的查找和路由机制,使节点能够快速找到其他节点,并在网络中建立连接。
相关概念
节点ID
每个以太坊节点都有一个唯一的节点ID,一个在secp256k1椭圆曲线上的密钥。节点之间的距离就是根据节点ID之间的keccak256哈希值的异或运算得出。
distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)
distance(n₁, n₂) = keccak256(n₁) XOR keccak256(n₂)
节点记录
在DiscoveryV4协议中,节点需要维护一个包含节点最新信息的节点记录(ENR)。其他节点可以随时通过发送ENRRequest数据包来请求本地记录。
ENR(Ethereum Node Record)是一种节点记录,用于在以太坊网络中存储节点的相关信息。每个节点在参与Discovery协议时都需要维护自己的ENR。
ENR是一个包含节点信息的结构化数据记录,可以包含以下信息:
1. 身份验证信息:例如节点的公钥和签名,用于验证节点的身份和消息的完整性。
2. 网络地址信息:包括节点的IP地址和端口号,用于网络通信。
3. 附加信息:可以包含一些其他自定义的或者特定于节点的信息,例如节点的特性、版本号、支持的网络协议等。
ENR使用“v4”身份方案,这是一种标识ENR版本和格式的约定。通过使用相同的身份方案,节点可以统一解析和处理ENR记录。
在Discovery协议中,节点可以通过发送ENRRequest数据包来请求其他节点的ENR记录。这样可以获取其他节点的最新信息,并维护网络中节点记录的一致性。
通过这种方式,节点可以通过Kademlia查找和ENRRequest数据包来获取其他节点的最新记录。这有助于维护网络中节点记录的一致性和及时性。
Kademlia表
节点将其他的节点信息保存在一个路由表中,因为节点之间的最大距离为256,故表有256行,每行最多可以保存16个节点信息。256行,由近到远,分别存有距离本地节点距离为的节点的信息(
)
每一行的节点按照最后一次联系的时间进行排序,最久没见的(不活跃节点)放在头部,最短没见的放在尾部。当有新的节点要插入某一行时,若该行没有存满,则直接插入尾部。若存满,对最不活跃的节点发送Ping包,如果没有收到回应,则认为该节点已死亡,取出该节点并抛弃,将新节点插入代表对应距离的那一行的尾部。
插入流程:
-
当遇到一个新节点N₁时,找到对应的k-bucket。
-
如果k-bucket中的条目少于k个,直接将N₁添加到k-bucket的尾部。
-
如果k-bucket已经包含k个条目,那么从k-bucket的头部取出最近最少出现的节点N₂。
-
发送一个Ping数据包给N₂,以重新验证其活跃性。Ping数据包用于检查节点是否仍然可达。
-
如果从N₂那里没有收到回复,就认为N₂已经失效。将N₂从k-bucket中移除,并将N₁添加到k-bucket的尾部。
节点证明
为了防止流量放大攻击,必须验证发送节点查询请求的节点是否参与了发现协议(为了攻击建立的虚拟节点刚刚创新,没有按照节点发现协议和网络中的节点建立和保持联系)。如果发送查询请求的节点在过去12小时内向被请求节点发送了匹配ping包哈希的有效Pong响应,那么发送方被认为是经过验证的。(猜想未看代码验证:以太坊中节点会定时ping路由表中的节点以保持联系)
(流量放大攻击(Traffic Amplification Attacks)是一种网络攻击技术,攻击者利用特定的网络协议或服务,将少量的网络请求转化为大量的响应流量,从而使目标系统面临过载或拒绝服务的风险。
这种攻击通常利用一些具有放大效应的协议或服务,例如域名系统(DNS)或网络时间协议(NTP)。攻击者发送伪造的请求到这些协议或服务上,而这些协议或服务会生成比原始请求更大的响应,将响应发送给受攻击的目标。通过频繁发送这样的伪造请求,攻击者可以将自己的流量放大成比例更大的响应流量,从而对目标系统造成压力。)应对流量放大攻击的关键在于识别出异常流量。
当发送方发送一个查询请求时,接收方会验证发送方是否在过去12小时内发送了有效的Pong响应,并且该响应中的ping哈希与查询请求中的ping哈希相匹配。只有当发送方经过验证,接收方才会响应其查询请求。
通过这种验证机制,可以确保只有经过验证的发送方才能参与发现协议,从而减少了流量放大攻击的可能性,并提高了网络的安全性。
递归检索(尚不明确,需要后续看代码)
一次检索可以得到k个距离目标节点最近的节点。
当进行"lookup"操作时,它会定位到离目标节点ID最近的k个节点。
"lookup"操作的发起者首先选择离目标节点最近的α个已知节点。然后,发起者向这些节点同时发送FindNode数据包。α是一个系统范围内的并发参数,比如设置为3。在递归步骤中,发起者会向之前查询中了解到的节点重新发送FindNode数据包。从发起者已经了解到的离目标最近的k个节点中,它会选择α个尚未查询过的节点,并向它们重新发送FindNode数据包。响应速度较慢的节点会被从考虑列表中移除,直到它们做出响应为止。
如果一轮FindNode查询未返回比已经发现的最近节点更近的节点,发起者会将FindNode重新发送给所有尚未查询过的k个最近节点。当发起者查询并获得了来自已经发现的k个最近节点的响应时,"lookup"操作终止。
通过这样的"lookup"操作,可以逐步定位到离目标节点最近的k个节点,以便进行进一步的通信或数据交换。在这个过程中,发起者通过并发地向多个节点发送FindNode数据包,以提高查询的效率和准确性,并根据响应情况动态调整查询的目标节点。
总结起来,"lookup"操作通过选择离目标节点最近的节点,并递归地向已知节点发送FindNode数据包,最终定位到离目标节点最近的k个节点。通过这种方式,在分布式网络中快速而准确地定位目标节点的邻居节点。
节点使用递归查找的方式来获取邻居节点。
开始时,查找发起节点从Node Table中提取α(比如3)个距离最closest节点,并发向这α个节点发送FindNode请求, 一次请求最多将返回k(比如16个)个距离发起节点最closest的节点。
收到Neighbors响应后,发起节点又从这些返回节点中抽取α个最closest节点(没有发送过请求的),重发FindNode请求,就这样递归下去(递归次数8次)。
在递归查找中,没有响应FindNode请求的节点,将会从Node Table删除。
传输协议
节点发现协议的数据包通过UDP发送,并限制每个包的大小不能超过1280bytes。
packet = packet-header || packet-data
每个数据包都有一个头部
packet-header = hash || signature || packet-type
hash = keccak256(signature || packet-type || packet-data)
signature = sign(packet-type || packet-data)
hash:是为了在同一个UDP端口上运行多个协议时使数据包格式可以被识别;
signature:每个数据包都由节点的ID进行签名。签名被编码为长度为65的字节数组(r,s,v),其中r、s为签名值,v为恢复ID。
packet-type:标明数据包的类型,长度为一个字节。
有效的数据包类型:
- 0x01: Ping
- 0x02: Pong
- 0x03: FindNode
- 0x04: Neighbors
- 0x05: ENRRequest
- 0x06:ENRResponse
每个数据包类型都有特定的用途和相关的数据格式。实现时,应根据数据包类型来解析和处理数据包。同时,应忽略不在数据包数据列表中的任何额外元素,以及列表之后的任何其他数据。
编码数据包时,应将数据包类型和数据按照RLP列表的格式进行编码。这样可以确保数据的一致性和可读性,并且保证了在不同实现之间的互操作性。
协议包
Ping包(0x01)
packet-data = [version, from, to, expiration, enr-seq ...]
version = 4
from = [sender-ip, sender-udp-port, sender-tcp-port]
to = [recipient-ip, recipient-udp-port, 0]
expiration:这是一个UNIX时间戳,代表这个数据包的有效时间,若接收到该包时,时间已经超过该值,则该包会被丢弃。
enr-seq:通过该字段,接收节点可以检查它是否拥有发送节点的最新记录。如果接收节点的记录版本较旧,它可以通过发送 ENRRequest 数据包来请求发送节点的最新记录。
收到一个ping数据包时,应该给发送节点回复pong包,也可以将ping包的发送节点加入本地的路由表。所有和版本不符合的参数应该被忽略;如果在过去的12小时内没有与发送方的通信,除了回复pong之外,还应发送ping以接收端点证明。
Pong包(0x02)
packet-data = [to, ping-hash, expiration, enr-seq, ...]
pong包是对ping包的回复,ping-hash应该等于相应ping数据包的哈希值。若接收到的pong数据包不包含最近ping数据包的哈希值,应将其忽略
expiration和enr-seq和ping包的字段作用一样。
FindNode包(0x03)
packet-data = [target, expiration, ...]
FindNode数据包用于请求与目标节点接近的节点信息。目标节点是一个64字节的secp256k1公钥。当接收到FindNode数据包时,接收方应该通过其本地表中找到的最接近目标节点的16个节点,回复Neighbors数据包。
为了防止流量放大攻击,只有在通过端点证明程序验证了FindNode的发送方之后,才应该发送Neighbors回复。
Neighbors包(0x04)
packet-data = [nodes, expiration, ...]
nodes = [[ip, udp-port, tcp-port, node-id], ...]
对FindNode包的回复
ENRRequest包(0x05)
packet-data = [expiration]
当接收到这种类型的数据包时,节点应该回复一个包含其节点记录当前版本的ENRResponse数据包。
为了防止放大攻击,发送ENRRequest的节点应该最近回复了一个ping数据包(就像对于FindNode一样)。过期字段是一个UNIX时间戳,应该和其他现有数据包一样处理,即如果它指向过去的时间,就不应该发送回复。
ENRResponse包(0x06)
packet-data = [request-hash, ENR]
这个数据包是对ENRRequest的响应。
request-hash是对要回复的整个ENRRequest数据包的哈希值。
接收方应该验证节点记录(即ENR)是否由发送者的公钥进行签名。
(本文参考以太坊devp2p官方文档)