diff --git a/include/bitcoin/database/error.hpp b/include/bitcoin/database/error.hpp index c8f52f92..8d3caceb 100644 --- a/include/bitcoin/database/error.hpp +++ b/include/bitcoin/database/error.hpp @@ -141,6 +141,7 @@ enum error_t : uint8_t /// txs archive txs_header, txs_empty, + txs_height, txs_confirm, txs_txs_put }; diff --git a/include/bitcoin/database/impl/query/archive_write.ipp b/include/bitcoin/database/impl/query/archive_write.ipp index a4bbb6a1..ed54581e 100644 --- a/include/bitcoin/database/impl/query/archive_write.ipp +++ b/include/bitcoin/database/impl/query/archive_write.ipp @@ -65,11 +65,10 @@ bool CLASS::set(const transaction& tx) NOEXCEPT } TEMPLATE -bool CLASS::set(const block& block, bool strong, bool bypass, - size_t height) NOEXCEPT +bool CLASS::set(const block& block, bool strong, bool bypass) NOEXCEPT { // This sets only the txs of a block with header/context already archived. - return !set_code(block, strong, bypass, height); + return !set_code(block, strong, bypass); } // set transaction @@ -350,27 +349,30 @@ code CLASS::set_code(header_link& out_fk, const block& block, // releases all memory for parts of itself, due to the custom allocator. TEMPLATE -code CLASS::set_code(const block& block, bool strong, bool bypass, - size_t height) NOEXCEPT +code CLASS::set_code(const block& block, bool strong, bool bypass) NOEXCEPT { header_link unused{}; - return set_code(unused, block, strong, bypass, height); + return set_code(unused, block, strong, bypass); } TEMPLATE code CLASS::set_code(header_link& out_fk, const block& block, bool strong, - bool bypass, size_t height) NOEXCEPT + bool bypass) NOEXCEPT { out_fk = to_header(block.get_hash()); if (out_fk.is_terminal()) return error::txs_header; + size_t height{}; + if (!get_height(height, out_fk)) + return error::txs_height; + return set_code(block, out_fk, strong, bypass, height); } TEMPLATE code CLASS::set_code(const block& block, const header_link& key, - bool strong, bool bypass, size_t /* height */) NOEXCEPT + bool strong, bool bypass, size_t height) NOEXCEPT { using namespace system; if (key.is_terminal()) @@ -392,16 +394,10 @@ code CLASS::set_code(const block& block, const header_link& key, return ec; using bytes = linkage::integer; + auto interval = get_interval(key, height); const auto size = block.serialized_size(true); const auto wire = possible_narrow_cast(size); - // TODO: compute and set interval hash for interval blocks as configured. - // TODO: create query to walk header.parent across full interval to collect - // TODO: merkle leaves and compute intermediate merkle root. This requires - // TODO: header.parent link traversal only, with read of hash for each. The - // TODO: full interval of hashes (e.g. 2048) is preallocated to a vector. - std::optional interval{}; - // ======================================================================== const auto scope = store_.get_transactor(); constexpr auto positive = true; diff --git a/include/bitcoin/database/impl/query/optional.ipp b/include/bitcoin/database/impl/query/optional.ipp index e06b8981..bb2ee8b0 100644 --- a/include/bitcoin/database/impl/query/optional.ipp +++ b/include/bitcoin/database/impl/query/optional.ipp @@ -259,6 +259,41 @@ code CLASS::get_confirmed_balance(std::atomic_bool& cancel, uint64_t& balance, return error::success; } +TEMPLATE +std::optional CLASS::get_interval(header_link link, + size_t height) const NOEXCEPT +{ + // Interval is enabled by address table. + if (!address_enabled()) + return {}; + + // Interval is the merkle root that spans 2^depth block hashes. + const auto span = system::power2(store_.interval_depth()); + + // power2() overflow returns zero. + if (is_zero(span)) + return {}; + + // One is a functional but undesirable case. + if (is_one(span)) + return get_header_key(link); + + // Interval ends at nth block where n is a multiple of span. + if (!system::is_multiple(add1(height), span)) + return {}; + + // Generate the leaf nodes for the span. + hashes leaves(span); + for (auto& leaf: std::views::reverse(leaves)) + { + leaf = get_header_key(link); + link = to_parent(link); + } + + // Generate the interval (merkle root) for the span ending on link header. + return system::merkle_root(std::move(leaves)); +} + ////TEMPLATE ////bool CLASS::set_address_output(const output& output, //// const output_link& link) NOEXCEPT diff --git a/include/bitcoin/database/impl/store.ipp b/include/bitcoin/database/impl/store.ipp index 0ff7eaf2..c87a9b7a 100644 --- a/include/bitcoin/database/impl/store.ipp +++ b/include/bitcoin/database/impl/store.ipp @@ -232,6 +232,12 @@ bool CLASS::turbo() const NOEXCEPT return configuration_.turbo; } +TEMPLATE +uint8_t CLASS::interval_depth() const NOEXCEPT +{ + return configuration_.interval_depth; +} + TEMPLATE code CLASS::create(const event_handler& handler) NOEXCEPT { diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index aafe1cde..4e6680ff 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -412,8 +413,7 @@ class query bool set(const block& block, const context& ctx, bool milestone, bool strong) NOEXCEPT; bool set(const transaction& tx) NOEXCEPT; - bool set(const block& block, bool strong, bool bypass, - size_t height) NOEXCEPT; + bool set(const block& block, bool strong, bool bypass) NOEXCEPT; /// Set transaction. code set_code(const transaction& tx) NOEXCEPT; @@ -439,10 +439,9 @@ class query const chain_context& ctx, bool milestone, bool strong) NOEXCEPT; /// Set block.txs (headers-first). - code set_code(const block& block, bool strong, bool bypass, - size_t height) NOEXCEPT; + code set_code(const block& block, bool strong, bool bypass) NOEXCEPT; code set_code(header_link& out_fk, const block& block, bool strong, - bool bypass, size_t height) NOEXCEPT; + bool bypass) NOEXCEPT; code set_code(const block& block, const header_link& key, bool strong, bool bypass, size_t height) NOEXCEPT; @@ -569,7 +568,12 @@ class query code get_minimum_unspent_outputs(std::atomic_bool& cancel, outpoints& out, const hash_digest& key, uint64_t value, bool turbo=false) const NOEXCEPT; code get_confirmed_balance(std::atomic_bool& cancel, - uint64_t& balance, const hash_digest& key, bool turbo=false) const NOEXCEPT; + uint64_t& balance, const hash_digest& key, + bool turbo=false) const NOEXCEPT; + + /// No value if header is not at configured interval. + std::optional get_interval(header_link header, + size_t height) const NOEXCEPT; bool is_filtered_body(const header_link& link) const NOEXCEPT; bool get_filter_body(filter& out, const header_link& link) const NOEXCEPT; diff --git a/include/bitcoin/database/settings.hpp b/include/bitcoin/database/settings.hpp index ab606f67..d1161e8a 100644 --- a/include/bitcoin/database/settings.hpp +++ b/include/bitcoin/database/settings.hpp @@ -37,7 +37,15 @@ struct BCD_API settings settings(system::chain::selection context) NOEXCEPT; /// Properties. + /// ----------------------------------------------------------------------- + + /// Enable concurrency in individual client-server queries. bool turbo; + + /// Depth of electrum merkle tree interval caching. + uint8_t interval_depth; + + /// Path to the database directory. std::filesystem::path path; /// Archives. diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index 2dfe0586..d7e3c7d9 100644 --- a/include/bitcoin/database/store.hpp +++ b/include/bitcoin/database/store.hpp @@ -60,6 +60,9 @@ class store /// Allow full throttle concurrent query execution (may use 100% CPU). bool turbo() const NOEXCEPT; + /// Depth of electrum merkle tree interval caching. + uint8_t interval_depth() const NOEXCEPT; + /// Methods. /// ----------------------------------------------------------------------- diff --git a/include/bitcoin/database/tables/archives/txs.hpp b/include/bitcoin/database/tables/archives/txs.hpp index 834367e3..5ba9e41b 100644 --- a/include/bitcoin/database/tables/archives/txs.hpp +++ b/include/bitcoin/database/tables/archives/txs.hpp @@ -53,7 +53,7 @@ struct txs return set_right(wire, offset, interval); } - // Intervals are set only if non-zero in database.interval. + // Intervals are optional based on store configuration. struct slab : public schema::txs { @@ -121,7 +121,8 @@ struct txs inline link count() const NOEXCEPT { return system::possible_narrow_cast(ct::size + - bytes::size + tx::size * number); + bytes::size + tx::size * number + + (interval.has_value() ? schema::hash : zero)); } inline bool to_data(finalizer& sink) const NOEXCEPT diff --git a/src/error.cpp b/src/error.cpp index a7877eb0..19916be2 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -134,6 +134,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) // txs archive { txs_header, "txs_header" }, { txs_empty, "txs_empty" }, + { txs_height, "txs_height" }, { txs_confirm, "txs_confirm" }, { txs_txs_put, "txs_txs_put" } }; diff --git a/src/settings.cpp b/src/settings.cpp index c075b275..4e33615c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -28,6 +28,7 @@ using namespace bc::system; settings::settings() NOEXCEPT : turbo{ false }, + interval_depth{ max_uint8 }, path{ "bitcoin" }, // Archives. diff --git a/test/error.cpp b/test/error.cpp index 824c0de1..7c608d03 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -686,6 +686,15 @@ BOOST_AUTO_TEST_CASE(error_t__code__txs_empty__true_exected_message) BOOST_REQUIRE_EQUAL(ec.message(), "txs_empty"); } +BOOST_AUTO_TEST_CASE(error_t__code__txs_height__true_exected_message) +{ + constexpr auto value = error::txs_height; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "txs_height"); +} + BOOST_AUTO_TEST_CASE(error_t__code__txs_confirm__true_exected_message) { constexpr auto value = error::txs_confirm; diff --git a/test/query/archive_write.cpp b/test/query/archive_write.cpp index 5842dcc6..5fa4a9cb 100644 --- a/test/query/archive_write.cpp +++ b/test/query/archive_write.cpp @@ -705,7 +705,7 @@ BOOST_AUTO_TEST_CASE(query_archive_write__set_block_txs__get_block__expected) BOOST_REQUIRE(!query.is_block(test::genesis.hash())); BOOST_REQUIRE(query.set(test::genesis.header(), test::context, milestone)); BOOST_REQUIRE(!query.is_associated(0)); - BOOST_REQUIRE(query.set(test::genesis, false, false, zero)); + BOOST_REQUIRE(query.set(test::genesis, false, false)); BOOST_REQUIRE(query.is_block(test::genesis.hash())); BOOST_REQUIRE(query.is_associated(0)); diff --git a/test/query/initialize.cpp b/test/query/initialize.cpp index c0285abe..3d1264d7 100644 --- a/test/query/initialize.cpp +++ b/test/query/initialize.cpp @@ -415,17 +415,17 @@ BOOST_AUTO_TEST_CASE(query_initialize__get_unassociated_above__gapped_candidate_ BOOST_REQUIRE_EQUAL(unassociated3.size(), 0u); // There are two unassociated blocks above block 1 (new fork point). - BOOST_REQUIRE(query.set(test::block1, false, false, zero)); + BOOST_REQUIRE(query.set(test::block1, false, false)); BOOST_REQUIRE(query.push_confirmed(query.to_header(test::block1.hash()), false)); BOOST_REQUIRE_EQUAL(query.get_all_unassociated().size(), 2u); // There is one unassociated block above block 2 (new fork point). - BOOST_REQUIRE(query.set(test::block2, false, false, zero)); + BOOST_REQUIRE(query.set(test::block2, false, false)); BOOST_REQUIRE(query.push_confirmed(query.to_header(test::block2.hash()), false)); BOOST_REQUIRE_EQUAL(query.get_all_unassociated().size(), 1u); // There are no unassociated blocks above block 3 (new fork point). - BOOST_REQUIRE(query.set(test::block3, false, false, zero)); + BOOST_REQUIRE(query.set(test::block3, false, false)); BOOST_REQUIRE(query.push_confirmed(query.to_header(test::block3.hash()), false)); BOOST_REQUIRE_EQUAL(query.get_all_unassociated().size(), 0u); } @@ -482,7 +482,7 @@ BOOST_AUTO_TEST_CASE(query_initialize__get_unassociated_count_above__gapped_cand BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(3, 1), 0u); // There is one unassociated block at block 2. - BOOST_REQUIRE(query.set(test::block3, false, false, zero)); // associated + BOOST_REQUIRE(query.set(test::block3, false, false)); // associated BOOST_REQUIRE_EQUAL(query.get_unassociated_count(), 1u); BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(0), 1u); BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(1), 1u); @@ -490,7 +490,7 @@ BOOST_AUTO_TEST_CASE(query_initialize__get_unassociated_count_above__gapped_cand BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(3), 0u); // There are no unassociated blocks. - BOOST_REQUIRE(query.set(test::block2, false, false, zero)); // associated + BOOST_REQUIRE(query.set(test::block2, false, false)); // associated BOOST_REQUIRE_EQUAL(query.get_unassociated_count(), 0u); BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(0), 0u); BOOST_REQUIRE_EQUAL(query.get_unassociated_count_above(1), 0u); diff --git a/test/query/optional.cpp b/test/query/optional.cpp index abdb68b5..30004858 100644 --- a/test/query/optional.cpp +++ b/test/query/optional.cpp @@ -227,6 +227,94 @@ BOOST_AUTO_TEST_CASE(query_optional__get_confirmed_balance__genesis__expected) BOOST_REQUIRE_EQUAL(out, 5000000000u); } +// Merkle root of test blocks [0..1] +constexpr auto root01 = system::base16_hash("abdc2227d02d114b77be15085c1257709252a7a103f9ac0ab3c85d67e12bc0b8"); + +// Merkle root of test blocks [2..4] +constexpr auto root02 = system::base16_hash("f2a2a2907abb326726a2d6500fe494f63772a941b414236c302e920bc1aa9caf"); + +// Merkle root of test blocks [0..4] +constexpr auto root04 = system::sha256::double_hash(root01, root02); + +BOOST_AUTO_TEST_CASE(query_optional__get_interval__depth_0__block_hash) +{ + settings settings{}; + settings.interval_depth = 0; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block2, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block3, context{ 0, 3, 0 }, false, false)); + + const auto header0 = query.to_header(test::genesis.hash()); + const auto header1 = query.to_header(test::block1.hash()); + const auto header2 = query.to_header(test::block2.hash()); + const auto header3 = query.to_header(test::block3.hash()); + BOOST_REQUIRE(!header0.is_terminal()); + BOOST_REQUIRE(!header1.is_terminal()); + BOOST_REQUIRE(!header2.is_terminal()); + BOOST_REQUIRE(!header3.is_terminal()); + BOOST_REQUIRE(query.get_interval(header0, 0).has_value()); + BOOST_REQUIRE(query.get_interval(header1, 1).has_value()); + BOOST_REQUIRE(query.get_interval(header2, 2).has_value()); + BOOST_REQUIRE(query.get_interval(header3, 3).has_value()); + BOOST_REQUIRE_EQUAL(query.get_interval(header0, 0).value(), test::genesis.hash()); + BOOST_REQUIRE_EQUAL(query.get_interval(header1, 1).value(), test::block1.hash()); + BOOST_REQUIRE_EQUAL(query.get_interval(header2, 2).value(), test::block2.hash()); + BOOST_REQUIRE_EQUAL(query.get_interval(header3, 3).value(), test::block3.hash()); +} + +BOOST_AUTO_TEST_CASE(query_optional__get_interval__depth_1__expected) +{ + settings settings{}; + settings.interval_depth = 1; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block2, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block3, context{ 0, 3, 0 }, false, false)); + + const auto header0 = query.to_header(test::genesis.hash()); + const auto header1 = query.to_header(test::block1.hash()); + const auto header2 = query.to_header(test::block2.hash()); + const auto header3 = query.to_header(test::block3.hash()); + BOOST_REQUIRE(!header0.is_terminal()); + BOOST_REQUIRE(!header1.is_terminal()); + BOOST_REQUIRE(!header2.is_terminal()); + BOOST_REQUIRE(!header3.is_terminal()); + BOOST_REQUIRE(!query.get_interval(header0, 0).has_value()); + BOOST_REQUIRE( query.get_interval(header1, 1).has_value()); + BOOST_REQUIRE(!query.get_interval(header2, 2).has_value()); + BOOST_REQUIRE( query.get_interval(header3, 3).has_value()); + BOOST_REQUIRE_EQUAL(query.get_interval(header1, 1).value(), root01); + BOOST_REQUIRE_EQUAL(query.get_interval(header3, 3).value(), root02); +} + +BOOST_AUTO_TEST_CASE(query_optional__get_interval__depth_2__expected) +{ + settings settings{}; + settings.interval_depth = 2; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(events_handler), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block2, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block3, context{ 0, 3, 0 }, false, false)); + + const auto header3 = query.to_header(test::block3.hash()); + BOOST_REQUIRE(!header3.is_terminal()); + BOOST_REQUIRE(query.get_interval(header3, 3).has_value()); + BOOST_REQUIRE_EQUAL(query.get_interval(header3, 3).value(), root04); +} + ////BOOST_AUTO_TEST_CASE(query_optional__set_filter__get_filter_and_head__expected) ////{ //// const auto& filter_head0 = system::null_hash; diff --git a/test/settings.cpp b/test/settings.cpp index f8beee0a..3767e3c6 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -24,6 +24,7 @@ BOOST_AUTO_TEST_CASE(settings__construct__default__expected) { database::settings configuration; BOOST_REQUIRE_EQUAL(configuration.turbo, false); + BOOST_REQUIRE_EQUAL(configuration.interval_depth, 255u); BOOST_REQUIRE_EQUAL(configuration.path, "bitcoin"); // Archives.