死磕以太坊源码分析之rlpx协议

本文主要参考自eth官方文档:rlpx协议

符号

  • X || Y:表示X和Y的串联
  • X ^ Y: X和Y按位异或
  • X[:N]:X的前N个字节
  • [X, Y, Z, ...]:[X, Y, Z, ...]的RLP递归编码
  • keccak256(MESSAGE):以太坊使用的keccak256哈希算法
  • ecies.encrypt(PUBKEY, MESSAGE, AUTHDATA):RLPx使用的非对称身份验证加密函数 AUTHDATA是身份认证的数据,并非密文的一部分 但是AUTHDATA会在生成消息tag前,写入HMAC-256哈希函数
  • ecdh.agree(PRIVKEY, PUBKEY):是PRIVKEY和PUBKEY之间的椭圆曲线Diffie-Hellman协商函数

ECIES加密

ECIES (Elliptic Curve Integrated Encryption Scheme) 非对称加密用于RLPx握手。RLPx使用的加密系统:

  • 椭圆曲线secp256k1基点G
  • KDF(k, len):密钥推导函数 NIST SP 800-56 Concatenation
  • MAC(k, m):HMAC函数,使用了SHA-256哈希
  • AES(k, iv, m):AES-128对称加密函数,CTR模式

假设Alice想发送加密消息给Bob,并且希望Bob可以用他的静态私钥kB解密。Alice知道Bob的静态公钥KB。

Alice为了对消息m进行加密:

  1. 生成一个随机数r并生成对应的椭圆曲线公钥R = r * G
  2. 计算共享密码S = Px,其中 (Px, Py) = r * KB
  3. 推导加密及认证所需的密钥kE || kM = KDF(S, 32)以及随机向量iv
  4. 使用AES加密 c = AES(kE, iv, m)
  5. 计算MAC校验 d = MAC(keccak256(kM), iv || c)
  6. 发送完整密文R || iv || c || d给Bob

Bob对密文R || iv || c || d进行解密:

  1. 推导共享密码S = Px, 其中(Px, Py) = r * KB = kB * R
  2. 推导加密认证用的密钥kE || kM = KDF(S, 32)
  3. 验证MACd = MAC(keccak256(kM), iv || c)
  4. 获得明文m = AES(kE, iv || c)

节点身份

所有的加密操作都基于secp256k1椭圆曲线。每个节点维护一个静态的secp256k1私钥。建议该私钥只能进行手动重置(例如删除文件或数据库条目)。


握手流程

RLPx连接基于TCP通信,并且每次通信都会生成随机的临时密钥用于加密和验证。生成临时密钥的过程被称作“握手” (handshake),握手在发起端(initiator, 发起TCP连接请求的节点)和接收端(recipient, 接受连接的节点)之间进行。

  1. 发起端向接收端发起TCP连接,发送auth消息
  2. 接收端接受连接,解密、验证auth消息(检查recovery of signature == keccak256(ephemeral-pubk))
  3. 接收端通过remote-ephemeral-pubk 和 nonce生成auth-ack消息
  4. 接收端推导密钥,发送首个包含Hello消息的数据帧 (frame)
  5. 发起端接收到auth-ack消息,导出密钥
  6. 发起端发送首个加密后的数据帧,包含发起端Hello消息
  7. 接收端接收并验证首个加密后的数据帧
  8. 发起端接收并验证首个加密后的数据帧
  9. 如果两边的首个加密数据帧的MAC都验证通过,则加密握手完成

如果首个数据帧的验证失败,则任意一方都可以断开连接。

握手消息

发送端:

auth = auth-size || enc-auth-body
auth-size = size of enc-auth-body, encoded as a big-endian 16-bit integer
auth-vsn = 4
auth-body = [sig, initiator-pubk, initiator-nonce, auth-vsn, ...]
enc-auth-body = ecies.encrypt(recipient-pubk, auth-body || auth-padding, auth-size)
auth-padding = arbitrary data

接收端:

ack = ack-size || enc-ack-body
ack-size = size of enc-ack-body, encoded as a big-endian 16-bit integer
ack-vsn = 4
ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-vsn, ...]
enc-ack-body = ecies.encrypt(initiator-pubk, ack-body || ack-padding, ack-size)
ack-padding = arbitrary data

实现必须忽略auth-vsn 和 ack-vsn中的所有不匹配。

实现必须忽略auth-body 和 ack-body中的所有额外列表元素。

握手消息互换后,密钥生成:

static-shared-secret = ecdh.agree(privkey, remote-pubk)
ephemeral-key = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = keccak256(ephemeral-key || keccak256(nonce || initiator-nonce))
aes-secret = keccak256(ephemeral-key || shared-secret)
mac-secret = keccak256(ephemeral-key || aes-secret)

帧结构

握手后所有的消息都按帧 (frame) 传输。一帧数据携带属于某一功能的一条加密消息。

分帧传输的主要目的是在单一连接上实现可靠的支持多路复用协议。其次,因数据包分帧,为消息认证码产生了适当的分界点,使得加密流变得简单了。通过握手生成的密钥对数据帧进行加密和验证。

帧头提供关于消息大小和消息源功能的信息。填充字节用于防止缓存区不足,使得帧组件按指定区块字节大小对齐。

frame = header-ciphertext || header-mac || frame-ciphertext || frame-mac
header-ciphertext = aes(aes-secret, header)
header = frame-size || header-data || header-padding
header-data = [capability-id, context-id]
capability-id = integer, always zero
context-id = integer, always zero
header-padding = zero-fill header to 16-byte boundary
frame-ciphertext = aes(aes-secret, frame-data || frame-padding)
frame-padding = zero-fill frame-data to 16-byte boundary

MAC

RLPx中的消息认证 (Message authentication) 使用了两个keccak256状态,分别用于两个传输方向。egress-mac和ingress-mac分别代表发送和接收状态,每次发送或者接收密文,其状态都会更新。初始握手后,MAC状态初始化如下:

发送端:

egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)

接收端:

egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)

当发送一帧数据时,通过即将发送的数据更新egress-mac状态,然后计算相应的MAC值。通过将帧头与其对应MAC值的加密输出异或来进行更新。这样做是为了确保对明文MAC和密文执行统一操作。所有的MAC值都以明文发送。

header-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ header-ciphertext
egress-mac = keccak256.update(egress-mac, header-mac-seed)
header-mac = keccak256.digest(egress-mac)[:16]

计算 frame-mac

egress-mac = keccak256.update(egress-mac, frame-ciphertext)
frame-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ keccak256.digest(egress-mac)[:16]
egress-mac = keccak256.update(egress-mac, frame-mac-seed)
frame-mac = keccak256.digest(egress-mac)[:16]

只要发送者和接受者按相同方式更新egress-mac和ingress-mac,并且在ingress帧中比对header-mac 和 frame-mac的值,就能对ingress帧中的MAC值进行校验。这一步应当在解密header-ciphertext 和 frame-ciphertext之前完成。


功能消息

初始握手后的所有消息均与“功能”相关。单个RLPx连接上就可以同时使用任何数量的功能。

功能由简短的ASCII名称和版本号标识。连接两端都支持的功能在隶属于“ p2p”功能的Hello消息中进行交换,p2p功能需要在所有连接中都可用。

消息编码

初始Hello消息编码如下:

frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integer

其中,msg-id是标识消息的由RLP编码的整数,msg-data是包含消息数据的RLP列表。

Hello之后的所有消息均使用Snappy算法压缩。请注意,压缩消息的frame-size指msg-data压缩前的大小。消息的压缩编码为:

frame-data = msg-id || snappyCompress(msg-data)
frame-size = length of (msg-id || msg-data) encoded as a 24bit big-endian integer

基于msg-id的复用

frame中虽然支持capability-id,但是在本RLPx版本中并没有将该字段用于不同功能之间的复用(当前版本仅使用msg-id来实现复用)。

每种功能都会根据需要分配尽可能多的msg-id空间。所有这些功能所需的msg-id空间都必须通过静态指定。在连接和接收Hello消息时,两端都具有共享功能(包括版本)的对等信息,并且能够就msg-id空间达成共识。

msg-id应当大于0x11(0x00-0x10保留用于“ p2p”功能)。


p2p功能

所有连接都具有“p2p”功能。初始握手后,连接的两端都必须发送Hello或Disconnect消息。在接收到Hello消息后,会话就进入激活状态,并且可以开始发送其他消息。由于前向兼容性,实现必须忽略协议版本中的所有差异。与处于较低版本的节点通信时,实现应尝试靠近该版本。

任何时候都可能会收到Disconnect消息。

Hello (0x00)

[protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...]

握手完成后,双方发送的第一包数据。在收到Hello消息前,不能发送任何其他消息。实现必须忽略Hello消息中所有其他列表元素,因为可能会在未来版本中用到。

  • protocolVersion当前p2p功能版本为第5版
  • clientId表示客户端软件身份,人类可读字符串, 比如"Ethereum(++)/1.0.0“
  • capabilities支持的子协议列表,名称及其版本:[[cap1, capVersion1], [cap2, capVersion2], ...]
  • listenPort节点的收听端口 (位于当前连接路径的接口),0表示没有收听
  • nodeIdsecp256k1的公钥,对应节点私钥

Disconnect (0x01)

[reason: P]

通知节点断开连接。收到该消息后,节点应当立即断开连接。如果是发送,正常的主机会给节点2秒钟读取时间,使其主动断开连接。

reason 一个可选整数,表示断开连接的原因:

Reason Meaning
0x00 Disconnect requested
0x01 TCP sub-system error
0x02 Breach of protocol, e.g. a malformed message, bad RLP, ...
0x03 Useless peer
0x04 Too many peers
0x05 Already connected
0x06 Incompatible P2P protocol version
0x07 Null node identity received - this is automatically invalid
0x08 Client quitting
0x09 Unexpected identity in handshake
0x0a Identity is the same as this node (i.e. connected to itself)
0x0b Ping timeout
0x10 Some other reason specific to a subprotocol

Ping (0x02)

[]

要求节点立即进行Pong回复。

Pong (0x03)

[]

回复节点的Ping包。


源码分析

主要功能

返回传输对象

返回一个transport对象,连接持续5秒

// handshakeTimeout 5
func newRLPX(fd net.Conn) transport {
....
}

读取消息

返回Msg对象,调用读写器的ReadMsg,连接持续30秒

func (t *rlpx) ReadMsg() (Msg, error) {
..
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
}

写入消息

调用读写器的WriteMsg写信息,连接持续20秒

func (t *rlpx) WriteMsg(msg Msg) error {
...
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
}

协议版本握手

协议握手,输入输出均是protoHandshake对象,包含了版本号、名称、容量、端口号、ID和一个扩展属性,握手时会对这些信息进行验证

加密握手

握手时主动发起者叫initiator

接收方叫receiver

分别对应两种处理方式initiatorEncHandshake和receiverEncHandshake

两种处理方式成功以后都会得到一个secrets对象,保存了共享密钥信息,它会跟原有的net.Conn对象一起生成一个帧处理器:rlpxFrameRW

握手双方使用到的信息有:各自的公私钥地址对(iPrv,iPub,rPrv,rPub)、各自生成的随机公私钥对(iRandPrv,iRandPub,rRandPrv,rRandPub)、各自生成的临时随机数(initNonce,respNonce).

其中i开头的表示发起方(initiator)信息,r开头的表示接收方(receiver)信息.

func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *ecdsa.PublicKey) (*ecdsa.PublicKey, error) {
var (
sec secrets
err error
)
if dial == nil {
sec, err = receiverEncHandshake(t.fd, prv) // 接收者
} else {
sec, err = initiatorEncHandshake(t.fd, prv, dial) //主动发起者
}
...
t.rw = newRLPXFrameRW(t.fd, sec)
t.wmu.Unlock()
return sec.Remote.ExportECDSA(), nil
}

这里我们就讲解一下主动握手部分源码initiatorEncHandshake:

①:初始化握手对象

h := &encHandshake{initiator: true, remote: ecies.ImportECDSAPublic(remote)}

②:生成验证信息

authMsg, err := h.makeAuthMsg(prv)
func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) {
// 生成己方随机数initNonce
h.initNonce = make([]byte, shaLen)
_, err := rand.Read(h.initNonce)
...
}
// 生成随机的一组公私钥对
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
...
}
// 生成静态共享秘密token(用己方私钥和对方公钥进行有限域乘法)
token, err := h.staticSharedSecret(prv)
...
}
// 和己方随机数异或后用随机生成的私钥签名
signed := xor(token, h.initNonce)
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
...
}
...
return msg, nil
}

③:封包,将验证信息和握手进行rlp编码并拼接前缀信息

authPacket, err := sealEIP8(authMsg, h)

④:通过conn发送消息

conn.Write(authPacket)

⑤:处理接收的信息,得到响应包

readHandshakeMsg比较简单。 首先用一种格式尝试解码。如果不行就换另外一种。应该是一种兼容性的设置。 基本上就是使用自己的私钥进行解码然后调用rlp解码成结构体。

结构体的描述就是下面的authRespV4,里面最重要的就是对端的随机公钥。 双方通过自己的私钥和对端的随机公钥可以得到一样的共享秘密。 而这个共享秘密是第三方拿不到的

	authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)

⑥:填充响应的respNonce(对方随机数,生成共享私钥用)和remoteRandomPub(对方的随机公钥)

 h.handleAuthResp(authRespMsg)

⑦:将请求包和响应包封装成共享秘密(secrets)

h.secrets(authPacket, authRespPacket)

到此RLPX 相关的比较重要的内容就解读差不多了。


参考

https://github.com/blockchainGuide/blockchainguide ☆ ☆ ☆ ☆ ☆

https://mindcarver.cn/ ☆ ☆ ☆ ☆ ☆

https://github.com/ethereum/devp2p/blob/master/rlpx.md

死磕以太坊源码分析之rlpx协议的更多相关文章

  1. 死磕以太坊源码分析之Kademlia算法

    死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...

  2. 死磕以太坊源码分析之p2p节点发现

    死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...

  3. 死磕以太坊源码分析之Fetcher同步

    死磕以太坊源码分析之Fetcher同步 Fetcher 功能概述 区块数据同步分为被动同步和主动同步: 被动同步是指本地节点收到其他节点的一些广播的消息,然后请求区块信息. 主动同步是指节点主动向其他 ...

  4. 死磕以太坊源码分析之Ethash共识算法

    死磕以太坊源码分析之Ethash共识算法 代码分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9 引言 目前以太坊中有两个共识算法的实现:cl ...

  5. 死磕以太坊源码分析之downloader同步

    死磕以太坊源码分析之downloader同步 需要配合注释代码看:https://github.com/blockchainGuide/ 这篇文章篇幅较长,能看下去的是条汉子,建议收藏 希望读者在阅读 ...

  6. 死磕以太坊源码分析之txpool

    死磕以太坊源码分析之txpool 请结合以下代码阅读:https://github.com/blockchainGuide/ 写文章不易,也希望大家多多指出问题,交个朋友,混个圈子哦 交易池概念原理 ...

  7. 死磕以太坊源码分析之MPT树-上

    死磕以太坊源码分析之MPT树-上 前缀树Trie 前缀树(又称字典树),通常来说,一个前缀树是用来存储字符串的.前缀树的每一个节点代表一个字符串(前缀).每一个节点会有多个子节点,通往不同子节点的路径 ...

  8. 死磕以太坊源码分析之MPT树-下

    死磕以太坊源码分析之MPT树-下 文章以及资料请查看:https://github.com/blockchainGuide/ 上篇主要介绍了以太坊中的MPT树的原理,这篇主要会对MPT树涉及的源码进行 ...

  9. 死磕以太坊源码分析之state

    死磕以太坊源码分析之state 配合以下代码进行阅读:https://github.com/blockchainGuide/ 希望读者在阅读过程中发现问题可以及时评论哦,大家一起进步. 源码目录 |- ...

随机推荐

  1. 【应用服务 App Service】App Service发生错误请求时,如何查看IIS Freb日志,从中得知错误所发生的模块,请求中所携带的Header信息

    问题描述 在使用Azure App Service时候,我们有时候对 一些请求发生错误毫无头绪,能从错误代码中知道请求错误,但是更多的信息呢? 当我们需要更多的信息时候,通常有以下的一些方式来查找问题 ...

  2. Linux命令行bash的快捷键

    提升效率 锁屏 Ctrl + s 敲什么命令没反应,但是敲上去了,屏幕上不做任何反应 Ctrl + q 再解锁 例如: 先Ctrl + s 锁屏 然后在命令行敲入 [root@C8-1 ~]# rm ...

  3. django—ORM相关

    常用的QuerySet方法 1.all() 查询表中所有数据,返回一个QuerySet对象列表 2.filter() 查询满足条件的数据,返回一个QuerySet对象列表 3.get() 查询指定的数 ...

  4. Docker 也是本地开发的一神器:部署单机版 Pulsar 和集群架构 Redis

    原文链接:Docker 也是本地开发的一神器:部署单机版 Pulsar 和集群架构 Redis 一.前言: 现在互联网的技术架构中,不断出现各种各样的中间件,例如 MQ.Redis.Zookeeper ...

  5. 洛谷 P2503 [HAOI2006]均分数据 随机化贪心

    洛谷P2503 [HAOI2006]均分数据(随机化贪心) 现在来看这个题就是水题,但模拟赛时想了1个小时贪心,推了一堆结论,最后发现贪心做 不了, 又想了半个小时dp 发现dp好像也做不了,在随机化 ...

  6. promise解决微信小程序中的request回调地狱

    //使用 request(url,methods ,data).then(res => { //服务器返回数据 console.log(res ); return request(url,met ...

  7. error: Microsoft Visual C++ 14.0 or greater is required. Get it with “Microsoft C++ Build Tools“

    python3 是用 VC++ 14 编译的, python27 是 VC++ 9 编译的, 安装 python3 的包需要编译的也是要 VC++ 14 以上支持的. 可以下载安装这个: 链接:htt ...

  8. [Luogu P2831] 愤怒的小鸟 (状压DP)

    题面: 传送门:https://www.luogu.org/problemnew/show/P2831 Solution 首先,我们可以先康一康题目的数据范围:n<=18,应该是状压或者是搜索. ...

  9. [P2114] [NOI2014]起床困难综合症 (位运算)

    题面 传送门:https://www.luogu.org/problemnew/show/P2114 Solution 一道很有意思的位运算题. 要做这一题,我们首先得了解一个很重要的特点 位运算过程 ...

  10. 团灭 LeetCode 股票买卖问题

    很多读者抱怨 LeetCode 的股票系列问题奇技淫巧太多,如果面试真的遇到这类问题,基本不会想到那些巧妙的办法,怎么办?所以本文拒绝奇技淫巧,而是稳扎稳打,只用一种通用方法解决所用问题,以不变应万变 ...