Implement bitcoind-compatible JSON-RPC and REST blockchain methods#795
Implement bitcoind-compatible JSON-RPC and REST blockchain methods#795dsbaars wants to merge 16 commits into
Conversation
getblock (verbosity 1/2), getblockcount, getblockhash, getblockheader, getblockchaininfo, getblockfilter, gettxout and getnetworkinfo, reusing the libbitcoin-system bitcoind json serializers with a protocol-level context injector (height, confirmations, mediantime, prev/next hash). Aggregate-heavy methods (getchainwork, getchaintxstats, getblockstats, gettxoutsetinfo, scantxoutset, verifychain, verifytxoutset, pruneblockchain, savemempool) remain explicit not_implemented.
Activate the REST path: bitcoind_target parses /rest/block/<hash>.<bin|hex|json> into a json-rpc model, handle_receive_get dispatches via rest_dispatcher_, and handle_get_block serves all three media types through new raw-http senders (send_data/send_hex/send_dom), which the bitcoind base lacked.
Extend the REST interface beyond block: block_hash (blockhashbyheight), block_txs (notxdetails), block_headers, block_part, block_spent_tx_outputs, block_filter, block_filter_headers and chain_information, with their Core REST url patterns parsed in bitcoind_target. Remaining endpoints (get_utxos[_confirmed], mempool[_information], fork_information) need mempool enumeration / deployment status / utxo semantics not yet exposed, so are left unimplemented.
Move median_time_past, inject_block_context, header_to_bitcoind and chain_name out of the duplicated anonymous namespaces in the rpc and rest protocol units into bitcoind_json.hpp, included by both.
A bare string literal selects value_t(boolean_t) over value_t(const string_t&) in the rpc::object_t initializer (const char* -> bool beats the user-defined string conversion), so subversion and warnings serialized as 'true'. Wrap them in std::string. Caught runtime-testing against a regtest node.
getrawtransaction serves any archived (confirmed) tx by txid: raw hex (verbose 0) or verbose JSON via the existing bitcoind_verbose serializer plus a new inject_tx_context helper (in_active_chain/blockhash/ confirmations/blocktime/time). libbitcoin archives all confirmed tx hash-addressable, so this is a built-in txindex. sendrawtransaction deserializes the hex tx, runs context-free check(), archives it via query.set_code() (so the existing protocol_transaction_ out_106 can serve it on getdata), broadcasts it onto the shared peer message bus to announce to peers, and returns the txid. No mempool subsystem required. TODO: contextual (connect) validation before archiving for policy/DoS hardening.
Two issues caught by runtime-testing against a regtest node:
- The verbose/maxfeerate params were declared optional<0_u32> but the
handlers take double; the rpc dispatcher threw bad_variant_access
("unexpected type") on any numeric arg. Declare as optional<0.0> to
match, consistent with getblock's verbosity.
- getrawtransaction verbose used bitcoind_verbose(tx), which on a
standalone transaction falls back to libbitcoin's plain inputs/outputs
form (no txid). Use bitcoind(tx) for Core's txid/hash/size/vsize/
weight/vin/vout/hex fields (same encoding getblock verbosity 2 embeds).
Cross-checked all declared bitcoind signatures against Core's RPCMethod definitions (bitcoin/bitcoin src/rpc). Two fixes: - getblockstats: hash_or_height was declared string_t, but Core accepts a height number OR a block hash (RPCArg::Type::NUM with skip_type_check). A numeric height threw 'unexpected type' at dispatch. Declare as value_t (the dispatcher passes it through untyped), matching Core; verified both a numeric height and a hash now reach the handler. - getrawtransaction: param named 'verbose'; Core's canonical name is 'verbosity' (with 'verbose' as an alias), and getblock already uses 'verbosity'. Rename for named-parameter compatibility; positional dispatch (used by LND/btcwallet) is unaffected.
|
Thanks! Just giving this a quick visual scan, and it looks good. A full set of passing functional tests (see electrum and native/block) would help get this merged much more quickly. We should also have acceptance tests (@eynhaender @echennells ?) to verify interface compliance. |
Return target (expanded from bits), verificationprogress (confirmed/candidate height), initialblockdownload, and warnings, alongside the existing fields. chainwork and size_on_disk remain omitted, as they require a cumulative-work index and store-size accounting respectively.
Covers getrawtransaction (raw, verbose, coinbase, segwit, unknown txid) and sendrawtransaction (invalid and malformed input; the broadcast path is gated behind BITCOIND_ALLOW_BROADCAST to avoid relaying on mainnet), plus the REST endpoints (block json/hex/bin, notxdetails, spent, blockhashbyheight, headers, blockpart, and the basic filters). Verified against a synced mainnet node. getblockchaininfo's chainwork and size_on_disk assertions are relaxed to match the implementation.
Build an in-process HTTP harness on the bitcoind test fixture (beast POST for json-rpc, GET for REST, replacing the raw-socket placeholder) and add deterministic acceptance tests against the ten-block mock store: getblockcount/getbestblockhash/getblockhash/getblockheader/getblock/getblockchaininfo/gettxout/getrawtransaction/sendrawtransaction/getnetworkinfo, the not_implemented set, and the REST endpoints (chaininfo, block json/hex/bin, notxdetails, spent, blockhashbyheight, headers, blockpart, blockfilter). A witness-store fixture covers segwit getrawtransaction (wtxid != txid, vsize == ceil(weight/4)). These run in CI without a synced node.
|
Thanks for the suggestion. I have added both. Functional tests ( Acceptance tests ( I also extended |
|
Thanks! Just a clarification on test terminology. We use these terms (sort of informally):
We target full Unit Test coverage and Component and/or Functional as necessary to ensure expected aggregate behavior. No external dependencies (e.g. Python or other tools), just Boost Test in our pattern. Can be data-driven (which is when we allow loops in a test). Acceptance is outside of the build (against the executable or compiled lib). Currently we have Python client-server tests in libbitcoin-server but I'm not sure what the status of that is. @eynhaender ? |
Boost.Test, in-build, no external dependencies, per the libbitcoin test taxonomy (unit = lowest-level function, component = aggregate over a class). Unit (test/parsers/bitcoind_target.cpp, replacing the stub): cover the pure bitcoind_target() REST path parser across every route and error path (media mapping, missing/invalid target/hash/number, leading-zero, non-basic filter); error and media cases are data-driven. Unit + component (test/protocols/bitcoind/bitcoind_json.cpp, new): pure header_to_bitcoind field mapping, plus chain_name, median_time_past (BIP113), inject_block_context (genesis/middle/tip) and inject_tx_context (confirmed/unknown) against the ten-block mock store via a minimal store+query fixture (no server/socket). Register the new file in Makefile.am and the vs2022/vs2026 vcxproj/filters.
evoskuil
left a comment
There was a problem hiding this comment.
Very nice, minor comments. Suggest @eynhaender look at the python acceptance tests.
Relocate the bitcoind json context helpers out of the inline header (bitcoind_json.hpp) into protocol_bitcoind_rpc as protected static methods, implemented in protocol_bitcoind_rpc_json.cpp and inherited by the rest protocol. Removes them from the broad server namespace and keeps the implementation out of headers. chain_name now derives each network's genesis from system::settings (per the chain::selection enumeration) rather than hardcoded hashes; signet remains a documented stub pending its selection. Source idiom: pass hash_cptr by const& in the rest handlers (native/electrum convention), use to_shared(std::move()) and std::from_chars() in the target parser. Tests: bitcoind_target gains real coverage, bitcoind_json exercises the helpers via a test seam; rest_text() strips line terminators, display hashes are computed once at file scope, header bytes are hashed directly, and the verbose comments are dropped. Register protocol_bitcoind_rpc_json.cpp in Makefile.am and the vs2022/vs2026 project files.
|
Thanks for the review, all addressed in
One deviation: I kept a leading-zero guard before Full Boost suite passes. @evoskuil would you mind taking another look when you have a moment? |
evoskuil
left a comment
There was a problem hiding this comment.
Sorry to dribble in the reviews, I just had more time just now.
Query stored median_time_past instead of recomputing; use add1/to_half/ floored_subtract/possible_sign_cast and typed literals; drop size_t casts into boost::json; static chain_name lookup; starts_with/trim_right; bool shared_ptr checks. Sweep the same patterns across the bitcoind handlers (fixes a height-as-link mediantime call) and align the rpc tests.
|
Thanks, all addressed in
While sweeping for the same patterns I found a latent bug: the REST Three intentional deviations: I kept a leading-zero guard before Full Boost suite passes. |
| } | ||
| } | ||
|
|
||
| boost::json::object protocol_bitcoind_rpc::header_to_bitcoind( |
There was a problem hiding this comment.
This is just pure object serialization from the type, so should be implemented in system as a serializer.
This is the corresponding impl for the block (header wasn't implemented yet).
https://github.com/libbitcoin/libbitcoin-system/blob/master/src/chain/json/block.cpp
https://github.com/libbitcoin/libbitcoin-system/blob/master/test/chain/json/block.cpp
Query stored median_time_past; confirmations as add1(floored_subtract(top, height)) with no cast or ternary; chain_name as a static lookup comparing hash_digest (signet via constexpr base16_hash; testnet3). REST block_headers streams get_confirmed_headers + get_wire_header into one pre-sized buffer (no per-item copy or masked failure); data/text/json senders; blockpart uses is_lesser, ceilinged_add and std::next; fluent get_height. possible_wide_cast for the rpc value variant; initialblockdownload via is_current.
|
Thanks, all addressed in
Two intentional notes: dropping the confirmations ternary removes the One deferred item: |
|
Weekly team meeting on our slack channel #general today at noon NYC time. Open invite on system wiki side menu. |
|
Would love to join, but about to board a plane to BTC Prague 😅 |
| // Resolve every prevout spent by the block's non-coinbase transactions. | ||
| chain::output_cptrs spent{}; | ||
| const auto& txs = *block->transactions_ptr(); | ||
| for (size_t tx = 1; tx < txs.size(); ++tx) |
There was a problem hiding this comment.
For data and text this loop can operate directly over the stream, avoiding vector allocation. For the json this is prob preferrable.
| const auto& txs = *block->transactions_ptr(); | ||
| for (size_t tx = 1; tx < txs.size(); ++tx) | ||
| for (const auto& in: *txs.at(tx)->inputs_ptr()) | ||
| if (const auto out = query.get_output(query.to_output(in->point()))) |
There was a problem hiding this comment.
This condition is suppressing a store failure.
| http::read(socket_, buffer, response, ec); | ||
| BOOST_CHECK_MESSAGE(!ec, ec.message()); | ||
| BOOST_CHECK_EQUAL(response.result(), http::status::ok); | ||
| return system::data_chunk(response.body().begin(), response.body().end()); |
eynhaender
left a comment
There was a problem hiding this comment.
Ran python acceptance tests against both bitcoind JSON-RPC and REST endpoints and all look good. Implementation also aligns with the existing structure.
Summary
The
bitcoindcompatibility protocol (src/protocols/bitcoind/) was largely stubbed. This PR implements the read-only blockchain surface plus raw-transaction retrieval and broadcast, reusing the existing query layer and the libbitcoin-systembitcoind*JSON serializers. Chain-context fields that the serializers intentionally omit (height, confirmations, next/prev hash, mediantime, blocktime) are injected at the protocol layer, mirroringprotocol_native.Motivation: enabling Bitcoin Core-compatible clients to read chain state and broadcast transactions against a libbitcoin node.
JSON-RPC methods implemented (return live data)
getbestblockhash,getblock(verbosity 0/1/2),getblockchaininfo,getblockcount,getblockhash,getblockheader,getblockfilter,gettxout,getnetworkinfo,getrawtransaction,sendrawtransaction.getrawtransactionserves any archived (confirmed) transaction by txid. libbitcoin archives all confirmed transactions hash-addressably, i.e. an implicit txindex. Returns raw hex (verbosity 0) or Core-format JSON (txid/hash/size/vsize/weight/vin/vout/hexplus injectedin_active_chain/blockhash/confirmations/blocktime/time).sendrawtransactiondeserializes the tx, runs context-freecheck(), archives it viaquery.set_code()so the existingprotocol_transaction_out_106can serve it ongetdata, broadcasts it onto the shared peer-message bus to announce to peers, and returns the txid. No mempool subsystem is required for this path.REST endpoints implemented
/rest/chaininfo.json,/rest/block/<hash>,/rest/block/notxdetails/<hash>,/rest/block/spent/<hash>,/rest/blockhashbyheight/<height>,/rest/headers/<count>/<hash>,/rest/blockfilter/basic/<hash>,/rest/blockfilterheaders/basic/<hash>,/rest/blockpart/<hash>/<offset>/<size>. Per-endpoint media type (binary / hex / json) is derived from the URL extension.bitcoind_targetparses the Core REST URL scheme into a json-rpc request model that the REST dispatcher routes.Deliberately left as clean
not_implementedgetblockstats,getchaintxstats,getchainwork(need aggregate/cumulative indexes),gettxoutsetinfo,verifytxoutset,scantxoutset(need a UTXO-set scan/snapshot),verifychain(full revalidation),pruneblockchain(pruning),savemempool(needs a mempool). These dispatch and return a structurednot_implementederror rather than failing.Notes / design
median_time_past,inject_block_context,inject_tx_context,header_to_bitcoind,chain_name) are hoisted intoinclude/bitcoin/server/protocols/bitcoind_json.hppand used by both the RPC and REST units.getnetworkinfostring fields (subversion,warnings) were being selected asvalue_t(bool)from bare string literals; fixed.RPCMethoddefinitions (bitcoin/bitcoinsrc/rpc).getblockstatshash_or_heightnow accepts a height or a hash (Core'sskip_type_checkbehavior, viavalue_t), andgetrawtransaction's verbosity param uses Core's canonical nameverbosity.Testing
getrawtransaction(raw + verbose),sendrawtransaction(submit / invalid / unknown paths), andgetblockv1/v2 context injection verified.bitcoindtranslation units already existed; the one new file is header-only).Open questions for maintainers
sendrawtransactioncurrently archives the tx before broadcasting and performs only context-freecheck(). Withoutchaser_transactionthere is no contextual (UTXO/policy) validation or eviction of never-mined transactions. This is acceptable for broadcast, but I would welcome guidance on whether to gate it behind a flag or add minimal validation. ATODOmarks the spot.getchainworkandverifytxoutset, which are not standard Bitcoin Core RPCs (Core exposes chainwork as a field, and hasloadtxoutset/dumptxoutset).getrawmempool/getmempoolinfo/testmempoolacceptand the RESTgetutxos/mempool/forkendpoints. These require a queryable mempool, which is currently stubbed inchaser_transaction/protocol_transaction_in.