From 420222b5b945d2ad7b45d4475f8cda99d44147db Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 13:27:46 -0400 Subject: [PATCH 01/21] Style. --- .../protocol_bitcoind_transaction.cpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 68906ba2..64fa4efa 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -153,8 +153,6 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, sequenced)); } - script script{}; - uint64_t satoshi{}; const auto outs = std::make_shared(); // Appends one address or data output from a name/value pair. @@ -175,19 +173,21 @@ code protocol_bitcoind_transaction::build_transaction(chain::transaction& out, return error::bitcoind::success; } + script script{}; + if (output_script(script, name, p2kh_, p2sh_, witness_)) + return error::bitcoind::invalid_address_or_key; + // bitcoind also accepts a quoted amount (slop; not special-cased). if (!std::holds_alternative(item.value())) return error::bitcoind::type_error; - if (output_script(script, name, p2kh_, p2sh_, witness_)) - return error::bitcoind::invalid_address_or_key; - - const auto btc = std::get(item.value()); - if (!to_integer(satoshi, btc * satoshi_per_bitcoin, true) || - satoshi > system_settings().max_money()) + uint64_t satoshis{}; + const auto bitcoins = std::get(item.value()); + if (!to_integer(satoshis, bitcoins * satoshi_per_bitcoin, true) || + satoshis > system_settings().max_money()) return error::bitcoind::type_error; - outs->push_back(to_shared(satoshi, std::move(script))); + outs->push_back(to_shared(satoshis, std::move(script))); return error::bitcoind::success; }; @@ -325,7 +325,7 @@ bool protocol_bitcoind_transaction::handle_send_raw_transaction(const code& ec, return true; } - const auto tx = to_shared(data, true); + const auto tx = emplace_shared(data, true); if (!tx->is_valid()) { send_error(error::bitcoind::deserialization_error); From 78e2d590d9e589b47213f302f8d1da3791608932 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 13:28:09 -0400 Subject: [PATCH 02/21] DRY, use fluent query calls. --- src/protocols/bitcoind/protocol_bitcoind_mining.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index eae93e00..5000e74c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -224,16 +224,15 @@ bool protocol_bitcoind_mining::handle_submit_block(const code& ec, } constexpr auto witness = true; - const auto block = to_shared(data, witness); + const auto block = emplace_shared(data, witness); if (!block->is_valid()) { send_error(error::bitcoind::deserialization_error); return true; } - // A known header without its block still organizes (the submitheader flow). - const auto link = archive().to_header(block->hash()); - if (!link.is_terminal() && archive().is_associated(link)) + const auto& query = archive(); + if (query.is_associated(query.to_header(block->hash()))) { send_result(std::string{ "duplicate" }, 32); return true; @@ -256,7 +255,7 @@ bool protocol_bitcoind_mining::handle_submit_header(const code& ec, return true; } - const auto header = to_shared(data); + const auto header = emplace_shared(data); if (!header->is_valid()) { send_error(error::bitcoind::deserialization_error); From 672624c24a3c2b93c30a19a7121021c601d9135e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:22:31 -0400 Subject: [PATCH 03/21] Distinguish reorg from fault in gettxout. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index c29cb011..a243e687 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -546,38 +546,26 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out(const code& ec, return true; } + // Return only unspent output. const auto& query = archive(); const auto output_link = query.to_output(hash, index); - - // bitcoind returns json null for a missing or confirmed-spent output; with - // mempool ignored this matches gettxout's include_mempool=false semantics - // (is_spent would also count unconfirmed/conflicting/invalid-block spenders). if (output_link.is_terminal() || query.is_confirmed_spent(output_link)) { send_result(null_t{}, 42); return true; } + const auto top = query.get_top_confirmed(); + const auto header_link = query.to_confirmed(top); const auto output = query.get_output(output_link); - if (!output) - { - send_result(null_t{}, 42); - return true; - } - - // Output's tx must exist. - const auto tx_link = query.to_tx(hash); - if (tx_link.is_terminal()) + const auto tx_link = query.to_output_tx(output_link); + if (!output || tx_link.is_terminal() || header_link.is_terminal()) { send_error(error::bitcoind::internal_error); return true; } - // Derive header from top for consistent depth result (also cheaper). - const auto top = query.get_top_confirmed(); - const auto header_link = query.to_confirmed(top); - - // An archived but unconfirmed output is not an unspent coin. + // Return only confirmed output. size_t height{}; if (!query.get_tx_height(height, tx_link)) { From 56bce7100034dc6cd055cc1a6decfa8e870efae7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:24:16 -0400 Subject: [PATCH 04/21] Report getblockstats store fault as internal error. --- src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index a243e687..4caf2e69 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -399,7 +399,7 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, const auto block = query.get_block(link, true); if (!block || !query.populate_without_metadata(*block)) { - send_error(error::bitcoind::misc_error); + send_error(error::bitcoind::internal_error); return true; } From b6bdab0af64c7c2d984c7b7bc612b83631e81c90 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:26:02 -0400 Subject: [PATCH 05/21] Split absence from fault in getdeploymentinfo. --- src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 4caf2e69..714a16d4 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -1339,13 +1339,18 @@ bool protocol_bitcoind_blockchain::handle_get_deployment_info(const code& ec, } link = query.to_header(hash); + if (link.is_terminal()) + { + send_error(error::bitcoind::invalid_address_or_key, blockhash, + blockhash.size()); + return true; + } } size_t height{}; if (!query.get_height(height, link)) { - send_error(error::bitcoind::invalid_address_or_key, blockhash, - blockhash.size()); + send_error(error::bitcoind::internal_error); return true; } From 45d6607227e5ec16af4c68ed9b1246d7caeec3fb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:27:48 -0400 Subject: [PATCH 06/21] Pin ancestry in the getchaintxstats window. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 714a16d4..a5ae3918 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -511,14 +511,20 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, if (is_nonzero(window)) { - const auto first = floored_subtract(height, window); - const auto past = query.to_confirmed(first); + // The pinned ancestry makes the window walk reorg-stable. + database::header_links branch{}; + if (!query.get_ancestry(branch, link, add1(window))) + { + send_error(error::bitcoind::internal_error); + return true; + } + const auto interval = floored_subtract(median_time_past(query, link), - median_time_past(query, past)); + median_time_past(query, branch.back())); size_t txs{}; - for (auto index = add1(first); index <= height; ++index) - txs += query.get_tx_count(query.to_confirmed(index)); + for (auto index = zero; index < window; ++index) + txs += query.get_tx_count(branch.at(index)); result.emplace("window_interval", interval); result.emplace("window_tx_count", txs); From c1a470a0ee7eddcf30fb3c53eee05610cfe9e478 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:29:34 -0400 Subject: [PATCH 07/21] Pin ancestry in scanblocks. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index a5ae3918..31fbd890 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -523,7 +523,7 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, median_time_past(query, branch.back())); size_t txs{}; - for (auto index = zero; index < window; ++index) + for (size_t index{}; index < window; ++index) txs += query.get_tx_count(branch.at(index)); result.emplace("window_interval", interval); @@ -1666,13 +1666,20 @@ bool protocol_bitcoind_blockchain::handle_scan_blocks(const code& ec, } } + // The pinned ancestry makes the range walk reorg-stable. + database::header_links branch{}; + if (!query.get_ancestry(branch, query.to_confirmed(to), add1(to - from))) + { + send_error(error::bitcoind::internal_error); + return true; + } + array_t relevant{}; - for (auto height = from; height <= to; ++height) + for (auto it = branch.rbegin(); it != branch.rend(); ++it) { - const auto link = query.to_confirmed(height); - const auto hash = query.get_header_key(link); + const auto hash = query.get_header_key(*it); neutrino::block_filter filter{ hash, {} }; - if (!query.get_filter_body(filter.filter, link)) + if (!query.get_filter_body(filter.filter, *it)) { send_error(error::bitcoind::internal_error); return true; From 72613fd521e7b0aeb8779b1027b6d8513de8b687 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:31:35 -0400 Subject: [PATCH 08/21] Pin ancestry in getnetworkhashps. --- .../bitcoind/protocol_bitcoind_mining.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index 5000e74c..c9b66e4a 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -110,13 +110,20 @@ bool protocol_bitcoind_mining::handle_get_network_hash_ps(const code& ec, return true; } + // The pinned ancestry makes the window walk reorg-stable. + database::header_links branch{}; + if (!query.get_ancestry(branch, query.to_confirmed(target), add1(window))) + { + send_error(error::bitcoind::internal_error); + return true; + } + // The window timespan is bounded by its observed timestamps. - const auto first = target - window; auto minimum = max_uint32; auto maximum = min_uint32; - for (auto index = first; index <= target; ++index) + for (const auto& ancestor: branch) { - const auto header = query.get_header(query.to_confirmed(index)); + const auto header = query.get_header(ancestor); if (!header) { send_error(error::bitcoind::internal_error); @@ -135,8 +142,8 @@ bool protocol_bitcoind_mining::handle_get_network_hash_ps(const code& ec, uint256_t start_work{}; uint256_t end_work{}; - if (!query.get_branch_work(start_work, query.to_confirmed(first)) || - !query.get_branch_work(end_work, query.to_confirmed(target))) + if (!query.get_branch_work(start_work, branch.back()) || + !query.get_branch_work(end_work, branch.front())) { send_error(error::bitcoind::internal_error); return true; From 65bc8fa10df22d6decd386239accd08a98dacc0e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:39:09 -0400 Subject: [PATCH 09/21] Populate prevout metadata instead of re-searching. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 7 +++---- .../bitcoind/protocol_bitcoind_transaction.cpp | 2 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 12 ++++++++++++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 31fbd890..a0bbe31c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -191,7 +191,7 @@ bool protocol_bitcoind_blockchain::handle_get_block(const code& ec, inject_block_context(model.as_object(), query, link, block->header()); if (level >= block_verbosity::verbose && - query.populate_without_metadata(*block)) + query.populate_with_metadata(*block)) { auto entry = model.as_object().at("tx").as_array().begin(); std::ranges::for_each(*block->transactions_ptr(), diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 46b83cf2..b1ab2ad1 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -139,7 +139,7 @@ void protocol_bitcoind::inject_tx_context(boost::json::object& out, } } -// The tx must be populated (populate_without_metadata). +// The tx must be populated (populate_with_metadata). void protocol_bitcoind::inject_tx_prevouts(boost::json::object& out, const node::query& query, const chain::transaction& tx) NOEXCEPT { @@ -147,13 +147,12 @@ void protocol_bitcoind::inject_tx_prevouts(boost::json::object& out, auto entry = out.at("vin").as_array().begin(); std::ranges::for_each(*tx.inputs_ptr(), [&](const auto& in) NOEXCEPT { - const auto spent = query.to_tx(in->point().hash()); - if (query.get_tx_height(height, spent)) + if (query.get_tx_height(height, in->metadata.parent_tx)) { auto put = value_from(bitcoind(*in->prevout)).as_object(); boost::json::object prevout { - { "generated", query.is_coinbase(spent) }, + { "generated", in->metadata.coinbase }, { "height", height }, { "value", put.at("value") }, { "scriptPubKey", std::move(put.at("scriptPubKey")) } diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp index 64fa4efa..a06b92eb 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp @@ -300,7 +300,7 @@ bool protocol_bitcoind_transaction::handle_get_raw_transaction(const code& ec, auto model = value_from(bitcoind(*tx)); inject_tx_context(model.as_object(), query, link); if (level == verbosity::json_verbose && !tx->is_coinbase() && - query.populate_without_metadata(*tx)) + query.populate_with_metadata(*tx)) { inject_tx_prevouts(model.as_object(), query, *tx); model.as_object()["fee"] = diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 716df47d..12d10ae1 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -264,6 +264,18 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__spend_verbosity2__fee) BOOST_REQUIRE(!tx.at(0).at("vin").at(0).as_object().contains("prevout")); } +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getrawtransaction__spend_verbose__prevout) +{ + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, false, false)); + BOOST_REQUIRE(query_.set(test::mock_block11, database::context{ 0, 11, 0 }, false, false)); + + const auto txid = test::mock_block11.transactions_ptr()->front()->hash(false); + const auto response = rpc("getrawtransaction", hash_param(txid, "2")); + const auto& vin = response.at("result").at("vin").at(0); + REQUIRE_NO_THROW_TRUE(vin.at("prevout").at("generated").as_bool()); + BOOST_REQUIRE_EQUAL(vin.at("prevout").at("height").as_int64(), 3); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblock__verbosity4__clamped_tx_objects) { const auto response = rpc("getblock", hash_param(test::block9_hash, "4")); From bbd5b2b4a60f15153b788d2702772bd8aad736de Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:40:59 -0400 Subject: [PATCH 10/21] Read timestamps for median time past. --- .../bitcoind/protocol_bitcoind_json.cpp | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index b1ab2ad1..2caef598 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -39,22 +40,19 @@ double protocol_bitcoind::progress(size_t blocks, size_t headers) NOEXCEPT uint32_t protocol_bitcoind::median_time_past(const node::query& query, const database::header_link& link) NOEXCEPT { - std::vector times{}; - times.reserve(chain::median_time_past_interval); - - for (auto walk = link; !walk.is_terminal() && - times.size() < chain::median_time_past_interval; + size_t count{}; + std_array times{}; + for (auto walk = link; !walk.is_terminal() && count < times.size(); walk = query.to_parent(walk)) - { - const auto header = query.get_header(walk); - if (!header) + if (!query.get_timestamp(times.at(count++), walk)) return 0_u32; - times.push_back(header->timestamp()); - } + if (is_zero(count)) + return 0_u32; - std::sort(times.begin(), times.end()); - return times.empty() ? 0_u32 : times.at(to_half(times.size())); + const auto median = std::next(times.begin(), to_half(count)); + std::nth_element(times.begin(), median, std::next(times.begin(), count)); + return *median; } // A getchainstates entry for candidate or confirmed at the link (top). From 51ef7c1fe70531d8b61d939942973a316a4a6d34 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 14:43:03 -0400 Subject: [PATCH 11/21] Gate active chain on is_confirmed_block. --- src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp | 8 ++++---- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index a0bbe31c..385c0d1c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -389,7 +389,7 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, } size_t height{}; - if (!query.get_height(height, link) || query.to_confirmed(height) != link) + if (!query.get_height(height, link) || !query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key); return true; @@ -468,7 +468,7 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, } size_t height{}; - if (!query.get_height(height, link) || query.to_confirmed(height) != link) + if (!query.get_height(height, link) || !query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key, blockhash, blockhash.size()); @@ -679,7 +679,7 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out_set_info(const code& ec, } if (!query.get_height(height, link) || - query.to_confirmed(height) != link) + !query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key); return true; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 14fbf362..1ff4f68b 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -329,7 +329,7 @@ bool protocol_bitcoind_rest::handle_get_block_headers(const code& ec, const auto link = query.to_header(*hash); size_t height{}; if (!query.get_height(height, link) || - (query.to_confirmed(height) != link)) + !query.is_confirmed_block(link)) { send_not_found(); return true; From a66bccdf2b6adc8787dee4b420badce0e2f1f0a6 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 15:02:14 -0400 Subject: [PATCH 12/21] Read median time past from stored child context. --- .../server/protocols/protocol_bitcoind.hpp | 7 +++- .../bitcoind/protocol_bitcoind_blockchain.cpp | 20 +++++---- .../bitcoind/protocol_bitcoind_json.cpp | 41 +++++++++++-------- .../bitcoind/protocol_bitcoind_rest.cpp | 2 +- src/protocols/btcd/protocol_btcd_utility.cpp | 4 +- test/mocks/blocks.cpp | 18 ++++---- test/protocols/bitcoind/bitcoind_json.cpp | 12 ++++-- 7 files changed, 60 insertions(+), 44 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp index 10534356..d5c638ce 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp @@ -88,12 +88,14 @@ class BCS_API protocol_bitcoind /// Model composition. static double progress(size_t blocks, size_t headers) NOEXCEPT; static uint32_t median_time_past(const node::query& query, + const system::settings& settings, const database::header_link& link) NOEXCEPT; static network::rpc::object_t chain_states_entry(const node::query& query, const database::header_link& link, double progress, bool validated) 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; @@ -107,7 +109,8 @@ class BCS_API protocol_bitcoind /// The getblockchaininfo result, bitcoind field set (btcd augments it). /// False if the store is inconsistent, the caller sends the error. static bool chain_info(network::rpc::object_t& out, - const node::query& query, bool pruned, bool current) NOEXCEPT; + const node::query& query, const system::settings& settings, + bool pruned, bool current) NOEXCEPT; /// The createmultisig result, empty if a key is invalid or the p2sh /// embedded script exceeds one push element. diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 385c0d1c..c550765b 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -188,7 +188,9 @@ bool protocol_bitcoind_blockchain::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()); + const auto& settings = system_settings(); + const auto& header = block->header(); + inject_block_context(model.as_object(), query, settings, link, header); if (level >= block_verbosity::verbose && query.populate_with_metadata(*block)) @@ -221,8 +223,8 @@ bool protocol_bitcoind_blockchain::handle_get_block_chain_info(const code& ec, return false; object_t out{}; - if (!chain_info(out, archive(), node_settings().limited_blocks, - is_current_chain(true))) + if (!chain_info(out, archive(), system_settings(), + node_settings().limited_blocks, is_current_chain(true))) { send_error(error::bitcoind::internal_error); return true; @@ -346,7 +348,7 @@ bool protocol_bitcoind_blockchain::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; } @@ -408,8 +410,8 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, settings.subsidy_interval_blocks, settings.initial_subsidy(), settings.forks.bip42); - auto result = block_stats(*block, height, median_time_past(query, link), - subsidy); + const auto mtp = median_time_past(query, settings, link); + auto result = block_stats(*block, height, mtp, subsidy); // An empty selection returns all statistics, otherwise the named subset. if (stats.empty()) @@ -519,8 +521,10 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, return true; } - const auto interval = floored_subtract(median_time_past(query, link), - median_time_past(query, branch.back())); + const auto& settings = system_settings(); + const auto start = median_time_past(query, settings, branch.back()); + const auto end = median_time_past(query, settings, link); + const auto interval = floored_subtract(end, start); size_t txs{}; for (size_t index{}; index < window; ++index) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 2caef598..5df3dd4e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -36,23 +35,28 @@ double protocol_bitcoind::progress(size_t blocks, size_t headers) NOEXCEPT std::min(1.0, to_floating(blocks) / headers); } -// bitcoind's mtp window includes the block's own timestamp (reproduced). +// bitcoind's mtp window includes the block: the child's stored context mtp. uint32_t protocol_bitcoind::median_time_past(const node::query& query, + const system::settings& settings, const database::header_link& link) NOEXCEPT { - size_t count{}; - std_array times{}; - for (auto walk = link; !walk.is_terminal() && count < times.size(); - walk = query.to_parent(walk)) - if (!query.get_timestamp(times.at(count++), walk)) - return 0_u32; - - if (is_zero(count)) + size_t height{}; + if (!query.get_height(height, link)) return 0_u32; - const auto median = std::next(times.begin(), to_half(count)); - std::nth_element(times.begin(), median, std::next(times.begin(), count)); - return *median; + database::context ctx{}; + const auto child = query.to_confirmed(add1(height)); + if (!child.is_terminal() && (query.to_parent(child) == link) && + query.get_context(ctx, child)) + return ctx.mtp; + + // The top block has no child, its promoted chain state carries the value. + const auto key = query.get_header_key(link); + const auto state = query.get_chain_state(settings, key); + if (!state) + return 0_u32; + + return chain::chain_state{ *state, settings }.context().median_time_past; } // A getchainstates entry for candidate or confirmed at the link (top). @@ -85,8 +89,8 @@ network::rpc::object_t protocol_bitcoind::chain_states_entry( } void protocol_bitcoind::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)) @@ -99,7 +103,7 @@ void protocol_bitcoind::inject_block_context(boost::json::object& out, // bitcoind reports -1 confirmations for a block not on the active chain. out["confirmations"] = confirmed ? to_signed(add1(floored_subtract(top, height))) : -1; - out["mediantime"] = median_time_past(query, link); + out["mediantime"] = median_time_past(query, settings, link); // Cumulative work to this block, big-endian per bitcoind chainwork. uint256_t work{}; @@ -207,7 +211,8 @@ std::string protocol_bitcoind::chain_name(const node::query& query) NOEXCEPT // Shared by the bitcoind blockchain subgroup and the btcd endpoint, which // augments the result with bip9_softforks (required by lnd). bool protocol_bitcoind::chain_info(network::rpc::object_t& out, - const node::query& query, bool pruned, bool current) NOEXCEPT + const node::query& query, const system::settings& settings, + bool pruned, bool current) NOEXCEPT { const auto blocks = query.get_top_confirmed(); const auto headers = query.get_top_candidate(); @@ -230,7 +235,7 @@ bool protocol_bitcoind::chain_info(network::rpc::object_t& out, { "target", encode_hash(from_uintx(chain::compact::expand(bits))) }, { "difficulty", header->difficulty() }, { "time", header->timestamp() }, - { "mediantime", median_time_past(query, link) }, + { "mediantime", median_time_past(query, settings, link) }, { "verificationprogress", progress(blocks, headers) }, { "initialblockdownload", !current }, { "chainwork", encode_hash(from_uintx(work)) }, diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 1ff4f68b..12780a50 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -630,7 +630,7 @@ bool protocol_bitcoind_rest::handle_get_chain_information(const code& ec, { "bits", encode_base16(to_big_endian(header->bits())) }, { "difficulty", header->difficulty() }, { "time", header->timestamp() }, - { "mediantime", median_time_past(query, link) }, + { "mediantime", median_time_past(query, system_settings(), link) }, { "pruned", node_settings().limited_blocks } }, 256); return true; diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index 940c3f1c..cd8bd64b 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -65,8 +65,8 @@ bool protocol_btcd::handle_get_block_chain_info(const code& ec, return false; object_t out{}; - if (!chain_info(out, archive(), node_settings().limited_blocks, - is_current_chain(true))) + if (!chain_info(out, archive(), system_settings(), + node_settings().limited_blocks, is_current_chain(true))) { send_error(error::btcd::internal_error); return true; diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index 843261dc..d23f33ef 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -97,15 +97,15 @@ boost::json::value parse_json(std::string_view value) NOEXCEPT bool setup_ten_block_store(query_t& query) NOEXCEPT { return query.initialize(genesis) && - query.set(block1, database::context{ 0, 1, 0 }, false, false) && - query.set(block2, database::context{ 0, 2, 0 }, false, false) && - query.set(block3, database::context{ 0, 3, 0 }, false, false) && - query.set(block4, database::context{ 0, 4, 0 }, false, false) && - query.set(block5, database::context{ 0, 5, 0 }, false, false) && - query.set(block6, database::context{ 0, 6, 0 }, false, false) && - query.set(block7, database::context{ 0, 7, 0 }, false, false) && - query.set(block8, database::context{ 0, 8, 0 }, false, false) && - query.set(block9, database::context{ 0, 9, 0 }, false, false) && + query.set(block1, database::context{ 0, 1, 1231006505 }, false, false) && + query.set(block2, database::context{ 0, 2, 1231469665 }, false, false) && + query.set(block3, database::context{ 0, 3, 1231469665 }, false, false) && + query.set(block4, database::context{ 0, 4, 1231469744 }, false, false) && + query.set(block5, database::context{ 0, 5, 1231469744 }, false, false) && + query.set(block6, database::context{ 0, 6, 1231470173 }, false, false) && + query.set(block7, database::context{ 0, 7, 1231470173 }, false, false) && + query.set(block8, database::context{ 0, 8, 1231470988 }, false, false) && + query.set(block9, database::context{ 0, 9, 1231470988 }, false, false) && query.push_confirmed(query.to_header(block1_hash), true) && query.push_confirmed(query.to_header(block2_hash), true) && query.push_confirmed(query.to_header(block3_hash), true) && diff --git a/test/protocols/bitcoind/bitcoind_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp index 6cf852c8..ff0e9f5c 100644 --- a/test/protocols/bitcoind/bitcoind_json.cpp +++ b/test/protocols/bitcoind/bitcoind_json.cpp @@ -120,20 +120,22 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__chain_name__mainnet_genesis__main) // The median of mainnet block 0..5 timestamps (self-inclusive, as bitcoind). BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__self_inclusive_window) { + const system::settings settings{ chain::selection::mainnet }; const auto link = query_.to_header(test::block5_hash); - BOOST_REQUIRE_EQUAL(json::median_time_past(query_, link), 1231470173u); + BOOST_REQUIRE_EQUAL(json::median_time_past(query_, settings, link), 1231470173u); } // inject_block_context BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__middle__height_confirmations_siblings) { + const system::settings settings{ chain::selection::mainnet }; const auto link = query_.to_header(test::block5_hash); const auto header = query_.get_header(link); BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, settings, link, *header); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 5u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 5); @@ -144,12 +146,13 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__middle__height_confirm BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__genesis__no_previous) { + const system::settings settings{ chain::selection::mainnet }; const auto link = query_.to_header(test::block0_hash); const auto header = query_.get_header(link); BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, settings, link, *header); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 0u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 10); @@ -159,12 +162,13 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__genesis__no_previous) BOOST_AUTO_TEST_CASE(bitcoind_json__inject_block_context__tip__no_next) { + const system::settings settings{ chain::selection::mainnet }; const auto link = query_.to_header(test::block9_hash); const auto header = query_.get_header(link); BOOST_REQUIRE(header); boost::json::object out{}; - json::inject_block_context(out, query_, link, *header); + json::inject_block_context(out, query_, settings, link, *header); BOOST_REQUIRE_EQUAL(out.at("height").to_number(), 9u); BOOST_REQUIRE_EQUAL(out.at("confirmations").to_number(), 1); From ffa90d1803542e5b5881ba7eecbcdb23a8c9e9f1 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:06:32 -0400 Subject: [PATCH 13/21] Fix missing conditional in electrum::broadcast_tx. --- src/protocols/electrum/protocol_electrum_transactions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp index ab7c200e..aa60c3eb 100644 --- a/src/protocols/electrum/protocol_electrum_transactions.cpp +++ b/src/protocols/electrum/protocol_electrum_transactions.cpp @@ -399,7 +399,7 @@ code protocol_electrum::validate_tx( code protocol_electrum::broadcast_tx( const chain::transaction::cptr& tx) NOEXCEPT { - const auto ec = validate_tx(*tx); + if (const auto ec = validate_tx(*tx)) return ec; BROADCAST(peer::transaction, to_shared(tx)); From 04599e3441f9b8b23157eaac8275910b8bdc3eb5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:13:20 -0400 Subject: [PATCH 14/21] Call confirmed chain state explicitly. --- src/protocols/bitcoind/protocol_bitcoind.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 2 +- src/protocols/bitcoind/protocol_bitcoind_mining.cpp | 2 +- src/protocols/electrum/protocol_electrum_transactions.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind.cpp b/src/protocols/bitcoind/protocol_bitcoind.cpp index 02ea943a..194f25c3 100644 --- a/src/protocols/bitcoind/protocol_bitcoind.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind.cpp @@ -363,7 +363,7 @@ code protocol_bitcoind::validate_tx( const auto& settings = system_settings(); const auto link = query.to_confirmed(query.get_top_confirmed()); const auto key = query.get_header_key(link); - const auto state = query.get_chain_state(settings, key); + const auto state = query.get_confirmed_chain_state(settings, key); // The store always has chain state for the confirmed top. if (!state) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 5df3dd4e..6a85c13b 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -52,7 +52,7 @@ uint32_t protocol_bitcoind::median_time_past(const node::query& query, // The top block has no child, its promoted chain state carries the value. const auto key = query.get_header_key(link); - const auto state = query.get_chain_state(settings, key); + const auto state = query.get_confirmed_chain_state(settings, key); if (!state) return 0_u32; diff --git a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp index c9b66e4a..bd79bd03 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_mining.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp @@ -176,7 +176,7 @@ bool protocol_bitcoind_mining::handle_get_mining_info(const code& ec, // The pool state over the top block carries the next work required. const auto& bitcoin = system_settings(); const auto key = query.get_header_key(link); - const auto state = query.get_chain_state(bitcoin, key); + const auto state = query.get_confirmed_chain_state(bitcoin, key); if (!state) { send_error(error::bitcoind::internal_error); diff --git a/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp index aa60c3eb..a92e6f55 100644 --- a/src/protocols/electrum/protocol_electrum_transactions.cpp +++ b/src/protocols/electrum/protocol_electrum_transactions.cpp @@ -385,7 +385,7 @@ code protocol_electrum::validate_tx( const auto& settings = system_settings(); const auto link = query.to_confirmed(query.get_top_confirmed()); const auto key = query.get_header_key(link); - const auto state = query.get_chain_state(settings, key); + const auto state = query.get_confirmed_chain_state(settings, key); // The store always has chain state for the confirmed top. if (!state) From 03914f93b2f831d8e2f2cf2df5ef8fdd20bb36ee Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:13:32 -0400 Subject: [PATCH 15/21] Navigate to the confirmed child for median time past. --- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 6a85c13b..06f87ed7 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -40,14 +40,8 @@ uint32_t protocol_bitcoind::median_time_past(const node::query& query, const system::settings& settings, const database::header_link& link) NOEXCEPT { - size_t height{}; - if (!query.get_height(height, link)) - return 0_u32; - database::context ctx{}; - const auto child = query.to_confirmed(add1(height)); - if (!child.is_terminal() && (query.to_parent(child) == link) && - query.get_context(ctx, child)) + if (query.get_context(ctx, query.to_confirmed_child(link))) return ctx.mtp; // The top block has no child, its promoted chain state carries the value. From 034b1311917f9bd418a484eddb1dbb889883c73c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:13:32 -0400 Subject: [PATCH 16/21] Test promoted top median time past. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 12d10ae1..f68e1973 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -294,6 +294,13 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockheader__genesis__self_inclusive_media BOOST_REQUIRE_EQUAL(response.at("result").at("mediantime").as_int64(), 1231006505); } +// The top block has no stored child context, so its mtp is promoted. +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockheader__top__promoted_mediantime) +{ + const auto response = rpc("getblockheader", hash_param(test::block9_hash, "true")); + BOOST_REQUIRE_EQUAL(response.at("result").at("mediantime").as_int64(), 1231471428); +} + BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) { const auto response = rpc("getblockchaininfo"); From 3cfd6bd2d3e5b63b55a60e859bec9a73b084285f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:30:11 -0400 Subject: [PATCH 17/21] Rename median_time_past to median_time. --- include/bitcoin/server/parsers/bitcoind_block_stats.hpp | 2 +- include/bitcoin/server/protocols/protocol_bitcoind.hpp | 2 +- src/parsers/bitcoind_block_stats.cpp | 4 ++-- src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp | 8 ++++---- src/protocols/bitcoind/protocol_bitcoind_json.cpp | 8 ++++---- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 2 +- test/protocols/bitcoind/bitcoind_json.cpp | 8 ++++---- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/include/bitcoin/server/parsers/bitcoind_block_stats.hpp b/include/bitcoin/server/parsers/bitcoind_block_stats.hpp index 4cd542c1..14172c91 100644 --- a/include/bitcoin/server/parsers/bitcoind_block_stats.hpp +++ b/include/bitcoin/server/parsers/bitcoind_block_stats.hpp @@ -26,7 +26,7 @@ namespace server { /// All bitcoind getblockstats statistics of a prevout-populated block. BCS_API network::rpc::object_t block_stats(const system::chain::block& block, - size_t height, uint32_t median_time_past, uint64_t subsidy) NOEXCEPT; + size_t height, uint32_t median_time, uint64_t subsidy) NOEXCEPT; } // namespace server } // namespace libbitcoin diff --git a/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp index d5c638ce..1f5b88cf 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp @@ -87,7 +87,7 @@ class BCS_API protocol_bitcoind /// Model composition. static double progress(size_t blocks, size_t headers) NOEXCEPT; - static uint32_t median_time_past(const node::query& query, + static uint32_t median_time(const node::query& query, const system::settings& settings, const database::header_link& link) NOEXCEPT; static network::rpc::object_t chain_states_entry(const node::query& query, diff --git a/src/parsers/bitcoind_block_stats.cpp b/src/parsers/bitcoind_block_stats.cpp index cc09df34..b4eb79d2 100644 --- a/src/parsers/bitcoind_block_stats.cpp +++ b/src/parsers/bitcoind_block_stats.cpp @@ -75,7 +75,7 @@ static std::array fee_rate_percentiles( } object_t block_stats(const chain::block& block, size_t height, - uint32_t median_time_past, uint64_t subsidy) NOEXCEPT + uint32_t median_time, uint64_t subsidy) NOEXCEPT { const auto& txs = *block.transactions_ptr(); @@ -165,7 +165,7 @@ object_t block_stats(const chain::block& block, size_t height, { "maxfeerate", max_fee_rate }, { "maxtxsize", max_tx_size }, { "medianfee", truncated_median(fees) }, - { "mediantime", median_time_past }, + { "mediantime", median_time }, { "mediantxsize", truncated_median(sizes) }, { "minfee", min_fee == max_uint64 ? zero : min_fee }, { "minfeerate", min_fee_rate == max_uint64 ? zero : min_fee_rate }, diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index c550765b..05a1f871 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -410,8 +410,8 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, settings.subsidy_interval_blocks, settings.initial_subsidy(), settings.forks.bip42); - const auto mtp = median_time_past(query, settings, link); - auto result = block_stats(*block, height, mtp, subsidy); + const auto median = median_time(query, settings, link); + auto result = block_stats(*block, height, median, subsidy); // An empty selection returns all statistics, otherwise the named subset. if (stats.empty()) @@ -522,8 +522,8 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, } const auto& settings = system_settings(); - const auto start = median_time_past(query, settings, branch.back()); - const auto end = median_time_past(query, settings, link); + const auto start = median_time(query, settings, branch.back()); + const auto end = median_time(query, settings, link); const auto interval = floored_subtract(end, start); size_t txs{}; diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp index 06f87ed7..0ff7b4ee 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_json.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_json.cpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -36,7 +36,7 @@ double protocol_bitcoind::progress(size_t blocks, size_t headers) NOEXCEPT } // bitcoind's mtp window includes the block: the child's stored context mtp. -uint32_t protocol_bitcoind::median_time_past(const node::query& query, +uint32_t protocol_bitcoind::median_time(const node::query& query, const system::settings& settings, const database::header_link& link) NOEXCEPT { @@ -97,7 +97,7 @@ void protocol_bitcoind::inject_block_context(boost::json::object& out, // bitcoind reports -1 confirmations for a block not on the active chain. out["confirmations"] = confirmed ? to_signed(add1(floored_subtract(top, height))) : -1; - out["mediantime"] = median_time_past(query, settings, link); + out["mediantime"] = median_time(query, settings, link); // Cumulative work to this block, big-endian per bitcoind chainwork. uint256_t work{}; @@ -229,7 +229,7 @@ bool protocol_bitcoind::chain_info(network::rpc::object_t& out, { "target", encode_hash(from_uintx(chain::compact::expand(bits))) }, { "difficulty", header->difficulty() }, { "time", header->timestamp() }, - { "mediantime", median_time_past(query, settings, link) }, + { "mediantime", median_time(query, settings, link) }, { "verificationprogress", progress(blocks, headers) }, { "initialblockdownload", !current }, { "chainwork", encode_hash(from_uintx(work)) }, diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 12780a50..5bb238bf 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -630,7 +630,7 @@ bool protocol_bitcoind_rest::handle_get_chain_information(const code& ec, { "bits", encode_base16(to_big_endian(header->bits())) }, { "difficulty", header->difficulty() }, { "time", header->timestamp() }, - { "mediantime", median_time_past(query, system_settings(), link) }, + { "mediantime", median_time(query, system_settings(), link) }, { "pruned", node_settings().limited_blocks } }, 256); return true; diff --git a/test/protocols/bitcoind/bitcoind_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp index ff0e9f5c..5528d282 100644 --- a/test/protocols/bitcoind/bitcoind_json.cpp +++ b/test/protocols/bitcoind/bitcoind_json.cpp @@ -31,7 +31,7 @@ static std::string as_text(const boost::json::value& value) NOEXCEPT struct json : server::protocol_bitcoind { - using protocol_bitcoind::median_time_past; + using protocol_bitcoind::median_time; using protocol_bitcoind::inject_block_context; using protocol_bitcoind::inject_tx_context; using protocol_bitcoind::header_to_bitcoind; @@ -115,14 +115,14 @@ BOOST_AUTO_TEST_CASE(bitcoind_json__chain_name__mainnet_genesis__main) BOOST_REQUIRE_EQUAL(json::chain_name(query_), "main"); } -// median_time_past +// median_time // The median of mainnet block 0..5 timestamps (self-inclusive, as bitcoind). -BOOST_AUTO_TEST_CASE(bitcoind_json__median_time_past__self_inclusive_window) +BOOST_AUTO_TEST_CASE(bitcoind_json__median_time__self_inclusive_window) { const system::settings settings{ chain::selection::mainnet }; const auto link = query_.to_header(test::block5_hash); - BOOST_REQUIRE_EQUAL(json::median_time_past(query_, settings, link), 1231470173u); + BOOST_REQUIRE_EQUAL(json::median_time(query_, settings, link), 1231470173u); } // inject_block_context From 11e26950558be1cc699ed2783a0f99f1a17f75cb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 16:45:04 -0400 Subject: [PATCH 18/21] Navigate block spent tx outputs. --- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 5bb238bf..80683610 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -466,10 +466,9 @@ bool protocol_bitcoind_rest::handle_get_block_spent_tx_outputs(const code& ec, return true; } - constexpr auto witness = true; const auto& query = archive(); - const auto block = query.get_block(query.to_header(*hash), witness); - if (!block) + const auto link = query.to_header(*hash); + if (!query.is_associated(link)) { send_not_found(); return true; @@ -477,11 +476,9 @@ bool protocol_bitcoind_rest::handle_get_block_spent_tx_outputs(const code& ec, // Resolve every prevout spent by the block's non-coinbase transactions. chain::output_cptrs spent{}; - const auto& txs = *block->transactions_ptr(); - for (auto tx = one; 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()))) - spent.push_back(out); + for (const auto& out: query.to_block_prevouts(link)) + if (const auto output = query.get_output(out)) + spent.push_back(output); size_t size{}; for (const auto& output: spent) From 5e82d2f776daa1d70a612010a45acc169e69208e Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 17:16:58 -0400 Subject: [PATCH 19/21] Support the query form of rest headers. --- builds/gnu/Makefile.am | 3 + .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 3 + .../libbitcoin-server.vcxproj | 2 + .../libbitcoin-server.vcxproj.filters | 6 ++ .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 3 + .../libbitcoin-server.vcxproj | 2 + .../libbitcoin-server.vcxproj.filters | 6 ++ include/bitcoin/server.hpp | 1 + .../bitcoin/server/parsers/bitcoind_query.hpp | 33 ++++++++ include/bitcoin/server/parsers/parsers.hpp | 1 + src/parsers/bitcoind_query.cpp | 75 +++++++++++++++++++ src/parsers/bitcoind_target.cpp | 11 ++- .../bitcoind/protocol_bitcoind_rest.cpp | 10 ++- test/parsers/bitcoind_query.cpp | 64 ++++++++++++++++ test/parsers/bitcoind_target.cpp | 14 +++- test/protocols/bitcoind/bitcoind_rest.cpp | 16 ++++ 18 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 include/bitcoin/server/parsers/bitcoind_query.hpp create mode 100644 src/parsers/bitcoind_query.cpp create mode 100644 test/parsers/bitcoind_query.cpp diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index 716b796f..fc2da4c3 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -60,6 +60,7 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/parsers/bitcoind_block_stats.cpp \ ${srcdir}/../../src/parsers/bitcoind_descriptor.cpp \ ${srcdir}/../../src/parsers/bitcoind_merkle.cpp \ + ${srcdir}/../../src/parsers/bitcoind_query.cpp \ ${srcdir}/../../src/parsers/bitcoind_script.cpp \ ${srcdir}/../../src/parsers/bitcoind_target.cpp \ ${srcdir}/../../src/parsers/btcd_filter.cpp \ @@ -182,6 +183,7 @@ include_bitcoin_server_parsers_HEADERS = \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_block_stats.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_descriptor.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_merkle.hpp \ + ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_query.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_script.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/btcd_filter.hpp \ @@ -266,6 +268,7 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/parsers/bitcoind_block_stats.cpp \ ${srcdir}/../../test/parsers/bitcoind_descriptor.cpp \ ${srcdir}/../../test/parsers/bitcoind_merkle.cpp \ + ${srcdir}/../../test/parsers/bitcoind_query.cpp \ ${srcdir}/../../test/parsers/bitcoind_target.cpp \ ${srcdir}/../../test/parsers/electrum_version.cpp \ ${srcdir}/../../test/parsers/native_query.cpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj index 827b5562..882f1466 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -129,6 +129,7 @@ + $(IntDir)test_parsers_electrum_version.obj diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index ca0576ff..2a4c0059 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -72,6 +72,9 @@ src\parsers + + src\parsers + src\parsers diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index 0bb09d0d..b650cda3 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -132,6 +132,7 @@ + @@ -220,6 +221,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 6c9dae31..5f052b7b 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -108,6 +108,9 @@ src\parsers + + src\parsers + src\parsers @@ -368,6 +371,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj index 2083eddb..27b1eea8 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -129,6 +129,7 @@ + $(IntDir)test_parsers_electrum_version.obj diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index ca0576ff..2a4c0059 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -72,6 +72,9 @@ src\parsers + + src\parsers + src\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index e90a146d..34d36d14 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -132,6 +132,7 @@ + @@ -220,6 +221,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 6c9dae31..5f052b7b 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -108,6 +108,9 @@ src\parsers + + src\parsers + src\parsers @@ -368,6 +371,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 187668f3..80f12783 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/server/parsers/bitcoind_query.hpp b/include/bitcoin/server/parsers/bitcoind_query.hpp new file mode 100644 index 00000000..2e6b8352 --- /dev/null +++ b/include/bitcoin/server/parsers/bitcoind_query.hpp @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_PARSERS_BITCOIND_QUERY_HPP +#define LIBBITCOIN_SERVER_PARSERS_BITCOIND_QUERY_HPP + +#include + +namespace libbitcoin { +namespace server { + +BCS_API bool bitcoind_query(network::rpc::request_t& out, + const std::string& target) NOEXCEPT; + +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/parsers/parsers.hpp b/include/bitcoin/server/parsers/parsers.hpp index 917c93ac..8c45a8b7 100644 --- a/include/bitcoin/server/parsers/parsers.hpp +++ b/include/bitcoin/server/parsers/parsers.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include diff --git a/src/parsers/bitcoind_query.cpp b/src/parsers/bitcoind_query.cpp new file mode 100644 index 00000000..d4573dc7 --- /dev/null +++ b/src/parsers/bitcoind_query.cpp @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +using namespace system; +using namespace network; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +template +static bool to_number(Number& out, const std::string_view& token) NOEXCEPT +{ + if (token.empty() || (token.size() > one && token.starts_with('0'))) + return false; + + const auto end = std::next(token.data(), token.size()); + const auto result = std::from_chars(token.data(), end, out); + return result.ec == std::errc{} && result.ptr == end; +} + +bool bitcoind_query(rpc::request_t& out, const std::string& target) NOEXCEPT +{ + wallet::uri uri{}; + if (!uri.decode(target)) + return false; + + // Caller must have provided a request.params object. + if (!out.params.has_value() || + !std::holds_alternative(out.params.value())) + return false; + + auto query = uri.decode_query(); + auto& params = std::get(out.params.value()); + + // Count is optional (defaulted by the target parser where applicable). + if (const auto count = query.find("count"); count != query.end()) + { + uint32_t value{}; + if (!to_number(value, count->second)) + return false; + + params["count"] = value; + } + + return true; +} + +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/parsers/bitcoind_target.cpp b/src/parsers/bitcoind_target.cpp index 1a861617..db2c2003 100644 --- a/src/parsers/bitcoind_target.cpp +++ b/src/parsers/bitcoind_target.cpp @@ -203,19 +203,18 @@ code bitcoind_target(request_t& out, const std::string_view& path) NOEXCEPT return error::success; } - // /rest/headers//. + // /rest/headers/.?count= (count defaults to 5) and the + // legacy /rest/headers//. form. if (target == "headers") { if (segment == segments.size()) return error::missing_target; - uint32_t count{}; - if (!to_number(count, segments[segment++])) + uint32_t count{ 5 }; + if ((segments.size() - segment > one) && + !to_number(count, segments[segment++])) return error::invalid_number; - if (segment == segments.size()) - return error::missing_hash; - std::string name{}; uint8_t media{}; if (!split_leaf(name, media, segments[segment++])) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 80683610..992b9d2e 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -108,7 +108,8 @@ void protocol_bitcoind_rest::handle_receive_get(const code& ec, // Parse the REST url into a json-rpc model and dispatch to a handler. // Malformed parameters are bad requests, unknown targets are not found. request_t model{}; - if (const auto fault = bitcoind_target(model, get->target())) + const auto target = get->target(); + if (const auto fault = bitcoind_target(model, target)) { if ((fault == error::invalid_hash) || (fault == error::invalid_number) || @@ -122,6 +123,13 @@ void protocol_bitcoind_rest::handle_receive_get(const code& ec, return; } + // Overlay query string parameters onto the parsed model. + if (!bitcoind_query(model, target)) + { + send_bad_request(*get); + return; + } + if (rest_dispatcher_.notify(model)) send_not_found(); } diff --git a/test/parsers/bitcoind_query.cpp b/test/parsers/bitcoind_query.cpp new file mode 100644 index 00000000..09d31deb --- /dev/null +++ b/test/parsers/bitcoind_query.cpp @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +BOOST_AUTO_TEST_SUITE(bitcoind_query_tests) + +using namespace system; +using namespace network::rpc; +using object_t = network::rpc::object_t; + +constexpr auto expected_hash = base16_hash( + "0000000000000000000000000000000000000000000000000000000000000042"); +static const std::string test_hash = encode_hash(expected_hash); + +static const object_t& params_of(const request_t& request) NOEXCEPT +{ + BOOST_REQUIRE(request.params.has_value()); + BOOST_REQUIRE(std::holds_alternative(request.params.value())); + return std::get(request.params.value()); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__count__overlays_target_default) +{ + request_t out{}; + const auto target = "/rest/headers/" + test_hash + ".json?count=7"; + BOOST_REQUIRE(!bitcoind_target(out, target)); + BOOST_REQUIRE(bitcoind_query(out, target)); + BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("count").value()), 7u); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__no_query__target_default) +{ + request_t out{}; + const auto target = "/rest/headers/" + test_hash + ".json"; + BOOST_REQUIRE(!bitcoind_target(out, target)); + BOOST_REQUIRE(bitcoind_query(out, target)); + BOOST_REQUIRE_EQUAL(std::get(params_of(out).at("count").value()), 5u); +} + +BOOST_AUTO_TEST_CASE(parsers__bitcoind_query__malformed_count__false) +{ + request_t out{}; + const auto target = "/rest/headers/" + test_hash + ".json?count=abc"; + BOOST_REQUIRE(!bitcoind_target(out, target)); + BOOST_REQUIRE(!bitcoind_query(out, target)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/parsers/bitcoind_target.cpp b/test/parsers/bitcoind_target.cpp index 775c56e6..4a311882 100644 --- a/test/parsers/bitcoind_target.cpp +++ b/test/parsers/bitcoind_target.cpp @@ -80,7 +80,7 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__error_paths__expected) { "/rest/blockhashbyheight/01.json", server::error::invalid_number }, { "/rest/headers", server::error::missing_target }, { "/rest/headers/abc/" + test_hash + ".json", server::error::invalid_number }, - { "/rest/headers/3", server::error::missing_hash }, + { "/rest/headers/3", server::error::invalid_target }, { "/rest/headers/3/nothex.json", server::error::invalid_hash }, { "/rest/blockfilter", server::error::missing_target }, { "/rest/blockfilter/extended/" + test_hash + ".json", server::error::invalid_target }, @@ -222,6 +222,18 @@ BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__headers__block_headers) BOOST_REQUIRE_EQUAL(media_of(object), to_value(media_type::application_json)); } +BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__headers_no_query__default_count) +{ + request_t out{}; + const auto path = "/rest/headers/" + test_hash + ".bin"; + BOOST_REQUIRE(!bitcoind_target(out, path)); + BOOST_REQUIRE_EQUAL(out.method, "block_headers"); + + const auto& object = params_of(out); + BOOST_REQUIRE_EQUAL(std::get(object.at("count").value()), 5u); + BOOST_REQUIRE_EQUAL(*hash_of(object), expected_hash); +} + // blockfilter BOOST_AUTO_TEST_CASE(parsers__bitcoind_target__blockfilter_basic__block_filter) diff --git a/test/protocols/bitcoind/bitcoind_rest.cpp b/test/protocols/bitcoind/bitcoind_rest.cpp index 5dc2af90..2ff058c2 100644 --- a/test/protocols/bitcoind/bitcoind_rest.cpp +++ b/test/protocols/bitcoind/bitcoind_rest.cpp @@ -136,6 +136,22 @@ BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_json__count_three_from_block5) BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("hash")), block5); } +BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_json__query_count_two_from_block5) +{ + const auto result = rest_json("/rest/headers/" + block5 + ".json?count=2"); + BOOST_REQUIRE(result.is_array()); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 2u); + BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("hash")), block5); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_json__no_query__default_count_five) +{ + const auto result = rest_json("/rest/headers/" + block5 + ".json"); + BOOST_REQUIRE(result.is_array()); + BOOST_REQUIRE_EQUAL(result.as_array().size(), 5u); + BOOST_REQUIRE_EQUAL(as_text(result.at(0).at("hash")), block5); +} + BOOST_AUTO_TEST_CASE(bitcoind_rest__headers_hex__one_header__eighty_bytes) { const auto hex = rest_text("/rest/headers/1/" + block9 + ".hex"); From e10f2dd840d7d3d6b1f5b0222bfc1b8814f1789d Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 17:16:58 -0400 Subject: [PATCH 20/21] Split absence from fault in rest headers. --- src/protocols/bitcoind/protocol_bitcoind_rest.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 992b9d2e..94e0f31c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -332,17 +332,21 @@ bool protocol_bitcoind_rest::handle_get_block_headers(const code& ec, return true; } - // bitcoind serves headers only for a hash on the active chain. const auto& query = archive(); - const auto link = query.to_header(*hash); - size_t height{}; - if (!query.get_height(height, link) || - !query.is_confirmed_block(link)) + const auto header_link = query.to_header(*hash); + if (!query.is_confirmed_block(header_link)) { send_not_found(); return true; } + size_t height{}; + if (!query.get_height(height, header_link)) + { + send_internal_server_error(database::error::integrity); + return true; + } + constexpr size_t maximum_headers = 2000; constexpr auto header_size = chain::header::serialized_size(); const auto limit = lesser(count, maximum_headers); From 298b9e4d7573e81d359ed136c4411d5c9fcd1264 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 26 Aug 2026 17:16:58 -0400 Subject: [PATCH 21/21] Split absence from fault in confirmed block gates. --- .../bitcoind/protocol_bitcoind_blockchain.cpp | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 05a1f871..40d7313c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -390,13 +390,19 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec, return true; } - size_t height{}; - if (!query.get_height(height, link) || !query.is_confirmed_block(link)) + if (!query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key); return true; } + size_t height{}; + if (!query.get_height(height, link)) + { + send_error(error::bitcoind::internal_error); + return true; + } + // Fees require prevout values, populated from the store. const auto block = query.get_block(link, true); if (!block || !query.populate_without_metadata(*block)) @@ -469,14 +475,20 @@ bool protocol_bitcoind_blockchain::handle_get_chain_tx_stats(const code& ec, link = query.to_header(hash); } - size_t height{}; - if (!query.get_height(height, link) || !query.is_confirmed_block(link)) + if (!query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key, blockhash, blockhash.size()); return true; } + size_t height{}; + if (!query.get_height(height, link)) + { + send_error(error::bitcoind::internal_error); + return true; + } + // The default window is one month of blocks, bounded by the block height. constexpr auto month_seconds = 30_size * 24 * 60 * 60; const auto month = month_seconds / system_settings().block_spacing_seconds; @@ -682,12 +694,17 @@ bool protocol_bitcoind_blockchain::handle_get_tx_out_set_info(const code& ec, return true; } - if (!query.get_height(height, link) || - !query.is_confirmed_block(link)) + if (!query.is_confirmed_block(link)) { send_error(error::bitcoind::invalid_address_or_key); return true; } + + if (!query.get_height(height, link)) + { + send_error(error::bitcoind::internal_error); + return true; + } } monitor(true); @@ -1424,13 +1441,19 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( constexpr auto witness = true; const auto link = query.to_header(hash); const auto block = query.get_block(link, witness); - size_t height{}; - if (!block || !query.get_height(height, link)) + if (!block) { send_error(error::bitcoind::invalid_address_or_key); return true; } + size_t height{}; + if (!query.get_height(height, link)) + { + send_error(error::bitcoind::internal_error); + return true; + } + // Confirmed spends are unconditional (as bitcoind, from undo data). const auto encoded = encode_hash(hash); if (!query.populate_without_metadata(*block))