From f390620f3a0f19ec26ecdce5913acbd7bb928d3d Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 25 Mar 2026 19:04:40 -0400 Subject: [PATCH 1/2] Add/test get_position_tx(). --- .../database/impl/query/archive_read.ipp | 11 +++++++ include/bitcoin/database/query.hpp | 4 ++- .../bitcoin/database/tables/archives/txs.hpp | 31 +++++++++++++++++++ test/query/archive_read.cpp | 28 +++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/database/impl/query/archive_read.ipp b/include/bitcoin/database/impl/query/archive_read.ipp index 080f21a8..b5ba3675 100644 --- a/include/bitcoin/database/impl/query/archive_read.ipp +++ b/include/bitcoin/database/impl/query/archive_read.ipp @@ -189,6 +189,17 @@ bool CLASS::get_tx_position(size_t& out, const tx_link& link) const NOEXCEPT return true; } +TEMPLATE +tx_link CLASS::get_position_tx(const header_link& link, + size_t position) const NOEXCEPT +{ + table::txs::get_at_position txs{ {}, position }; + if (!store_.txs.at(to_txs(link), txs)) + return {}; + + return txs.tx_fk; +} + // Sizes. // ---------------------------------------------------------------------------- diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 2c90974f..19a94c46 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -344,8 +344,10 @@ class query inline hash_digest get_point_hash(const point_link& link) const NOEXCEPT; /// False position implies not confirmed (or fault). - bool get_tx_position(size_t& out, const tx_link& link) const NOEXCEPT; bool get_tx_height(size_t& out, const tx_link& link) const NOEXCEPT; + bool get_tx_position(size_t& out, const tx_link& link) const NOEXCEPT; + tx_link get_position_tx(const header_link& link, + size_t position) const NOEXCEPT; /// Sizes. bool get_tx_size(size_t& out, const tx_link& link, diff --git a/include/bitcoin/database/tables/archives/txs.hpp b/include/bitcoin/database/tables/archives/txs.hpp index e5f1bca0..f90ab83d 100644 --- a/include/bitcoin/database/tables/archives/txs.hpp +++ b/include/bitcoin/database/tables/archives/txs.hpp @@ -288,6 +288,37 @@ struct txs size_t position{}; }; + struct get_at_position + : public schema::txs + { + inline link count() const NOEXCEPT + { + BC_ASSERT(false); + return {}; + } + + inline bool from_data(reader& source) NOEXCEPT + { + // tx sizes + source.skip_bytes(skip_sizes); + + // tx fks + const auto number = source.read_little_endian(); + if (position < number) + { + source.skip_bytes(position * tx::size); + tx_fk = source.read_little_endian(); + return source; + } + + source.invalidate(); + return source; + } + + const size_t position{}; + tx::integer tx_fk{}; + }; + struct get_coinbase : public schema::txs { diff --git a/test/query/archive_read.cpp b/test/query/archive_read.cpp index f00930d5..cb4a9359 100644 --- a/test/query/archive_read.cpp +++ b/test/query/archive_read.cpp @@ -428,6 +428,12 @@ BOOST_AUTO_TEST_CASE(query_archive_read__get_tx_position__confirmed__expected) BOOST_REQUIRE(query.get_tx_position(out, 4)); BOOST_REQUIRE_EQUAL(out, 0u); BOOST_REQUIRE(!query.get_tx_position(out, 5)); + + BOOST_REQUIRE_EQUAL(query.get_position_tx(0, 0), 0u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(1, 0), 1u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(2, 0), 2u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(2, 1), 3u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(3, 0), 4u); } BOOST_AUTO_TEST_CASE(query_archive_read__get_tx_position__always__expected) @@ -474,6 +480,28 @@ BOOST_AUTO_TEST_CASE(query_archive_read__get_tx_position__always__expected) BOOST_REQUIRE(!query.get_tx_position(out, 5)); } + +BOOST_AUTO_TEST_CASE(query_archive_read__get_position_tx__always__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block2a, context{ 0, 2, 0 }, false, false)); + BOOST_REQUIRE(query.set(test::block3a, context{ 0, 3, 0 }, false, false)); + + BOOST_REQUIRE_EQUAL(query.get_position_tx(0, 0), 0u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(1, 0), 1u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(2, 0), 2u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(2, 1), 3u); + BOOST_REQUIRE_EQUAL(query.get_position_tx(3, 0), 4u); + BOOST_REQUIRE(query.get_position_tx(3, 1).is_terminal()); + BOOST_REQUIRE(query.get_position_tx({}, 0).is_terminal()); +} + BOOST_AUTO_TEST_CASE(query_archive_read__get_tx_sizes__coinbase__204) { settings settings{}; From e86c9c51688b2fe37b8d0e03520decfb3d16c548 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Wed, 25 Mar 2026 23:19:00 -0400 Subject: [PATCH 2/2] Move block::merkle_branch() and tests from chain::. --- .../bitcoin/database/impl/query/merkle.ipp | 33 +++- include/bitcoin/database/query.hpp | 4 + test/query/merkle.cpp | 144 ++++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/database/impl/query/merkle.ipp b/include/bitcoin/database/impl/query/merkle.ipp index 37eed9b6..8a1428e7 100644 --- a/include/bitcoin/database/impl/query/merkle.ipp +++ b/include/bitcoin/database/impl/query/merkle.ipp @@ -31,6 +31,37 @@ namespace database { // merkle // ---------------------------------------------------------------------------- +// static/protected +TEMPLATE +CLASS::positions CLASS::merkle_branch(size_t leaf, size_t leaves, + bool compress) NOEXCEPT +{ + using namespace system; + BC_ASSERT(leaves <= power2(sub1(bits))); + BC_ASSERT(is_even(leaves) || is_one(leaves)); + + positions branch{}; + if (is_zero(leaves) || leaf >= leaves) + return branch; + + // Upper bound, actual count may be less given compression. + branch.reserve(ceilinged_log2(leaves)); + + for (auto width = one, current = leaves; current > one;) + { + const auto sibling = bit_xor(leaf, one); + if (!compress || sibling < current) + branch.emplace_back(sibling, width); + + ++current; + shift_left_into(width); + shift_right_into(leaf); + shift_right_into(current); + } + + return branch; +} + // protected TEMPLATE CLASS::hash_option CLASS::create_interval(header_link link, @@ -87,7 +118,7 @@ void CLASS::merge_merkle(hashes& path, hashes&& leaves, size_t first, ++size; } - for (const auto& row: block::merkle_branch(first, size + lift)) + for (const auto& row: merkle_branch(first, size + lift)) { hashes subroot{}; if (const auto leaf = row.sibling * row.width; leaf < size) diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 19a94c46..ea8cf0da 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -768,11 +768,15 @@ class query /// merkle /// ----------------------------------------------------------------------- + struct position { size_t sibling; size_t width; }; + using positions = std::vector; // merkle related utilities static hash_digest partial_subroot(hashes&& tree, size_t span) NOEXCEPT; static void merge_merkle(hashes& path, hashes&& leaves, size_t first, size_t lift) NOEXCEPT; + static positions merkle_branch(size_t leaf, size_t leaves, + bool compress=false) NOEXCEPT; // merkle related configuration size_t interval_depth() const NOEXCEPT; diff --git a/test/query/merkle.cpp b/test/query/merkle.cpp index 95488ffe..db358199 100644 --- a/test/query/merkle.cpp +++ b/test/query/merkle.cpp @@ -59,7 +59,9 @@ class merkle_accessor { public: using base = test::query_accessor; + using positions = base::positions; using base::base; + using base::merkle_branch; using base::interval_span; using base::create_interval; using base::get_confirmed_interval; @@ -69,6 +71,148 @@ class merkle_accessor using base::get_merkle_root_and_proof; }; +// merkle_branch + +BOOST_AUTO_TEST_CASE(query_merkle___merkle_branch__leaf_zero__empty) +{ + BOOST_REQUIRE(merkle_accessor::merkle_branch(0, 0, true).empty()); + BOOST_REQUIRE(merkle_accessor::merkle_branch(0, 0, false).empty()); +} + +BOOST_AUTO_TEST_CASE(query_merkle___merkle_branch__one__zero) +{ + auto branch = merkle_accessor::merkle_branch(1, 2, true); + BOOST_REQUIRE_EQUAL(branch.size(), 1u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + + branch = merkle_accessor::merkle_branch(1, 2, false); + BOOST_REQUIRE_EQUAL(branch.size(), 1u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); +} + +BOOST_AUTO_TEST_CASE(query_merkle___merkle_branch__three__two_and_zero) +{ + auto branch = merkle_accessor::merkle_branch(3, 4, true); + BOOST_REQUIRE_EQUAL(branch.size(), 2u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); + + branch = merkle_accessor::merkle_branch(3, 4, false); + BOOST_REQUIRE_EQUAL(branch.size(), 2u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); +} + +BOOST_AUTO_TEST_CASE(query_merkle___merkle_branch__seven__six_four_and_zero) +{ + auto branch = merkle_accessor::merkle_branch(7, 8, true); + BOOST_REQUIRE_EQUAL(branch.size(), 3u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 6u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); + BOOST_REQUIRE_EQUAL(branch[2].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[2].width, 4u); + + branch = merkle_accessor::merkle_branch(7, 8, false); + BOOST_REQUIRE_EQUAL(branch.size(), 3u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 6u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); + BOOST_REQUIRE_EQUAL(branch[2].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[2].width, 4u); +} + +BOOST_AUTO_TEST_CASE(block__merkle_branch__medium_power_of_two__expected) +{ + const merkle_accessor::positions expected{ { 14, 1 }, { 6, 2 }, { 2, 4 }, { 0, 8 } }; + + auto branch = merkle_accessor::merkle_branch(15, 16, true); + BOOST_REQUIRE_EQUAL(branch.size(), 4u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 14u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 6u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); + BOOST_REQUIRE_EQUAL(branch[2].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[2].width, 4u); + BOOST_REQUIRE_EQUAL(branch[3].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[3].width, 8u); + + branch = merkle_accessor::merkle_branch(15, 16, false); + BOOST_REQUIRE_EQUAL(branch.size(), 4u); + BOOST_REQUIRE_EQUAL(branch[0].sibling, 14u); + BOOST_REQUIRE_EQUAL(branch[0].width, 1u); + BOOST_REQUIRE_EQUAL(branch[1].sibling, 6u); + BOOST_REQUIRE_EQUAL(branch[1].width, 2u); + BOOST_REQUIRE_EQUAL(branch[2].sibling, 2u); + BOOST_REQUIRE_EQUAL(branch[2].width, 4u); + BOOST_REQUIRE_EQUAL(branch[3].sibling, 0u); + BOOST_REQUIRE_EQUAL(branch[3].width, 8u); +} + +BOOST_AUTO_TEST_CASE(block__merkle_branch__power_of_two_minus_one__expected) +{ + constexpr auto leaf = 1023u; + constexpr auto size = system::ceilinged_log2(add1(leaf)); + auto branch = merkle_accessor::merkle_branch(leaf, add1(leaf), true); + BOOST_REQUIRE_EQUAL(branch.size(), size); + BOOST_REQUIRE_EQUAL(branch.front().sibling, 1022u); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(size))); + + branch = merkle_accessor::merkle_branch(leaf, add1(leaf), false); + BOOST_REQUIRE_EQUAL(branch.size(), size); + BOOST_REQUIRE_EQUAL(branch.front().sibling, 1022u); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(size))); +} + +BOOST_AUTO_TEST_CASE(block__merkle_branch__odd_large_leaf_with_duplication__expected) +{ + constexpr auto leaf = 2047u; + constexpr auto size = system::ceilinged_log2(add1(leaf)); + auto branch = merkle_accessor::merkle_branch(leaf, add1(leaf), true); + BOOST_REQUIRE_EQUAL(branch.size(), size); + BOOST_REQUIRE_EQUAL(branch.front().sibling, 2046u); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(size))); + + branch = merkle_accessor::merkle_branch(leaf, add1(leaf), false); + BOOST_REQUIRE_EQUAL(branch.size(), size); + BOOST_REQUIRE_EQUAL(branch.front().sibling, 2046u); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(size))); +} + +BOOST_AUTO_TEST_CASE(block__merkle_branch__maximum_non_overflow__expected) +{ + constexpr auto maximum = sub1(system::power2(sub1(bits))); + auto branch = merkle_accessor::merkle_branch(maximum, add1(maximum), true); + BOOST_REQUIRE_EQUAL(branch.size(), sub1(bits)); + BOOST_REQUIRE_EQUAL(branch.front().sibling, sub1(maximum)); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(sub1(bits)))); + + branch = merkle_accessor::merkle_branch(maximum, add1(maximum), false); + BOOST_REQUIRE_EQUAL(branch.size(), sub1(bits)); + BOOST_REQUIRE_EQUAL(branch.front().sibling, sub1(maximum)); + BOOST_REQUIRE_EQUAL(branch.front().width, 1u); + BOOST_REQUIRE_EQUAL(branch.back().sibling, 0u); + BOOST_REQUIRE_EQUAL(branch.back().width, system::power2(sub1(sub1(bits)))); +} + // interval_span BOOST_AUTO_TEST_CASE(query_merkle__interval_span__uninitialized__max_size_t)