The Portal wire protocol is the default peer-to-peer protocol by which Portal nodes communicate.
The Portal wire protocol enables nodes to communicate over the Node Discovery Protocol v5 layer using the TALKREQ
and TALKRESP
messages.
Sub-protocols using the Portal wire protocol MUST choose a byte string to serve as the protocol identifier. Messages are differentiated between different sub-protocol by this protocol identifier which is set in the TALKREQ
message.
Sub-protocol using the wire protocol MAY choose to exclude certain message types and MUST specify which messages are supported.
Unsupported messages SHOULD receive a TALKRESP
message with an empty payload.
All protocol identifiers consist of two bytes. The first byte is "P
" (0x50
), to indicate "the Portal network", the second byte is a specific network identifier.
Currently defined mainnet protocol identifiers:
- Inclusive range of
0x5000
-0x5009
: Reserved for future networks or network upgrades 0x500A
: Execution State Network0x500B
: Execution History Network0x500C
: Beacon Chain Network0x500D
: Execution Canonical Transaction Index Network (planned but not implemented)0x500E
: Execution Verkle State Network (planned but not implemented)0x500F
: Execution Transaction Gossip Network (planned but not implemented)
Angelfood is the name for our current test network.
Currently defined angelfood
protocol identifiers:
0x504A
: Execution State Network0x504B
: Execution History Network0x504C
: Beacon Chain Network0x504D
: Execution Canonical Transaction Index Network (planned but not implemented)0x504E
: Execution Verkle State Network (planned but not implemented)0x504F
: Execution Transaction Gossip Network (planned but not implemented)
Nodes in the portal network are represented by their EIP-778 Ethereum Node Record (ENR) from the Discovery v5 network. A node's node-id
is derived according to the node's identity scheme, which is specified in the node's ENR. A node's node-id
represents its address in the DHT. Node IDs are interchangeable between 32 byte identifiers and 256 bit integers.
Content keys are used to request or offer specific content data. As such the content key and content data can be thought of as a key-value pair with nodes in the network storing the content data and the content key being the identifier used to request and retrieve the data.
Each sub-protocol defines the set of supported content keys and the corresponding data payloads for each content key. The encoding of content keys is defined at the sub-protocol level.
Content keys are passed in their encoded format as byte strings to the messages defined in the Portal wire protocol.
Content IDs are derived from the content key. The Content ID can be represented interchangeably as either a 32 byte value or a 256 bit integer. The Content ID defines the address of the content in the DHT. The function for deriving the Content ID from a content key is defined at the sub-protocol level.
The SHA256 Content ID derivation function is defined here for convenience as it is the most commonly used:
content_id = sha256(encoded_content_key)
The transmission of data that is too large to fit a single packet is done using uTP.
The Portal wire protocol currently implements uTP over the
TALKREQ/TALKRESP
messages. Future plans are to move to the sub-protocol data transmission in order to use a protocol native mechanism for establishing packet streams between clients.
The messages in the protocol are transmitted using the TALKREQ
and TALKRESP
messages from the base Node Discovery Protocol.
All messages in the protocol have a request-response interaction:
- Request messages MUST be sent using a
TALKREQ
message. - Response messages MUST be sent using the corresponding
TALKRESP
message.
All messages are encoded as an SSZ Union type.
message = Union[ping, pong, find_nodes, nodes, find_content, content, offer, accept]
serialized_message = SSZ.serialize(message)
The serialized_message
is the payload passed to the request
field of the TALKREQ
message or the reponse
field of the TALKRESP
message.
The type values for the Union
are the SSZ Containers specified per message type.
The transmission of content
data that is too large to fit a single packet is done over uTP.
Request message to check if a node is reachable, communicate basic information about our node, and request basic information about the recipient node. Additionally sub-protocol can define a schema for the payload
field to exchange additional information.
selector = 0x00
ping = Container(enr_seq: uint64, payload_type: uint16, payload: ByteList[1100])
enr_seq
: The node's current sequence number of their ENR record.payload_type
: Custom payload type identifier as defined in Ping Custom Payload Extensions.payload
: Custom SSZ payload as defined in Ping Custom Payload Extensions.
Response message to Ping(0x00)
selector = 0x01
pong = Container(enr_seq: uint64, payload_type: uint16, payload: ByteList[1100])
enr_seq
: The node's current sequence number of their ENR record.payload_type
: Custom payload type identifier as defined in Ping Custom Payload Extensions.payload
: Custom SSZ payload as defined in Ping Custom Payload Extensions.
Request message to get ENR records from the recipient's routing table at the given logarithmic distances. The distance of 0
indicates a request for the recipient's own ENR record.
selector = 0x02
find_nodes = Container(distances: List[uint16, limit=256])
distances
: a sorted list of distances for which the node is requesting ENR records for.- Each distance MUST be within the inclusive range
[0, 256]
- Each distance in the list MUST be unique.
- Each distance MUST be within the inclusive range
Response message to FindNodes(0x02).
selector = 0x03
nodes = Container(total: uint8, enrs: List[ByteList[2048], limit=32])
total
: The total number ofNodes
response messages being sent. Currently fixed to only 1 response message.enrs
: List of byte strings, each of which is an RLP encoded ENR record.- Individual ENR records MUST correspond to one of the requested distances.
- It is invalid to return multiple ENR records for the same
node_id
. - The ENR record of the requesting node SHOULD be filtered out of the list.
Request message to get the content
with content_key
. In case the recipient does not have the data, a list of ENR records of nodes that are closest to the requested content.
selector = 0x04
find_content = Container(content_key: ByteList[2048])
content_key
: The encoded content key for the content being requested.
Response message to Find Content (0x04).
This message can contain any of
- a uTP connection ID
- the requested content
- a list of ENRs
selector = 0x05
content = Union[connection_id: Bytes2, content: ByteList[2048], enrs: List[ByteList[2048], 32]]
connection_id
: Connection ID to set up a uTP stream to transmit the requested data.- Connection ID values SHOULD be randomly generated.
content
: byte string of the requested content.- This field MUST be used when the requested data can fit in this single response.
enrs
: List of byte strings, each of which is an RLP encoded ENR record.- The list of ENR records MUST be closest nodes to the requested content that the responding node has stored.
- The set of derived
node_id
values from the ENR records MUST be unique. - The ENR record of the requesting & responding node SHOULD be filtered out of the list.
If the node does not hold the requested content, and the node does not know of any nodes with eligible ENR values, then the node MUST return enrs
as an empty list.
Upon sending this message with a connection_id
, the sending node SHOULD listen for an incoming uTP stream with the generated connection_id
.
Upon receiving this message with a connection_id
, the receiving node SHOULD initiate a uTP stream with the received connection_id
.
The Union
defined in the content
field of the Content (0x05)
message is defined as below:
connection_id
selector = 0x00
ssz-type = Bytes2
content
selector = 0x01
ssz-type = ByteList[2048]
enrs
selector = 0x02
ssz-type = List[ByteList[2048], 32]
Request message to offer a set of content_keys
that this node has content
available for.
selector = 0x06
offer = Container(content_keys: List[ByteList[2048], limit=64])
content_keys
: A list of encodedcontent_key
entries.
Response message to Offer (0x06).
Signals interest in receiving the offered data from the corresponding Offer message.
selector = 0x07
accept = Container(connection_id: Bytes2, content_keys: BitList[limit=64]]
connection_id
: Connection ID to set up a uTP stream to transmit the requested data.- ConnectionID values SHOULD be randomly generated.
content_keys
: Signals which content keys are desired.- A bit-list corresponding to the offered keys with the bits in the positions of the desired keys set to
1
.
- A bit-list corresponding to the offered keys with the bits in the positions of the desired keys set to
Upon sending this message, the requesting node SHOULD listen for an incoming uTP stream with the generated connection_id
.
Upon receiving this message, the serving node SHOULD initiate a uTP stream with the received connection_id
.
Up to 64 content items can be sent over the uTP stream after an Offer
request and Accept
response.
In order to be able to discern these different content items, a variable length unsigned integer (varint) MUST be prefixed to each content item. The varint MUST hold the size, in bytes, of the consecutive content item.
The varint encoding used is Unsigned LEB128.
The maximum size allowed for this application is limited to uint32
.
The content item itself MUST be encoded as is defined for each specific network and content type.
The encoded data of n encoded content items to be send over the stream can be formalized as:
# n encoded content items to be send over the stream, with n <= 64
encoded_content_list = [content_0, content_1, ..., content_n]
# encoded data to be send over the stream
encoded_data = varint(len(content_0)) + content_0 + varint(len(content_1)) + content_1 + ... + varint(len(content_n)) + content_n
Each sub protocol must specify a distance function for computing the distance between either two nodes in the network or a node and a piece of content.
The XOR distance
function is defined as:
distance(a: uint256, b: uint256) = a XOR b
Similarly, we define a logdistance
function identically to the Discovery v5 network.
logdistance(a: uint256, b: uint256) = log2(distance(a, b))
A collection of test vectors for this specification can be found in the Portal wire test vectors document.
Sub-networks that use the Portal Wire Protocol will form an independent overlay DHT which requires nodes to maintain a separate routing table from the one used in the base Discv5 protocol.
We define the "standard" routing table as follows:
We adapt the node state from the Discovery v5 protocol. Assume identical definitions for the replication parameter k
and a node's k-bucket table. Also assume that the routing table follows the structure and evolution described in section 2.4 of the Kademlia paper.
Nodes keep information about other nodes in a routing table of k-buckets. This routing table is specific to a sub protocol and is distinct from the node's underlying Discovery v5 routing table or the routing table of any other sub protocols.
A node should regularly refresh the information it keeps about its neighbors. We follow section 4.1 of the Kademlia paper to improve efficiency of these refreshes. A node delays Ping
checks until it has a useful message to send to its neighbor.
When a node discovers some previously unknown node, and the corresponding k-bucket is full, the newly discovered node is put into a replacement cache sorted by time last seen. If a node in the k-bucket fails a liveness check, and the replacement cache for that bucket is non-empty, then that node is replaced by the most recently seen node in the replacement cache.
Consider a node in some k-bucket to be "stale" if it fails to respond to β messages in a row, where β is a system parameter. β may be a function of the number of previous successful liveness checks or of the age of the neighbor. If the k-bucket is not full, and the corresponding replacement cache is empty, then stale nodes should only be flagged and not removed. This ensures that a node who goes offline temporarily does not void its k-buckets.
Most networks that use the Portal Wire Protocol will track some additional state about nodes in the network.
Nodes in the network are expected to maintain a database of information with the following information:
node-entry := (node-id, ip, port)
node-id := uint256
ip := IPv4 or IPv6 address
port := UDP port number
Sub protocols may define additional node state information which should be tracked in the node state database. This information will typically be transmitted in the Ping.payload
and Pong.payload
fields.
Here we define a collection of generic algorithms which can be applied to a sub-protocol implementing the wire protocol.
The term lookup refers to the lookup algorithm described in section 2.3 of the Kademlia paper.
A node lookup is the execution of the algorithm to find the k
closest nodes to some node-id
.
A content lookup is the execution of the algorithm to find the content with content-id
or the k
closest nodes to content-id
.
A FindNode
request is used for a node lookup, and a FindContent
request for a content lookup.
We follow the join procedure described in the Kademlia paper.
In order to join the network, a node u
must know some node v
who is already participating in the network. Node u
inserts v
into the appropriate k-bucket and then sends a FindNode
request to v
in order to discover more nodes in the network. Then, node u
refreshes all k-buckets with distances further than its closest neighbor. To refresh a bucket, a node selects a random node ID in the bucket's range and performs a FindNode
request with a distance that maps to that ID.
A node's routing table is initially populated by the FindNode
messages that the node sends when it joins the network.
Following the join phase, a node's k-buckets are generally kept fresh by network traffic. When a node learns of a new contact (through node lookups), it attempts to insert the contact into the appropriate k-bucket. A node keeps track of the last node lookup it performed for each k-bucket, and it will regularly refresh any k-buckets with no recent lookups.
To find a piece of content for content-id
, a node performs a content lookup via FindContent
.
The concept of content storage is only applicable to sub-protocols that implement persistant storage of data.
Content will get stored by a node when:
- the node receives the content through the
Offer
-Accept
message flow and the content falls within the node's radius - the node requests content through the
FindContent
-Content
message flow and the content falls within the node's radius
The network cannot make guarantees about the storage of particular content. A lazy node may ignore all Offer
messages. A malicious node may send Accept
messages and ignore the data transmissions. The Offer
- Accept
mechanism is in place to require that nodes explicitly accept some data before another node attempts to transmit that data. The mechanism prevents the unnecessary consumption of bandwidth in the presence of lazy nodes. However, it does not defend against malicious nodes who accept offers for data with no intent to store it.
We use the term neighborhood gossip to refer to the process through which content is disseminated to all of the DHT nodes near the location in the DHT where the content is located.
The process works as follows:
- A DHT node is offered and receives a piece of content that it is interested in.
- This DHT node checks their routing table for
k
nearby DHT nodes that should also be interested in the content. Thosek
nodes SHOULD not include the node that originally provided aformentioned content. - If the DHT node finds
n
or more DHT nodes interested it selectsn
of these nodes and offers the content to them. - If the DHT node finds less than
n
DHT nodes interested, it launches a node lookup with targetcontent-id
and it offers the content to maximumn
of the newly discovered nodes.
The process above should quickly saturate the area of the DHT where the content is located and naturally terminate as more nodes become aware of the content.
When a node in the network is doing a content lookup, it will practically perform a recursive find using the FindContent
and Content
messages.
During the course of this recursive find, it may encounter nodes along the search path which do not have the content but for which the content-id
does fall within their announced radius. These are nodes that should be interested in storing this content unless their radius was recently changed.
If the node doing the lookup successfully retrieves the content from another node, it should send an Offer
message for that content to those interested nodes. This mechanism is designed to help spread content to nodes that may not yet be aware of it.