Archive of the libbitcoin Slack #general channel.
[log] protocol = true level, or toggle on p <enter> at the console.[node] delay_inbound = false [log] protocol = true [web] connections = 1000 bind = 0.0.0.0:80 [explore] connections = 1000 bind = 0.0.0.0:81
[block|header|tx]/hash query interface, with ?format=[json|text|data] formatting, as well as the embedded html.chain:: object model. All objects, down to op.data stored on std::share_ptr<> instances. Then to serialize out the entire object model is walked on a system::ostream to create a single buffer. For text (hex) encoding the buffer is then translated to hex using system::base16_encode. For json the object modal is walked directly by boost::json.
For wire/text there is really no need whatsoever to translated through the normal form (object model). The wire serialization can be trivially produced directly onto the stream from store serialization. This would bypass countless allocations and copies. Scripts and witnesses for example, making up the bulk of all data, do not even require parse, they can just be written to the stream. (Though it would certainly be cleaner if no for the tragic segwit decision to serialize witnesses independently from inputs.) I'd estimate that this would cut large block deserialization time in half, and on my old machine that would put it under 20ms.system::stream can be used over a preallocated contiguous buffer. But eventually this could even just stream write out to the port in chunks. It's possible to write the message heading without reading the object, since we always have the size indexed. So stream direct from store into the port with zero translation of scripts, witnesses, and point hashes, and the rest is just just organizing the pieces. I'd estimate 2-3 days of work for the whole thing to serialize blocks and txs directly from store to wire. The store queries no more than 1 day.json. Furthermore, for text/hex serialization, we can provide a encoding streamer, just like our system::sha256_writer for example, that performs hex encoding on the fly, writing 2 bytes for every 1, and with a single double buffer size requirement. So with the fixed buffer can also use the fast system::ostream. That cuts allocation from 3x to 2x, and of course allows it to be chunked.json, that's a different problem, since boost::json operates over an object model. However the boost::json::value (intermediate json DOM) could be populated directly from store queries as well. In other words, have the query produce the model instance - instead of having the boost::json value convertor produce it from the annotated object model. This again would cut out the normal form queries. Those are really only necessary for validation. That's probably 2 days of work in the store - first day to figure out the proper boost::json pattern. That would cut way down on memory allocations and probably cut the json output time in half (presently 75ms for large blocks on my machine, vs. 40ms for hex, and 35ms for wire encoding). So maybe json in 40ms, hex in 20ms, and 18ms for wire. Cut that in half for a 2x faster CPU, as I'm running 2.1GHz threads and these are entirely CPU-bound single threaded operations.[node] priority = true
[node] thread_priority = true memory_priority = true
node settings default to true, which raises the thread priority above network. Setting memory priority to false will lower the process memory priority below OS defaults, and that's a new option. Also, memory priority only functions on windows.node.priority setting in config either remove or rename it for use with future builds, or you will get a error on node startup.path setting for a site will allow that to override the embedded site. This was the original intent, just wasn't thinking last night at 4am. That allows us to test changes without rebuilding, allows customers to build their own sites, and allows default config to be safe/easy. It'll be checked in in a bit, just waiting for the node repo to clear CI.Database creation failed with error 'file failed to load, disk may be full'.
--settings option. You could divide them all by 10 by just dropping the last digit.2025-11-03T12:41:43Z.2 Accepted socket connection [192.168.0.219:51007] on binding [192.168.0.219:82].
2025-11-03T12:41:43Z.2 Inbound socket channel start [192.168.0.219:51007] success
2025-11-03T12:41:43Z.3 Response [Switching Protocols] http/1.1 (c) keep [192.168.0.219:51007] {}
2025-11-03T12:41:43Z.3 Websocket upgraded [192.168.0.219:51007]hex_reader, so we now have streaming parse and serialization (hex_writer) for binary (p2p), base16 (text/hex), json (rpc, explore, etc.), files (html) - both embedded (POD buffer/span) and file system (file handle).http, ws, p2p, electrum/sv1, sv2, zeromq). That will drop into a header std::variant, which will result in the current universal http message (variant body with http header only) becoming a universal tcp message.
This will allow send/receive over the same proxy::write(message) and proxy::read(message) methods with pass-through to the contained socket's two analogous methods. All aspects of framing implemented in the headers (todo), all aspects of body parse and serialization contained in the bodies (which is complete). The socket already handles tcp/p2p, http/ws using a std::optional switch between the tcp and ws stream. This will become generalized to a variant socket, with four stream types, which you can think of as tcp, tls, tcp+ws, and tls+ws. tcp and tls can upgrade to ws (from http messaging), and tls or tcp will be the initialization of any endpoint (i.e. an endpoint is either always tls or not tls). ws is really about framing, but dropping the upgrade process down to the socket simplifies implementation, due to the complexity of ws framing. It really operates more like a transport layer.tls on tcp and ws on either. The upgrade from one to the next destroys the original socket, so the new one simply takes its place in the variant. So to support all forms of tcp transport, we will still only require the resources of a single socket instance (and the variant type flag). Same for the universal tcp message. One c++ type with just two variants (header and body), no other state. All aspects of the transport protocol are buried in the socket, all aspects of message framing are controlled by message headers and all aspects of message encoding are controlled by message bodies. Messages, being entirely decoupled from transport, framing, and encoding, define the application protocol. This cleanly separates the transport and message plumbing from application semantics. And as such we can run any application protocol in any encoding(s) over any transport(s).tls above refers to http/s, also applicable as sv2/s (which is not http), as well as p2p/s (bip324) and zmq/s (curve-cp). All of these will be handled by the same tls socket abstraction and are distinct only in the handshake callback, which is handled by the channel base type that derives from the proxy, which embeds the socket.(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
bitcoind, electrum, and stratum_v1)./// rpc interface requires methods, type, size, and mode.
struct bitcoind
{
static constexpr std::tuple methods
{
method<"getbestblockhash">{},
method<"getblock", string_t, double>{ "blockhash", "verbosity" },
method<"getblockchaininfo">{},
method<"getblockcount">{},
method<"getblockfilter", string_t, string_t>{ "blockhash", "filtertype" },
method<"getblockfrompeer", string_t, double>{ "blockhash", "peer_id" },
method<"getblockhash", double>{ "height" },
method<"getblockheader", string_t, bool>{ "blockhash", "verbose" },
method<"getblockstats", string_t, array_t>{ "hash_or_height", "stats" },
method<"getchaintips">{},
method<"getchaintxstats", option<double, 42>, option<string_t, "42">>{ "nblocks", "blockhash" },
method<"getdifficulty">{},
method<"getmempoolancestors", string_t, bool>{ "txid", "verbose" },
method<"getmempooldescendants", string_t, bool>{ "txid", "verbose" },
method<"getmempoolentry", string_t>{ "txid" },
method<"getmempoolinfo">{},
method<"getrawmempool", bool, bool>{ "verbose", "mempool_sequence" },
method<"gettxout", string_t, double, bool>{ "txid", "n", "include_mempool" },
method<"gettxoutproof", array_t, string_t>{ "txids", "blockhash" },
method<"gettxoutsetinfo", string_t, bool, double>{ "hash_type", "use_index", "scan_blocks" },
method<"preciousblock", string_t>{ "blockhash" },
method<"pruneblockchain", double>{ "height" },
method<"savemempool">{},
method<"scantxoutset", string_t, array_t>{ "action", "scanobjects" },
method<"verifychain", double, double>{ "checklevel", "nblocks" },
method<"verifytxoutproof", string_t>{ "proof" },
};
using type = decltype(methods);
static constexpr auto size = std::tuple_size_v<type>;
static constexpr group mode = group::either;
};option<double, 42>. I'll work this in tomorrow. The outcome will be that handlers can reliably bind to a fixed parameter set and the default values will simply be injected when they are allowed to be defaulted and are omitted from the call. If disallowed (not configured as optional) it's just an error for the parameter to be missing.bitcoind, electrum, and stratum_v1 (whatever subsets of these can be supported with just a node). The handlers are just subscriptions, like any protocol. And implementation of will be straightforward to the extent that the required information is in the store. In v4 we won't have the tx pool, so those calls will return not implemented. This dispatcher operates over the json-rpc request_t (and response_t) message, which can be produced from any transport. The channel binds the socket to the endpoint, feeds the above distributor, and attaches protocols. The protocols subscribe to messages, which in this case are dispatched by name and parameters to the strongly typed handler created by the protocol, a closure over which is pushed to the distributor as a subscription.static constexpr std::tuple methods
{
method<"getbestblockhash">{},
method<"getblockhash", double>{ "height" },
method<"getblock", string_t, option<1.0>>{ "blockhash", "verbosity" },
method<"getblockheader", string_t, option<true>>{ "blockhash", "verbose" },
method<"getblockstats", string_t, array_t>{ "hash_or_height", "stats" },
method<"getchaintxstats", option<42.0>, option<"hello"_t>>{ "nblocks", "blockhash" },
};option<""_t> - couldn't get it down to just option<""> but the use of the existing text array conversion _t will at least will let me sleep well tonight. :? So for rpc default value types, the default is provided and the type is inferred. option<1.0> - requires the decimal, since these are doubles (json), and option<true> true or false. array_t and object_t don't have defaults, as they can just be empty collections.value_t variant parameter value type, when it comes time to generalize beyond the json-rpc type system.method<"foo"> is perfectly achievable as a type, but this is not possible for a specialized class template (other types in the same option<...> tag. So the _t converts the string to a user-defined-literal, which is a std::array<char, N> which can be handled (because it's not a char*), and the option instance just exposes it as a std::string_view and types it as a string_t (one of our json-rpc types).error: sorry, non-type template argument of type 'double' is not yet supported method<"getblock", string_t, option<1.0>>
clang18 (~Feb 2024).clang18 finally has consteval support, so we are down to one last c++20 workaround.rpc dispatch code, with complete testing, and just defined all of the json-rpc interfaces using it (bitcoind, electrum, and stratum_v1).
Also spent all day Friday on construction at a friend's house, so really just 2 days since the meeting. The bulk of the work over that time was in fully integrating support for optional (defaulted) and nullable (explicitly null passed) method arguments.
I also refactored the file and namespace layout so that rpc and json are now entirely independent of json-rpc. IOW we have rpc, and we have json parse/serialize. And since our rpc model is consistent with json-rpc, it inherently supports it just by serializing via json.bitcoind, electrum, stratum_v1, http/s, and p2p. One small template to generate the dispatch over the simple method signature declarations, and then just implement the message handlers vis subscription to the generated dispatcher, within any number of attached protocols. Any other messaging protocols will be trivial to add and all will perform just as well as p2p.http interface above is really just for a generic directory/embedded server (e.g. admin) site. The block explore site will use a different interface (restful query). While it will also run over http/s and ws/s transports, as with all of the interfaces, it will be independent of transport, encoding, and serialization. The interface definitions above are about the message structure - supporting fully-generalized dispatch. I've previously completed most of the work to generalize transports (http/s, ws/s, p2p/s, tcp/s zmq/s, encodings (data, text, json, html), and serialization (json-rpc, peer, explore, web, sv2). The design is to use a single pair of proxy methods (send/receive), a single pair of messages (request/response), and a single interface (generic rpc dispatch of request), with multiple message bodies to provide parse/serialize from/to all encodings, multiple message headers to prove framing for all transports, including body selection on read, and two pairs of fundamental transports (tcp, tcp/s, ws, ws/s). Really that's just tcp and ws with tls added, though there are three forms of secure sockets (tls, CurveCP, and bitcoin p2p has its own special brand). Other transports like http, zmq, and p2p are just tcp from this perspective, as only ws and tls require socket integration (framing).struct value_t
{
using inner_t = std::variant
<
null_t,
boolean_t,
number_t,
string_t,
array_t,
object_t,
any_t
>;
...
};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;
}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>{}
};
};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;
} 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;
});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)>;
};optional<> parameters, with defaults configured, and nullable<> parameters, where the json (or other) request can explicitly pass a null_t. In the former case, if the parameter is not provided, the default is injected into the method call. If a null is provided, the parameter will have no value (nullables get passed as a std::optional<T>). Optionals only trailing in the method signatures. All part of the magical dispatch, most of which is just compile time code. Very little actually happens at run time, and actually less than what was the case with the hand-rolled p2p dispatch. For example, the full dispatch map for each interface is defined and built statically, one for all connections to the node.version message was getting handled, but the verack message failed to route. Turns out I had naively use the method names here, as opposed to the message names. This defines that named message gets routed to what method given what parameters. So this is the corrected version:
static constexpr std::tuple methods
{
method<"addr", messages::peer::address::cptr>{},
method<"alert", messages::peer::alert::cptr>{},
method<"block", messages::peer::block::cptr>{},
method<"filteradd", messages::peer::bloom_filter_add::cptr>{},
method<"filterclear", messages::peer::bloom_filter_clear::cptr>{},
method<"filterload", messages::peer::bloom_filter_load::cptr>{},
method<"cfilter", messages::peer::client_filter::cptr>{},
method<"cfcheckpt", messages::peer::client_filter_checkpoint::cptr>{},
method<"cfheaders", messages::peer::client_filter_headers::cptr>{},
method<"cmpctblock", messages::peer::compact_block::cptr>{},
method<"blocktxn", messages::peer::compact_transactions::cptr>{},
method<"feefilter", messages::peer::fee_filter::cptr>{},
method<"getaddr", messages::peer::get_address::cptr>{},
method<"getblocks", messages::peer::get_blocks::cptr>{},
method<"getcfcheckpt", messages::peer::get_client_filter_checkpoint::cptr>{},
method<"getcfheaders", messages::peer::get_client_filter_headers::cptr>{},
method<"getcfilters", messages::peer::get_client_filters::cptr>{},
method<"getblocktxn", messages::peer::get_compact_transactions::cptr>{},
method<"getdata", messages::peer::get_data::cptr>{},
method<"getheaders", messages::peer::get_headers::cptr>{},
method<"headers", messages::peer::headers::cptr>{},
method<"inv", messages::peer::inventory::cptr>{},
method<"mempool", messages::peer::memory_pool::cptr>{},
method<"merkleblock", messages::peer::merkle_block::cptr>{},
method<"notfound", messages::peer::not_found::cptr>{},
method<"ping", messages::peer::ping::cptr>{},
method<"pong", messages::peer::pong::cptr>{},
method<"reject", messages::peer::reject::cptr>{},
method<"sendaddrv2", messages::peer::send_address_v2::cptr>{},
method<"sendcmpct", messages::peer::send_compact::cptr>{},
method<"sendheaders", messages::peer::send_headers::cptr>{},
method<"tx", messages::peer::transaction::cptr>{},
method<"version", messages::peer::version::cptr>{},
method<"verack", messages::peer::version_acknowledge::cptr>{},
method<"wtxidrelay", messages::peer::witness_tx_id_relay::cptr>{}
};"verack" message routes to the version_acknowledge message subscribers. Should have a read on performance impact soon.error C1128: number of sections exceeded object file format limit: compile with /bigobj900,000 on the new dispatch architecture last night, 36 seconds better than the record run, at 59.3 mins.[network] settings has been reduced and there are now three new config sections: [outbound], [inbound], and [manual]. Other config sections are unaffected.
Note that there is no "seeding" section, despite there being a seeding and the above four p2p session types. This is because seeding is actually part of outbound from a configuration perspective.
Each new section drives from the same base configuration settings structure (tcp_server) as the other network services. There is also an intermediate http_server derivation, and html_server derived from that. In each case sharing common base properties.[network] # common network settings # ... [inbound] # tcp_server properties secure = false connections = 100 bind = [::]:8333 bind = 0.0.0.0:8333 inactivity_minutes = 10 expiration_minutes = 60 # inbound properties enable_loopback = false self = 42.42.42.42:8333 self = [abcd:abcd:abcd:abcd::abcd:abcd:abcd]:8333 [outbound] # tcp_server properties # ... # outbound properties enable_ipv6 = true connect_batch_size = 5 host_pool_capacity = 10,000 seeding_timeout_seconds = 60 seed = seed1.libbitcoin.net:8333 seed = seed2.libbitcoin.net:8333 seed = seed3.libbitcoin.net:8333 [manual] # tcp_server properties # ... # manual properties peer = 123.123.123.123:8333 peer = 124.123.123.123:8333 [web] # tcp_server properties # ... # http_server properties server = libbitcoin/web host = localhost:80 host = my-server # html_server properties path = m:/server default = ws.html [explore] # tcp_server properties # ... # http_server properties # ... # html_server properties # ... [bitcoind] # tcp_server properties # ... # http_server properties # ... [electrum] # tcp_server properties # ... [stratum_v1] # tcp_server properties # ... [stratum_v2] # tcp_server properties # ...
bind properties will apply to outbound and manual connections and eventually allow internal multihomed load balancing. connections will apply to manual as a way to optionally select a subset of listed peers. secure is transport layer security for the endpoint. That's not currently implemented and is presently a placeholder. Given that we may want to enable secure and non-secure endpoints for each of the above services concurrently, that my just result in a distinct section name for each (e.g. [web/s] and [web]).[socket] section, as that will be folded into http properties. And I haven't shown the [zeromq] section because that's not stubbed in yet.static constexpr std::tuple methods
{
// The block hash or height is logically required.
method<"block", uint8_t, nullable<system::hash_cptr>, nullable<uint32_t>>{ "version", "hash", "height" },
method<"header", uint8_t, nullable<system::hash_cptr>, nullable<uint32_t>>{ "version", "hash", "height" },
method<"filter", uint8_t, nullable<system::hash_cptr>, nullable<uint32_t>>{ "version", "hash", "height" },
method<"block_txs", uint8_t, nullable<system::hash_cptr>, nullable<uint32_t>>{ "version", "hash", "height" },
// The block hash or height is logically required.
// The position parameter is out of URL order because it's required.
method<"block_tx", uint8_t, uint32_t, nullable<system::hash_cptr>, nullable<uint32_t>>{ "version", "position", "hash", "height" },
// All parameters are required.
method<"transaction", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"input", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"inputs", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"input_script", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"input_scripts", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"input_witness", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"input_witnesses", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"output", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"outputs", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"output_script", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"output_scripts", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"output_spender", uint8_t, system::hash_cptr, uint32_t>{ "version", "hash", "index" },
method<"output_spenders", uint8_t, system::hash_cptr>{ "version", "hash" },
method<"address", uint8_t, system::hash_cptr>{ "version", "hash" }
};/// Pagination and filtering are via query string.
enum explore_targets
{
/// /v[]/block/hash/[bkhash] {1}
/// /v[]/block/height/[height] {1}
block,
/// /v[]/block/hash/[bkhash]/header {1}
/// /v[]/block/height/[height]/header {1}
header,
/// /v[]/block/hash/[bkhash]/filter {1}
/// /v[]/block/height/[height]/filter {1}
filter,
/// -----------------------------------------------------------------------
/// /v[]/block/hash/[bkhash]/transactions {all txs in the block}
/// /v[]/block/height/[height]/transactions {all txs in the block}
block_txs,
/// /v[]/block/hash/[bkhash]/transaction/[position] {1}
/// /v[]/block/height/[height]/transaction/[position] {1}
block_tx,
/// /v[]/transaction/[txhash] {1}
transaction,
/// -----------------------------------------------------------------------
/// /v[]/input/[txhash]/[index] {1}
input,
/// /v[]/inputs/[txhash] {all inputs in the tx}
inputs,
/// /v[]/input/[txhash]/[index]/script {1}
input_script,
/// /v[]/input/[txhash]/scripts {all input scripts in the tx}
input_scripts,
/// /v[]/input/[txhash]/[index]/witness {1}
input_witness,
/// /v[]/input/[txhash]/witnesses {all witnesses in the tx}
input_witnesses,
/// -----------------------------------------------------------------------
/// /v[]/output/[txhash]/[index] {1}
output,
/// /v[]/outputs/[txhash] {all outputs in the tx}
outputs,
/// /v[]/output/[txhash]/[index]/script {1}
output_script,
/// /v[]/output/[txhash]/scripts {all output scripts in the tx}
output_scripts,
/// /v[]/output/[txhash]/[index]/spender {1 - confirmed}
output_spender,
/// /v[]/output/[txhash]/spenders {all}
output_spenders,
/// -----------------------------------------------------------------------
/// /v[]/address/[output-script-hash] {all}
address
};BOOST_AUTO_TEST_CASE(path_to_request__block_tx_height_valid__expected)
{
request_t request{};
BOOST_REQUIRE(!path_to_request(request, "/v42/block/height/123456/transaction/7"));
BOOST_REQUIRE_EQUAL(request.method, "block_tx");
BOOST_REQUIRE(request.params.has_value());
const auto& params = request.params.value();
BOOST_REQUIRE(std::holds_alternative<object_t>(params));
const auto& object = std::get<object_t>(request.params.value());
BOOST_REQUIRE_EQUAL(object.size(), 3u);
const auto version = std::get<uint8_t>(object.at("version").value());
BOOST_REQUIRE_EQUAL(version, 42u);
const auto height = std::get<uint32_t>(object.at("height").value());
BOOST_REQUIRE_EQUAL(height, 123456u);
const auto position = std::get<uint32_t>(object.at("position").value());
BOOST_REQUIRE_EQUAL(position, 7u);
}path_to_request(out, url.path) we get the routable request (out), and by calling dispatcher_.notify(out) we invoke all subscribers for the given request method. Defining the dispatcher amounts to:
using explore = publish<explore_methods>; using interface = rpc::interface::peer::dispatch; using dispatcher = rpc::dispatcher<interface>;
explore_methods is defined by the interface above.using explore = rpc::publish<explore_methods>; using dispatcher = rpc::dispatcher<explore>;
explore endpoint will be up and running./v1/block/hash/[hash] /v1/block/height/[height] /v1/block/hash/[hash]/header /v1/block/height/[height]/header /v1/transaction/[hash]
?format=json|text|data&witness=true|false
broadcaster<Interface> derived from the existing dispatcher<Interface. So now all dispatch/broadcast is implemented in the same templates, and the big macro-based hacks are gone.