死磕以太坊源码分析之rlpx协议
死磕以太坊源码分析之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 ConcatenationMAC(k, m)
:HMAC函数,使用了SHA-256哈希AES(k, iv, m)
:AES-128对称加密函数,CTR模式
假设Alice想发送加密消息给Bob,并且希望Bob可以用他的静态私钥kB
解密。Alice知道Bob的静态公钥KB
。
Alice为了对消息m
进行加密:
- 生成一个随机数
r
并生成对应的椭圆曲线公钥R = r * G
- 计算共享密码
S = Px
,其中(Px, Py) = r * KB
- 推导加密及认证所需的密钥
kE || kM = KDF(S, 32)
以及随机向量iv
- 使用AES加密
c = AES(kE, iv, m)
- 计算MAC校验
d = MAC(keccak256(kM), iv || c)
- 发送完整密文
R || iv || c || d
给Bob
Bob对密文R || iv || c || d
进行解密:
- 推导共享密码
S = Px
, 其中(Px, Py) = r * KB = kB * R
- 推导加密认证用的密钥
kE || kM = KDF(S, 32)
- 验证MAC
d = MAC(keccak256(kM), iv || c)
- 获得明文
m = AES(kE, iv || c)
节点身份
所有的加密操作都基于secp256k1椭圆曲线。每个节点维护一个静态的secp256k1私钥。建议该私钥只能进行手动重置(例如删除文件或数据库条目)。
握手流程
RLPx连接基于TCP通信,并且每次通信都会生成随机的临时密钥用于加密和验证。生成临时密钥的过程被称作“握手” (handshake),握手在发起端(initiator, 发起TCP连接请求的节点)和接收端(recipient, 接受连接的节点)之间进行。
- 发起端向接收端发起TCP连接,发送
auth
消息 - 接收端接受连接,解密、验证
auth
消息(检查recovery of signature ==keccak256(ephemeral-pubk)
) - 接收端通过
remote-ephemeral-pubk
和nonce
生成auth-ack
消息 - 接收端推导密钥,发送首个包含Hello消息的数据帧 (frame)
- 发起端接收到
auth-ack
消息,导出密钥 - 发起端发送首个加密后的数据帧,包含发起端Hello消息
- 接收端接收并验证首个加密后的数据帧
- 发起端接收并验证首个加密后的数据帧
- 如果两边的首个加密数据帧的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表示没有收听nodeId
secp256k1的公钥,对应节点私钥
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/ ☆ ☆ ☆ ☆ ☆
死磕以太坊源码分析之rlpx协议的更多相关文章
- 死磕以太坊源码分析之Kademlia算法
死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...
- 死磕以太坊源码分析之p2p节点发现
死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...
- 死磕以太坊源码分析之Fetcher同步
死磕以太坊源码分析之Fetcher同步 Fetcher 功能概述 区块数据同步分为被动同步和主动同步: 被动同步是指本地节点收到其他节点的一些广播的消息,然后请求区块信息. 主动同步是指节点主动向其他 ...
- 死磕以太坊源码分析之Ethash共识算法
死磕以太坊源码分析之Ethash共识算法 代码分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9 引言 目前以太坊中有两个共识算法的实现:cl ...
- 死磕以太坊源码分析之downloader同步
死磕以太坊源码分析之downloader同步 需要配合注释代码看:https://github.com/blockchainGuide/ 这篇文章篇幅较长,能看下去的是条汉子,建议收藏 希望读者在阅读 ...
- 死磕以太坊源码分析之txpool
死磕以太坊源码分析之txpool 请结合以下代码阅读:https://github.com/blockchainGuide/ 写文章不易,也希望大家多多指出问题,交个朋友,混个圈子哦 交易池概念原理 ...
- 死磕以太坊源码分析之MPT树-上
死磕以太坊源码分析之MPT树-上 前缀树Trie 前缀树(又称字典树),通常来说,一个前缀树是用来存储字符串的.前缀树的每一个节点代表一个字符串(前缀).每一个节点会有多个子节点,通往不同子节点的路径 ...
- 死磕以太坊源码分析之MPT树-下
死磕以太坊源码分析之MPT树-下 文章以及资料请查看:https://github.com/blockchainGuide/ 上篇主要介绍了以太坊中的MPT树的原理,这篇主要会对MPT树涉及的源码进行 ...
- 死磕以太坊源码分析之state
死磕以太坊源码分析之state 配合以下代码进行阅读:https://github.com/blockchainGuide/ 希望读者在阅读过程中发现问题可以及时评论哦,大家一起进步. 源码目录 |- ...
随机推荐
- shell中将带分隔符的字符串转为数组
shell中将字符串列表转换成数组,需要将数组用括号来表示,元素用"空格"符号分割开,格式如下: array_name=(value1 ... valuen) 使用内置的分割符IF ...
- Linux文件操作常用命令
一.一些文件操作命令. 1.cd /home 进入"home目录" 2.cd ../ 返回上一级目录 3.cd - 返回上次所在的目录 4.pwd 显示工程路径 5.ll 显示 ...
- Mybatis---00Mybatis入门
一.什么是Mybatis Mybatis框架是一个半ORM框架.Mybatis是一个优秀的基于 java 的持久层框架,它内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去 ...
- SpringApplication.run(xxx.class, args)背后的东东——整体脉络
从spring到springmvc,再到springboot.springcloud,应用程序api开发调用方面都已经非常熟悉,但对spring背后的扩展机制:为何一个简单的main方法可以实现这么强 ...
- model的基础操作
1.创建表 1.1 Meta源信息 from django.db import models class UserInfo(models.Model): username = models.CharF ...
- Maven的介绍及使用
一.Maven简介 Maven 是一个项目管理工具,可以对 Java 项目进行构建.依赖管理,是一个自动化构建工具. 自动化构建工具:将原材料(java.js.css.html....)->产品 ...
- 【QT】子类化QObject+moveToThread实现多线程
往期链接: <QThread源码浅析> <子类化QThread实现多线程> 从往期<QThread源码浅析>可知,在Qt4.4之前,run 是纯虚函数,必须子类化Q ...
- 微信小程序获取请求数据
<%@ WebHandler Language="C#" Class="CodeTest" %> using System; using Syste ...
- 利用Kotlin扩展函数实现任意View的点击处理(点击效果和防止快速点击)
利用Kotlin扩展函数实现View的点击处理(点击效果和防止快速点击) kotlin经典写法: view?.setOnClickListener { //实现 } 项目实践证明,这种写法存在问题 例 ...
- 大数据分析中数据治理的重要性,从一个BI项目的失败来分析
很多企业在做BI项目时,一开始的目标都是想通过梳理管理逻辑,帮助企业搭建可视化管理模型与深化管理的精细度,及时发现企业经营管理中的问题. 但在项目实施和验收时,BI却变成了报表开发项目,而报表的需求往 ...