Since my last update I've been working on a minimal implementation of a consensus layer peer-to-peer networking stack.
It can be found here.
In this update I’d like to make an overview on its current state, the resources I used and the next steps.
Following the consensus layer specifications, the peer discovery service implements the Node Discovery Protocol v5. I’m using the Sigma Prime rust implementation.
This is a simplified code snippet for the service constructor:
The constructor receives the libp2p Keypair
and parses it to a CombinedKey
before building the enr with the default listen sockets:
Once the enr is built, we create the default config for the discv5 service and proceed to spin up an instance of it. We add a single bootnode, although we could add multiple ones, and start the service. With the service running we should also obtain the event stream, important for implementing polling.
So far we have a running discovery service on its own, but we should integrate this with the libp2p networking stack. The first step is to implement the swarm::NetworkBehaviour
trait on our struct. This allows us to use the Discovery
struct in the defined Behaviour
.
Not every method of the trait is implemented, but these will be sufficient for our current needs.
The addresses_of_peer
method is used by libp2p when dialing peers. Peers are identified by a PeerId
but that’s not useful for dialing. Whenever we add a new peer, it’s important to store the peer Multiaddr
so we can later implement a PeerId -> Multiaddr
map.
The poll
method is the main driver of the behaviour. At first there’s a check to start a single peer discovery query. This should be later changed to correctly implement a query manager.
If the query futures have finalized and returned a result, we return the discovered peers wrapped in Poll::Ready
and NetworkBehaviourAction::GenerateEvent
. We’ll later see how this is processed by the libp2p swarm.
The last thing I’d like to review about the discovery is the find_peers
associated method.
Discv5 allows us to find new peers based on a predicate. This is a great feature, since we can filter which peers we want to establish a connection with. We setup a random NodeId
as a target since there isn’t a specific node we want to connect to.
The find_node_predicate
methods returns a Future
, that is pushed to a futures Vec
to poll until the query is finalized.
I based the libp2p setup on the gossipsub chat example available on the rust-libp2p repository.
However, I’m using tokio instead of async-std. Libp2p has a built-in tokio based TCP transport with both Yamux and Mplex for substream multiplexing. This is great since it’s the same config detailed in the CL p2p specifications.
It’s now easy to spin up the discovery service thanks to all our previous work. We just have to send a reference to our secp256k1
Keypair.
For the configuration, the validation mode should be set to ValidationMode::Anonymous
and the privacy argument for the constructor should also be MessageAuthenticity::Anonymous
.
We create the custom behaviour with Gossipsub and our Discv5 implementation, and we derive the swarm::NetworkBehaviour
trait on it. Now it’s possible to create our Swarm, with a tokio executor, and listen on the correct socket.
And we’re running. The last step is to loop through the swarm executor and act based on the events. I only implemented the add_explicit_peer
method for our discovered peers, this will dial them if they’re not already connected.
There are two main topics to continue this work on
Consensus Layer peers will drop the connection if the connecting peer ENR eth2
field doesn't hold the correct (equal) fork data, generated from the chain specs, such as these for Bellatrix:
But it's not as easy as adding a simple struct! This should be SSZ encoded and parsed, more details in the next update!
Incoming gossipsub
messages are SSZ encoded and compressed using Snappy. So once the connections are stable, the next step is implementing the correct processing for the messages.