libbitcoin is a bitcoin library targeted towards high end use. The library places a heavy focus around asychronicity. This enables a big scope for future scalability as each component has its own thread pool. By increasing the number of threads for that component the library is able to scale outwards across CPU cores. This will be vital in the future as the demands of the bitcoin network grow.
Another core design principle is libbitcoin is not a framework, but a toolkit. Frameworks hinder development during the latter stages of a development cycle, enforce one style of coding and do not work well with other frameworks. By contrast, we have gone to great pains to make libbitcoin function as an independent set of mutual components with no dependencies between them.
The approach we took to our threaded design is built not around the data, but around tasks. On a finer level: operations. libbitcoin is a toolkit library that uses the proactor design pattern. It implements the proactor pattern through the use of completion handlers like in boost::asio.
- Portability. The library should support a range of commonly used operating systems, and provide consistent behaviour across these operating systems.
- Scalability. The library should facilitate the development of network applications that scale to thousands of concurrent connections. The library implementation for each operating system should use the mechanism that best enables this scalability.
- Efficiency. The library should support techniques such as scatter-gather I/O, and allow programs to minimise data copying.
- Model concepts in an intuitive manner. The library models different subsystems of bitcoin in a clear and intuitive manner. We choose abstractions that allow designing a wide range of applications that rely on bitcoin.
- Basis for further abstraction. The library should permit the development of other libraries that provide higher levels of abstraction. For example, implementations of the bitcoin protocol in other networks such as tor.
- No blocking. No blocking ever occurs waiting for another thread to complete (except possibly on a low level within boost dispatches- but that is uncommon).
- UNIX approach. The library attempts to provide small units of functionality that perform one single task. Our philosophy is to break down higher level functionality into small parts and to simply provide those parts. The cost is inconvenience. The benefit is flexibility.
Basic libbitcoin anatomy
Before using libbitcoin it may be useful to get a conceptual picture of the various parts of libbitcoin, your program, and how they work together.
libbitcoin is centered around various components that provide critical infrastructure for bitcoin functionality. These components are called ”services”. Services run inside their own thread contexts and can exist in multiple threads. Interacting with a service is done through their interface.
Services implement thread-safe interfaces as a strict rule. Tasks are submitted to services and upon completion, your program will be notified. Services are self contained units and are locally encapsulated. They implement an interface for which various implementations may exist- the blockchain service has an implementation for postgresql (unmaintained for now) and berkeleydb.
There are two basic service actions in libbitcoin:
- Operation. Perform operation. Notified upon completion.
- Subscribe. Subscribe to possible event. Notified upon event.
All methods on services return immediately.
When calling a method on a service to initiate an action, your program is submitting a piece of work to that service’s proactor engine to complete. Once the program is ready, it will take that piece of work from the queue, complete it and then call the completion handler passed to it.
The program calls send on the network service passing a message packet and a completion handler. The send function call returns immediately and the program continues on.
The task gets submitted to the network service. Once the network service is ready and has completed its previous tasks, it awakens and grabs the latest piece of work from the queue (send message).
The network service sends the packet to the bitcoin network asynchronously. Upon completion it calls the completion handler passed to it, and fetches the next piece of work to complete. The program continues on from where it left off.
Note that the network service can exist in multiple threads, and it may be performing another piece of work while doing this send. Scalability is resolved in this way by having services able to run with any defined number of threads.
At any time, the network service can respond with a packet from other bitcoin nodes. The behaviour of other bitcoin nodes is unpredictable. For that libbitcoin uses a subscribe-notify pattern.
To be notified of certain events, an interest is expressed in notification of those events. When those events occur, the service will notify all the interested parties.
The program subscribes to a certain event with the service passing a callback handler. The service registers their subscription and returns immediately. The program continues on.
At some point, the event occurs inside the service. All the subscribes handlers are called. The program continues on. The subscription queue is cleared.
In libbitcoin, subscriptions are cleared once called. This removes the need for a complex API with subscribe/unsubscribe and subscription_id semantics. There are 3 possible workflows for subscription APIs:
- Subscribe. Notification. Unsubscribe.
- Subscribe. Notification.
- Subscribe. Before notification occurs, unsubscribe.
The last workflow is rare and not used much in programs. With libbitcoin style subscriptions, the above translates to:
- Subscribe. Notification.
- Subscribe. Notification. Re-subscribe.
- Subscribe. When/if notification occurs, the program checks a custom boolean and sees that it is ignoring this event. Does not respond to the event and returns from the function.
This leads to a concise API. libbitcoin-style subscription semantics removes the need for storing the state of a subscription and leaves the task up to the service to manage.
Everything in libbitcoin is designed in mind with dealing to things in the moment. Things happen and the program deals with it then and there. A task is initiated, thrown off into ‘the cloud’ and forgotten.
The proactor design pattern: concurrency without threads
- Asynchronous Operation
Defines an operation that is executed asynchronously, such as an asynchronous send to the network.
- Completion Event Queue
Buffers completion events until they are dequeued by an asynchronous event demultiplexer.
- Completion Handler
Processes the result of an asynchronous operation. These are function objects, often created using std::bind.
- Asynchronous Event Demultiplexer
Blocks waiting for events to occur on the completion event queue, and returns a completed event to its caller.
Calls the asynchronous event demultiplexer to dequeue events, and dispatches the completion handler (i.e. invokes the function object) associated with the event. This abstraction is represented by the boost io_service class.
Application-specific code that starts asynchronous operations. The initiator interacts with an asynchronous operation processor via a high-level interface such as the blockchain interface, which in turn delegates to a service like postgresql_blockchain.
- Decoupling threading from concurrency
Long-duration operations are performed asynchronously by the implementation on behalf of the application. Consequently applications do not need to spawn many threads in order to increase concurrency.
- Performance and scalability
Implementation strategies such as thread-per-connection (which a synchronous-only approach would require) can degrade system performance, due to increased context switching, synchronisation and data movement among CPUs. With asynchronous operations it is possible to avoid the cost of context switching by minimising the number of operating system threads – typically a limited resource – and only activating the logical threads of control that have events to process.
- Simplified application synchronisation
Asynchronous operation completion handlers can be written as though they exist in a single-threaded environment, and so application logic can be developed with little or no concern for synchronisation issues.
- Function composition.
Function composition refers to the implementation of functions to provide a higher-level operation, such as sending a message in a particular format. Each function is implemented in terms of multiple calls to lower-level operations.
For example, consider the bitcoin version exchange when connecting to a bitcoin node. A version message is sent on both sides with a verack response back. A hypothetical handshake operation could be implemented using a send version operation and a subscription to verack messages.
- The version message is sent.
- A verack response back is registered.
- We receive a valid version message and send successfully send a verack response back.
To compose functions in an asynchronous model, asynchronous operations can be chained together. That is, a completion handler for one operation can initiate the next. Starting the first call in the chain can be encapsulated so that the caller need not be aware that the higher-level operation is implemented as a chain of asynchronous operations.
The ability to compose new operations in this way simplifies the development of higher levels of abstraction above this bitcoin library, such as abstraction layers to ease development.
- Program complexity
It is more difficult to develop applications using asynchronous mechanisms due to the separation in time and space between operation initiation and completion. Applications may also be harder to debug due to the inverted flow of control.
- Memory usage
Buffer space must be committed for the duration of an operation, which may continue indefinitely, and a separate buffer is required for each concurrent operation. The Reactor pattern, on the other hand, does not require buffer space until ready for an operation.