P2P和以太坊

以太坊文档翻译2-rlpx

2018-08-29  本文已影响0人  jection

devP2P是以太坊的P2P实现库,包含两个模块:

本文翻译自以太坊官方文档,原文请参考:
https://github.com/ethereum/devp2p/blob/master/rlpx.md

加密网络和传输协议(rlpx)


术语

实现功能

传输(Transport)

网络组建(Network Formation)

节点发现(Node Discovery)

RLPx节点发现使用的是kademlia-like,基于UDP协议,与Kademlia有很大的不同,区别如下:

kademlia-like的特点:

  1. k桶的大小是16(Kademlia中的k)
  2. 查找节点请求的并发值是3(Kademlia中的alpha)
  3. 每一条的递归次数是8(Kademlia中的b)
  4. eviction 检查的间隔是75毫秒
  5. 请求超时时间是300毫秒
  6. k桶的刷新间隔是3600毫秒
  7. 为了减少重放攻击,设置了一个请求过期时间戳(3秒),超过这个时间戳的请求将会被忽略;
  8. 为了减少数据报文被切片,限制每个报文最大1280 bytes,这也是ipv6报文的最小大小
  9. 数据包使用rlp编码序列化
  10. 数据包被签名,接收到数据包需要使用公钥(node id)来校验签名,确认数据的来源和内容是否一致
  11. 提供一系列“潜在”节点
    报文格式
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曲线;
握手分两个阶段进行:

  1. 第一阶段是密钥交换,秘钥交换的是一个包含PFS(完全正向加密)临时key的ECIES加密信息。
  2. 第二阶段是身份验证和协议谈判,这个握手是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的原因,有三种类型细微差别的数据帧:

  1. normal :普通帧,只有一个帧的数据包
  2. chunked-0 :multi-frame数据包的第1个帧
  3. 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电脑发送明文。

术语说明:

流量控制(Flow Control)

注意:RLPx在最初版本实现时只设置一个固定的窗口大小(8 kb)来控制流量;公平排队和流量控制(DeltaUpdate包)暂时还没实现。

动态分帧(Dynamic framing)是在双发发送数据帧时,限制发送方的窗口大小和活动协议数量的一个过程,通过定义发送数据窗口和协议窗口来实现流量控制。

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个缓冲区:

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.
上一篇下一篇

猜你喜欢

热点阅读