RLPx Encrypted Transmission Protocol - Second-Earth/setchain GitHub Wiki
RLPx is based on the TCP transmission protocol for setnode encrypted communication. The name RLPx comes from RLP serialization.
X || Y
Represents the splicing of X and Y
X ^ Y
X and Y bitwise exclusive OR
X[:N]
The first N bytes of X
[X, Y, Z, ...]
[X, Y, Z, ...] RLP encoding
keccak256(MESSAGE)
keccak256 hash algorithm
ecies.encrypt(PUBKEY, MESSAGE, AUTHDATA)
Asymmetric authentication encryption function used by RLPx.
AUTHDATA is the data for identity authentication, not part of the ciphertext.
But AUTHDATA will write the HMAC-256 hash function before generating the message tag.
ecdh.agree(PRIVKEY, PUBKEY)
ECDH key agreement function.
ecdsa.sign(PRIVKEY, signed)
ecdsa signature, PRIVKEY is the private key for signing, and signed is the hash of the message.
ECIES asymmetric encryption is used for RLPx handshake. RLPx uses the parameters of the encryption system:
-Elliptic curve secp256k1 base point G
.
-KDF(k, len)
: Key Derivation Function 《NIST SP 800-56 Concatenation Key Derivation Function》SEC 5.8.1
-MAC(k, m)
: HMAC function, using SHA-256 hash
-AES(k, iv, m)
: AES-128 symmetric encryption, CTR mode.
Alice wants to send an encrypted message to Bob, and expects Bob to be able to decrypt it with his private key kB
. Alice knows Bob's public key KB
.
Alice wants to encrypt the message m
:
- Generate a random number
r
and generate the corresponding public keyR = r * G
. - Calculate the shared password
S = Px
, where(Px, Py) = r * KB
. - Derive the key
kE || kM = KDF(S, 32)
and the random vectoriv
for encryption authentication. - Use AES encryption
c = AES(kE, iv, m)
. - Calculate the MAC check
d = MAC(keccak256(kM), iv || c)
. - Send the complete ciphertext
R || iv || c || d
to Bob.
Bob decrypts the ciphertext R || iv || c || d
:
- Derive the shared password
S = Px
, where(Px, Py) = r _ KB = kB _ R
. - Derive the key for encryption authentication
kE || kM = KDF(S, 32)
. - Calculate and check MAC
d = MAC(keccak256(kM), iv || c)
. - Decrypt the plaintext
m = AES(kE, iv || c)
.
All encryption operations are based on secp256k1 elliptic curve. Each node maintains a static secp256k1 private key. The private key can only be reset manually (delete the private key file).
RLPx is based on TCP communication, and each communication generates a random temporary key for encryption.
Initiator
auth = auth-size || enc-auth-body
auth-size = size of enc-auth-body, encoded as a big-endian 16-bit integer
auth-version = 5
auth-body = [sig, initiator-pubk, initiator-nonce, auth-version, NetID, ...]
sig = ecdsa.sign(initiator-ephemeral-privkey, static-shared-secret ^ initiator-nonce)
enc-auth-body = ecies.encrypt(recipient-pubk, auth-body, auth-size)
-initiator-ephemeral-privkey: randomly generated private key -initiator-nonce: random number -NetID: Network ID -static-shared-secret: See below for calculation method
Recipient
ack = ack-size || enc-ack-body
ack-size = size of enc-ack-body, encoded as a big-endian 16-bit integer
ack-version = 5
ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-version, NetID, ...]
enc-ack-body = ecies.encrypt(initiator-pubk, ack-body, ack-size)
-The difference between auth-version
and ack-version
will not cause an error (used for protocol upgrade)
-The extra fields of auth-body
and ack-body
will be ignored (used for protocol upgrade)
-Different NetID
will cause handshake failure
Handshake key generation
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)
After the handshake, all messages are transmitted in frames. One frame of data carries a packet of encrypted messages.
The frame header provides information about the size of the message and the function of the message source. Padding is used to align the encrypted data block (depending on the smallest encrypted block of the encryption algorithm).
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
Message authentication in RLPx uses two keccak256 states, one for each transmission direction. egress-mac
and ingress-mac
represent the sending and receiving status respectively. Each time a ciphertext is sent or received, its status will be updated. After the initial handshake, the MAC state is initialized as follows:
The initial state of the sender:
egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
The initial state of the receiver:
egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
When sending a frame of data, update the status of egress-mac
through the sent data, and then calculate the corresponding MAC value.
Calculate header-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]
Calculate 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]
As long as the sender and receiver update egress-mac
and ingress-mac
in the same way, the ciphertext can be verified.
Hello
message
frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integer
-msg-id
is an RLP-encoded integer.
-msg-data
is a message list after RLP encoding.
The Hello
message is the first packet of data after the handshake is completed. All messages after the Hello
message will be compressed using the Snappy algorithm.
Compressed message:
frame-data = msg-id || snappyCompress(msg-data)
frame-size = length of (msg-id || snappyCompress(msg-data)) encoded as a 24bit
big-endian integer
Although capability-id
and context-id
are supported in the frame, these two fields are not used. The current version uses msg-id to distinguish different messages.
The value of msg-id
application layer message is greater than 0x11 (0x00-0x10 are reserved for p2p capability
)
After the handshake negotiation is completed, the connected parties need to send a Hello
message.
At any time, you may receive a Disconnect
message.
[protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...]
After the handshake is completed, the first packet of data sent by both parties. Before receiving the Hello
message, no other messages (except Disconnect
) can be sent.
-Extra fields in the Hello
message will be ignored (used for protocol upgrade)
-protocolVersion
: The current version is 5
-clientId
: node name, a human-readable string, such as "set-P2P"
-capabilities
: list of supported sub-protocols, names and versions
-listenPort
: The listening port of the node. 0 means no monitoring.
-nodeId
: The public key of secp256k1, corresponding to the private key of the node identity.
[reason: P]
Notify the node to disconnect. After receiving this message, the node will immediately disconnect. If it is sending, the normal host will disconnect after sending.
-reason
: An optional integer that indicates the reason for the disconnection:
-0x00
Disconnect requested;
-0x01
TCP sub-system error;
-0x02
Breach of protocol, e.g. a malformed message, bad RLP, incorrect magic number;
-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 (i.e. a different identity to a previous connection/what a
trusted peer told us).
-0x0a
Identity is the same as this node (i.e. connected to itself);
-0x0b
Timeout on receiving a message (i.e. nothing received since sending last ping);
-0x0c
Peer is in blacklist.
-0x10
Some other reason specific to a subprotocol.
[]
Heartbeat, request reply Pong
package
[]
Reply to Ping
package