Everything goes over the network

A Better network layer implementation

About

Most Bitcoin products are network-native and that means a stress on fast and cheap networking. Different apps primarily communicate over the network to keep their data fresh and it is common to have a list of peers and multiple services per peer.

For this purpose Flowee has the Network Manager component and the P2P component which is built on top of it.

The Network Manager classes have the following features:

  • direct socket communication;
  • making the common easy (and fast) and the complex possible;
  • zero-copy sending/receiving of messages;
  • lock-free operation for sending/receiving of messages;
  • asynchronous operation. By default uses multithreading;
  • usage of zero singletons and zero global variables;
  • priority-queues. (all messages are not equal);
  • message-based network layer, allowing the user to send messages to certain services on the remote peer;
  • (optional) optimized binary protocol, for the highest speed;
  • built in temporary banning (punishment) of misbehaving connections.

What is it used for?

The p2p library uses this for the communication between Bitcoin Cash nodes and your application. (Notice that ’the hub’ at this point does not yet use this).

The basic network manager is the component used in the Hub, as well as components talking to the Hub. The network connection is used by all the Flowee APIs, for instance.

Technical introduction

The network manager is connection based. To connect to the Hub you would write:

WorkerThreads threads;
NetworkManager manager(threads.ioService());
EndPoint ep;
ep.announcePort = 1235;
ep.ipAddress = boost::asio::ip::address_v4::loopback();
manager.setAutoApiLogin(true, "api-cookie");
auto connection = manager.connection(ep);
// TODO setup callbacks
connection.connect():

The network manager class uses multi-threading and for this example we start a WorkerThreads class. This instance is typically shared throughout the application.

As detailed in the API docs, the port we connect to is 1235, and in this case we assume the hub runs on localhost.

The setAutoApiLogin() method is to allow authentication so not everyone can just connect to the Hub. We use ‘cookie’ authentication and this example assumes the cookie filename is ‘api-cookie’.

Last, we create a connection and start to connect.

The NeworkManager will continue to try to connect to the server as long as there are NetworkConnection instances. Re-connect is automatic and login is also done automatically.

In Flowee the network handling is completely asynchronous. We use callbacks to allow you to handle network events. For instance when a message is received from peer.

A simple example of a peer sending a question and receiving an answer follows:

class MyPeer {
public:
  MyPeer(NetworkConnection && con) : m_con(std::move(con) {
    m_con.setOnIncomingMessage(std::bind(&onIncomingMessage, this, std::placeholders::_1));
  }

  void ask() {
    m_pool.reserve(10);
    Streaming::MessageBuilder builder(m_pool);
    m_con.send(builder.message(Api::UtilService, Api::Util::CreateAddress));
  }
  void onIncomingMessage(const Message &message) {
    if (message.serviceId() == Api::UtilService
            && message.messageId() == Api::CreateAddressReply) {
        // handle message
    }
  }

private:
  Streaming::BufferPool m_pool;
  NetworkConnection m_con;
};

This example sends a message with the service-id UtilService and the message-id CreateAddress. The message itself is empty. It expects a reply from the same service-id but with the message-id of CreateAddressReply.

For now you can just accept that we use a service by that ID and the service provides a call CreateAddress. More in-depth description of the concepts are here, and more about the calls can be found in the API docs.

The line starting with m_con.send() creates a Message object and queues it for the network-manager to send. Please note that the send() call returns immediately and the actual sending is done asynchronously. This implies that you can queue a lot of messages in one go, this works fine and is even good for through-put as the entire set of messages is send as one over the network connection. It is even allowed to queue messages while the connection is not yet connected. They will be send the moment the connection is established.

Not shown in the example are the other two callbacks you can register setOnConnected and setOnDisconnected.
Risking throwing too many features at you in one go I do want to point out that one actual socket-connection is not limited to one NetworkConnection object. As such you can have various specialized classes for certain types of info all communicating with the Hub over the same socket connection.

Lets move over the the actual parsing of a message:

Streaming::MessageParser parser(message.body());
if (message.messageId() == Api::Util::CreateAddressReply) {
    auto type = parser.next();
    while (type == Streaming::FoundTag) {
        if (parser.tag() == Api::Util::PrivateAddres)
            logDebug() << "Private:" << parser.stringData();
        if (parser.tag() == Api::Util::BitcoinAddress)
            logDebug() << "Public:" << parser.stringData();

        type = parser.next();
    }
}

Flowee uses a tagged message format which are easy to read and create using the MessageParser and MessageWriter classes. In this example we simply loop over the message in search of two known tags, which we then print. We expect the fields to be strings in this example, but it may be useful to point out that booleans, numbers and byte-arrays are also supported.