Skip to content

Introduction to the Libbitcoin Network Library

James Chiang edited this page Oct 28, 2018 · 6 revisions

This article is intended as an initial overview to the p2p networking stack of the Libbitcoin library. We focus on how the various networking classes seed and establish stable in- and outbound peer connections. A closer look on how an application can interface with the bc::network stack is provided in subsequent documentation chapters.

The P2P networking class

The p2p object provides methods to seed and establish p2p connections according to the bc::network::settings object it was constructed with. Each constructed bc::network::p2p instance will behave as a single network peer.

class p2p
  : public enable_shared_from_base<p2p>, noncopyable
{
public:

    // ...
    void broadcast(const Message& message, channel_handler handle_channel,
    result_handler handle_complete);
    // ...
    p2p(const settings& settings);
    // ...
    virtual void start(result_handler handler);
    // ...
    virtual void run(result_handler handler);
    // ...
    virtual bool stop();
    // ...
    virtual void subscribe_connection(connect_handler handler);
    virtual void subscribe_stop(result_handler handler);
    // ...
    virtual void connect(const config::endpoint& peer);
    // ...
private:
    // ...
    const settings& settings_;
    // ...
    bc::atomic<session_manual::ptr> manual_;
    // ...
    threadpool threadpool_;
    // ...
    hosts hosts_;
    pending_connectors pending_connect_;
    pending_channels pending_handshake_;
    pending_channels pending_close_;
    // ...
}

Network communication through the p2p class

Calling p2p::start() will initialise the p2p object, and begin the seed session with the goal of acquiring a minimum number of hosts addresses to potentially connect with in the subsequent outbound session. p2p::run() will begin the ongoing in- and outbound network sessions, each managing its own set of socket connections.

It is possible to broadcast a message to all active channels from the p2p object by calling broadcast(message, channel_handler, result_handler). Each send operation on each active channel will invoke the channel_handler, which is invoked with a channel pointer to the channel instance the message was sent on:

typedef std::function<void(const code&, channel::ptr)> channel_handler;

The p2p::subscribe_connection(connect_handler) method will create a new subscription to each new channel creation event, thereby enabling channel-level access from parent p2p object from the begin of its lifetime. The connect_handler is a functor with the following signature:

typedef std::function<bool(const code&, channel::ptr)> connect_handler;

The newly created channel::ptr is passed to each separate subscription handler, which allows the handler to perform a channel-specific method call.

P2P: Parent Class of the Network Stack

The p2p class provides a general interface to p2p communications, but also hosts resources used by other networking objects, such as sessions, protocols and channels.

p2p objects contain a threadpool, which external sessions, protocol and channels will dispatch handlers to. The threadpool class itself spawns a pool of threads, which each block at boost::asio::io_service::run(), until a handler can be dequeued/invoked or the associated boost::asio::work destructor is called. For further details on the boost::asio library, please refer to the documentation.

Note that all pending and established socket channels are registered with the p2p object. After a successful socket connection, the socket pointer is removed from pending_connect_ and registered in pending_handshake_, while it negotiates the highest common p2p protocol version with its peer. Once this is completed, the channel is registered in pending_close_.

Relationship between P2P and Session

It may be apparent now that the p2p object provides a only very high level interface to the underlying connections on the Bitcoin p2p network. Starting and running the p2p object is designed to manage various sessions, as shown below:

p2p & session objects

Starting the p2p object will first initialise manual and seed sessions.

Sessions are initialised as heap allocated objects. As long as a shared pointer instance to the session continues to persist, the session will not be destructed. With the exception of the manual session, session objects do not have shared pointers registered in the p2p object. Instead, their lifetime is generally extended by binding a new shared_pointer instance to the callback of each subsequent asynchronous operation during its ongoing connection loop.

  {
      // ...
      make_shared<session_type>(p2p)->start();
      // ...
  }
  // ...
  void session_type::start()
  {
      // ...
      async_operation(            
          std::bind(handler, shared_from_base<session_type>(), args...));
      // ...
  }

In the example above, the asynchronous operation in session_type::start() is being called through a shared session pointer. The shared_from_base<class>() is a Libbitcoin helper which returns an valid shared pointer when only this is accessible: The session object itself is unavailable in the scope of the start method call. This utility returns the same shared pointer as the commonly used shared_from_this<class>() for a given class. However, when this class is inherited, it will return a shared_pointer<derived_class> instead. In this sense, shared_from_base<class>() is inheritance proof.

The session_manual class is the only session type which has a stored shared pointer in the p2p parent and can therefore be accessed by via the p2p object. This session begins to maintain channels to peers set in bc::network::settings::peers when p2p::run() is called. Additional manual connections to peers can also be initialised anytime with the p2p::connect(endpoint_peer) method during the p2p object lifetime.

A session_seed will establish channels to bc::network::settings::seeds and attempt to acquire additional peer addresses to connect to during the subsequent session_outbound. Seeded endpoints are updated in the p2p::hosts_ object. The seed session will only succeed if a minimum of 100 new host are found. Once the seed session has completed attempts to establish seed connections to all seed hosts, it will call completion handlers and destruct.

p2p::run() will call the manual session to begin connecting to permanent manual peers, as well as calling the inbound and outbound connection loops to start. The session_inbound object accepts new inbound channels from peers as long as the total connection count has not been reached. The session_outbound object will initiate outbound channels up to the outbound channel number defined in the settings object passed to the parent p2p constructor.

Relationship between Session/Protocol/Channel

A closer look at the session types reveals the interface design between sessions, protocols and channels.

Seed Session

For each successful connection attempt to a seed in setting::seeds, the seed session will create a channel and register such with the parent p2p object. The messaging loop on each channel is managed by a single or multiple dedicated protocol objects which are attached. (Only a single channel is illustrated below, although a given session will manage multiple channels).

seed session

Each new channel is initially only attached to a single protocol_version instance. The version protocol(version/verack) negotiates the highest common version with the remote peer, which is then set in the channel object, which in turn is registered in the parent p2p object.

Attaching a protocol to channels follows the lifetime management pattern seen with session objects.

{
    // ...
    make_shared<protocol_type>(p2p, channel_ptr)->start();
    // ...
}
// ...
void protocol_type::start()
{
    // ...
    async_messaging_operation(            
        std::bind(handler, shared_from_base<protocol_type>(), args...));
    // ...
}
// ...
void protocol_type::handler()
{
    // ...
    next_async_messaging_operation(            
        std::bind(next_handler, shared_from_base<protocol_type>(), args...));
    // ...
}
// ...

As is the case with session objects, the lifetime of the protocol is extended by binding a valid instance of a shared pointer to subsequent handlers throughout the asynchronous messaging loop.

The initial version protocol ends after the channel version has been negotiated. Subsequently, the ping, reject and seed protocols are attached. The ping protocol (ping/pong) keeps the channel alive, whereas the reject protocol handles received reject messages. The protocol_seed object requests(get_address) and handles received address messages from its seeding peers. Completion of the seed protocol results in the closing of seed channels and invocation of the session_seed.start(...) handler.

Outbound Session

outbound session

The session_outbound maintains a connection loop which makes a best effort to maintain a number of outbound connections set in settings::outbound_connections. The protocols attached to each new channel follows the pattern seen in the seed session, except that the a protocol_address (get_address/address) instance is attached after the version is negotiated.

In the case of both in- and outbound sessions, the attached protocols attempt to maintain the lifetime of the underlying channels until the settings::channel_expiration duration has passed or a channel error occurs.

Application Network Interfaces

An application interfacing with a default network::p2p object can subscribe to messages and channel events or broadcast messages to all available channels as described earlier in this document.

In order to interface with the p2p message protocol on the protocol and channel level, custom classes can be designed to inherit and override p2p, session and protocol classes. Please consider subsequent p2p networking chapter sections for such inheritance examples.