以太坊执行层P2P网络架构与设计:Discv5

本文详细介绍了以太坊P2P网络的架构,重点解析了节点发现协议v5(Discv5),包括RLP编码、ECDH密钥交换、HKDF密钥衍生、初始节点获取、节点握手认证流程,以及节点查询和主题检索机制。通过理解这些基础知识,读者可以深入理解以太坊网络中节点如何连接和通信。
摘要由CSDN通过智能技术生成

概述

读者可以前往我的博客获得更好的阅读体验。

关于以太坊的P2P网络问题,目前的资料较为零散,本文尝试结合具体的go-ethereum源代码,尽可能为读者完整介绍以太坊所使用的ÐΞVp2p(devp2p)网络架构和运转流程。

devp2p各协议栈之间的关系可以参考下图:

DevP2P stack

其中,Node Discovery Protocol v5运行在UDP上,其余均运行在TCP协议上。

本文会以节点第一次进入以太坊P2P网络的流程为主线,介绍Node Discovery Protocol v5中各个模块的构成和作用。

前置知识

本文将介绍的很多组件均依赖于RLP,所以在此处我们对其首先进行介绍。

RLP 编码

在以太坊中,我们使用的最基本的编码格式就是RLP编码(递归长度前缀解码),这是一种特殊的序列化形式。RLP可以对以下内容进行编码:

  • 字符串(String),在编码过程中会被转换为纯粹的二进制数据字节
  • 列表(list)

为了方便读者理解RLP编码的具体功能和实现,我们将采用先介绍解码,再介绍编码的形式。

对于RLP的解码,流程如下:

  1. 根据输入的第一个字节(即前缀)判断解码的数据类型、实际数据的长度和偏移量
  2. 根据数据类型和偏移量进行解码
  3. 根据第一步返回的实际数据长度寻找下一个前缀并重复解码流程

上述流程也说明了此编码协议的名称的由来。读者可以通过此工具为RLP online辅助学习。

接下来我们介绍二进制字节解码的规则:

  1. 前缀在[0x00, 0x7f]范围内,我们认为其自身就是数据,且解码类型为单个二进制字节
  2. 前缀为0x80,解码数据为空值(如uint(0)""等)
  3. 前缀为[0x81, 0xb7],解码数据为字符串且前缀后跟长度等于第一个字节减去 0x80 的二进制字节
  4. 前缀为[0xb8, 0xbf],解码数据为二进制字节,读取前缀后将前缀与0xb7相减获得第二部分的长度,然后读取第二部分的长度即二进制字节

接下来,我们给出具体的实例:

0x8b68656c6c6f20776f726c64

对于上述内容进行解码,首先我们读取第一字节0x8b,发现此字节符合条件3,我们协议计算字符串长度为0x8b - 0x80,即 11 bytes,这意味着字符串的具体长度为11 bytes。我们依照此长度读取11 bytes,就获得了解码后的数据0x68656c6c6f20776f726c64

如果你可以确认此数据的编码格式就可以进行下一步编码,在此处我们给出的数据使用了UTF-8编码,转换后为hello world

另一个例子如下:

0xb90400(61)*1024

(61)*1024的意思为61连续重复1024次。依照上述流程,我们首先读取前缀0xb9,发现其大于0xb8,相减后获得0xb9 - 0xb7,即2 bytes。我们读取后2 bytes,即0400(1024),这是最终的二进制字节长度,我们再读取1024 bytes,获得(61) * 1024

同理,此处我们也使用了utf-8编码,转换后为A

综合使用上述规则,我们可以编码长度低于65536 bytes的二进制字节。

接下来,我们介绍列表的解码规则:

  1. 前缀为0xc0,解码数据为空列表
  2. 前缀为[0xc1, 0xf7],解码数据为列表,且长度为前缀与0xc0的差
  3. 前缀为[0xf8, 0xff], 解码数据为列表,读取前缀后将前缀与0xf7相减获得第二部分的长度,然后读取第二部分的长度即列表长度

此规则与二进制字节的解码规则类似。

一个简单的例子:

0xcc 8568656c6c6f 85776f726c64

上述示例加空格只为了更好划分结构

我们首先读取前缀0xcc,发现此数据结构为列表且长度为0xcc - 0xc0,即12。获得此消息后我们对后文数据进行读取,后文数据为两个字符串,读者可以根据上文给出的方法自行解码。将最后的数据使用utf-8解码后获得["hello","world"]

如果读者对代码实现感兴趣,我非常建议阅读go-ethereum源代码

ECDH

ECDH是一种基于椭圆函数曲线的Diffie–Hellman密钥协商算法。通过此算法,两个节点可以在不安全的信道中交换生成对称密码学的密钥。为方便对算法实现进行介绍,我们假设节点A与节点B需要进行ECDH生成对称密码学密钥,同时节点A和节点B都可以简单获得对方的公钥。

交换流程如下:

  1. 节点查询对方的公钥,并与自己的私钥相乘
  2. 获得的乘法结果即为对称密码学密钥

证明如下:

节点A的公钥为 Q A = d A ⋅ G Q_A = d_A\cdot G QA=dAG ,其中 d A d_A dA 为节点A的私钥

节点B的公钥为 Q B = d B ⋅ G Q_B = d_B\cdot G QB=dBG ,其中 d B d_B dB 为节点A的私钥

依照上述给出的流程,结果如下:

节点A生成的密钥为 d A ⋅ Q B = d A ⋅ d B ⋅ G d_A\cdot Q_B = d_A\cdot d_B\cdot G dAQB=dAdBG

节点B生成的密钥为 d B ⋅ Q A = d B ⋅ d A ⋅ G d_B\cdot Q_A = d_B\cdot d_A\cdot G dBQA=dBdAG

显然,生成的密钥是相同的,完成了密钥的交换。

HKDF

HKDF是一种基于HMAC的密钥推导算法,由RFC5869进行了相关定义。其功能为给定随机密钥生成材料(如上文获得的ECDH对称密钥),经过HKDF函数可以获得给定长度的具有高密码学强度的密钥。简单来说,可以将此算法看作密钥长度转换算法。

在了解HKDF前,我们首先需要知道HMAC的运作原理,如下图:

HMAC

我们在此处不详细介绍具体流程。读者若感兴趣,建议阅读《图解密码技术》 第三版的 194 页部分,非常详细介绍了相关的计算流程。上图即来自此书。

在了解了HMAC流程后,我们就可以介绍HKDF的相关流程。我们以上文生成的密钥作为基础材料,记为secret,并使用 SHA256 作为哈希算法,目标是获得长度为 256 位的标准密钥。

第一步,将secret作为信息,使用给定或随机生成的salt作为密钥进行HMAC计算获得MAC
第二步,我们需要获得 256 位的密钥,计算密钥长度与哈希算法输出值之间的比值并向上取整,此数值记为n。由于此处我们选择的哈希算法与密钥长度正好相同,所以计算结果为n = 1。接下来,我们进行以下计算:

T(0) = empty string (zero length)
T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
...
T(n) = HMAC-Hash(PRK, T(n-1) | info | hex(n))

将上述结果进行拼接T = T(1) | T(2) | T(3) | ... | T(N),最后选择T的前 256 位作为密钥。

Go语言中,我们可以使用以下golang.org/x/crypto中的官方实现:

func New(hash func() hash.Hash, secret, salt, info []byte) io.Reader

具体文档可参考这里

Node Discovery Protocol v5

作为一个全新的节点,加入以太坊网络最重要的一步就是尽可能与其他节点建立链接,完善自己的节点列表。下图表示了发现以太坊网络节点的流程:

初始节点获得
节点握手认证
获得其他节点

我们会逐一解释其中每一个过程使用的具体协议和协议定义。

初始节点获得

当我们启动一个全新节点时,我们会与go-ethereum规定的启动节点进行通信,这些节点的地址被硬编码在go-ethereum源代码中,读者可以前往此处查看。我们可以看到其中有几种典型的编码格式,如下:

  1. 形如enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303,其中d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666为节点运营者的未压缩公钥,其余数据为IP地址和端口号

  2. 形如enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA,此形式使用了ENR编码格式,由EIP778进行了相关定义。

  3. 形如enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE,此形式使用了enrtree的编码格式,由EIP1459进行定义,主要用于通过DNS查询对等节点

上述不同的编码格式中enode格式最简单且具有自解释性,我们不会在后文进行讨论。后文主要讨论enrenrtree形式。读者可以通过enode中的公钥在EtherNode查询到所有节点的信息。

ENR

ENR相较于较好理解的enode格式由以下优势:

  1. 可拓展性高,由于ENR使用了以太坊中通用的RLP编码,我们可以在其中加入其他数据
  2. 安全性好,ENR要求用户用户在数据内加入签名,避免了伪造节点信息的情况

在编码中,我们需要依次编码以下数据:

  • signature 签名,对于记录信息的签名
  • seq 序列号,每当记录重新修改发布后一个增加此值
  • 其他键值数据

此处的其他键值数据理论上可以随意填写,填写时应保证键在前、值在后,同时尽可以保证字符使用ASCII编码。但EIP1459对于以下字段进行了预定义:

id(0x6964) 当前节点身份方案的名称,如v4
secp256k1(0x736563703235366b31) 压缩后的公钥( 33 bytes)
ip(0x6970) 节点的 IP 地址
tcp(0x746370) 节点的 TCP 端口号
udp(0x756470) 节点的 UDP 端口号
ip6(0x697036) 节点的 IPv6 地址( 16 bytes )
tcp6(0x74637036) 节点 IPv6 地址使用的 TCP 端口号
udp6(0x75647036) 节点 IPv6 地址使用的 UDP 端口号

其中键后的括号表示其名称的ASCII编码结果

接下来,我们给出以下参数:

  • 私钥 0xb71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291
  • seq 01
  • id “v4”
  • ip 127.0.0.1
  • udp port 30303

我们使用上述参数构建此节点的enr

首先我们需要获得公钥,通过

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WongSSH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值