3. Overview

3.1. Rationale

As libbitcoin is an asynchronous toolkit, there is no one way of developing programs - it is up to the individual author. Different approaches are better for different situations.

3.2. Core Concepts and Functionality

3.2.1. 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 LevelDB.

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.

Broadly speaking the main services in libbitcoin can be divided down three lines into network, blockchain and supporting services.

3.2.1.1. Network services

these services are concerned with the network side of things.

  • channel
  • network
  • protocol
  • hosts
  • handshake

3.2.1.2. Blockchain services

Bitcoin’s blockchain is usually disk oriented. Backends all implement the blockchain interface allowing programs to utilise backends using the same code.

  • blockchain (abstract interface for blockchain backends)
  • bdb_blockchain
  • leveldb_blockchain

3.2.1.3. Supporting services

These services utilise other services and provide additional functionality. They can be thought of as wrapper services composed of other services.

  • poller
  • transaction_pool
  • session

3.2.2. Threadpools and Services

3.2.2.1. Theory

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.

_images/operations.png

This program calls send on the network service passing a data type to be sent, and a completion handler. The send function call returns immediately and the program continues on.

The task gets submitted to the service. Once the service is ready and has completed its previous tasks, it awakens and grabs the latest piece of work from the queue (send object).

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 this 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 using a threadpool object.

3.2.2.2. Practice

Services are objects inside libbitcoin that perform asynchronous operations like doing a long disk operation or calling other services. Their constructor accepts a threadpool object.

Work that is submitted to the services is added to that threadpool, and will be queued in that context.

// Define 2 threadpools with 6 threads in total.
// disk_pool has 4 threads, and net_pool has 2.
// Operations submitted to net_pool will only run in those 2 threads
// spawned by net_pool.
threadpool disk_pool(4), net_pool(2);
// This blockchain service will submit work to disk_pool
blockchain chain(disk_pool);
// Completion handler.
auto blockchain_started = [](
    const std::error_code&)
{
    // Check whether the status was successful.
    if (ec)
        log_error() << "Starting blockchain: " << ec.message();
    // ... do stuff
};
// Invoke the call. Returns immediately.
chain.start("database/", blockchain_started);

3.2.3. Data Types

These core types are used around the library in multiple places where working with hashes or raw data chunks.

type data_chunk

Raw bytes. A chunk of data. This type is equivalent to std::vector<uint8_t>.

type hash_digest

32 byte result of a SHA256 hash. Same type as std::array<uint8_t, 32>.

type short_hash

20 byte result of a RIPEMD hash. Typically used for decoded Bitcoin addresses (RIPEMD hash of public key). Same as std::array<uint8_t, 20>.

Additionally helper functions exist for working with these types.

void extend_data<D, T>(D& chunk, const T& other)

Insert other at the end of chunk.

T cast_chunk<T>(data_chunk chunk, bool reverse=false)
data_chunk uncast_type<T>(T value, bool reverse=false)

Convert chunk to and from different integer types. i.e cast_chunk<uint32_t>(chunk) converts chunk into a 4 byte integer. Assumes chunk.size() >= 4 bytes.

std::string encode_hex<T>(T data)

Encode data into a hex string.

hash_digest block_hash = hash_block_header(genesis_block());
// Displays 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
std::cout << encode_hex(block_hash) << std::endl;
data_chunk decode_hex(std::string byte_stream)

Decode hex string into raw data.

HashType decode_hex_digest<HashType>(std::string byte_stream)

Decode hex string into a hash type (either hash_digest or short_hash).

3.2.4. Logging

libbitcoin internally uses its own logging system. There are five log_level.

log_debug() << "Internal debug output.";
log_info() << "Info to user.";
log_warning() << "Warnings.";
log_error() << "Errors.";
log_fatal() << "Fatal errors.";

The log output can be redirected for a log_level using log_X::set_output_function(). The output function follows this format:

void output(log_level level, const std::string& domain,
    const std::string& body);

By defining a custom function and using std::bind(), we can send output of a certain log_level to a file. See examples/fullnode.cpp for a demonstration.

std::ofstream outfile("debug.log");
log_debug().set_output_function(
    std::bind(output_to_file, std::ref(outfile), _1, _2, _3));

log_X() functions optionally take a domain argument. This specifies the context where the message originates from.

#define LOG_NETWORK     "network"

...

log_debug(LOG_NETWORK) << "Forcing disconnect due to timeout.";

3.2.5. std::error_code

libbitcoin generally does not use exceptions, instead relying on std::error_code objects. Objects of this type hold an error code value.

#include <bitcoin/bitcoin.hpp>
#include <iostream>

int main()
{
    // error_code without a value.
    std::error_code ec;
    if (!ec)
        std::cout << "std::error_code is not set." << std::endl;
    // Now set a value.
    ec = bc::error::operation_failed;
    if (ec)
        std::cout << "std::error_code value: " << ec.message() << std::endl;
    return 0;
}

Output:

std::error_code is not set.
std::error_code value: Operation failed

3.2.6. std::bind

std::bind() is for partial function application.

Suppose you have a function object f which takes 3 arguments:

f(a, b, c)

You want a new function object which only takes two arguments, defined as:

g(a, b) = f(a, 4, b)

g is a “partial application” of the function f: the middle argument has already been specified, and there are two left to go.

You can use std::bind to get g:

auto g = std::bind(f, _1, 4, _2);

This enables libbitcoin methods that expect one function type to be mapped to different methods that expect different or additional arguments. std::bind() allows data to be bound to a function only for the duration of that function object.

#include <functional>
#include <iostream>

using std::placeholders::_1;

int add(int x, int y)
{
    return x + y;
}

int main()
{
    // add is a function that takes two integers and returns their sum.
    // add5 is defined as a functor of one argument, denoted _1, that calls
    // add on its argument with the constant 5 as the first argument.
    auto add5 = std::bind(add, 5, _1);
    std::cout << add5(2) << std::endl;
    return 0;
}

To bind class methods, pass the class pointer in as the second argument:

std::bind(&my_class::foo, this, _1, _2, "somedata", _3);

If you want to keep the object alive until the end of the function object, then pass an std::shared_ptr instead. You can use shared_from_this() to return an object’s std::shared_ptr if needed. See here for info on std::enable_shared_from_this.

The equivalent using C++ lambdas of above would be:

auto add5 = [](int y)
    {
        return add(5, y);
    };

// If using shared_from_this() then use the line below and
// pass in this_ptr instead of this.
//   auto this_ptr = shared_from_this();
// And use this_ptr->foo(...) instead.
auto bar = [this](int x, data_chunk d, int y)
    {
        return foo(x, d, "somedata", y);
    };

3.2.7. std::promise and std::future

The class std::promise provides a facility to store a value that is later acquired asynchronously via a std::future object, that the std::promise can supply.

std::promise and std::future enable you to turn asynchronous methods into synchronous ones. libbitcoin makes extensive use of completion handlers to signal the result of a method invocation. Combining these classes enables you to block in the same thread of control until the result is returned.

std::promise<std::error_code> ec_promise;
// Completion handler called when blockchain is started.
auto blockchain_started_handler =
    [&ec_promise](const std::error_code& ec)
    {
        ec_promise.set_value(ec);
    };
chain.start("database", blockchain_started_handler);
// Block until ec_promise value is set in the handler.
std::error_code ec = ec_promise.get_future().get();
if (ec)
    std::cerr << "Error starting blockchain: " << ec.message() << std::endl;

A promise is a vehicle for passing the return value from the thread executing a function to the thread that cashes in on the function future.

A future is the synchronization object constructed around the receiving end of the promise channel.

So, if you want to use a future, you end up with a promise that you use to get the result of the asynchronous processing.