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_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/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/include/bitcoin/server/protocols/protocol_bitcoind.hpp b/include/bitcoin/server/protocols/protocol_bitcoind.hpp
index 10534356..1f5b88cf 100644
--- a/include/bitcoin/server/protocols/protocol_bitcoind.hpp
+++ b/include/bitcoin/server/protocols/protocol_bitcoind.hpp
@@ -87,13 +87,15 @@ 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,
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/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/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.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_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp
index c29cb011..40d7313c 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.
@@ -188,10 +188,12 @@ 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_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(),
@@ -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;
}
@@ -388,18 +390,24 @@ bool protocol_bitcoind_blockchain::handle_get_block_stats(const code& ec,
return true;
}
- size_t height{};
- if (!query.get_height(height, link) || query.to_confirmed(height) != 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))
{
- send_error(error::bitcoind::misc_error);
+ send_error(error::bitcoind::internal_error);
return true;
}
@@ -408,8 +416,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 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())
@@ -467,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.to_confirmed(height) != 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;
@@ -511,14 +525,22 @@ 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);
- const auto interval = floored_subtract(median_time_past(query, link),
- median_time_past(query, past));
+ // 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& settings = system_settings();
+ 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{};
- for (auto index = add1(first); index <= height; ++index)
- txs += query.get_tx_count(query.to_confirmed(index));
+ for (size_t index{}; index < window; ++index)
+ txs += query.get_tx_count(branch.at(index));
result.emplace("window_interval", interval);
result.emplace("window_tx_count", txs);
@@ -546,38 +568,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))
{
@@ -684,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.to_confirmed(height) != 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);
@@ -1351,13 +1366,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;
}
@@ -1421,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))
@@ -1667,13 +1693,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;
diff --git a/src/protocols/bitcoind/protocol_bitcoind_json.cpp b/src/protocols/bitcoind/protocol_bitcoind_json.cpp
index 46b83cf2..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.
@@ -35,26 +35,22 @@ 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).
-uint32_t protocol_bitcoind::median_time_past(const node::query& query,
+// bitcoind's mtp window includes the block: the child's stored context mtp.
+uint32_t protocol_bitcoind::median_time(const node::query& query,
+ const system::settings& settings,
const database::header_link& link) NOEXCEPT
{
- std::vector times{};
- times.reserve(chain::median_time_past_interval);
+ database::context ctx{};
+ if (query.get_context(ctx, query.to_confirmed_child(link)))
+ return ctx.mtp;
- for (auto walk = link; !walk.is_terminal() &&
- times.size() < chain::median_time_past_interval;
- walk = query.to_parent(walk))
- {
- const auto header = query.get_header(walk);
- if (!header)
- return 0_u32;
-
- times.push_back(header->timestamp());
- }
+ // 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_confirmed_chain_state(settings, key);
+ if (!state)
+ return 0_u32;
- std::sort(times.begin(), times.end());
- return times.empty() ? 0_u32 : times.at(to_half(times.size()));
+ return chain::chain_state{ *state, settings }.context().median_time_past;
}
// A getchainstates entry for candidate or confirmed at the link (top).
@@ -87,8 +83,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))
@@ -101,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, link);
+ out["mediantime"] = median_time(query, settings, link);
// Cumulative work to this block, big-endian per bitcoind chainwork.
uint256_t work{};
@@ -139,7 +135,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 +143,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")) }
@@ -210,7 +205,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();
@@ -233,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, 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_mining.cpp b/src/protocols/bitcoind/protocol_bitcoind_mining.cpp
index eae93e00..bd79bd03 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;
@@ -169,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);
@@ -224,16 +231,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 +262,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);
diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp
index 14fbf362..94e0f31c 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();
}
@@ -324,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.to_confirmed(height) != 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);
@@ -466,10 +478,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 +488,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)
@@ -630,7 +639,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(query, system_settings(), link) },
{ "pruned", node_settings().limited_blocks }
}, 256);
return true;
diff --git a/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp b/src/protocols/bitcoind/protocol_bitcoind_transaction.cpp
index 68906ba2..a06b92eb 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