From ee2e710d659d66c4ce69c7825cf6db750343e741 Mon Sep 17 00:00:00 2001 From: echennells Date: Sun, 28 Jun 2026 00:27:55 +0000 Subject: [PATCH] Add chainwork and size_on_disk to bitcoind blockchain responses. chainwork is required by typed clients (rust-bitcoincore-rpc, corepc) on getblockchaininfo and getblockheader; size_on_disk on getblockchaininfo. Derived from chain_state cumulative work and cached by block hash (immutable) to bound the per-call genesis-span cost. --- .../protocols/protocol_bitcoind_rpc.hpp | 6 ++- .../bitcoind/protocol_bitcoind_rpc.cpp | 15 ++++++- .../bitcoind/protocol_bitcoind_rpc_json.cpp | 43 ++++++++++++++++++- test/protocols/bitcoind/bitcoind_json.cpp | 40 +++++++++++++++-- 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index 7ae6c9e4..152e3529 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp @@ -116,8 +116,12 @@ class BCS_API protocol_bitcoind_rpc const database::header_link& link) NOEXCEPT; static bool parse_verbosity(double& verbosity, const network::rpc::value_t& value, double missing) NOEXCEPT; + static std::string to_chain_work(const node::query& query, + const system::settings& settings, + const system::hash_digest& hash) NOEXCEPT; static void inject_block_context(boost::json::object& out, - const node::query& query, const database::header_link& link, + const node::query& query, const system::settings& settings, + const database::header_link& link, const system::chain::header& header) NOEXCEPT; static void inject_tx_context(boost::json::object& out, const node::query& query, const database::tx_link& link) NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index a4e33b7c..96003a88 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -242,7 +242,8 @@ bool protocol_bitcoind_rpc::handle_get_block(const code& ec, value_from(bitcoind_hashed(*block)) : value_from(bitcoind_verbose(*block)); - inject_block_context(model.as_object(), query, link, block->header()); + inject_block_context(model.as_object(), query, system_settings(), + link, block->header()); send_result(std::move(model), two * block->serialized_size(witness)); return true; } @@ -277,6 +278,14 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, // TODO: blocks/headers is a misnomer (off-by-one), intended? using namespace chain; + const auto chain_work = to_chain_work(query, system_settings(), + query.get_header_key(link)); + if (chain_work.empty()) + { + send_error(database::error::integrity); + return true; + } + send_result(object_t { { "chain", chain_name(query) }, @@ -290,6 +299,8 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, { "mediantime", median_time_past(query, link) }, { "verificationprogress", progress }, { "initialblockdownload", !is_current_chain(true) }, + { "chainwork", chain_work }, + { "size_on_disk", query.archive_size() }, { "pruned", false }, { "warnings", std::string{} } }, 512); @@ -402,7 +413,7 @@ bool protocol_bitcoind_rpc::handle_get_block_header(const code& ec, auto out = header_to_bitcoind(*header); out["nTx"] = query.get_tx_count(link); - inject_block_context(out, query, link, *header); + inject_block_context(out, query, system_settings(), link, *header); send_result(value{ std::move(out) }, 512); return true; } diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp index aadafaf1..d4fda8ae 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp @@ -18,6 +18,9 @@ */ #include +#include +#include +#include #include #include #include @@ -55,9 +58,41 @@ bool protocol_bitcoind_rpc::parse_verbosity(double& verbosity, return true; } +// Sum of work from genesis to the identified header, as bitcoind encodes +// it (chain_state spans the chain to accumulate, as with organization). +// Work to a given header is immutable, so a single-entry cache requires no +// invalidation and absorbs the dominant access pattern (repeated queries +// at the tip). Distinct historical queries still pay the span computation. +std::string protocol_bitcoind_rpc::to_chain_work(const node::query& query, + const system::settings& settings, const hash_digest& hash) NOEXCEPT +{ + using cache_t = std::pair; + static std::mutex mutex{}; + static std::shared_ptr cache{}; + + // The span computation runs outside the lock; only access is guarded. + { + const std::lock_guard lock{ mutex }; + if (cache && cache->first == hash) + return cache->second; + } + + const auto state = query.get_chain_state(settings, hash); + if (!state) + return {}; + + const auto work = encode_hash(from_uintx(state->cumulative_work())); + { + const std::lock_guard lock{ mutex }; + cache = std::make_shared(hash, work); + } + + return work; +} + void protocol_bitcoind_rpc::inject_block_context(boost::json::object& out, - const node::query& query, const database::header_link& link, - const chain::header& header) NOEXCEPT + const node::query& query, const system::settings& settings, + const database::header_link& link, const chain::header& header) NOEXCEPT { size_t height{}; if (!query.get_height(height, link)) @@ -69,6 +104,10 @@ void protocol_bitcoind_rpc::inject_block_context(boost::json::object& out, out["confirmations"] = add1(floored_subtract(top, height)); out["mediantime"] = median_time_past(query, link); + const auto chain_work = to_chain_work(query, settings, header.hash()); + if (!chain_work.empty()) + out["chainwork"] = chain_work; + if (header.previous_block_hash() != null_hash) out["previousblockhash"] = encode_hash(header.previous_block_hash()); diff --git a/test/protocols/bitcoind/bitcoind_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp index 145ab605..98f24a51 100644 --- a/test/protocols/bitcoind/bitcoind_json.cpp +++ b/test/protocols/bitcoind/bitcoind_json.cpp @@ -33,12 +33,21 @@ struct json { using protocol_bitcoind_rpc::median_time_past; using protocol_bitcoind_rpc::parse_verbosity; + using protocol_bitcoind_rpc::to_chain_work; using protocol_bitcoind_rpc::inject_block_context; using protocol_bitcoind_rpc::inject_tx_context; using protocol_bitcoind_rpc::header_to_bitcoind; using protocol_bitcoind_rpc::chain_name; }; +// Cumulative chain work to genesis/tip, encoded as bitcoind serializes it. +constexpr auto genesis_chain_work = + "0000000000000000000000000000000000000000000000000000000100010001"; +constexpr auto block5_chain_work = + "0000000000000000000000000000000000000000000000000000000600060006"; +constexpr auto tip_chain_work = + "0000000000000000000000000000000000000000000000000000000a000a000a"; + // header_to_bitcoind // ---------------------------------------------------------------------------- @@ -179,13 +188,14 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__middle__height_confirm BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, config_.bitcoin, link, *header); chain::context ctx{}; BOOST_REQUIRE(query_.get_context(ctx, link)); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 5u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 5); BOOST_REQUIRE_EQUAL(out.at("mediantime").to_number(), ctx.median_time_past); + BOOST_REQUIRE_EQUAL(as_text(out.at("chainwork")), block5_chain_work); BOOST_REQUIRE_EQUAL(as_text(out.at("previousblockhash")), encode_hash(test::block4_hash)); BOOST_REQUIRE_EQUAL(as_text(out.at("nextblockhash")), encode_hash(test::block6_hash)); } @@ -197,10 +207,11 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__genesis__no_previous) BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, config_.bitcoin, link, *header); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 0u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 10); + BOOST_REQUIRE_EQUAL(as_text(out.at("chainwork")), genesis_chain_work); BOOST_REQUIRE(!out.contains("previousblockhash")); BOOST_REQUIRE_EQUAL(as_text(out.at("nextblockhash")), encode_hash(test::block1_hash)); } @@ -212,14 +223,37 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__tip__no_next) BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, config_.bitcoin, link, *header); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 9u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 1); + BOOST_REQUIRE_EQUAL(as_text(out.at("chainwork")), tip_chain_work); BOOST_REQUIRE_EQUAL(as_text(out.at("previousblockhash")), encode_hash(test::block8_hash)); BOOST_REQUIRE(!out.contains("nextblockhash")); } +// to_chain_work + +BOOST_AUTO_TEST_CASE(bitcoind_json__to_chain_work__genesis__genesis_proof) +{ + const auto work = json::to_chain_work(query_, config_.bitcoin, test::block0_hash); + BOOST_REQUIRE_EQUAL(work, genesis_chain_work); +} + +BOOST_AUTO_TEST_CASE(bitcoind_json__to_chain_work__unknown_hash__empty) +{ + const auto work = json::to_chain_work(query_, config_.bitcoin, system::null_hash); + BOOST_REQUIRE(work.empty()); +} + +BOOST_AUTO_TEST_CASE(bitcoind_json__to_chain_work__repeated__cached_value_consistent) +{ + const auto first = json::to_chain_work(query_, config_.bitcoin, test::block9_hash); + const auto second = json::to_chain_work(query_, config_.bitcoin, test::block9_hash); + BOOST_REQUIRE_EQUAL(first, tip_chain_work); + BOOST_REQUIRE_EQUAL(first, second); +} + // inject_tx_context BOOST_AUTO_TEST_CASE(bitcoind_json__inject_tx_context__confirmed_coinbase__block_context)