Bitcoin full node and query server

Nov 9, 2025

View in context in the libbitcoin Slack #general mirror.

evoskuil03:07
Since the request bodies are now entirely independent of encoding, across all protocols and endpoints, the question becomes, what's the normal form for any type of request. The most general form we have is json-rpc. The c++ representation is independent of json, existing to define the mapping from the byte stream into json DOM and onto the objects. This can accommodate everything including REST object requests (explore), page/file requests (web admin site), json-rpc (electrum, stratum_v1, bitcoind), custom-serialized rpc (stratum_v2), and any of the above over other protocols, such as ws/zmq - to include subscription. The channels are a man-in-the-middle between the proxy/socket and the protocols. Protocols deal with semantics and store/node interaction. Proxy/socket deals with transport-level considerations. So it's the channel's job to translate between the universal message type (to/from proxy) and subscribe/send of the protocols. Subscribers allow the protocols to bind arbitrary and fully-parameterized class methods to the channel's incoming message stream. The channel owns a distributor which holds protocol-subscribed closures and invokes them based on channel invoking distributor.notify(message). So the current question is how to best generalize the distributor and keep the channels simple. We want a protocol for any endpoint handling a common request to be reusable across those endpoints. So for example, we can attach a "get chain object" protocol to any of the channels that provide that type of request, whether REST or RPC, whether HTTP or WS or JSON-RPC. No reason we should write a block, header, tx, address, etc. getter for each of these. We've already dealt with normalization across encodings, so how to generalize these calls, and keep each strongly-typed for the protocols. By deserializing json-rpc from stream to our rpc object model (c++), and doing the same for all other request types (regardless of encoding or structure), we can then use a single pair of methods (one for positional and the other for named parameters) in the base distributor to map rpc parameters into strongly-typed method calls. So then the question becomes, how to establish the method binding. Each rpc call has a "method" name, each p2p message has a "command" header field, and each REST getter has an object name that it intends to get (e.g. "block"). These text names map to an RPC "function", so they just get dropped into the normal form, as the request.method field of the universal rpc request object. In json-rpc request, this method is already embedded in the body of the request. In an http REST query, it's in the message header (as the target path). In an p2p message it's in the fixed-width header. So the method maps to the same field, and the parameters, such as the hash association for a REST block request, ends up in the request.params list. Ok, so now the distributor can define a set of subscribers, one per method, with the strongly-typed argument list for each, and a mapping from method text name to subscriber produces dispatch to the appropriate method subscriber (we do this already for p2p). In p2p, that's it. Just forward the object to the subscriber, as all handlers have the same signature (the object). But with RPC we are now generalized into arbitrary method signatures, which we have required to be strongly-typed. So how to efficiently validate, map, and invoke the selected method using the loose collections of parameters, either ordered or named? Enter the absolute magic of c++20. Given nothing but the method reference, and a variadic reference to it's arbitrary argument list, we can:
(1) map the arguments to the method (strongly typed)
(2) validate args against the method
- matching type
- matching name (or position)
- no excess args
(3) invoke the method with the args
Each of these is just one line of code.
👍 1🙌 1