以太坊文档翻译2-rlpx
devP2P是以太坊的P2P实现库,包含两个模块:
- rlpx
- discv4
本文翻译自以太坊官方文档,原文请参考:
https://github.com/ethereum/devp2p/blob/master/rlpx.md
加密网络和传输协议(rlpx)
术语
-
EC
:椭圆曲线(Elliptic Curve) -
ECC
:椭圆曲线密码学(Elliptic curve cryptography),一种建立公开密钥加密的演算法,基于[椭圆曲线]数学。 -
DSA
:数字签名算法(Digital Signature Algorithm) -
ECDSA
:椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm),是DSA算法的一个变体,也就是基于椭圆曲线密码学来生成数字签名。 -
DH
:Diffie-Hellman,是Whitefield Diffie和Martin Hellman在1976年公布的一种密钥协商算法,而不是加解密算法,目的在于使两个用户安全的交换一个共享秘钥,用于两边通讯报文的加密。 -
ECDH
:椭圆曲线密钥磋商算法(Elliptic-curve Diffie–Hellman),是DH算法的一种变体。它允许各自拥有一个椭圆曲线公私密钥对的双方,在不安全的通道上建立共享密钥。这个共享秘钥可能直接用作key,或使用KDF衍生出另一个key,这个key用于双方加密通信使用的对称密钥密码。 -
ECDHE
:字面多了一个E,E(ephemeral)代表了“临时”,ECDHE每条会话都重新计算一个临时密钥(Ra、Rb),故一条会话被解密后,其他会话仍旧安全,所以相比ECDH它是正向安全的。 -
secp256k1
:一种椭圆曲线加密算法,全称是ECDSA-secp256k1,用于计算私钥所对应的非压缩公钥 -
ECIES
:椭圆曲线综合加密方案(Elliptic Curve Integrated Encryption Scheme),是Certicom公司提出的公钥加密方案,可以抵挡选择明文攻击和选择密文攻击 -
PFS
:完全前向保密(Perfect Forward Secrecy) -
KDF
:秘钥导出函数(Key derivation function),使用伪随机函数从秘密值(eg.主密钥)导出一个或多个密钥。KDF可用于将密钥扩展到更长的密钥或获得所需格式的密钥(eg.将[Diffie-Hellman密钥交换]的结果的组元素转换为用于AES的对称密钥)。 -
AES
:高级加密标准(Advanced Encryption Standard) -
CTR模式
:计算器模式(Counter (CTR)),是AES算法的一种加密模式 -
多协议
:rlpx所说的多个协议运行在一个连接上,多个协议包括:Whisper的ssh、Swarm的bzz、ethereum的eth
实现功能
- 节点发现(Node Discovery)和网络组建(Network Formation)
- 加密握手(Encrypted Handshake)
- 加密传输(Encrypted transport)
- 协议帧Protocol Mux (framing)
- 流量控制(Flow Control)
- 节点选择策略(Peer Preference Strategies)
- Peer Reputation
- 安全(Security)
- 认证连接authenticated connectivity (ECDH+ECDHE, AES128)
- 认证发现协议authenticated discovery protocol (ECDSA)
- 加密传输encrypted transport (AES256)
- 协议共享一个连接提供了统一的带宽protocols sharing a connection are provided uniform bandwidth (framing)
- 节点使用一个统一的网络拓扑nodes have access to a uniform network topology
- 节点以统一的方式连接网络peers can uniformly connect to network
- localised peer reputation model
传输(Transport)
- 多个节点的通信使用一个连接,节点之间可以长时间无干扰地通信;
- 加密,认证加密可以提供通信的机密性和防止网络破坏,这一点对形成良好的网络和节点之间无干扰地进行长时间通信非常重要;
- 流量控制,用于确保给每个节点分配的通信带宽一样。
网络组建(Network Formation)
- 新入网的节点可以找到存在节点并连接
- 节点之间使用统一的网络拓扑连接
- 节点标识符是随机的
rlpx使用k桶作为p2p网络发现协议,没有实现DHT功能。
节点发现(Node Discovery)
RLPx节点发现使用的是kademlia-like,基于UDP协议,与Kademlia有很大的不同,区别如下:
- 数据包是经过签名的
- node id使用的是公钥
- 排除了DHT功能,FIND_VALUE 和STORE 包没有实现。
- xor distance的计算使用sha3(nodeid)
kademlia-like的特点:
- k桶的大小是16(Kademlia中的k)
- 查找节点请求的并发值是3(Kademlia中的alpha)
- 每一条的递归次数是8(Kademlia中的b)
- eviction 检查的间隔是75毫秒
- 请求超时时间是300毫秒
- k桶的刷新间隔是3600毫秒
- 为了减少重放攻击,设置了一个请求过期时间戳(3秒),超过这个时间戳的请求将会被忽略;
- 为了减少数据报文被切片,限制每个报文最大1280 bytes,这也是ipv6报文的最小大小
- 数据包使用rlp编码序列化
- 数据包被签名,接收到数据包需要使用公钥(node id)来校验签名,确认数据的来源和内容是否一致
- 提供一系列“潜在”节点
报文格式
hash || signature || packet-type || packet-data
hash: sha3(signature || packet-type || packet-data) // used to verify integrity of datagram
signature: sign(privkey, sha3(packet-type || packet-data))
signature: sign(privkey, sha3(pubkey || packet-type || packet-data)) // implementation w/MCD
packet-type: single byte < 2**7 // valid values are [1,4]
packet-data: RLP encoded list. Packet properties are serialized in the order in which they're defined. See packet-data below.
Packet Data (packet-data):
All data structures are RLP encoded.
Total payload of packet (excluding IP headers) must be no greater than 1280 bytes.
NodeId: The node's public key.
inline: Properties are appened to current list instead of encoded as list.
Maximum byte size of packet is noted for reference.
timestamp: When packet was created (number of seconds since epoch).
PingNode packet-type: 0x01
struct PingNode
{
h256 version = 0x3;
Endpoint from;
Endpoint to;
uint32_t timestamp;
};
Pong packet-type: 0x02
struct Pong
{
Endpoint to;
h256 echo;
uint32_t timestamp;
};
FindNeighbours packet-type: 0x03
struct FindNeighbours
{
NodeId target; // Id of a node. The responding node will send back nodes closest to the target.
uint32_t timestamp;
};
Neighbors packet-type: 0x04
struct Neighbours
{
list nodes: struct Neighbour
{
inline Endpoint endpoint;
NodeId node;
};
uint32_t timestamp;
};
struct Endpoint
{
bytes address; // BE encoded 4-byte or 16-byte address (size determines ipv4 vs ipv6)
uint16_t udpPort; // BE encoded 16-bit unsigned
uint16_t tcpPort; // BE encoded 16-bit unsigned
}
加密握手(Encrypted Handshake)
节点通过握手来建立连接,一旦连接成功,通讯的数据包会被封装成AES-256加密的帧(frames)。会话的共享密钥通过使用KDF(秘钥导出函数)从ECDHE交换的密钥衍生出来。ECC(椭圆曲线密码学)使用的是secp256k1曲线;
握手分两个阶段进行:
- 第一阶段是密钥交换,秘钥交换的是一个包含PFS(完全正向加密)临时key的ECIES加密信息。
- 第二阶段是身份验证和协议谈判,这个握手是DEVp2p的一部分,用于交换每个节点支持的功能。实现如何处理第二阶段握手的结果。
这里使用的ECIES是一种变体,依赖于Shoup博士定义的ECIES实现,有一些模式是可塑的、不必要使用,如果消息身份验证失败,解密不会发生。
有两种类型的连接。一个是已知节点的连接,一个是新的节点连接。已知节点是指曾经连接过,且用来请求连接的会话token是有效的。
如果在已知节点的初始化连接过程中握手失败,节点将从节点表中删除。由于IPv4的空间局限和ISP的原因,握手失败的情况很常见,所以,握手失败时,暂时不用从节点表删除节点。
握手:
New: authInitiator -> E(remote-pubk, S(ephemeral-privk, static-shared-secret ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x0)
authRecipient -> E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0)
Known: authInitiator = E(remote-pubk, S(ephemeral-privk, token ^ nonce) || H(ephemeral-pubk) || pubk || nonce || 0x1)
authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found
authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found
static-shared-secret = ecdh.agree(privkey, remote-pubk)
ephemeral-shared-secret = ecdh.agree(ephemeral-privk, remote-ephemeral-pubk)
握手后生成的值:
ephemeral-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = sha3(ephemeral-shared-secret || sha3(nonce || initiator-nonce))
token = sha3(shared-secret)
aes-secret = sha3(ephemeral-shared-secret || shared-secret)
# destroy shared-secret
mac-secret = sha3(ephemeral-shared-secret || aes-secret)
# destroy ephemeral-shared-secret
Initiator:
egress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-sent-init)
# destroy nonce
ingress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-recvd-ack)
# destroy remote-nonce
Recipient:
egress-mac = sha3.update(mac-secret ^ initiator-nonce || auth-sent-ack)
# destroy nonce
ingress-mac = sha3.update(mac-secret ^ recipient-nonce || auth-recvd-init)
# destroy remote-nonce
创建认证连接:
1. initiator generates auth from ecdhe-random, static-shared-secret, and nonce (auth = authInitiator handshake)
2. initiator connects to remote and sends auth
3. optionally, remote decrypts and verifies auth (checks that recovery of signature == H(ephemeral-pubk))
4. remote generates authAck from remote-ephemeral-pubk and nonce (authAck = authRecipient handshake)
optional: remote derives secrets and preemptively sends protocol-handshake (steps 9,11,8,10)
5. initiator receives authAck
6. initiator derives shared-secret, aes-secret, mac-secret, ingress-mac, egress-mac
7. initiator sends protocol-handshake
8. remote receives protocol-handshake
9. remote derives shared-secret, aes-secret, mac-secret, ingress-mac, egress-mac
10. remote authenticates protocol-handshake
11. remote sends protocol-handshake
12. initiator receives protocol-handshake
13. initiator authenticates protocol-handshake
13. cryptographic handshake is complete if mac of protocol-handshakes are valid; permanent-token is replaced with token
14. begin sending/receiving data
All packets following auth, including protocol negotiation handshake, are framed.
分帧(Framing)
对数据包组装成帧主要目的是为了支持多路复用(多个协议在一个连接上通信),其次,可以为MAC(消息认证码)产生一个合理的分界点,支持加密数据流变得直通。因此,帧被握手过程产生的秘钥认证。
当同个rlpx发送数据时,数据包会被组装成帧。帧头部提供了包的大小、包的协议。当数据包的size大于窗口大小时,需要多个帧来表示数据包,这叫multi-frame。
因为存在multi-frame的原因,有三种类型细微差别的数据帧:
- normal :普通帧,只有一个帧的数据包
- chunked-0 :multi-frame数据包的第1个帧
- chunked-n:multi-frame数据包的第n(非1)个帧
normal = not chunked
chunked-0 = First frame of a multi-frame packet
chunked-n = Subsequent frames for multi-frame packet
|| is concatenate
^ is xor
Single-frame packet:
header || header-mac || frame || frame-mac
Multi-frame packet:
header || header-mac || frame-0 ||
[ header || header-mac || frame-n || ... || ]
header || header-mac || frame-last || frame-mac
header: frame-size || header-data || padding
frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)
header-data:
normal: rlp.list(protocol-type[, context-id])
chunked-0: rlp.list(protocol-type, context-id, total-packet-size)
chunked-n: rlp.list(protocol-type, context-id)
values:
protocol-type: < 2**16
context-id: < 2**16 (optional for normal frames)
total-packet-size: < 2**32
padding: zero-fill to 16-byte boundary
header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest
frame:
normal: rlp(packet-type) [|| rlp(packet-data)] || padding
chunked-0: rlp(packet-type) || rlp(packet-data...)
chunked-n: rlp(...packet-data) || padding
padding: zero-fill to 16-byte boundary (only necessary for last frame)
frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest))
egress-mac: h256, continuously updated with egress-bytes*
ingress-mac: h256, continuously updated with ingress-bytes*
消息身份验证是通过不断更新egress-mac或ingress-mac密文发送(出口)或接收的字节数(入口);头执行更新的xor的头的加密输出相应的mac(见例如header-mac以上)。这样做是为了确保统一的mac明文和密文进行操作。所有mac电脑发送明文。
术语说明:
- “Packet”:是对通信数据的泛称,因为并不是所有的通信数据叫message;
- “Frame”:是指通过RLPx传输的数据包;
- “message”:在本文中,指的是使用消息认证码(MAC)认证的文本(密文或明文)。
流量控制(Flow Control)
注意:RLPx在最初版本实现时只设置一个固定的窗口大小(8 kb)来控制流量;公平排队和流量控制(DeltaUpdate包)暂时还没实现。
动态分帧(Dynamic framing)是在双发发送数据帧时,限制发送方的窗口大小和活动协议数量的一个过程,通过定义发送数据窗口和协议窗口来实现流量控制。
-
总窗口大小(window-size)
1个连接建立时,将初始化总窗口大小为8 kb,所有在这个连接上面的协议将共享这个窗口大小。 -
活动协议数
一个协议被认为是活动的,如果队列包含一个或多个数据包 -
协议窗口( protocol window)
协议窗口初始大小是32位,协议窗口是每个协议允许发送数据帧的大小,是对接收端的接收能力的测量,发送端不能发送大于协议窗口的数据帧。在发送每个数据帧后,发送方将逐渐减少协议窗口大小,当窗口大小小于或等于0,发送者必须暂停发送数据帧。接收端处理完部分数据,释放缓冲区空间可以接收新数据时,会回复一个DeltaUpdate包通知发送方。
pws = protocol-window-size = window-size / active-protocol-count
The initial window-size is 8KB.
A protocol is considered active if it's queue contains one or more packets.
DeltaUpdate protocol-type: 0x0, packet-type: 0x0
struct DeltaUpdate
{
unsigned size; // < 2**31
}
多路复用协议技术(Multiplexing )通过动态分帧(Dynamic framing)和公平排队(fair queueing)实现。
在发送端的网络层,每个协议将维护2个队列和3个缓冲区:
- 正常数据包的队列和优先级数据包队列
- chunked-frame缓冲区、normal-frame缓冲区和priority-frame缓冲区
If priority packet and normal packet exist: send up to pws/2 bytes from each (priority first!)
else if priority packet and chunked-frame exist: send up to pws/2 bytes from each
else if normal packet and chunked-frame exist: send up to pws/2 bytes from each
else read pws bytes from active buffer
If there are bytes leftover -- for example, if the bytes sent is < pws, then repeat the cycle.