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(satoshi, std::move(script))); + outs->push_back(to_shared(satoshis, std::move(script))); return error::bitcoind::success; }; @@ -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"] = @@ -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); 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/src/protocols/electrum/protocol_electrum_transactions.cpp b/src/protocols/electrum/protocol_electrum_transactions.cpp index ab7c200e..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) @@ -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)); 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/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_json.cpp b/test/protocols/bitcoind/bitcoind_json.cpp index 6cf852c8..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,25 +115,27 @@ 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_, link), 1231470173u); + BOOST_REQUIRE_EQUAL(json::median_time(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); 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"); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 716df47d..f68e1973 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")); @@ -282,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");