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/impl/types/difference_set.ipp b/include/bitcoin/database/impl/types/difference_set.ipp
new file mode 100644
index 000000000..b80bdd7c4
--- /dev/null
+++ b/include/bitcoin/database/impl/types/difference_set.ipp
@@ -0,0 +1,72 @@
+/**
+ * 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/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/difference_set.hpp b/include/bitcoin/database/types/difference_set.hpp
new file mode 100644
index 000000000..e7ed7e284
--- /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
+
+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{};
+ };
+
+ // This is guarded by shard::mutex.
+ std::array shards_{};
+};
+
+} // namespace database
+} // namespace libbitcoin
+
+#define TEMPLATE template
+#define CLASS difference_set
+
+#include
+
+#undef CLASS
+#undef TEMPLATE
+
+#endif
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.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.
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/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;
}
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()