Bitcoin full node and query server

Nov 19, 2025

View in context in the libbitcoin Slack #general mirror.

evoskuil09:41
A big part of the work today, apart from the dispatcher integration, was working out how to patch arbitrary c++ objects into the rpc model, which is fundamentally based on the simple json type system (though in c++). So for example, how to make it carry a block, or an http request, as a function parameter, without reducing all objects to text or json structure. So this ended up being one additional type added to our value_t variant for json parameters.
struct value_t
{
    using inner_t = std::variant
    <
        null_t,
        boolean_t,
        number_t,
        string_t,
        array_t,
        object_t,
        any_t
    >;
...
};
The new any_t is our own class that wraps std::any. This provides entirely safe and strongly typed object containment with full type erasure. In order to not have this inflate the value_t variant it always contains a std::shared_ptr<Type> in the std::any and exposes it as the the shared pointer. The important thing is that it's just a single type added to the variant, but it's mapped in dispatch based on the inner Type. So when the http channel wants to dispatch a request, it just does this...
    if (const auto code = dispatcher_.notify(request_t
    {
        .method = "get",
        .params = { array_t{ any_t{ tag_request<verb::get>(request) } } }
    }))
    {
        stop(code);
        return;
    }
The any_t wraps the std::shared_ptr<request>, not to be confused with request_t which is the rpc base object (e.g. a json-rpc request). The any_t drops into an array_t as the .params property of the rpc request. .params can be array_t (unnamed elements) or object_t (named elements), and the .method is a string_t that selects the rpc method. in a json-rpc call the whole thing is just deserialized off the wire using boost::json. Eventually that will be happening with all of the request types. Note tag_request<verb::get>(...) above wraps the http request, which is itself a shared_ptr<http::request>. This is an "tag" that allows dispatch based on the inner type of the pointer. It adds no size to the original pointer, and aliases the request as get/put/post, etc. That routing is defined here:
struct http_methods
{
    static constexpr std::tuple methods
    {
        method<"get", http::method::get::cptr>{},
        method<"head", http::method::head::cptr>{},
        method<"post", http::method::post::cptr>{},
        method<"put", http::method::put::cptr>{},
        method<"delete", http::method::delete_::cptr>{},
        method<"trace", http::method::trace::cptr>{},
        method<"options", http::method::options::cptr>{},
        method<"connect", http::method::connect::cptr>{},
        method<"unknown", http::method::unknown::cptr>{}
    };
};
Notice the method name "get" aligns with the rpc object (request_t.method). This is how the request_t.params end up in a method call. Those are defined in a protocol, and subscribed to messages by the inner type:
void protocol_http::start() NOEXCEPT
{
    BC_ASSERT(stranded());

    if (started())
        return;

    SUBSCRIBE_CHANNEL(method::get::cptr,     handle_receive_get,     _1, _2);
    SUBSCRIBE_CHANNEL(method::head::cptr,    handle_receive_head,    _1, _2);
    SUBSCRIBE_CHANNEL(method::post::cptr,    handle_receive_post,    _1, _2);
    SUBSCRIBE_CHANNEL(method::put::cptr,     handle_receive_put,     _1, _2);
    SUBSCRIBE_CHANNEL(method::delete_::cptr, handle_receive_delete,  _1, _2);
    SUBSCRIBE_CHANNEL(method::trace::cptr,   handle_receive_trace,   _1, _2);
    SUBSCRIBE_CHANNEL(method::options::cptr, handle_receive_options, _1, _2);
    SUBSCRIBE_CHANNEL(method::connect::cptr, handle_receive_connect, _1, _2);
    SUBSCRIBE_CHANNEL(method::unknown::cptr, handle_receive_unknown, _1, _2);
    protocol::start();
}

// Handle disallowed-by-default methods (override to implement).
// ----------------------------------------------------------------------------

bool protocol_http::handle_receive_get(const code& ec,
    const method::get::cptr& get) NOEXCEPT
{
    send_method_not_allowed(*get, ec);
    return true;
}

bool protocol_http::handle_receive_post(const code& ec,
    const method::post::cptr& post) NOEXCEPT
{
    send_method_not_allowed(*post, ec);
    return true;
}

bool protocol_http::handle_receive_put(const code& ec,
    const method::put::cptr& put) NOEXCEPT
{
    send_method_not_allowed(*put, ec);
    return true;
}

bool protocol_http::handle_receive_head(const code& ec,
    const method::head::cptr& head) NOEXCEPT
{
    send_method_not_allowed(*head, ec);
    return true;
}

bool protocol_http::handle_receive_delete(const code& ec,
    const method::delete_::cptr& delete_) NOEXCEPT
{
    send_method_not_allowed(*delete_, ec);
    return true;
}

bool protocol_http::handle_receive_trace(const code& ec,
    const method::trace::cptr& trace) NOEXCEPT
{
    send_method_not_allowed(*trace, ec);
    return true;
}

bool protocol_http::handle_receive_options(const code& ec,
    const method::options::cptr& options) NOEXCEPT
{
    send_method_not_allowed(*options, ec);
    return true;
}

bool protocol_http::handle_receive_connect(const code& ec,
    const method::connect::cptr& connect) NOEXCEPT
{
    send_method_not_allowed(*connect, ec);
    return true;
}

bool protocol_http::handle_receive_unknown(const code& ec,
    const method::unknown::cptr& unknown) NOEXCEPT
{
    send_method_not_allowed(*unknown, ec);
    return true;
}
It's also possible to bind the handlers just based on the identity of the method, as opposed to the first param shared_ptr inner type, like this (test case):
        instance.subscribe(
            [&](const code& ec, mock::all_required, bool a, double b, std::string c)
            {
                called = true;
                result_a = a;
                result_b = b;
                result_c = c;
                promise8.set_value(ec);
                return true;
            });
The mock::all_required is the actual method type and uniquely identifies the handler as a unique signature. For object_t params, the method descriptors also support names for correlation (a, b, c, below)...
struct mock_methods
{
    static constexpr std::tuple methods
    {
        method<"empty_method">{},
        method<"all_required", bool, double, std::string>{ "a", "b", "c" },
        method<"with_options", std::string, optional<4.2>, optional<true>>{ "a", "b", "c" },
        method<"with_nullify", std::string, nullable<double>, nullable<bool>>{ "a", "b", "c" },
        method<"with_combine", std::string, nullable<bool>, optional<4.2>>{ "a", "b", "c" },
        method<"not_required", nullable<bool>, optional<4.2>>{ "a", "b" }
    };

    // Derive this from above in c++26 using reflection.
    using empty_method = at<0, decltype(methods)>;
    using all_required = at<1, decltype(methods)>;
    using with_options = at<2, decltype(methods)>;
    using with_nullify = at<3, decltype(methods)>;
    using with_combine = at<4, decltype(methods)>;
    using not_required = at<5, decltype(methods)>;
};