ENRs and multiaddrs
Ethereum Node Records are self-certified node records. Nodes craft and disseminate ENRs for themselves, proving authorship via a cryptographic signature. ENRs are sequentially indexed, enabling conflicts to be resolved.
ENRs are key-value records with string-indexed ASCII keys. They can store arbitrary information, but EIP-778 specifies a pre-defined dictionary, including IPv4 and IPv6 addresses, secp256k1 public keys, etc.
Comparing ENRs and multiaddrs is like comparing apples and oranges. ENRs are self-certified containers of identity, addresses, and metadata about a node. Multiaddrs are address strings with the peculiarity that they’re self-describing, composable and future-proof. An ENR can contain multiaddrs, and multiaddrs can be derived securely from the fields of an authenticated ENR.
Consensus discovery endpoints
The Ethereum Node Record (ENR) for an Ethereum consensus client MUST contain the following entries (exclusive of the sequence number and signature, which MUST be present in an ENR):
- The compressed secp256k1 publickey, 33 bytes (
secp256k1field).
The ENR MAY contain the following entries:
- An IPv4 address (
ipfield) and/or IPv6 address (ip6field). - An IPv4 QUIC port (
quicfield) representing the local libp2p QUIC (UDP) listening port and/or the corresponding IPv6 port (quic6field). - An IPv4 TCP port (
tcpfield) representing the local libp2p TCP listening port and/or the corresponding IPv6 port (tcp6field). - An IPv4 UDP port (
udpfield) representing the local discv5 listening port and/or the corresponding IPv6 port (udp6field).
Specifications of these parameters can be found in the ENR Specification.
Multiaddresses
multiaddrs are used throughout libp2p for encoding network addresses. When addresses need to be shared or exchanged between processes, they are encoded in the binary representation of multiaddr.
When exchanging addresses, peers send a multiaddr containing both their network
address and peer id, as described in the section on the p2p
multiaddr.
multiaddr basics
A multiaddr is a sequence of instructions that can be traversed to some destination.
For example, the /ip4/198.51.100/tcp/1234 multiaddr starts with ip4, which is
the lowest-level protocol that requires an address. The tcp protocol runs on
top of ip4, so it comes next.
The multiaddr above consists of two components, the /ip4/198.51.100 component,
and the /tcp/1234 component. It’s not possible to split either one further;
/ip4 alone is an invalid multiaddr, because the ip4 protocol was defined to
require a 32 bit address. Similarly, tcp requires a 16 bit port number.
Although we referred to /ip4/198.51.100 and /tcp/1234 as “components” of a
larger TCP/IP address, each is actually a valid multiaddr according to the
multiaddr spec. However, not every syntactically valid multiaddr is a
functional description of a process in the network. As we’ve seen, even a
simple TCP/IP connection requires composing two multiaddrs into one. See the
section on composing multiaddrs for information on how
multiaddrs can be combined, and the
Transport multiaddrs section for the combinations that
describe valid transport addresses.
The multiaddr protocol table contains all currently defined protocols and the length of their address components.
Composing multiaddrs
As shown above, protocol addresses can be composed within a multiaddr in a way that mirrors the composition of protocols within a networking stack.
The terms generally used to describe composition of multiaddrs are “encapsulation” and “decapsulation”, and they essentially refer to adding and removing protocol components from a multiaddr, respectively.
Encapsulation
A protocol is said to be “encapsulated within” another protocol when data from an “inner” protocol is wrapped by another “outer” protocol, often by re-framing the data from the inner protocol into the type of packets, frames or datagrams used by the outer protocol.
Some examples of protocol encapsulation are HTTP requests encapsulated within TCP/IP streams, or TCP segments themselves encapsulated within IP datagrams.
The multiaddr format was designed so that addresses encapsulate each other in
the same manner as the protocols that they describe. The result is an address
that begins with the “outermost” layer of the network stack and works
progressively “inward”. For example, in the address /ip4/198.51.100/tcp/80/ws,
the outermost protocol is IPv4, which encapsulates TCP streams, which in turn
encapsulate WebSockets.
All multiaddr implementations provide a way to encapsulate two multiaddrs into
a composite. For example, /ip4/198.51.100 can encapsulate /tcp/42 to become
/ip4/198.51.100/tcp/42.
Decapsulation
Decapsulation takes a composite multiaddr and removes an “inner” multiaddr from it, returning the result.
For example, if we start with /ip4/198.51.100/tcp/1234/ws and decapsulate /ws,
the result is /ip4/198.51.100/tcp/1234.
It’s important to note that decapsulation returns the original multiaddr up to
the last occurrence of the decapsulated multiaddr. This may remove more than
just the decapsulated component itself if there are more protocols encapsulated
within it. Using our example above, decapsulating either /tcp/1234/ws or
/tcp/1234 from /ip4/198.51.100/tcp/1234/ws will result in /ip4/198.51.100. This is
unsurprising if you consider the utility of the /ip4/198.51.100/ws address that
would result from simply removing the tcp component.
The p2p multiaddr
libp2p defines the p2p multiaddr protocol, whose address component is the
peer id of a libp2p peer. The text representation of a p2p
multiaddr looks like this:
/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5NWhere QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N is the string representation
of a peer’s peer ID derived from its public key.
By itself, a p2p address does not give you enough addressing information to
locate a peer on the network; it is not a transport address. However, like the
ws protocol for WebSockets, a p2p address can be encapsulated
within another multiaddr.
For example, the above p2p address can be combined with the transport address
on which the node is listening:
/ip4/198.51.100/tcp/1234/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5NThis combination of transport address plus p2p address is the format in which
peers exchange addresses over the wire in the identify protocol
and other core libp2p protocols.
From discovery records to dialable addresses
discv5 SHOULD be integrated into the client’s libp2p stack by implementing an
adaptor to make it conform to the
service discovery
and
peer routing
abstractions and interfaces (go-libp2p links provided).
Inputs to operations include peer IDs (when locating a specific peer) or capabilities (when searching for peers with a specific capability), and the outputs will be multiaddrs converted from the ENR records returned by the discv5 backend.
This integration enables the libp2p stack to subsequently form connections and streams with discovered peers.