From f8952bc2b6f0ba8921c795a8fef9488fff1d6a81 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 24 Aug 2026 15:47:11 -0400 Subject: [PATCH 1/4] Style, use namespace and alias. --- src/file/utilities.cpp | 125 +++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 68 deletions(-) diff --git a/src/file/utilities.cpp b/src/file/utilities.cpp index 116aaa825..b58558222 100644 --- a/src/file/utilities.cpp +++ b/src/file/utilities.cpp @@ -34,11 +34,19 @@ #define ADVISED_ONLY(name) #endif +#if defined(POSIX_FADV_DONTNEED) + #define DONTNEED_ONLY(name) name +#else + #define DONTNEED_ONLY(name) +#endif + namespace libbitcoin { namespace database { namespace file { using namespace system; +using namespace std::filesystem; +using errorno_t = system::error::errorno_t; // Many return values are redundant with return codes. // There is no reason to obtain the return value of these non-discardables. @@ -61,8 +69,8 @@ inline path trim(const path& value) NOEXCEPT // differentiate from false with failure code (and never a code on success). bool is_directory(const path& directory) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - return std::filesystem::is_directory(system::extended_path(directory), ec); + code ec{ errorno_t::no_error }; + return is_directory(system::extended_path(directory), ec); } bool clear_directory(const path& directory) NOEXCEPT @@ -72,11 +80,11 @@ bool clear_directory(const path& directory) NOEXCEPT code clear_directory_ex(const path& directory) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; + code ec{ errorno_t::no_error }; const auto path = system::extended_path(directory); - std::filesystem::remove_all(path, ec); + remove_all(path, ec); if (ec) return ec; - std::filesystem::create_directories(path, ec); + create_directories(path, ec); return ec; } @@ -87,19 +95,19 @@ bool create_directory(const path& directory) NOEXCEPT code create_directory_ex(const path& directory) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; + code ec{ errorno_t::no_error }; const auto path = system::extended_path(trim(directory)); - const auto created = std::filesystem::create_directories(path, ec); + const auto created = create_directories(path, ec); if (ec || created) return ec; - return system::error::errorno_t::is_a_directory; + return errorno_t::is_a_directory; } // We don't use ec because it gets set (not found) when false, but no way to // differentiate from false with failure code (and never a code on success). bool is_file(const path& filename) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - return std::filesystem::is_regular_file(filename, ec); + code ec{ errorno_t::no_error }; + return is_regular_file(filename, ec); } bool create_file(const path& filename) NOEXCEPT @@ -120,15 +128,13 @@ code create_file_ex(const path& filename) NOEXCEPT // noexcept. if (!file.good()) - return system::error::errorno_t::not_a_stream; + return errorno_t::not_a_stream; // Sets failbit (but not noexcept). file.close(); // noexcept. - return file.good() ? - system::error::errorno_t::no_error : - system::error::errorno_t::not_a_stream; + return file.good() ? errorno_t::no_error : errorno_t::not_a_stream; } catch (const std::ios_base::failure& e) { @@ -156,22 +162,20 @@ code create_file_ex(const path& to, const uint8_t* data, size_t size) NOEXCEPT // noexcept. if (!file.good()) - return system::error::errorno_t::not_a_stream; + return errorno_t::not_a_stream; // May throw. file.write(pointer_cast(data), size); // noexcept. if (!file.good()) - return system::error::errorno_t::not_a_stream; + return errorno_t::not_a_stream; // Sets failbit (but not noexcept). file.close(); // noexcept. - return file.good() ? - system::error::errorno_t::no_error : - system::error::errorno_t::stream_timeout; + return file.good() ? errorno_t::no_error : errorno_t::stream_timeout; } catch (const std::ios_base::failure& e) { @@ -190,8 +194,8 @@ bool remove(const path& name) NOEXCEPT // Error code is not set if file did not exist. code remove_ex(const path& name) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - std::filesystem::remove(system::extended_path(name), ec); + code ec{ errorno_t::no_error }; + remove(system::extended_path(name), ec); return ec; } @@ -204,10 +208,8 @@ bool rename(const path& from, const path& to) NOEXCEPT // directory|file code rename_ex(const path& from, const path& to) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - std::filesystem::rename(system::extended_path(from), - system::extended_path(to), ec); - + code ec{ errorno_t::no_error }; + rename(system::extended_path(from), system::extended_path(to), ec); return ec; } @@ -220,10 +222,8 @@ bool copy(const path& from, const path& to) NOEXCEPT // file code copy_ex(const path& from, const path& to) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - std::filesystem::copy_file(system::extended_path(from), - system::extended_path(to), ec); - + code ec{ errorno_t::no_error }; + copy_file(system::extended_path(from), system::extended_path(to), ec); return ec; } @@ -237,15 +237,13 @@ bool copy_directory(const path& from, const path& to) NOEXCEPT code copy_directory_ex(const path& from, const path& to) NOEXCEPT { if (file::is_directory(to)) - return system::error::errorno_t::is_a_directory; + return errorno_t::is_a_directory; if (!file::is_directory(from)) - return system::error::errorno_t::not_a_directory; - - code ec{ system::error::errorno_t::no_error }; - std::filesystem::copy(system::extended_path(from), - system::extended_path(to), ec); + return errorno_t::not_a_directory; + code ec{ errorno_t::no_error }; + copy(system::extended_path(from), system::extended_path(to), ec); return ec; } @@ -255,19 +253,16 @@ bool discharge_directory(const path& to) NOEXCEPT return !discharge_directory_ex(to); } -// Discharge an archival clone from the page cache (posix): the clone is -// written but not soon read, and its cached pages and pending writeback -// otherwise stand while staging allocates, so the kernel preserves the cache -// by displacing the fresh anonymous set to swap. Synchronize before advising, -// as dirty pages do not discharge. -#if defined(POSIX_FADV_DONTNEED) // directory -code discharge_directory_ex(const path& to) NOEXCEPT +// Remove an archival clone from the page cache. Synchronize before advising. +code discharge_directory_ex(const path& DONTNEED_ONLY(to)) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - std::filesystem::directory_iterator it{ system::extended_path(to), ec }; + code ec{ errorno_t::no_error }; + +#if defined(POSIX_FADV_DONTNEED) + directory_iterator it{ system::extended_path(to), ec }; - for (const std::filesystem::directory_iterator end{}; !ec && (it != end); + for (const directory_iterator end{}; !ec && (it != end); it.increment(ec)) { if (!it->is_regular_file(ec) || ec) @@ -297,34 +292,28 @@ code discharge_directory_ex(const path& to) NOEXCEPT if (!close(descriptor)) return system::error::get_errno(); } +#endif // POSIX_FADV_DONTNEED return ec; } -#else -// directory -code discharge_directory_ex(const path&) NOEXCEPT -{ - return system::error::errorno_t::no_error; -} -#endif // POSIX_FADV_DONTNEED // File sizing. // ---------------------------------------------------------------------------- -bool size(size_t& out, const std::filesystem::path& filename) NOEXCEPT +bool size(size_t& out, const path& filename) NOEXCEPT { return !size_ex(out, filename); } -code size_ex(size_t& out, const std::filesystem::path& filename) NOEXCEPT +code size_ex(size_t& out, const path& filename) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - const auto size = std::filesystem::file_size( - system::extended_path(filename), ec); + code ec{ errorno_t::no_error }; + const auto size = file_size(system::extended_path(filename), ec); + if (ec) + return ec; - if (ec) return ec; if (is_limited(size)) - return system::error::errorno_t::value_too_large; + return errorno_t::value_too_large; out = possible_narrow_cast(size); return ec; @@ -337,15 +326,15 @@ bool space(size_t& out, const path& filename) NOEXCEPT code space_ex(size_t& out, const path& filename) NOEXCEPT { - code ec{ system::error::errorno_t::no_error }; - const auto space = std::filesystem::space( - system::extended_path(filename), ec); + code ec{ errorno_t::no_error }; + const auto space_ = space( system::extended_path(filename), ec); + if (ec) + return ec; - if (ec) return ec; - if (is_limited(space.available)) - return system::error::errorno_t::value_too_large; + if (is_limited(space_.available)) + return errorno_t::value_too_large; - out = possible_narrow_cast(space.available); + out = possible_narrow_cast(space_.available); return ec; } @@ -447,7 +436,7 @@ bool size(size_t& out, int file_descriptor) NOEXCEPT { if (file_descriptor == -1) { - system::error::set_errno(system::error::errorno_t::invalid_argument); + system::error::set_errno(errorno_t::invalid_argument); return false; } @@ -472,7 +461,7 @@ bool size(size_t& out, int file_descriptor) NOEXCEPT #endif if (is_limited(sbuf.st_size)) { - system::error::set_errno(system::error::errorno_t::value_too_large); + system::error::set_errno(errorno_t::value_too_large); return false; } From 9c1b4b89c0392d485fbdc4947531f1f845302544 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 24 Aug 2026 20:55:08 -0400 Subject: [PATCH 2/4] Add difference_set. --- .../database/impl/types/difference_set.ipp | 71 ++++++++++++ .../bitcoin/database/types/difference_set.hpp | 103 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 include/bitcoin/database/impl/types/difference_set.ipp create mode 100644 include/bitcoin/database/types/difference_set.hpp diff --git a/include/bitcoin/database/impl/types/difference_set.ipp b/include/bitcoin/database/impl/types/difference_set.ipp new file mode 100644 index 000000000..7f53e0f7d --- /dev/null +++ b/include/bitcoin/database/impl/types/difference_set.ipp @@ -0,0 +1,71 @@ +/** + * 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_DATABASE_TYPES_DIFFERENCE_SET_IPP +#define LIBBITCOIN_DATABASE_TYPES_DIFFERENCE_SET_IPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +TEMPLATE +void CLASS::toggle(uint64_t id, uint32_t index) NOEXCEPT +{ + using namespace system; + constexpr auto golden = 0x9e3779b97f4a7c15_u64; + const auto key = to_key(id, index); + auto& shard = shards_[shift_right(key * golden, key_bits - shard_bits)]; + const std::lock_guard lock{ shard.mutex }; + auto& mask = shard.map[key]; + mask ^= bit_right(bit_and(index, index_mask)); + if (is_zero(mask)) + shard.map.erase(key); +} + +TEMPLATE +typename CLASS::entries CLASS::drain() NOEXCEPT +{ + size_t count{}; + for (const auto& shard: shards_) + count += shard.map.size(); + + entries out{}; + out.reserve(count); + for (auto& shard: shards_) + { + for (const auto& entry: shard.map) + out.emplace_back(entry.first, entry.second); + + shard.map.clear(); + } + + std::sort(out.begin(), out.end()); + return out; +} + +BC_POP_WARNING() + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/types/difference_set.hpp b/include/bitcoin/database/types/difference_set.hpp new file mode 100644 index 000000000..4c87bed87 --- /dev/null +++ b/include/bitcoin/database/types/difference_set.hpp @@ -0,0 +1,103 @@ +/** + * 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_DATABASE_TYPES_DIFFERENCE_SET_HPP +#define LIBBITCOIN_DATABASE_TYPES_DIFFERENCE_SET_HPP + +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace database { + +/// Multiset symmetric difference, exact and enumerable. Each presentation of +/// an element toggles its membership, so even multiplicities cancel. Elements +/// are (id, index) pairs, held as Mask-width index windows of an id, with the +/// id and window number packed into a 64 bit key. Thread safe. +template +class difference_set +{ +public: + DELETE_COPY_MOVE(difference_set); + + using entry = std::pair; + using entries = std::vector; + + static constexpr auto key_bits = bits; + static constexpr auto mask_bits = bits; + static constexpr auto window_bits = WindowBits; + static constexpr auto index_bits = system::floored_log2(mask_bits); + static_assert(window_bits < key_bits); + + static constexpr uint64_t to_key(uint64_t id, uint32_t index) NOEXCEPT + { + using namespace system; + return bit_or(shift_left(id, window_bits), + shift_right(index, index_bits)); + } + + static constexpr uint64_t to_id(uint64_t key) NOEXCEPT + { + return system::shift_right(key, window_bits); + } + + static constexpr uint64_t to_index(uint64_t key) NOEXCEPT + { + using namespace system; + return shift_left(bit_and(key, unmask_right(window_bits)), + index_bits); + } + + difference_set() NOEXCEPT = default; + ~difference_set() = default; + + /// Toggle element membership (thread safe). + void toggle(uint64_t id, uint32_t index) NOEXCEPT; + + /// Sorted odd-multiplicity elements, emptying the set (not thread safe). + entries drain() NOEXCEPT; + +private: + static constexpr auto shard_bits = 4_size; + static constexpr auto index_mask = + system::possible_narrow_cast(sub1(mask_bits)); + + struct shard + { + std::mutex mutex{}; + boost::unordered_flat_map map{}; + }; + + std::array shards_{}; +}; + +} // namespace database +} // namespace libbitcoin + +#define TEMPLATE template +#define CLASS difference_set + +#include + +#undef CLASS +#undef TEMPLATE + +#endif From 45794a86e6cd1e077710f39df83536a545c2c77b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 24 Aug 2026 20:55:08 -0400 Subject: [PATCH 3/4] Add unspent totals query. --- builds/gnu/Makefile.am | 14 +- .../libbitcoin-database-test.vcxproj | 7 +- .../libbitcoin-database-test.vcxproj.filters | 3 + .../libbitcoin-database.vcxproj | 4 + .../libbitcoin-database.vcxproj.filters | 45 ++-- .../libbitcoin-database-test.vcxproj | 7 +- .../libbitcoin-database-test.vcxproj.filters | 3 + .../libbitcoin-database.vcxproj | 4 + .../libbitcoin-database.vcxproj.filters | 45 ++-- include/bitcoin/database.hpp | 2 + .../bitcoin/database/impl/query/unspent.ipp | 224 ++++++++++++++++++ include/bitcoin/database/query.hpp | 15 ++ .../database/tables/archives/output.hpp | 29 +++ include/bitcoin/database/types/types.hpp | 2 + .../bitcoin/database/types/unspent_totals.hpp | 46 ++++ test/query/unspent.cpp | 82 +++++++ 16 files changed, 498 insertions(+), 34 deletions(-) create mode 100644 include/bitcoin/database/impl/query/unspent.ipp create mode 100644 include/bitcoin/database/types/unspent_totals.hpp create mode 100644 test/query/unspent.cpp diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index b6238fb5f..83fffbc66 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -134,7 +134,8 @@ include_bitcoin_database_impl_query_HEADERS = \ ${srcdir}/../../include/bitcoin/database/impl/query/properties_tx.ipp \ ${srcdir}/../../include/bitcoin/database/impl/query/query.ipp \ ${srcdir}/../../include/bitcoin/database/impl/query/sequences.ipp \ - ${srcdir}/../../include/bitcoin/database/impl/query/sizes.ipp + ${srcdir}/../../include/bitcoin/database/impl/query/sizes.ipp \ + ${srcdir}/../../include/bitcoin/database/impl/query/unspent.ipp include_bitcoin_database_impl_query_addressdir = \ ${includedir}/bitcoin/database/impl/query/address @@ -208,6 +209,12 @@ include_bitcoin_database_impl_store_HEADERS = \ ${srcdir}/../../include/bitcoin/database/impl/store/store_tables.ipp \ ${srcdir}/../../include/bitcoin/database/impl/store/store_unload_close.ipp +include_bitcoin_database_impl_typesdir = \ + ${includedir}/bitcoin/database/impl/types + +include_bitcoin_database_impl_types_HEADERS = \ + ${srcdir}/../../include/bitcoin/database/impl/types/difference_set.ipp + include_bitcoin_database_locksdir = \ ${includedir}/bitcoin/database/locks @@ -317,6 +324,7 @@ include_bitcoin_database_types_HEADERS = \ ${srcdir}/../../include/bitcoin/database/types/association.hpp \ ${srcdir}/../../include/bitcoin/database/types/associations.hpp \ ${srcdir}/../../include/bitcoin/database/types/block_state.hpp \ + ${srcdir}/../../include/bitcoin/database/types/difference_set.hpp \ ${srcdir}/../../include/bitcoin/database/types/fee_rate.hpp \ ${srcdir}/../../include/bitcoin/database/types/header_state.hpp \ ${srcdir}/../../include/bitcoin/database/types/history.hpp \ @@ -327,7 +335,8 @@ include_bitcoin_database_types_HEADERS = \ ${srcdir}/../../include/bitcoin/database/types/tx_state.hpp \ ${srcdir}/../../include/bitcoin/database/types/type.hpp \ ${srcdir}/../../include/bitcoin/database/types/types.hpp \ - ${srcdir}/../../include/bitcoin/database/types/unspent.hpp + ${srcdir}/../../include/bitcoin/database/types/unspent.hpp \ + ${srcdir}/../../include/bitcoin/database/types/unspent_totals.hpp # Tests. #============================================================================== @@ -392,6 +401,7 @@ test_libbitcoin_database_test_SOURCES = \ ${srcdir}/../../test/query/properties_tx.cpp \ ${srcdir}/../../test/query/sequences.cpp \ ${srcdir}/../../test/query/sizes.cpp \ + ${srcdir}/../../test/query/unspent.cpp \ ${srcdir}/../../test/query/address/address_balance.cpp \ ${srcdir}/../../test/query/address/address_history.cpp \ ${srcdir}/../../test/query/address/address_outpoints.cpp \ diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj index c9a26d1e5..b2f4bdab9 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -195,6 +195,9 @@ + + $(IntDir)test_query_unspent.obj + @@ -244,7 +247,9 @@ - + + $(IntDir)test_types_unspent.obj + diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index 5cbbc61a5..fdbd9b0d4 100644 --- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -255,6 +255,9 @@ src\query + + src\query + src diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj index 8dafce85b..4688b3b64 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj @@ -211,6 +211,7 @@ + @@ -222,6 +223,7 @@ + @@ -285,6 +287,7 @@ + @@ -300,6 +303,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters index 9a99d8c69..dbf332312 100644 --- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -49,51 +49,54 @@ {62D7FBEE-4D52-424A-0000-00000000000D} - + {62D7FBEE-4D52-424A-0000-00000000000E} - + {62D7FBEE-4D52-424A-0000-00000000000F} - + {62D7FBEE-4D52-424A-0000-000000000001} - + {62D7FBEE-4D52-424A-0000-000000000002} - + {62D7FBEE-4D52-424A-0000-000000000003} - + {62D7FBEE-4D52-424A-0000-000000000004} - + {62D7FBEE-4D52-424A-0000-000000000005} - + {62D7FBEE-4D52-424A-0000-000000000006} - + {62D7FBEE-4D52-424A-0000-000000000007} - + {62D7FBEE-4D52-424A-0000-000000000008} - + {62D7FBEE-4D52-424A-0000-000000000009} - + {62D7FBEE-4D52-424A-0000-000000000010} - + {62D7FBEE-4D52-424A-0000-0000000000A1} - + {62D7FBEE-4D52-424A-0000-0000000000B1} - + {62D7FBEE-4D52-424A-0000-0000000000C1} + + {62D7FBEE-4D52-424A-0000-0000000000D1} + @@ -350,6 +353,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database\types @@ -383,6 +389,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database @@ -568,6 +577,9 @@ include\bitcoin\database\impl\query + + include\bitcoin\database\impl\query + include\bitcoin\database\impl\store @@ -613,6 +625,9 @@ include\bitcoin\database\impl\store + + include\bitcoin\database\impl\types + diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj index 70424600b..2869b4756 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj @@ -195,6 +195,9 @@ + + $(IntDir)test_query_unspent.obj + @@ -244,7 +247,9 @@ - + + $(IntDir)test_types_unspent.obj + diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters index 5cbbc61a5..fdbd9b0d4 100644 --- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj.filters @@ -255,6 +255,9 @@ src\query + + src\query + src diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj index 9222bd248..5aa1ad631 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj @@ -211,6 +211,7 @@ + @@ -222,6 +223,7 @@ + @@ -285,6 +287,7 @@ + @@ -300,6 +303,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters index 9a99d8c69..dbf332312 100644 --- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters @@ -49,51 +49,54 @@ {62D7FBEE-4D52-424A-0000-00000000000D} - + {62D7FBEE-4D52-424A-0000-00000000000E} - + {62D7FBEE-4D52-424A-0000-00000000000F} - + {62D7FBEE-4D52-424A-0000-000000000001} - + {62D7FBEE-4D52-424A-0000-000000000002} - + {62D7FBEE-4D52-424A-0000-000000000003} - + {62D7FBEE-4D52-424A-0000-000000000004} - + {62D7FBEE-4D52-424A-0000-000000000005} - + {62D7FBEE-4D52-424A-0000-000000000006} - + {62D7FBEE-4D52-424A-0000-000000000007} - + {62D7FBEE-4D52-424A-0000-000000000008} - + {62D7FBEE-4D52-424A-0000-000000000009} - + {62D7FBEE-4D52-424A-0000-000000000010} - + {62D7FBEE-4D52-424A-0000-0000000000A1} - + {62D7FBEE-4D52-424A-0000-0000000000B1} - + {62D7FBEE-4D52-424A-0000-0000000000C1} + + {62D7FBEE-4D52-424A-0000-0000000000D1} + @@ -350,6 +353,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database\types @@ -383,6 +389,9 @@ include\bitcoin\database\types + + include\bitcoin\database\types + include\bitcoin\database @@ -568,6 +577,9 @@ include\bitcoin\database\impl\query + + include\bitcoin\database\impl\query + include\bitcoin\database\impl\store @@ -613,6 +625,9 @@ include\bitcoin\database\impl\store + + include\bitcoin\database\impl\types + diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp index b80167d39..ed11b0c6e 100644 --- a/include/bitcoin/database.hpp +++ b/include/bitcoin/database.hpp @@ -85,6 +85,7 @@ #include #include #include +#include #include #include #include @@ -96,5 +97,6 @@ #include #include #include +#include #endif diff --git a/include/bitcoin/database/impl/query/unspent.ipp b/include/bitcoin/database/impl/query/unspent.ipp new file mode 100644 index 000000000..4572db3e7 --- /dev/null +++ b/include/bitcoin/database/impl/query/unspent.ipp @@ -0,0 +1,224 @@ +/** + * 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_DATABASE_QUERY_UNSPENT_IPP +#define LIBBITCOIN_DATABASE_QUERY_UNSPENT_IPP + +#include +#include +#include + +namespace libbitcoin { +namespace database { + +// Unspent. +// ---------------------------------------------------------------------------- + +// The unspent set is the symmetric difference of creates and spends over +// the branch: paired events cancel and the residue is the utxo set. +static_assert(tx_link::bits <= bits - difference_set<>::window_bits); + +TEMPLATE +bool CLASS::is_bip30_exception(bool& out, + const header_link& link) const NOEXCEPT +{ + using namespace system; + + static const chain::checkpoint first + { + "00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec", + 91842 + }; + + static const chain::checkpoint second + { + "00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721", + 91880 + }; + + out = false; + size_t height{}; + if (!get_height(height, link)) + return false; + + if (height != first.height() && height != second.height()) + return true; + + // With the rule unconfigured duplicates are unconstrained (no exception). + if (!script::is_enabled(initialized_forks(), chain::flags::bip30_rule)) + return true; + + const auto key = get_header_key(link); + out = key == (height == first.height() ? first.hash() : second.hash()); + return true; +} + +TEMPLATE +code CLASS::scan_unspent(difference_set<>& set, const stopper& cancel, + const header_links& branch, bool turbo) const NOEXCEPT +{ + using namespace system; + std::atomic_bool fail{}; + const auto parallel = poolstl::execution::par_if(turbo); + + // Reverse order in best effort attempt to minimize set size. + std::for_each(parallel, branch.crbegin(), branch.crend(), + [&](const auto& element) NOEXCEPT + { + if (cancel || fail) + return; + + auto bip30_exception = false; + const header_link link{ element }; + if (!is_bip30_exception(bip30_exception, link)) + { + fail = true; + return; + } + + // The bip30 exception blocks are coinbase only. + if (bip30_exception) + return; + + auto coinbase = true; + table::output::get_spendable value{}; + for (const auto& tx: to_transactions(link)) + { + if (cancel || fail) + return; + + uint32_t index{}; + for (const auto& out: to_outputs(tx)) + { + if (!store_.output.get(out, value)) + { + fail = true; + return; + } + + if (!value.unspendable) + set.toggle(tx, index); + + ++index; + } + + if (coinbase) + { + coinbase = false; + continue; + } + + for (const auto& in: to_points(tx)) + { + const auto point = get_point_key(in); + auto it = store_.tx.it(point.hash()); + if (!it) + { + fail = true; + return; + } + + // Duplicate txs require the confirmed instance. + auto spent = *it; + if (++it && !is_confirmed_tx(spent)) + { + for (spent = tx_link::terminal; it; ++it) + { + if (is_confirmed_tx(*it)) + { + spent = *it; + break; + } + } + + if (spent.is_terminal()) + { + fail = true; + return; + } + } + + set.toggle(spent, point.index()); + } + } + }); + + if (fail) + return error::integrity; + + if (cancel) + return error::query_canceled; + + return error::success; +} + +TEMPLATE +code CLASS::count_unspent(unspent_totals& out, const stopper& cancel, + const difference_set<>::entries& survivors) const NOEXCEPT +{ + using namespace system; + + out = {}; + output_links puts{}; + auto previous = max_uint64; + table::output::get_spendable value{}; + for (const auto& [key, mask]: survivors) + { + if (cancel) + return error::query_canceled; + + const auto tx = difference_set<>::to_id(key); + if (tx != previous) + { + previous = tx; + ++out.transactions; + puts = to_outputs(possible_narrow_cast(tx)); + } + + const auto base = difference_set<>::to_index(key); + for (auto bits = mask; !is_zero(bits); + bits = bit_and(bits, sub1(bits))) + { + // A surviving spend with no create implies store inconsistency. + const auto index = base + right_zeros(bits); + if (index >= puts.size() || + !store_.output.get(puts[index], value)) + return error::integrity; + + ++out.outputs; + out.value += value.value; + out.script_bytes += value.script_size; + } + } + + return error::success; +} + +TEMPLATE +code CLASS::get_unspent_totals(const stopper& cancel, unspent_totals& out, + const header_links& branch, bool turbo) const NOEXCEPT +{ + difference_set<> set{}; + const auto ec = scan_unspent(set, cancel, branch, turbo); + return ec ? ec : count_unspent(out, cancel, set.drain()); +} + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index e5639aa48..503a0208d 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -768,6 +768,13 @@ class query bool set_filter_head(const header_link& link, const hash_digest& head, const hash_digest& hash) NOEXCEPT; + /// Unspent. + /// ----------------------------------------------------------------------- + + /// Unspent output totals over the branch (administrative scan). + code get_unspent_totals(const stopper& cancel, unspent_totals& out, + const header_links& branch, bool turbo=false) const NOEXCEPT; + protected: /// Network /// ----------------------------------------------------------------------- @@ -968,6 +975,13 @@ class query static point::cptr make_point(hash_digest&& hash, uint32_t index) NOEXCEPT; + bool is_bip30_exception(bool& out, + const header_link& link) const NOEXCEPT; + code scan_unspent(difference_set<>& set, const stopper& cancel, + const header_links& branch, bool turbo) const NOEXCEPT; + code count_unspent(unspent_totals& out, const stopper& cancel, + const difference_set<>::entries& survivors) const NOEXCEPT; + // Not thread safe. size_t get_fork_() const NOEXCEPT; @@ -1032,6 +1046,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) #include #include #include +#include BC_POP_WARNING() diff --git a/include/bitcoin/database/tables/archives/output.hpp b/include/bitcoin/database/tables/archives/output.hpp index 0f9c74211..e85e68f99 100644 --- a/include/bitcoin/database/tables/archives/output.hpp +++ b/include/bitcoin/database/tables/archives/output.hpp @@ -153,6 +153,35 @@ struct output bool match{}; }; + struct get_spendable + : public schema::output + { + inline link count() const NOEXCEPT + { + BC_ASSERT(false); + return {}; + } + + inline bool from_data(reader& source) NOEXCEPT + { + using namespace system; + source.skip_bytes(tx::size); + value = source.read_variable(); + script_size = possible_narrow_cast( + source.read_variable()); + + // bitcoind's utxo set exclusion (op_return lead or oversized). + constexpr auto op_return = to_value(chain::opcode::op_return); + unspendable = (script_size > chain::max_script_size) || + (!is_zero(script_size) && (source.read_byte() == op_return)); + return source; + } + + uint64_t value{}; + size_t script_size{}; + bool unspendable{}; + }; + struct get_parent_value : public schema::output { diff --git a/include/bitcoin/database/types/types.hpp b/include/bitcoin/database/types/types.hpp index 9913332d8..feb2ed375 100644 --- a/include/bitcoin/database/types/types.hpp +++ b/include/bitcoin/database/types/types.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -32,5 +33,6 @@ #include #include #include +#include #endif diff --git a/include/bitcoin/database/types/unspent_totals.hpp b/include/bitcoin/database/types/unspent_totals.hpp new file mode 100644 index 000000000..c30037b4c --- /dev/null +++ b/include/bitcoin/database/types/unspent_totals.hpp @@ -0,0 +1,46 @@ +/** + * 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_DATABASE_TYPES_UNSPENT_TOTALS_HPP +#define LIBBITCOIN_DATABASE_TYPES_UNSPENT_TOTALS_HPP + +#include + +namespace libbitcoin { +namespace database { + +/// Spendable unspent output totals, accumulated over a branch scan. +struct BCD_API unspent_totals +{ + /// Unspent outputs. + size_t outputs{}; + + /// Transactions with at least one unspent output. + size_t transactions{}; + + /// Total serialized script size of unspent outputs. + size_t script_bytes{}; + + /// Total value of unspent outputs. + uint64_t value{}; +}; + +} // namespace database +} // namespace libbitcoin + +#endif diff --git a/test/query/unspent.cpp b/test/query/unspent.cpp new file mode 100644 index 000000000..ef4191928 --- /dev/null +++ b/test/query/unspent.cpp @@ -0,0 +1,82 @@ +/** + * 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" +#include "../mocks/blocks.hpp" +#include "../mocks/chunk_store.hpp" + +BOOST_FIXTURE_TEST_SUITE(query_unspent_tests, test::directory_setup_fixture) + +BOOST_AUTO_TEST_CASE(query_unspent__get_unspent_totals__empty_branch__success_zeros) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + + const stopper cancel{}; + unspent_totals totals{}; + BOOST_REQUIRE(!query.get_unspent_totals(cancel, totals, {})); + BOOST_REQUIRE_EQUAL(totals.outputs, 0u); + BOOST_REQUIRE_EQUAL(totals.transactions, 0u); + BOOST_REQUIRE_EQUAL(totals.script_bytes, 0u); + BOOST_REQUIRE_EQUAL(totals.value, 0u); +} + +BOOST_AUTO_TEST_CASE(query_unspent__get_unspent_totals__two_block_branch__expected) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + 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)); + + // Each block is one coinbase with one unspent p2pk output (67 bytes). + const stopper cancel{}; + unspent_totals totals{}; + const header_links branch{ header_link{ 2 }, header_link{ 1 } }; + BOOST_REQUIRE(!query.get_unspent_totals(cancel, totals, branch)); + BOOST_REQUIRE_EQUAL(totals.outputs, 2u); + BOOST_REQUIRE_EQUAL(totals.transactions, 2u); + BOOST_REQUIRE_EQUAL(totals.script_bytes, 134u); + BOOST_REQUIRE_EQUAL(totals.value, 10'000'000'000u); +} + +BOOST_AUTO_TEST_CASE(query_unspent__get_unspent_totals__cancelled__query_canceled) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE(!store.create(test::events_handler)); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1, context{ 0, 1, 0 }, false, false)); + + const stopper cancel{ true }; + unspent_totals totals{}; + const header_links branch{ header_link{ 1 } }; + const auto ec = query.get_unspent_totals(cancel, totals, branch); + BOOST_REQUIRE_EQUAL(ec, error::query_canceled); +} + +BOOST_AUTO_TEST_SUITE_END() From 839dd6911784c2ecbd463b0e292c96f329b1ca35 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 25 Aug 2026 11:30:42 -0400 Subject: [PATCH 4/4] Style. --- include/bitcoin/database/impl/types/difference_set.ipp | 1 + include/bitcoin/database/types/difference_set.hpp | 6 +++--- include/bitcoin/database/types/unspent.hpp | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/bitcoin/database/impl/types/difference_set.ipp b/include/bitcoin/database/impl/types/difference_set.ipp index 7f53e0f7d..b80bdd7c4 100644 --- a/include/bitcoin/database/impl/types/difference_set.ipp +++ b/include/bitcoin/database/impl/types/difference_set.ipp @@ -35,6 +35,7 @@ void CLASS::toggle(uint64_t id, uint32_t index) NOEXCEPT constexpr auto golden = 0x9e3779b97f4a7c15_u64; const auto key = to_key(id, index); auto& shard = shards_[shift_right(key * golden, key_bits - shard_bits)]; + const std::lock_guard lock{ shard.mutex }; auto& mask = shard.map[key]; mask ^= bit_right(bit_and(index, index_mask)); diff --git a/include/bitcoin/database/types/difference_set.hpp b/include/bitcoin/database/types/difference_set.hpp index 4c87bed87..e7ed7e284 100644 --- a/include/bitcoin/database/types/difference_set.hpp +++ b/include/bitcoin/database/types/difference_set.hpp @@ -19,7 +19,6 @@ #ifndef LIBBITCOIN_DATABASE_TYPES_DIFFERENCE_SET_HPP #define LIBBITCOIN_DATABASE_TYPES_DIFFERENCE_SET_HPP -#include #include #include #include @@ -77,8 +76,8 @@ class difference_set private: static constexpr auto shard_bits = 4_size; - static constexpr auto index_mask = - system::possible_narrow_cast(sub1(mask_bits)); + static constexpr auto index_mask = system::possible_narrow_cast( + sub1(mask_bits)); struct shard { @@ -86,6 +85,7 @@ class difference_set boost::unordered_flat_map map{}; }; + // This is guarded by shard::mutex. std::array shards_{}; }; diff --git a/include/bitcoin/database/types/unspent.hpp b/include/bitcoin/database/types/unspent.hpp index 08709646e..154a1d9bf 100644 --- a/include/bitcoin/database/types/unspent.hpp +++ b/include/bitcoin/database/types/unspent.hpp @@ -45,7 +45,7 @@ struct BCD_API unspent /// Comparison operator based on Electrum unspent status sort. bool operator<(const unspent& other) const NOEXCEPT; - /// Equivalence: !LT && !GT (note that fee is never considered). + /// Equivalence: !LT && !GT. bool operator==(const unspent& other) const NOEXCEPT; /// Tx hash and index of output within the tx.