Skip to content

Simple Query from REQ sockets

JamesC edited this page May 19, 2018 · 4 revisions

The Libbitcoin Bitcoin Server (BS) can be queried with ZMQ REQ (requester) or DEALER sockets. When connecting to the Bitcoin Server and sending a query, the client application creates a REQ or DEALER socket, which is then connected to the server ROUTER socket endpoint. A message containing a stack of message frames which adhere to the BS Server query protocol is then composed by the client application and sent to the REQ or DEALER socket for transmission.

In this introductory query example, we will first consider querying the Libbitcoin-Server with the ZMQ REQ socket, which is strictly synchronous (request-reply) and can only connect to a single endpoint. Please see the bottom of this chapter for limitations of querying with REQ sockets.

In this example, we use the Libbitcoin bc::protocol::zmq library, which provides a convenient c++ interface to the ZMQ framework, so make sure to install this dependency before compiling the examples below. We also use the libbitcoin library for handling the reply payloads with stream readers.

Creating a REQ socket to query a Router

The first thing our client application needs to do when querying the BC server is to create a ZMQ context and REQ socket. We use the bc::protocol::zmq::context and bc::protocol::zmq::socketclasses from the libbitcoin-protocol library.

#include <iostream>
#include <bitcoin/protocol.hpp> // ZMQ Framework
#include <bitcoin/bitcoin.hpp>  // Data, Stream Reader
int main() {

    // Setup of zmq context & socket.
    //--------------------------------------------------------------------------

    bc::protocol::zmq::context my_context(true); //started
    bc::protocol::zmq::socket my_requester(
        my_context,
        bc::protocol::zmq::socket::role::requester
    );

Note that we can only instantiate exactly one ZMQ context in our process. A single ZMQ context object, however, can be used to instantiate multiple ZMQ sockets, or be passed on to separate threads, which can each instantiate their own independent child sockets. The context object therefore is thread-safe, but the sockets are not. In our introductory example, we will stick to a single context and a single socket in a single process thread.

Curve Authentification in ZMQ

It is possible to initiate a secure query connection with the BC server using the CURVE mechanism specified in detail over here. For the purposes of this example, it suffices to understand that the BC Server is configured as the curve server, so as the curve client, we must set the server key and our own private/public key pair (certificate). We can generate a local curve key pair by initialising a bc::protocol::zmq::certificate object.

    // Setup of zmq curve authentification.
    //--------------------------------------------------------------------------
    bc::config::sodium server_key(")nNv4Ji=CU:}@<LOu-<QvB)b-PIh%PX[)?mH>XAl");
    my_requester.set_curve_client(server_key);

    // Generated certificate: private/public keys.
    bc::protocol::zmq::certificate my_certificate;
    bc::config::sodium my_private_key = my_certificate.private_key();
    bc::config::sodium my_public_key = my_certificate.public_key();

    // Prints out base85 representation:
    std::cout << my_private_key.to_string()
              << std::endl
              << my_public_key.to_string()
              << std::endl;

    my_requester.set_certificate(my_certificate);

We can now connect to the server endpoint after initialising the ZMQ context, creating our REQ socket and setting the curve authentication options for a secure connection to the endpoint.

    // Connect to client socket to server ROUTER endpoint.
    //--------------------------------------------------------------------------

    bc::code ec;
    bc::config::endpoint public_endpoint("tcp://testnet1.libbitcoin.net:19081");

    // Connect errors not handled in this example.
    ec = my_requester.connect(public_endpoint);

It is worthwhile to note that zmq connect does not return an error if the connection was not successful. In fact, as long as the endpoint is well-formed and valid, ZMQ will remain silent. For example, if a false zmq certificate is provided, the curve authentication will fail silently. Read more about specific connect errors here.

Compose & Send the Query Message

We are now ready to compose and send our query message. A ZMQ message consists of a frame stack, which must adhere to the Libbitcoin Bitcoin Server API message protocol.

requester-request-response

Notice in the schematic above that our requester socket prepends an empty *delimiter frame to our message which ZMQ hides from our client application. In this case, the BC server will return a response with a delimiter as well, which also remains hidden from the receiving application. Our example could be written with a dealer socket, which does not hide any message envelope frames from the application. If an application with a dealer socket queries the BS server without the delimiter, the BS server will reply in kind with a delimiter-free message.

    // Compose & send request message.
    //--------------------------------------------------------------------------

    bc::protocol::zmq::message my_request;

    // dealer socket: leading delimiter is optional
    // requester socket: delimiter is added by zmq, hidden from application.
    // bc::data_chunk delimiter({});

    std::string command = "blockchain.fetch_last_height";
    uint32_t message_id(0); // 4-byte message identifier
    bc::data_chunk payload({});

    // my_request.enqueue(delimiter); // Omit if using requester socket.
    my_request.enqueue(bc::to_chunk(command));
    my_request.enqueue(bc::to_chunk(bc::to_little_endian(message_id)));
    my_request.enqueue(payload);

    // Socket send: Success/Failure
    if ((ec = my_request.send(my_requester)))
        {
        std::cout << ec.message() << std::endl;
        return 1;
        }

The ZMQ documentation provides more details on possible message send errors.

Polling for a reply

Now that we have sent our request to our service, we can ask our requester socket to receive a message, which will block the application until a received message is ready to be dequeued. It is a good idea therefore, to first poll the socket (whilst setting a polling time-out) for available messages before calling the receive method on the socket.

    // Poll socket and receive reply.
    //--------------------------------------------------------------------------

    // Poll our socket.
    bc::protocol::zmq::poller my_poller;
    my_poller.add(my_requester);
    bc::protocol::zmq::identifier my_requester_id = my_requester.id();
    bc::protocol::zmq::identifiers socket_ids = my_poller.wait(2000);

    // Only initiate receive if there are messages queued for socket,
    // ...since receive() is blocking if no messages queued.
    if (socket_ids.contains(my_requester_id))
    {

        bc::protocol::zmq::message server_response;
        server_response.receive(my_requester);

Notice we can add multiple sockets to our poller and poll them simultaneously. The poller will return a zmq::indentifier list of all sockets which have messages that are ready to be dequeued.

Parsing the Reply (with Streams)

We can now simply dequeue each frame from our received message as shown below. When parsing the payload, the bc::istream_reader can be useful in filtering the payloads, which can include error codes, hashes uint types and other byte sequences. In order to instantiate an istream reader, we need to provide it with a byte stream object. Libbitcoin provides such a stream source class called bc::data_source, which can be instantiated with a data chunk parameter.

The istream_reader provides the filters we need to read the error code and 4-byte height from the response payload.

        std::string my_message_command =
            server_response.dequeue_text();

        uint32_t my_message_id;
        server_response.dequeue(my_message_id);

        bc::data_chunk reply_payload;
        server_response.dequeue(reply_payload);

        // Use stream to format response data chunk.
        bc::data_source reply_byte_stream(reply_payload);
        bc::istream_reader reply_byte_stream_reader(reply_byte_stream);

        // Use istream class error read method.
        if ((ec = reply_byte_stream_reader.read_error_code())) {
            // if response begins with error, return.
            std::cout << ec.message() << std::endl;
            return 1;
        }

        // Read height: uint32_t / 4 Bytes LE
        uint32_t height = reply_byte_stream_reader.read_4_bytes_little_endian();
        std::cout << height << std::endl;
    }

    else
    {
      std::cout << "Request timed out..." << std::endl;
    }

    // Close socket.
    my_requester.stop();

    // Close context.
    my_context.stop();

    return 0;

}

Limitations of the REQ Socket

The REQ socket is strictly synchronous and must adhere to the request-reply pattern. It cannot perform any subscriptions with the Libbitcoin BS server, nor can it a single REQ socket connect to multiple server endpoints.

Subscriptions

See here for subscribing to notifications from the Libbitcoin BS server with a ZMQ dealer socket.

Connecting to multiple endpoints

See here for querying multiple server endpoints with a single ZMQ dealer socket.

Clone this wiki locally