diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am
index 3a3819cae..f8143b859 100644
--- a/builds/gnu/Makefile.am
+++ b/builds/gnu/Makefile.am
@@ -58,10 +58,10 @@ src_libbitcoin_database_la_SOURCES = \
${srcdir}/../../src/memory/mman.cpp \
${srcdir}/../../src/memory/mstage.cpp \
${srcdir}/../../src/memory/utilities.cpp \
- ${srcdir}/../../src/types/envelope.cpp \
+ ${srcdir}/../../src/tables/envelope.cpp \
${srcdir}/../../src/types/history.cpp \
${srcdir}/../../src/types/multisig_view.cpp \
- ${srcdir}/../../src/types/unspent.cpp
+ ${srcdir}/../../src/types/unspent_output.cpp
include_bitcoindir = \
${includedir}/bitcoin
@@ -85,6 +85,7 @@ include_bitcoin_database_filedir = \
${includedir}/bitcoin/database/file
include_bitcoin_database_file_HEADERS = \
+ ${srcdir}/../../include/bitcoin/database/file/advice.hpp \
${srcdir}/../../include/bitcoin/database/file/file.hpp \
${srcdir}/../../include/bitcoin/database/file/rotator.hpp \
${srcdir}/../../include/bitcoin/database/file/utilities.hpp
@@ -210,11 +211,18 @@ 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_unspentdir = \
+ ${includedir}/bitcoin/database/impl/unspent
-include_bitcoin_database_impl_types_HEADERS = \
- ${srcdir}/../../include/bitcoin/database/impl/types/difference_set.ipp
+include_bitcoin_database_impl_unspent_HEADERS = \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_counter.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_matcher.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_muhash.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_reader.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_scanner.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_serial.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_spans.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/unspent/unspent_writer.ipp
include_bitcoin_database_locksdir = \
${includedir}/bitcoin/database/locks
@@ -237,6 +245,7 @@ include_bitcoin_database_memory_HEADERS = \
${srcdir}/../../include/bitcoin/database/memory/mmaps.hpp \
${srcdir}/../../include/bitcoin/database/memory/mstage.hpp \
${srcdir}/../../include/bitcoin/database/memory/reader.hpp \
+ ${srcdir}/../../include/bitcoin/database/memory/release.hpp \
${srcdir}/../../include/bitcoin/database/memory/settings.hpp \
${srcdir}/../../include/bitcoin/database/memory/streamers.hpp \
${srcdir}/../../include/bitcoin/database/memory/utilities.hpp
@@ -272,6 +281,7 @@ include_bitcoin_database_tablesdir = \
include_bitcoin_database_tables_HEADERS = \
${srcdir}/../../include/bitcoin/database/tables/context.hpp \
+ ${srcdir}/../../include/bitcoin/database/tables/envelope.hpp \
${srcdir}/../../include/bitcoin/database/tables/event.hpp \
${srcdir}/../../include/bitcoin/database/tables/names.hpp \
${srcdir}/../../include/bitcoin/database/tables/schema.hpp \
@@ -326,8 +336,8 @@ include_bitcoin_database_types_HEADERS = \
${srcdir}/../../include/bitcoin/database/types/associations.hpp \
${srcdir}/../../include/bitcoin/database/types/block_amounts.hpp \
${srcdir}/../../include/bitcoin/database/types/block_state.hpp \
+ ${srcdir}/../../include/bitcoin/database/types/constants.hpp \
${srcdir}/../../include/bitcoin/database/types/difference_set.hpp \
- ${srcdir}/../../include/bitcoin/database/types/envelope.hpp \
${srcdir}/../../include/bitcoin/database/types/fee_rate.hpp \
${srcdir}/../../include/bitcoin/database/types/header_state.hpp \
${srcdir}/../../include/bitcoin/database/types/history.hpp \
@@ -338,10 +348,24 @@ 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_coin.hpp \
+ ${srcdir}/../../include/bitcoin/database/types/unspent_output.hpp \
${srcdir}/../../include/bitcoin/database/types/unspent_totals.hpp
+include_bitcoin_database_unspentdir = \
+ ${includedir}/bitcoin/database/unspent
+
+include_bitcoin_database_unspent_HEADERS = \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_counter.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_matcher.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_muhash.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_reader.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_scanner.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_serial.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_spans.hpp \
+ ${srcdir}/../../include/bitcoin/database/unspent/unspent_writer.hpp
+
# Tests.
#==============================================================================
# Target test 'test/libbitcoin-database-test'
@@ -378,6 +402,7 @@ test_libbitcoin_database_test_SOURCES = \
${srcdir}/../../test/locks/interprocess_lock.cpp \
${srcdir}/../../test/memory/accessor.cpp \
${srcdir}/../../test/memory/mmap.cpp \
+ ${srcdir}/../../test/memory/release.cpp \
${srcdir}/../../test/memory/utilities.cpp \
${srcdir}/../../test/mocks/blocks.cpp \
${srcdir}/../../test/primitives/arrayhead.cpp \
@@ -468,9 +493,11 @@ test_libbitcoin_database_test_SOURCES = \
${srcdir}/../../test/tables/optional/address.cpp \
${srcdir}/../../test/tables/optional/filter_bk.cpp \
${srcdir}/../../test/tables/optional/filter_tx.cpp \
+ ${srcdir}/../../test/types/difference_set.cpp \
${srcdir}/../../test/types/history.cpp \
${srcdir}/../../test/types/span.cpp \
- ${srcdir}/../../test/types/unspent.cpp
+ ${srcdir}/../../test/types/unspent_output.cpp \
+ ${srcdir}/../../test/unspent/unspent_commitment.cpp
TESTS = test_runner.sh
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 b2f4bdab9..c58402f96 100644
--- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj
@@ -129,6 +129,7 @@
+
$(IntDir)test_memory_utilities.obj
@@ -195,9 +196,7 @@
-
- $(IntDir)test_query_unspent.obj
-
+
@@ -245,11 +244,11 @@
+
-
- $(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 fdbd9b0d4..67f016b52 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
@@ -64,6 +64,9 @@
{5C4DA53A-8C06-4DA6-0000-000000000003}
+
+ {5C4DA53A-8C06-4DA6-0000-000000000004}
+
@@ -93,6 +96,9 @@
src\memory
+
+ src\memory
+
src\memory
@@ -369,15 +375,21 @@
src
+
+ src\types
+
src\types
src\types
-
+
src\types
+
+ src\unspent
+
diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
index 570e9b435..525c9929d 100644
--- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
@@ -136,16 +136,17 @@
$(IntDir)src_memory_utilities.obj
-
+
-
+
+
@@ -162,6 +163,7 @@
+
@@ -199,6 +201,7 @@
+
@@ -213,8 +216,8 @@
+
-
@@ -225,9 +228,18 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -307,7 +319,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
index c8b684323..67a4cf5a4 100644
--- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
@@ -49,7 +49,7 @@
{62D7FBEE-4D52-424A-0000-00000000000D}
-
+
{62D7FBEE-4D52-424A-0000-00000000000E}
@@ -82,21 +82,27 @@
{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}
+
+ {62D7FBEE-4D52-424A-0000-0000000000E1}
+
+
+ {62D7FBEE-4D52-424A-0000-0000000000F1}
+
@@ -132,8 +138,8 @@
src
-
- src\types
+
+ src\tables
src\types
@@ -141,7 +147,7 @@
src\types
-
+
src\types
@@ -158,6 +164,9 @@
include\bitcoin\database
+
+ include\bitcoin\database\file
+
include\bitcoin\database\file
@@ -206,6 +215,9 @@
include\bitcoin\database\memory
+
+ include\bitcoin\database\memory
+
include\bitcoin\database\memory
@@ -317,6 +329,9 @@
include\bitcoin\database\tables
+
+ include\bitcoin\database\tables
+
include\bitcoin\database\tables
@@ -359,10 +374,10 @@
include\bitcoin\database\types
-
+
include\bitcoin\database\types
-
+
include\bitcoin\database\types
@@ -395,15 +410,42 @@
include\bitcoin\database\types
-
+
include\bitcoin\database\types
-
+
include\bitcoin\database\types
include\bitcoin\database\types
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
include\bitcoin\database
@@ -637,8 +679,29 @@
include\bitcoin\database\impl\store
-
- include\bitcoin\database\impl\types
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
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 2869b4756..46d57736e 100644
--- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
@@ -129,6 +129,7 @@
+
$(IntDir)test_memory_utilities.obj
@@ -195,9 +196,7 @@
-
- $(IntDir)test_query_unspent.obj
-
+
@@ -245,11 +244,11 @@
+
-
- $(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 fdbd9b0d4..67f016b52 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
@@ -64,6 +64,9 @@
{5C4DA53A-8C06-4DA6-0000-000000000003}
+
+ {5C4DA53A-8C06-4DA6-0000-000000000004}
+
@@ -93,6 +96,9 @@
src\memory
+
+ src\memory
+
src\memory
@@ -369,15 +375,21 @@
src
+
+ src\types
+
src\types
src\types
-
+
src\types
+
+ src\unspent
+
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
index c2f545055..0c7659c7b 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
@@ -136,16 +136,17 @@
$(IntDir)src_memory_utilities.obj
-
+
-
+
+
@@ -162,6 +163,7 @@
+
@@ -199,6 +201,7 @@
+
@@ -213,8 +216,8 @@
+
-
@@ -225,9 +228,18 @@
-
+
+
+
+
+
+
+
+
+
+
@@ -307,7 +319,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
index c8b684323..67a4cf5a4 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
@@ -49,7 +49,7 @@
{62D7FBEE-4D52-424A-0000-00000000000D}
-
+
{62D7FBEE-4D52-424A-0000-00000000000E}
@@ -82,21 +82,27 @@
{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}
+
+ {62D7FBEE-4D52-424A-0000-0000000000E1}
+
+
+ {62D7FBEE-4D52-424A-0000-0000000000F1}
+
@@ -132,8 +138,8 @@
src
-
- src\types
+
+ src\tables
src\types
@@ -141,7 +147,7 @@
src\types
-
+
src\types
@@ -158,6 +164,9 @@
include\bitcoin\database
+
+ include\bitcoin\database\file
+
include\bitcoin\database\file
@@ -206,6 +215,9 @@
include\bitcoin\database\memory
+
+ include\bitcoin\database\memory
+
include\bitcoin\database\memory
@@ -317,6 +329,9 @@
include\bitcoin\database\tables
+
+ include\bitcoin\database\tables
+
include\bitcoin\database\tables
@@ -359,10 +374,10 @@
include\bitcoin\database\types
-
+
include\bitcoin\database\types
-
+
include\bitcoin\database\types
@@ -395,15 +410,42 @@
include\bitcoin\database\types
-
+
include\bitcoin\database\types
-
+
include\bitcoin\database\types
include\bitcoin\database\types
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
+
+ include\bitcoin\database\unspent
+
include\bitcoin\database
@@ -637,8 +679,29 @@
include\bitcoin\database\impl\store
-
- include\bitcoin\database\impl\types
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
+
+
+ include\bitcoin\database\impl\unspent
diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp
index 94a9bfef0..4f9b1ed90 100644
--- a/include/bitcoin/database.hpp
+++ b/include/bitcoin/database.hpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -37,6 +38,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -57,6 +59,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -86,8 +89,8 @@
#include
#include
#include
+#include
#include
-#include
#include
#include
#include
@@ -98,8 +101,17 @@
#include
#include
#include
-#include
#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#endif
diff --git a/include/bitcoin/database/error.hpp b/include/bitcoin/database/error.hpp
index e78813b4c..a8f677a2b 100644
--- a/include/bitcoin/database/error.hpp
+++ b/include/bitcoin/database/error.hpp
@@ -140,6 +140,7 @@ enum error_t : uint8_t
depth_limited,
invalid_cursor,
query_canceled,
+ branch_inactive,
invalid_argument,
missing_prevouts,
merkle_proof,
diff --git a/include/bitcoin/database/file/advice.hpp b/include/bitcoin/database/file/advice.hpp
new file mode 100644
index 000000000..d973fb6bf
--- /dev/null
+++ b/include/bitcoin/database/file/advice.hpp
@@ -0,0 +1,51 @@
+/**
+ * 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_FILE_ADVICE_HPP
+#define LIBBITCOIN_DATABASE_FILE_ADVICE_HPP
+
+#include
+
+namespace libbitcoin {
+namespace database {
+
+/// Expected read pattern of mapped storage, guiding kernel page advice. This
+/// is independent of the write pattern (structural): bodies are appended
+/// sequentially but read randomly by validation, so advising the kernel from
+/// the write pattern invites eviction of the read set under memory pressure.
+enum class advice : uint8_t
+{
+ /// Let the operating system decide (mixed or unpredictable access).
+ normal,
+
+ /// Random access (suppresses read ahead), preloaded (small maps only).
+ random,
+
+ /// One pass access (allows the kernel to free pages behind).
+ sequential,
+
+ /// Random access (suppresses read ahead) without preload, for maps too
+ /// large to reside: scattered reads otherwise fault a full read-ahead
+ /// window each, and that manufactured cache displaces the head set.
+ scattered
+};
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/file/file.hpp b/include/bitcoin/database/file/file.hpp
index c25ee580d..93ac5ec86 100644
--- a/include/bitcoin/database/file/file.hpp
+++ b/include/bitcoin/database/file/file.hpp
@@ -19,6 +19,7 @@
#ifndef LIBBITCOIN_DATABASE_FILE_FILE_HPP
#define LIBBITCOIN_DATABASE_FILE_FILE_HPP
+#include
#include
#include
diff --git a/include/bitcoin/database/file/utilities.hpp b/include/bitcoin/database/file/utilities.hpp
index c1b21151a..deb25c49c 100644
--- a/include/bitcoin/database/file/utilities.hpp
+++ b/include/bitcoin/database/file/utilities.hpp
@@ -21,7 +21,7 @@
#include
#include
-#include
+#include
namespace libbitcoin {
namespace database {
diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp
index a5602bd7c..b94b16525 100644
--- a/include/bitcoin/database/impl/memory/mmap_staging.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp
@@ -28,6 +28,7 @@
#endif
#include
#include
+#include
#include
namespace libbitcoin {
diff --git a/include/bitcoin/database/impl/primitives/hashhead.ipp b/include/bitcoin/database/impl/primitives/hashhead.ipp
index d88b280c6..c901ed1fe 100644
--- a/include/bitcoin/database/impl/primitives/hashhead.ipp
+++ b/include/bitcoin/database/impl/primitives/hashhead.ipp
@@ -109,6 +109,41 @@ bool CLASS::set_filter_k(size_t k) NOEXCEPT
}
}
+TEMPLATE
+uint32_t CLASS::derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT
+{
+ using namespace system;
+ constexpr auto gigabyte = power2(30u);
+ constexpr auto megabyte = power2(20u);
+ constexpr auto uncontested = power2(35u);
+#if defined(HAVE_APPLE)
+ constexpr auto contested = 10u * gigabyte;
+#else
+ constexpr auto contested = 8u * gigabyte;
+#endif
+
+ if (is_zero(expected) || is_zero(low) || is_zero(high))
+ return {};
+
+ const auto scaled = ceilinged_multiply(expected, 10);
+ const auto floored = scaled / low;
+ const auto ceiled = scaled / high;
+ const auto memory = system_memory();
+
+ if (memory <= contested)
+ return possible_narrow_cast(floored);
+
+ if (memory >= uncontested)
+ return possible_narrow_cast(ceiled);
+
+ // Megabytes, as the byte product overflows the word.
+ const auto over = (memory - contested) / megabyte;
+ const auto rise = floored_subtract(ceiled, floored);
+ constexpr auto span = (uncontested - contested) / megabyte;
+ return limit(floored + (ceilinged_multiply(rise, over) / span));
+}
+
TEMPLATE
size_t CLASS::optimal_k(size_t count, size_t buckets) NOEXCEPT
{
diff --git a/include/bitcoin/database/impl/primitives/hashmap.ipp b/include/bitcoin/database/impl/primitives/hashmap.ipp
index 523abe14a..6998e66f0 100644
--- a/include/bitcoin/database/impl/primitives/hashmap.ipp
+++ b/include/bitcoin/database/impl/primitives/hashmap.ipp
@@ -87,6 +87,13 @@ size_t CLASS::buckets() const NOEXCEPT
return head_.buckets();
}
+TEMPLATE
+uint32_t CLASS::derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT
+{
+ return head::derive_buckets(expected, low, high);
+}
+
TEMPLATE
size_t CLASS::filter_k() const NOEXCEPT
{
diff --git a/include/bitcoin/database/impl/primitives/hashmaps.ipp b/include/bitcoin/database/impl/primitives/hashmaps.ipp
index 75d33b65b..ad845e0c6 100644
--- a/include/bitcoin/database/impl/primitives/hashmaps.ipp
+++ b/include/bitcoin/database/impl/primitives/hashmaps.ipp
@@ -85,6 +85,13 @@ size_t CLASS::buckets() const NOEXCEPT
return head_.buckets();
}
+TEMPLATE
+uint32_t CLASS::derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT
+{
+ return head::derive_buckets(expected, low, high);
+}
+
TEMPLATE
size_t CLASS::filter_k() const NOEXCEPT
{
diff --git a/include/bitcoin/database/impl/primitives/headmap.ipp b/include/bitcoin/database/impl/primitives/headmap.ipp
index fc2393cea..8d56949c9 100644
--- a/include/bitcoin/database/impl/primitives/headmap.ipp
+++ b/include/bitcoin/database/impl/primitives/headmap.ipp
@@ -164,6 +164,44 @@ Link CLASS::at(size_t index) const NOEXCEPT
}
}
+TEMPLATE
+memory CLASS::get_memory() const NOEXCEPT
+{
+ return file_.get();
+}
+
+TEMPLATE
+Link CLASS::at(const memory& ptr, size_t index) const NOEXCEPT
+{
+ using namespace system;
+
+ // Buckets at or above the logical size are unallocated (count publishes).
+ if (index >= count())
+ return {};
+
+ const auto raw = ptr.offset(link_to_position(index));
+ if (is_null(raw))
+ return {};
+
+ if constexpr (aligned)
+ {
+ // Reads full padded word (masked by to_link).
+ const auto& head = *pointer_cast>(raw);
+ return to_link(head.load(std::memory_order_relaxed));
+ }
+ else
+ {
+ const auto& head = cell_array(raw);
+ cell value{};
+
+ mutex_.lock_shared();
+ cell_array(value) = head;
+ mutex_.unlock_shared();
+
+ return to_link(value);
+ }
+}
+
// NOT WRITER-WRITER THREAD SAFE (the logical top is read-write).
TEMPLATE
bool CLASS::push(const Link& link) NOEXCEPT
diff --git a/include/bitcoin/database/impl/query/address/address_unspent.ipp b/include/bitcoin/database/impl/query/address/address_unspent.ipp
index a291610e5..50a2c2e23 100644
--- a/include/bitcoin/database/impl/query/address/address_unspent.ipp
+++ b/include/bitcoin/database/impl/query/address/address_unspent.ipp
@@ -35,7 +35,7 @@ namespace database {
// ununsed
// Only unconfirmed outputs that are not spent (by an unconfirmed tx).
TEMPLATE
-code CLASS::get_unconfirmed_unspent(const stopper& cancel, unspents& out,
+code CLASS::get_unconfirmed_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo) const NOEXCEPT
{
output_links outs{};
@@ -51,7 +51,7 @@ code CLASS::get_unconfirmed_unspent(const stopper& cancel, unspents& out,
// this is only unconfirmeds, but checking both via is_spent() is
// much faster than calling is_unconfirmed_spent().
if (cancel || fail || is_spent(link))
- return unspent{};
+ return unspent_output{};
const auto out = get_tx_unconfirmed_unspent(link);
if (out.fault()) fail = true;
@@ -62,7 +62,7 @@ code CLASS::get_unconfirmed_unspent(const stopper& cancel, unspents& out,
// ununsed
// Only confirmed outputs that are not spent by a confirmed tx.
TEMPLATE
-code CLASS::get_confirmed_unspent(const stopper& cancel, unspents& out,
+code CLASS::get_confirmed_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo) const NOEXCEPT
{
output_links outs{};
@@ -76,7 +76,7 @@ code CLASS::get_confirmed_unspent(const stopper& cancel, unspents& out,
{
// Exclude if spent by confirmed tx, ignore unconfirmed spenders.
if (cancel || fail || is_confirmed_spent(link))
- return unspent{};
+ return unspent_output{};
const auto out = get_tx_confirmed_unspent(link);
if (out.fault()) fail = true;
@@ -87,7 +87,7 @@ code CLASS::get_confirmed_unspent(const stopper& cancel, unspents& out,
// server/electrum
// All outputs that are not spent by any tx (confirmed or unconfirmed).
TEMPLATE
-code CLASS::get_unspent(const stopper& cancel, unspents& out,
+code CLASS::get_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo) const NOEXCEPT
{
output_links outs{};
@@ -101,7 +101,7 @@ code CLASS::get_unspent(const stopper& cancel, unspents& out,
{
// Exclude if spent by any tx, confirmed or unconfirmed.
if (cancel || fail || is_spent(link))
- return unspent{};
+ return unspent_output{};
auto out = get_tx_unspent(link);
if (out.fault()) fail = true;
@@ -113,7 +113,7 @@ code CLASS::get_unspent(const stopper& cancel, unspents& out,
// ----------------------------------------------------------------------------
TEMPLATE
-unspent CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT
+unspent_output CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT
{
table::output::get_parent_value out{};
if (!store_.output.get(link, out))
@@ -125,8 +125,8 @@ unspent CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT
if ((index == point::null_index) || (hash == system::null_hash))
return {};
- auto height = unspent::unused_height;
- auto position = unspent::unconfirmed_position;
+ auto height = unspent_output::unused_height;
+ auto position = unspent_output::unconfirmed_position;
const auto block = find_strong(tx);
const auto at = get_confirmed_height(block);
@@ -141,9 +141,9 @@ unspent CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT
}
TEMPLATE
-unspent CLASS::get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT
+unspent_output CLASS::get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT
{
- // unspent is invalid in default construction with position.
+ // unspent_output is invalid in default construction with position.
table::output::get_parent_value out{};
if (!store_.output.get(link, out))
return {};
@@ -154,7 +154,7 @@ unspent CLASS::get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT
// Invalid with unconfirmed_position signals no fault.
if (at.is_terminal())
- return { .position = unspent::unconfirmed_position };
+ return { .position = unspent_output::unconfirmed_position };
size_t position{};
auto hash = get_tx_key(tx);
@@ -167,9 +167,9 @@ unspent CLASS::get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT
}
TEMPLATE
-unspent CLASS::get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEPT
+unspent_output CLASS::get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEPT
{
- // unspent is invalid in default construction with position.
+ // unspent_output is invalid in default construction with position.
table::output::get_parent_value out{};
if (!store_.output.get(link, out))
return {};
@@ -178,15 +178,15 @@ unspent CLASS::get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEP
// Invalid with unconfirmed_position signals no fault.
if (!get_confirmed_height(find_strong(tx)).is_terminal())
- return { .position = unspent::unconfirmed_position };
+ return { .position = unspent_output::unconfirmed_position };
auto hash = get_tx_key(tx);
const auto index = to_output_index(tx, link);
if ((index == point::null_index) || (hash == system::null_hash))
return {};
- return { { { std::move(hash), index }, out.value }, unspent::unused_height,
- unspent::unconfirmed_position };
+ return { { { std::move(hash), index }, out.value }, unspent_output::unused_height,
+ unspent_output::unconfirmed_position };
}
// utilities
@@ -196,7 +196,7 @@ unspent CLASS::get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEP
TEMPLATE
template
code CLASS::parallel_unspent_transform(const stopper& cancel, bool turbo,
- unspents& out, const output_links& outs, Functor&& functor) NOEXCEPT
+ unspent_outputs& out, const output_links& outs, Functor&& functor) NOEXCEPT
{
const auto policy = poolstl::execution::par_if(turbo);
stopper fail{};
@@ -215,7 +215,7 @@ code CLASS::parallel_unspent_transform(const stopper& cancel, bool turbo,
if (cancel)
return error::query_canceled;
- unspent::filter_sort_and_dedup(out);
+ unspent_output::filter_sort_and_dedup(out);
return error::success;
}
diff --git a/include/bitcoin/database/impl/query/unspent.ipp b/include/bitcoin/database/impl/query/unspent.ipp
index d482ba0de..427429b7e 100644
--- a/include/bitcoin/database/impl/query/unspent.ipp
+++ b/include/bitcoin/database/impl/query/unspent.ipp
@@ -19,53 +19,30 @@
#ifndef LIBBITCOIN_DATABASE_QUERY_UNSPENT_IPP
#define LIBBITCOIN_DATABASE_QUERY_UNSPENT_IPP
-#include
-#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())
+ const auto first = (height == bip30::first_exception);
+ const auto second = (height == bip30::second_exception);
+ if (!first && !second)
return true;
- // With the rule unconfigured duplicates are unconstrained (no exception).
if (!store_.envelope().forks.bip30)
return true;
- const auto key = get_header_key(link);
- out = key == (height == first.height() ? first.hash() : second.hash());
+ const auto& hash = first ? bip30::first.hash() : bip30::second.hash();
+ out = (get_header_key(link) == hash);
return true;
}
@@ -115,290 +92,60 @@ code CLASS::get_block_amounts(block_amounts& out,
}
TEMPLATE
-code CLASS::scan_unspent(difference_set<>& set, unspent_totals& out,
- const stopper& cancel, const header_links& branch, bool turbo) const NOEXCEPT
+code CLASS::get_unspent_totals(const stopper& cancel, unspent_totals& out,
+ const header_links& branch, bool turbo) const NOEXCEPT
{
- using namespace system;
- std::atomic_bool fail{};
- std::atomic unspendable{};
- std::atomic overwritten{};
- 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;
- }
-
- auto coinbase = true;
- table::output::get_spendable value{};
-
- // The bip30 exception blocks are coinbase only, their duplicated
- // outputs overwrite (destroy) those of the original blocks.
- if (bip30_exception)
- {
- for (const auto& tx: to_transactions(link))
- for (const auto& out_: to_outputs(tx))
- {
- if (!store_.output.get(out_, value))
- {
- fail = true;
- return;
- }
-
- overwritten += value.value;
- }
-
- return;
- }
- 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)
- unspendable += value.value;
- else
- 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;
+ out = {};
+ difference_set set{ outs_records() };
+ const unspent_scanner scanner{ *this, store_, cancel, turbo };
+ if (const auto ec = scanner.scan(set, out, branch))
+ return ec;
- out.unspendable = unspendable;
- out.bip30 = overwritten;
- return error::success;
+ const unspent_counter counter{ store_, cancel, turbo };
+ return counter.count(out, set);
}
TEMPLATE
-code CLASS::count_unspent(unspent_totals& out, const stopper& cancel,
- const difference_set<>::entries& survivors) const NOEXCEPT
+code CLASS::get_unspent_muhash(const stopper& cancel, unspent_totals& out,
+ hash_digest& digest, const header_links& branch, bool turbo) const NOEXCEPT
{
- using namespace system;
-
- 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;
-
- // Coin serialization is outpoint, height code, value, script.
- constexpr auto coin_overhead = hash_size + sizeof(uint32_t) +
- sizeof(uint32_t) + sizeof(uint64_t);
-
- ++out.outputs;
- out.value += value.value;
- out.script_bytes += value.script_size;
- out.coin_bytes += coin_overhead +
- variable_size(value.script_size) + value.script_size;
- }
- }
+ out = {};
+ difference_set set{ outs_records() };
+ const unspent_scanner scanner{ *this, store_, cancel, turbo };
+ if (const auto ec = scanner.scan(set, out, branch))
+ return ec;
- return error::success;
+ const unspent_muhash muhash{ store_, cancel, turbo };
+ return muhash.hash(out, digest, set);
}
TEMPLATE
-template
-code CLASS::visit_unspent(const Visitor& visit, const stopper& cancel,
- const difference_set<>::entries& survivors) const NOEXCEPT
+code CLASS::get_unspent_serialized(const stopper& cancel, unspent_totals& out,
+ hash_digest& digest, const header_links& branch, bool turbo) const NOEXCEPT
{
- using namespace system;
-
- unspent_coin coin{};
- output_links puts{};
- auto previous = max_uint64;
- const auto bip30 = store_.envelope().forks.bip30;
- table::output::get_coin out{};
- for (const auto& [key, mask]: survivors)
- {
- if (cancel)
- return error::query_canceled;
-
- const auto tx = difference_set<>::to_id(key);
- coin.first = (tx != previous);
- if (coin.first)
- {
- previous = tx;
- const auto link = possible_narrow_cast(tx);
- const auto at = get_confirmed_height(find_strong(link));
- coin.txid = get_tx_key(link);
- if (at.is_terminal() || coin.txid == null_hash)
- return error::integrity;
-
- coin.height = at.value;
- coin.coinbase = is_coinbase(link);
-
- // bitcoind retains duplicated coinbases at the overwriting
- // heights (bip30 exceptions), the store at the originals.
- if (bip30 && coin.coinbase)
- coin.height = (coin.height == 91812) ? 91842 :
- (coin.height == 91722) ? 91880 : coin.height;
-
- puts = to_outputs(link);
- }
-
- 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], out))
- return error::integrity;
-
- coin.index = possible_narrow_cast(index);
- coin.value = out.value;
- std::swap(coin.script, out.script);
- visit(coin);
- coin.first = false;
- }
- }
+ out = {};
+ difference_set set{ outs_records() };
+ const unspent_scanner scanner{ *this, store_, cancel, turbo };
+ if (const auto ec = scanner.scan(set, out, branch))
+ return ec;
- return error::success;
+ const unspent_serial serial{ store_, cancel, turbo };
+ return serial.hash(out, digest, set);
}
TEMPLATE
-code CLASS::get_unspent_totals(const stopper& cancel, unspent_totals& out,
+code CLASS::get_unspent_matches(const stopper& cancel, unspent_coins& out,
+ size_t& txouts, const std::unordered_set& keys,
const header_links& branch, bool turbo) const NOEXCEPT
{
- out = {};
- difference_set<> set{};
- const auto ec = scan_unspent(set, out, cancel, branch, turbo);
- return ec ? ec : count_unspent(out, cancel, set.drain());
-}
-
-TEMPLATE
-template
-code CLASS::get_unspent_coins(const stopper& cancel, unspent_totals& out,
- const Visitor& visit, const header_links& branch, bool ordered,
- bool turbo) const NOEXCEPT
-{
- using namespace system;
-
- out = {};
- difference_set<> set{};
- if (const auto ec = scan_unspent(set, out, cancel, branch, turbo))
+ unspent_totals unused{};
+ difference_set set{ outs_records() };
+ const unspent_scanner scanner{ *this, store_, cancel, turbo };
+ if (const auto ec = scanner.scan(set, unused, branch))
return ec;
- auto survivors = set.drain();
- if (ordered)
- {
- // Canonical order is (txid, index), as bitcoind's chainstate cursor.
- // The txid sort requires full materialization (memory expensive).
- using keyed_entry = std::pair::entry>;
- std_vector keyed{};
- keyed.reserve(survivors.size());
- for (const auto& item: survivors)
- {
- if (cancel)
- return error::query_canceled;
-
- const auto tx = difference_set<>::to_id(item.first);
- auto txid = get_tx_key(possible_narrow_cast(tx));
- if (txid == null_hash)
- return error::integrity;
-
- keyed.emplace_back(std::move(txid), item);
- }
-
- // Key order preserves window order within a transaction.
- std::sort(poolstl::execution::par_if(turbo), keyed.begin(),
- keyed.end(), [](const auto& left, const auto& right) NOEXCEPT
- {
- return (left.first == right.first) ?
- (left.second.first < right.second.first) :
- (left.first < right.first);
- });
-
- std::transform(keyed.begin(), keyed.end(), survivors.begin(),
- [](const auto& item) NOEXCEPT { return item.second; });
- }
-
- return visit_unspent(visit, cancel, survivors);
+ const unspent_matcher matcher{ store_, cancel, turbo };
+ return matcher.match(out, txouts, keys, set);
}
} // namespace database
diff --git a/include/bitcoin/database/impl/types/difference_set.ipp b/include/bitcoin/database/impl/types/difference_set.ipp
deleted file mode 100644
index b80bdd7c4..000000000
--- a/include/bitcoin/database/impl/types/difference_set.ipp
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * 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/impl/unspent/unspent_counter.ipp b/include/bitcoin/database/impl/unspent/unspent_counter.ipp
new file mode 100644
index 000000000..07f5b20cd
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_counter.ipp
@@ -0,0 +1,108 @@
+/**
+ * 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_UNSPENT_UNSPENT_COUNTER_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_COUNTER_IPP
+
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_counter(const Store& store, const stopper& cancel,
+ bool turbo) NOEXCEPT
+ : store_(store),
+ cancel_(cancel),
+ reader_(store, cancel),
+ spans_(store, cancel, turbo)
+{
+}
+
+TEMPLATE
+code CLASS::count(unspent_totals& out,
+ const difference_set& set) const NOEXCEPT
+{
+ std_vector totals(spans_.count());
+ const auto ec = spans_.for_each(set,
+ [&](size_t index, size_t begin, size_t end) NOEXCEPT
+ {
+ return span(totals.at(index), set, begin, end);
+ });
+
+ if (ec)
+ return ec;
+
+ for (const auto& total: totals)
+ unspent_writer::add(out, total);
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::span(unspent_totals& out, const difference_set& set,
+ size_t begin, size_t end) const NOEXCEPT
+{
+ output_links puts{};
+ auto previous = tx_link::terminal;
+ return reader_.elements(set, begin, end,
+ [&](const unspent_elements& elements) NOEXCEPT
+ {
+ puts.resize(elements.size());
+ if (const auto ec = reader_.read_puts(puts, zero, elements, zero,
+ elements.size()))
+ return ec;
+
+ return read_totals(out, previous, puts);
+ });
+}
+
+TEMPLATE
+code CLASS::read_totals(unspent_totals& out, tx_link::integer& previous,
+ const output_links& puts) const NOEXCEPT
+{
+ constexpr auto fixed = unspent_writer::fixed_size;
+
+ const auto ptr = store_.output.get_memory();
+ for (const auto& put: puts)
+ {
+ table::output::get_parent_coin output{};
+ if (!store_.output.get(ptr, put, output))
+ return error::integrity;
+
+ if (output.parent_fk != previous)
+ {
+ previous = output.parent_fk;
+ ++out.transactions;
+ }
+
+ const auto variable = variable_size(output.script_size);
+
+ ++out.outputs;
+ out.value += output.value;
+ out.script_bytes += output.script_size;
+ out.coin_bytes += fixed + variable + output.script_size;
+ }
+
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_matcher.ipp b/include/bitcoin/database/impl/unspent/unspent_matcher.ipp
new file mode 100644
index 000000000..d020fc4cc
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_matcher.ipp
@@ -0,0 +1,86 @@
+/**
+ * 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_UNSPENT_UNSPENT_MATCHER_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_MATCHER_IPP
+
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_matcher(const Store& store, const stopper& cancel,
+ bool turbo) NOEXCEPT
+ : reader_(store, cancel),
+ spans_(store, cancel, turbo)
+{
+}
+
+TEMPLATE
+code CLASS::match(unspent_coins& out, size_t& txouts, const keys& keys,
+ const difference_set& set) const NOEXCEPT
+{
+ BC_ASSERT(out.empty());
+
+ txouts = zero;
+ std_vector totals(spans_.count());
+ std_vector matches(spans_.count());
+ const auto ec = spans_.for_each(set,
+ [&](size_t index, size_t begin, size_t end) NOEXCEPT
+ {
+ return span(totals.at(index), matches.at(index), keys, set, begin,
+ end);
+ });
+
+ if (ec)
+ return ec;
+
+ for (auto index = zero; index < matches.size(); ++index)
+ {
+ txouts += totals.at(index).outputs;
+ auto& found = matches.at(index);
+ out.insert(out.end(),
+ std::make_move_iterator(found.begin()),
+ std::make_move_iterator(found.end()));
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::span(unspent_totals& out, unspent_coins& matches,
+ const keys& keys, const difference_set& set, size_t begin,
+ size_t end) const NOEXCEPT
+{
+ using namespace system;
+ return reader_.batch(set, begin, end,
+ [&](const unspent_coin& coin) NOEXCEPT
+ {
+ unspent_writer::add(out, coin);
+ if (keys.contains(sha256_hash(coin.script)))
+ matches.push_back(coin);
+ });
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_muhash.ipp b/include/bitcoin/database/impl/unspent/unspent_muhash.ipp
new file mode 100644
index 000000000..1d3090a70
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_muhash.ipp
@@ -0,0 +1,113 @@
+/**
+ * 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_UNSPENT_UNSPENT_MUHASH_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_MUHASH_IPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_muhash(const Store& store, const stopper& cancel,
+ bool turbo) NOEXCEPT
+ : reader_(store, cancel), spans_(store, cancel, turbo), store_(store)
+{
+}
+
+TEMPLATE
+code CLASS::hash(unspent_totals& out, hash_digest& digest,
+ const difference_set& set) const NOEXCEPT
+{
+ using namespace system;
+ std_vector totals(spans_.count());
+ std_vector partials(spans_.count());
+ const auto ec = spans_.for_each(set,
+ [&](size_t index, size_t begin, size_t end) NOEXCEPT
+ {
+ return span(totals.at(index), partials.at(index), set, begin, end);
+ });
+
+ if (ec)
+ return ec;
+
+ muhash3072 muhash{};
+ for (size_t index{}; index < partials.size(); ++index)
+ {
+ unspent_writer::add(out, totals.at(index));
+ muhash *= partials.at(index);
+ }
+
+ digest = muhash.flush();
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::span(unspent_totals& out, system::muhash3072& partial,
+ const difference_set& set, size_t begin, size_t end) const NOEXCEPT
+{
+ output_links puts{};
+ unspent_coins coins{};
+ auto previous = tx_link::terminal;
+
+ return reader_.elements(set, begin, end,
+ [&](const unspent_elements& at) NOEXCEPT
+ {
+ puts.resize(at.size());
+ coins.resize(at.size());
+ auto ec = reader_.fill(coins, puts, zero, previous, at, zero,
+ at.size());
+
+ if (!ec)
+ ec = insert(out, partial, coins, puts);
+
+ return ec;
+ });
+}
+
+TEMPLATE
+code CLASS::insert(unspent_totals& out, system::muhash3072& partial,
+ const unspent_coins& coins, const output_links& puts) const NOEXCEPT
+{
+ using namespace system;
+ const auto ptr = store_.output.get_memory();
+ for (size_t at{}; at < coins.size(); ++at)
+ {
+ hash_digest digest{};
+ stream::out::fast stream{ digest };
+ hash::sha256::fast sink{ stream };
+ const auto& coin = coins.at(at);
+ unspent_writer::write(sink, coin);
+ table::output::write_script tail{ {}, sink };
+ if (!store_.output.raw(ptr, puts.at(at), tail))
+ return error::integrity;
+
+ sink.flush();
+ partial.insert_hash(digest);
+ unspent_writer::add(out, coin.first, tail.value, tail.length);
+ }
+
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_reader.ipp b/include/bitcoin/database/impl/unspent/unspent_reader.ipp
new file mode 100644
index 000000000..a38412f6e
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_reader.ipp
@@ -0,0 +1,329 @@
+/**
+ * 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_UNSPENT_UNSPENT_READER_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_READER_IPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_reader(const Store& store,
+ const stopper& cancel) NOEXCEPT
+ : store_(store), cancel_(cancel)
+{
+}
+
+TEMPLATE
+template
+code CLASS::elements(const difference_set& set, size_t begin, size_t end,
+ const Flush& flush) const NOEXCEPT
+{
+ constexpr auto width = difference_set::word_bits;
+ unspent_elements elements{};
+
+ using namespace system;
+ using word = difference_set::word;
+ const auto last = ceilinged_divide(end, width);
+ for (auto index = floored_divide(begin, width); index < last; ++index)
+ {
+ if (cancel_)
+ return error::query_canceled;
+
+ const auto base = index * width;
+ auto bits = set.at(index);
+ if (base < begin)
+ bits = bit_and(bits, mask_right(begin - base));
+
+ if (base + width > end)
+ bits = bit_and(bits, unmask_right(end - base));
+
+ for (; !is_zero(bits); bits = bit_and(bits, sub1(bits)))
+ {
+ const auto link = base + right_zeros(bits);
+ elements.push_back(possible_narrow_cast(link));
+ }
+
+ if (elements.size() >= batch_size)
+ {
+ if (const auto ec = flush(elements))
+ return ec;
+
+ elements.clear();
+ }
+ }
+
+ return flush(elements);
+}
+
+TEMPLATE
+template
+code CLASS::batch(const difference_set& set, size_t begin, size_t end,
+ const Handler& handle) const NOEXCEPT
+{
+ output_links puts{};
+ unspent_coins coins{};
+ auto previous = tx_link::terminal;
+
+ return elements(set, begin, end, [&](const unspent_elements& at) NOEXCEPT
+ {
+ puts.resize(at.size());
+ coins.resize(at.size());
+ auto ec = fill(coins, puts, zero, previous, at, zero, at.size());
+ if (!ec)
+ ec = read_scripts(coins, zero, puts, at.size());
+
+ if (!ec)
+ for (const auto& coin: coins)
+ handle(coin);
+
+ return ec;
+ });
+}
+
+TEMPLATE
+code CLASS::fill(unspent_coins& out, output_links& puts, size_t offset,
+ tx_link::integer& previous, const unspent_elements& elements,
+ size_t begin, size_t end) const NOEXCEPT
+{
+ if (const auto ec = read_puts(puts, offset, elements, begin, end))
+ return ec;
+
+ tx_links parents{};
+ if (const auto ec = read_parents(parents, puts, offset, end - begin))
+ return ec;
+
+ if (const auto ec = read_transactions(out, offset, previous, parents,
+ elements, begin))
+ return ec;
+
+ header_links blocks{};
+ if (const auto ec = read_blocks(blocks, parents))
+ return ec;
+
+ std_vector heights{};
+ if (const auto ec = read_heights(heights, blocks))
+ return ec;
+
+ return confirm(out, offset, blocks, heights);
+}
+
+TEMPLATE
+code CLASS::read_scripts(unspent_coins& out, size_t offset,
+ const output_links& puts, size_t count) const NOEXCEPT
+{
+ const auto ptr = store_.output.get_memory();
+ for (size_t at{}; at < count; ++at)
+ {
+ table::output::get_coin output{};
+ if (!store_.output.get(ptr, puts.at(offset + at), output))
+ return error::integrity;
+
+ auto& coin = out.at(offset + at);
+ coin.out = { coin.out.point(), output.value };
+ std::swap(coin.script, output.script);
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_puts(output_links& out, size_t offset,
+ const unspent_elements& elements, size_t begin, size_t end) const NOEXCEPT
+{
+ const auto count = end - begin;
+
+ const auto ptr = store_.outs.puts.get_memory();
+ for (size_t at{}; at < count; ++at)
+ {
+ table::outs::get_output output{};
+ if (!store_.outs.puts.get(ptr, elements.at(begin + at), output))
+ return error::integrity;
+
+ out.at(offset + at) = output.out_fk;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_parents(tx_links& out, const output_links& puts,
+ size_t offset, size_t count) const NOEXCEPT
+{
+ out.resize(count);
+
+ const auto ptr = store_.output.get_memory();
+ for (size_t at{}; at < count; ++at)
+ {
+ table::output::get_parent output{};
+ if (!store_.output.get(ptr, puts.at(offset + at), output))
+ return error::integrity;
+
+ out.at(at) = output.parent_fk;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_hashes(system::hashes& out,
+ const tx_links& parents) const NOEXCEPT
+{
+ out.resize(parents.size());
+
+ const auto ptr = store_.tx.get_memory();
+ for (size_t at{}; at < parents.size(); ++at)
+ {
+ table::transaction::get_hash tx{};
+ if (!store_.tx.get(ptr, parents.at(at), tx))
+ return error::integrity;
+
+ out.at(at) = tx.hash;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_transactions(unspent_coins& out, size_t offset,
+ tx_link::integer& previous, const tx_links& parents,
+ const unspent_elements& elements, size_t begin) const NOEXCEPT
+{
+ auto parent = tx_link::terminal;
+ table::transaction::get_puts tx{};
+ table::transaction::get_hash hash{};
+
+ const auto ptr = store_.tx.get_memory();
+ for (size_t at{}; at < parents.size(); ++at)
+ {
+ if (parents.at(at) != parent)
+ {
+ parent = parents.at(at);
+ if (!store_.tx.get(ptr, parent, tx) ||
+ !store_.tx.get(ptr, parent, hash))
+ return error::integrity;
+ }
+
+ auto& coin = out.at(offset + at);
+ const auto index = elements.at(begin + at) - tx.outs_fk;
+ coin.first = (parent != previous);
+ coin.coinbase = tx.coinbase;
+ coin.out = { { hash.hash, index }, coin.out.value() };
+ previous = parent;
+ }
+
+ return error::success;
+}
+
+// The branch is inactive if a tx of it is no longer strong (reorganized).
+TEMPLATE
+code CLASS::read_blocks(header_links& out,
+ const tx_links& parents) const NOEXCEPT
+{
+ tx_link parent{};
+ auto block = header_link::terminal;
+ out.resize(parents.size());
+
+ const auto ptr = store_.strong_tx.get_memory();
+ for (size_t at{}; at < parents.size(); ++at)
+ {
+ if (parents.at(at) != parent)
+ {
+ parent = parents.at(at);
+ const auto fk = store_.strong_tx.first(ptr, parent);
+
+ table::strong_tx::record strong{};
+ if (!store_.strong_tx.get(ptr, fk, strong) || !strong.positive())
+ return error::branch_inactive;
+
+ block = strong.header_fk();
+ }
+
+ out.at(at) = block;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_heights(std_vector& out,
+ const header_links& blocks) const NOEXCEPT
+{
+ size_t height{};
+ header_link block{};
+ out.resize(blocks.size());
+
+ const auto ptr = store_.header.get_memory();
+ for (size_t at{}; at < blocks.size(); ++at)
+ {
+ if (blocks.at(at) != block)
+ {
+ block = blocks.at(at);
+ table::header::get_height get_height{};
+ if (!store_.header.get(ptr, block, get_height))
+ return error::integrity;
+
+ height = get_height.height;
+ }
+
+ out.at(at) = height;
+ }
+
+ return error::success;
+}
+
+// bitcoind retains duplicated coinbases at the overwriting heights (bip30
+// exceptions), the store at the originals.
+TEMPLATE
+code CLASS::confirm(unspent_coins& out, size_t offset,
+ const header_links& blocks, const std_vector& heights) const NOEXCEPT
+{
+ header_link block{};
+ const auto fork = store_.envelope().forks.bip30;
+
+ const auto ptr = store_.confirmed.get_memory();
+ for (size_t at{}; at < blocks.size(); ++at)
+ {
+ if (blocks.at(at) != block)
+ {
+ block = blocks.at(at);
+ if (store_.confirmed.at(ptr, heights.at(at)) != block)
+ return error::integrity;
+ }
+
+ auto& coin = out.at(offset + at);
+ coin.height = heights.at(at);
+ if (!fork || !coin.coinbase)
+ continue;
+
+ if (coin.height == bip30::first_original)
+ coin.height = bip30::first_exception;
+ else if (coin.height == bip30::second_original)
+ coin.height = bip30::second_exception;
+ }
+
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_scanner.ipp b/include/bitcoin/database/impl/unspent/unspent_scanner.ipp
new file mode 100644
index 000000000..da37f34b7
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_scanner.ipp
@@ -0,0 +1,307 @@
+/**
+ * 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_UNSPENT_UNSPENT_SCANNER_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_SCANNER_IPP
+
+#include
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_scanner(const query& query, const Store& store,
+ const stopper& cancel, bool turbo) NOEXCEPT
+ : query_(query), store_(store), cancel_(cancel), turbo_(turbo)
+{
+}
+
+TEMPLATE
+code CLASS::scan(difference_set& set, unspent_totals& out,
+ const header_links& branch) const NOEXCEPT
+{
+ if (branch.empty())
+ return error::success;
+
+ std_vector bounds{};
+ if (const auto ec = this->bounds(bounds, branch))
+ return ec;
+
+ const auto cuts = sub1(bounds.size());
+ std_vector unspendables(cuts, zero);
+ std_vector overwrittens(cuts, zero);
+ std::atomic_bool fail{};
+
+ // Static chunking is contiguous, so spans are interleaved across threads.
+ const auto width = cores();
+ std_vector index{};
+ index.reserve(cuts);
+ for (size_t lane{}; lane < width; ++lane)
+ for (auto at = lane; at < cuts; at += width)
+ index.push_back(at);
+
+ const auto parallel = poolstl::execution::par_if(turbo_);
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t at) NOEXCEPT
+ {
+ if (fail)
+ return;
+
+ if (span(set, unspendables.at(at), overwrittens.at(at), branch,
+ bounds.at(at), bounds.at(add1(at))))
+ fail = true;
+ });
+
+ if (fail)
+ return cancel_ ? error::query_canceled : error::integrity;
+
+ for (size_t at{}; at < cuts; ++at)
+ {
+ out.unspendable += unspendables.at(at);
+ out.bip30 += overwrittens.at(at);
+ }
+
+ return error::success;
+}
+
+// Spans are cut to equal tx counts, as blocks vary by orders of magnitude.
+TEMPLATE
+code CLASS::bounds(std_vector& out,
+ const header_links& branch) const NOEXCEPT
+{
+ BC_ASSERT(out.empty());
+
+ std_vector counts(branch.size());
+ std_vector blocks(branch.size());
+ std::iota(blocks.begin(), blocks.end(), zero);
+ const auto parallel = poolstl::execution::par_if(turbo_);
+
+ std::for_each(parallel, blocks.begin(), blocks.end(),
+ [&](size_t block) NOEXCEPT
+ {
+ counts.at(block) = query_.get_tx_count(branch.at(block));
+ });
+
+ size_t txs{};
+ for (const auto count: counts)
+ txs += count;
+
+ const auto width = cores();
+ const auto spans = std::min(branch.size(), width * per_thread);
+ const auto per_span = std::max(one, system::ceilinged_divide(txs, spans));
+
+ out.reserve(add1(spans));
+ out.push_back(zero);
+ size_t carry{};
+
+ for (size_t block{}; block < branch.size(); ++block)
+ {
+ carry += counts.at(block);
+ if (carry >= per_span && out.size() <= spans)
+ {
+ out.push_back(add1(block));
+ carry = zero;
+ }
+ }
+
+ if (out.back() != branch.size())
+ out.push_back(branch.size());
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::span(difference_set& set, uint64_t& unspendable,
+ uint64_t& overwritten, const header_links& branch, size_t begin,
+ size_t end) const NOEXCEPT
+{
+ buffers buffer{};
+ for (auto at = begin; at < end; ++at)
+ {
+ if (cancel_)
+ return error::query_canceled;
+
+ if (const auto ec = block(set, unspendable, overwritten, buffer,
+ branch.at(at)))
+ return ec;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::block(difference_set& set, uint64_t& unspendable,
+ uint64_t& overwritten, buffers& buffer,
+ const header_link& link) const NOEXCEPT
+{
+ auto bip30_exception = false;
+ if (!query_.is_bip30_exception(bip30_exception, link))
+ return error::integrity;
+
+ if (bip30_exception)
+ return this->overwritten(overwritten, link);
+
+ if (const auto ec = read_transactions(buffer.txs, link))
+ return ec;
+
+ if (const auto ec = toggle_creates(set, buffer.exclusions, buffer.txs))
+ return ec;
+
+ if (const auto ec = read_exclusions(unspendable, buffer.exclusions))
+ return ec;
+
+ if (const auto ec = read_points(buffer.spends, buffer.txs))
+ return ec;
+
+ return toggle_spends(set, buffer.spends);
+}
+
+// The bip30 exception blocks are coinbase only, their duplicated outputs
+// overwrite (destroy) those of the original blocks.
+TEMPLATE
+code CLASS::overwritten(uint64_t& out, const header_link& link) const NOEXCEPT
+{
+ table::output::get_spendable value{};
+ for (const auto& tx: query_.to_transactions(link))
+ {
+ for (const auto& put: query_.to_outputs(tx))
+ {
+ if (!store_.output.get(put, value))
+ return error::integrity;
+
+ out += value.value;
+ }
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_transactions(gets& out,
+ const header_link& link) const NOEXCEPT
+{
+ const auto links = query_.to_transactions(link);
+ out.resize(links.size());
+
+ const auto ptr = store_.tx.get_memory();
+ for (size_t at{}; at < links.size(); ++at)
+ if (!store_.tx.get(ptr, links.at(at), out.at(at)))
+ return error::integrity;
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::toggle_creates(difference_set& set, output_links& exclusions,
+ const gets& txs) const NOEXCEPT
+{
+ exclusions.clear();
+
+ const auto ptr = store_.outs.puts.get_memory();
+ for (const auto& tx: txs)
+ {
+ table::outs::get_excluded outs{ .number = tx.outs_count };
+ if (!store_.outs.puts.get(ptr, tx.outs_fk, outs))
+ return error::integrity;
+
+ auto excluded = outs.excluded.begin();
+ for (uint32_t index{}; index < outs.number; ++index)
+ {
+ if (excluded != outs.excluded.end() && excluded->first == index)
+ exclusions.push_back((excluded++)->second);
+ else
+ set.toggle(tx.outs_fk + index);
+ }
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_exclusions(uint64_t& out,
+ const output_links& exclusions) const NOEXCEPT
+{
+ const auto ptr = store_.output.get_memory();
+ for (const auto& put: exclusions)
+ {
+ table::output::get_spendable value{};
+ if (!store_.output.get(ptr, put, value))
+ return error::integrity;
+
+ out += value.value;
+ }
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::read_points(system::chain::points& out,
+ const gets& txs) const NOEXCEPT
+{
+ out.clear();
+
+ const auto ptr = store_.ins.get_memory();
+ for (const auto& tx: txs)
+ {
+ if (tx.coinbase)
+ continue;
+
+ for (ins_link::integer in{}; in < tx.ins_count; ++in)
+ {
+ table::ins_point::get_composed point{};
+ if (!store_.ins.get(ptr, tx.points_fk + in, point))
+ return error::integrity;
+
+ out.push_back(point.key);
+ }
+ }
+
+ return error::success;
+}
+
+// As validation, the first tx of the hash resolves the spend.
+TEMPLATE
+code CLASS::toggle_spends(difference_set& set,
+ const system::chain::points& spends) const NOEXCEPT
+{
+ const auto ptr = store_.tx.get_memory();
+ for (const auto& spend: spends)
+ {
+ const auto spent = store_.tx.first(ptr, spend.hash());
+ if (spent.is_terminal())
+ return error::integrity;
+
+ table::transaction::get_output prevout{ {}, spend.index() };
+ if (!store_.tx.get(ptr, spent, prevout) ||
+ prevout.outs_fk == outs_link::terminal)
+ return error::integrity;
+
+ set.toggle(prevout.outs_fk);
+ }
+
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_serial.ipp b/include/bitcoin/database/impl/unspent/unspent_serial.ipp
new file mode 100644
index 000000000..f163ca694
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_serial.ipp
@@ -0,0 +1,352 @@
+/**
+ * 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_UNSPENT_UNSPENT_SERIAL_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_SERIAL_IPP
+
+#include
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_serial(const Store& store, const stopper& cancel,
+ bool turbo) NOEXCEPT
+ : reader_(store, cancel),
+ spans_(store, cancel, turbo),
+ store_(store),
+ cancel_(cancel),
+ turbo_(turbo)
+{
+}
+
+// One folder thread for the run, each bucket handed off through 'ordered'
+// (ready) and returned (done), so a fold overlaps the fill of the next bucket.
+TEMPLATE
+code CLASS::hash(unspent_totals& out, hash_digest& digest,
+ const difference_set& set) const NOEXCEPT
+{
+ sizes offsets{};
+ unspent_elements elements{};
+ if (const auto ec = partition(elements, offsets, set))
+ return ec;
+
+ code folded{};
+ auto stop = false;
+ auto previous = system::null_hash;
+ system::stream::out::fast stream{ digest };
+ system::hash::sha256x2::fast sink{ stream };
+ unspent_coins coins{}, ordered{};
+ output_links puts{}, links{};
+ std::binary_semaphore ready{ 0 };
+ std::binary_semaphore done{ 1 };
+ std::thread folder
+ {
+ [&]() NOEXCEPT
+ {
+ for (ready.acquire(); !stop; ready.acquire())
+ {
+ folded = fold(out, sink, previous, ordered, links);
+ done.release();
+ }
+ }
+ };
+
+ code ec{};
+ sizes order{};
+ for (size_t bucket{}; bucket < buckets; ++bucket)
+ {
+ const auto begin = offsets.at(bucket);
+ const auto end = offsets.at(add1(bucket));
+ ec = fill(coins, puts, elements, begin, end);
+ done.acquire();
+ if (!ec)
+ ec = folded;
+
+ if (ec)
+ break;
+
+ this->order(order, coins);
+ gather(ordered, links, coins, puts, order);
+ ready.release();
+ }
+
+ if (!ec)
+ {
+ done.acquire();
+ ec = folded;
+ }
+
+ stop = true;
+ ready.release();
+ folder.join();
+ if (ec)
+ return ec;
+
+ sink.flush();
+ return error::success;
+}
+
+// Two passes over the set: count per chunk and bucket, then scatter.
+TEMPLATE
+code CLASS::partition(unspent_elements& out, sizes& offsets,
+ const difference_set& set) const NOEXCEPT
+{
+ const auto chunks = std::max(one, std::min(set.words(), spans_.count()));
+ const auto span = system::ceilinged_divide(set.words(), chunks);
+ sizes counts(chunks * buckets, zero);
+ sizes index(chunks);
+ std::iota(index.begin(), index.end(), zero);
+ const auto parallel = poolstl::execution::par_if(turbo_);
+ std::atomic_bool fail{};
+
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t chunk) NOEXCEPT
+ {
+ if (fail)
+ return;
+
+ const auto begin = chunk * span;
+ const auto end = std::min(set.words(), begin + span);
+ if (walk(set, begin, end,
+ [&](size_t slot, outs_link::integer) NOEXCEPT
+ {
+ ++counts.at(chunk * buckets + slot);
+ })) fail = true;
+ });
+
+ if (fail)
+ return cancel_ ? error::query_canceled : error::integrity;
+
+ starts(counts, offsets, chunks);
+ out.resize(offsets.back());
+
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t chunk) NOEXCEPT
+ {
+ if (fail)
+ return;
+
+ const auto begin = chunk * span;
+ const auto end = std::min(set.words(), begin + span);
+ if (walk(set, begin, end,
+ [&](size_t slot, outs_link::integer element) NOEXCEPT
+ {
+ out.at(counts.at(chunk * buckets + slot)++) = element;
+ })) fail = true;
+ });
+
+ if (fail)
+ return cancel_ ? error::query_canceled : error::integrity;
+
+ return error::success;
+}
+
+// emit(slot, element) for the set bits of the word range, in element order.
+TEMPLATE
+template
+code CLASS::walk(const difference_set& set, size_t begin, size_t end,
+ const Emit& emit) const NOEXCEPT
+{
+ constexpr auto width = difference_set::word_bits;
+ unspent_elements elements{};
+
+ for (auto word = begin; word < end; ++word)
+ {
+ if (cancel_)
+ return error::query_canceled;
+
+ using namespace system;
+ const auto base = word * width;
+ for (auto bits = set.at(word); !is_zero(bits);
+ bits = bit_and(bits, sub1(bits)))
+ {
+ const auto link = base + right_zeros(bits);
+ elements.push_back(possible_narrow_cast(link));
+ }
+ }
+
+ sizes slots{};
+ if (const auto ec = this->slots(slots, elements))
+ return ec;
+
+ for (size_t at{}; at < elements.size(); ++at)
+ emit(slots.at(at), elements.at(at));
+
+ return error::success;
+}
+
+TEMPLATE
+code CLASS::slots(sizes& out,
+ const unspent_elements& elements) const NOEXCEPT
+{
+ output_links puts(elements.size());
+ if (const auto ec = reader_.read_puts(puts, zero, elements, zero,
+ elements.size()))
+ return ec;
+
+ tx_links parents{};
+ if (const auto ec = reader_.read_parents(parents, puts, zero, puts.size()))
+ return ec;
+
+ system::hashes hashes{};
+ if (const auto ec = reader_.read_hashes(hashes, parents))
+ return ec;
+
+ out.resize(hashes.size());
+ for (auto at = zero; at < hashes.size(); ++at)
+ out.at(at) = hashes.at(at).front();
+
+ return error::success;
+}
+
+// Bucket offsets, then chunk starts within each bucket (counts in place).
+TEMPLATE
+void CLASS::starts(sizes& counts, sizes& offsets, size_t chunks) NOEXCEPT
+{
+ offsets.assign(add1(buckets), zero);
+ for (size_t chunk{}; chunk < chunks; ++chunk)
+ for (size_t bucket{}; bucket < buckets; ++bucket)
+ offsets.at(add1(bucket)) += counts.at(chunk * buckets + bucket);
+
+ for (size_t bucket{}; bucket < buckets; ++bucket)
+ offsets.at(add1(bucket)) += offsets.at(bucket);
+
+ for (size_t bucket{}; bucket < buckets; ++bucket)
+ {
+ auto start = offsets.at(bucket);
+ for (size_t chunk{}; chunk < chunks; ++chunk)
+ {
+ auto& slot = counts.at(chunk * buckets + bucket);
+ const auto count = slot;
+ slot = start;
+ start += count;
+ }
+ }
+}
+
+TEMPLATE
+code CLASS::fill(unspent_coins& out, output_links& puts,
+ const unspent_elements& elements, size_t begin, size_t end) const NOEXCEPT
+{
+ const auto size = end - begin;
+ out.resize(size);
+ puts.resize(size);
+ if (is_zero(size))
+ return error::success;
+
+ using namespace system;
+ const auto parallel = poolstl::execution::par_if(turbo_);
+ const auto chunks = std::max(one, std::min(size, spans_.count()));
+ const auto span = ceilinged_divide(size, chunks);
+ sizes index(ceilinged_divide(size, span));
+ std::iota(index.begin(), index.end(), zero);
+ std::atomic_bool fail{};
+
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t chunk) NOEXCEPT
+ {
+ if (fail)
+ return;
+
+ auto previous = tx_link::terminal;
+ const auto first = chunk * span;
+ const auto last = std::min(size, first + span);
+ if (reader_.fill(out, puts, first, previous, elements,
+ begin + first, begin + last))
+ fail = true;
+ });
+
+ if (fail)
+ return cancel_ ? error::query_canceled : error::integrity;
+
+ return error::success;
+}
+
+TEMPLATE
+void CLASS::order(sizes& out, const unspent_coins& coins) const NOEXCEPT
+{
+ out.resize(coins.size());
+ std::iota(out.begin(), out.end(), zero);
+ const auto parallel = poolstl::execution::par_if(turbo_);
+
+ std::sort(parallel, out.begin(), out.end(),
+ [&](size_t left, size_t right) NOEXCEPT
+ {
+ const auto& one = coins.at(left).out.point();
+ const auto& two = coins.at(right).out.point();
+ return (one.hash() == two.hash()) ?
+ (one.index() < two.index()) :
+ (one.hash() < two.hash());
+ });
+}
+
+TEMPLATE
+void CLASS::gather(unspent_coins& out, output_links& links,
+ unspent_coins& coins, const output_links& puts,
+ const sizes& order) const NOEXCEPT
+{
+ out.resize(coins.size());
+ links.resize(coins.size());
+ sizes index(coins.size());
+ std::iota(index.begin(), index.end(), zero);
+ const auto parallel = poolstl::execution::par_if(turbo_);
+
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t at) NOEXCEPT
+ {
+ out.at(at) = std::move(coins.at(order.at(at)));
+ links.at(at) = puts.at(order.at(at));
+ });
+}
+
+TEMPLATE
+code CLASS::fold(unspent_totals& out, system::writer& sink,
+ hash_digest& previous, unspent_coins& coins,
+ const output_links& puts) const NOEXCEPT
+{
+ const auto ptr = store_.output.get_memory();
+ for (size_t at{}; at < coins.size(); ++at)
+ {
+ if (cancel_)
+ return error::query_canceled;
+
+ auto& coin = coins.at(at);
+ const auto& hash = coin.out.point().hash();
+ coin.first = (hash != previous);
+ previous = hash;
+
+ unspent_writer::write(sink, coin);
+ table::output::write_script tail{ {}, sink };
+ if (!store_.output.raw(ptr, puts.at(at), tail))
+ return error::integrity;
+
+ unspent_writer::add(out, coin.first, tail.value, tail.length);
+ }
+
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_spans.ipp b/include/bitcoin/database/impl/unspent/unspent_spans.ipp
new file mode 100644
index 000000000..73946d133
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_spans.ipp
@@ -0,0 +1,143 @@
+/**
+ * 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_UNSPENT_UNSPENT_SPANS_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_SPANS_IPP
+
+#include
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::unspent_spans(const Store& store, const stopper& cancel,
+ bool turbo) NOEXCEPT
+ : store_(store), cancel_(cancel), turbo_(turbo)
+{
+}
+
+TEMPLATE
+size_t CLASS::count() const NOEXCEPT
+{
+ return cores() * per_thread;
+}
+
+TEMPLATE
+template
+code CLASS::for_each(const difference_set& set, const Span& span) const NOEXCEPT
+{
+ using namespace system;
+ std_vector bounds{};
+ if (const auto ec = this->bounds(bounds, set))
+ return ec;
+
+ // Static chunking is contiguous, so spans are interleaved across threads
+ // (unspent density rises with height, contiguous lanes are unbalanced).
+ const auto cuts = sub1(bounds.size());
+ const auto width = cores();
+ std_vector index{};
+ index.reserve(cuts);
+ for (size_t lane{}; lane < width; ++lane)
+ for (auto at = lane; at < cuts; at += width)
+ index.push_back(at);
+
+ std::atomic_bool fail{};
+ const auto parallel = poolstl::execution::par_if(turbo_);
+
+ std::for_each(parallel, index.begin(), index.end(),
+ [&](size_t chunk) NOEXCEPT
+ {
+ if (fail)
+ return;
+
+ if (span(chunk, bounds.at(chunk), bounds.at(add1(chunk))))
+ fail = true;
+ });
+
+ if (fail)
+ return cancel_ ? error::query_canceled : error::integrity;
+
+ return error::success;
+}
+
+TEMPLATE
+bool CLASS::to_tx_base(size_t& out, size_t element) const NOEXCEPT
+{
+ using namespace system;
+ const outs_link link{ possible_narrow_cast(element) };
+
+ table::outs::get_output outs{};
+ if (!store_.outs.puts.get(link, outs))
+ return false;
+
+ table::output::get_parent output{};
+ if (!store_.output.get(outs.out_fk, output))
+ return false;
+
+ table::transaction::get_output tx{};
+ if (!store_.tx.get(output.parent_fk, tx))
+ return false;
+
+ out = tx.outs_fk;
+ return true;
+}
+
+// Bounds are snapped to tx boundaries, as tx count is by distinct tx.
+TEMPLATE
+code CLASS::bounds(std_vector& out,
+ const difference_set& set) const NOEXCEPT
+{
+ BC_ASSERT(out.empty());
+ out.push_back(zero);
+ if (is_zero(set.size()))
+ {
+ out.push_back(zero);
+ return error::success;
+ }
+
+ using namespace system;
+ const auto chunks = std::min(set.size(), count());
+ const auto stride = ceilinged_divide(set.size(), chunks);
+ out.reserve(add1(chunks));
+
+ for (auto chunk = one; chunk < chunks; ++chunk)
+ {
+ const auto element = chunk * stride;
+ if (element >= set.size())
+ break;
+
+ size_t at{};
+ if (!to_tx_base(at, element))
+ return error::integrity;
+
+ if (at > out.back())
+ out.push_back(at);
+ }
+
+ out.push_back(set.size());
+ return error::success;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/unspent/unspent_writer.ipp b/include/bitcoin/database/impl/unspent/unspent_writer.ipp
new file mode 100644
index 000000000..e9a04d7ea
--- /dev/null
+++ b/include/bitcoin/database/impl/unspent/unspent_writer.ipp
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#ifndef LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_WRITER_IPP
+#define LIBBITCOIN_DATABASE_UNSPENT_UNSPENT_WRITER_IPP
+
+#include
+
+namespace libbitcoin {
+namespace database {
+
+inline void unspent_writer::write(system::writer& sink,
+ const unspent_coin& coin) NOEXCEPT
+{
+ using namespace system;
+ const auto coinbase = to_int(coin.coinbase);
+ const auto height = possible_narrow_cast(coin.height);
+
+ sink.write_bytes(coin.out.point().hash());
+ sink.write_4_bytes_little_endian(coin.out.point().index());
+ sink.write_4_bytes_little_endian(bit_or(shift_left(height), coinbase));
+}
+
+inline size_t unspent_writer::size(size_t script) NOEXCEPT
+{
+ return fixed_size + variable_size(script) + script;
+}
+
+inline void unspent_writer::add(unspent_totals& out, bool first,
+ uint64_t value, size_t script) NOEXCEPT
+{
+ if (first)
+ ++out.transactions;
+
+ ++out.outputs;
+ out.value += value;
+ out.script_bytes += script;
+ out.coin_bytes += size(script);
+}
+
+inline void unspent_writer::add(unspent_totals& out,
+ const unspent_coin& coin) NOEXCEPT
+{
+ add(out, coin.first, coin.out.value(), coin.script.size());
+}
+
+inline void unspent_writer::add(unspent_totals& out,
+ const unspent_totals& totals) NOEXCEPT
+{
+ out.transactions += totals.transactions;
+ out.outputs += totals.outputs;
+ out.value += totals.value;
+ out.script_bytes += totals.script_bytes;
+ out.coin_bytes += totals.coin_bytes;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/memory/memory.hpp b/include/bitcoin/database/memory/memory.hpp
index b60ba4102..0a1b5fe26 100644
--- a/include/bitcoin/database/memory/memory.hpp
+++ b/include/bitcoin/database/memory/memory.hpp
@@ -26,6 +26,10 @@
#include
#include
#include
+#include
+#include
+#include
#include
+#include
#endif
diff --git a/include/bitcoin/database/memory/release.hpp b/include/bitcoin/database/memory/release.hpp
new file mode 100644
index 000000000..83511e316
--- /dev/null
+++ b/include/bitcoin/database/memory/release.hpp
@@ -0,0 +1,125 @@
+/**
+ * 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_MEMORY_RELEASE_HPP
+#define LIBBITCOIN_DATABASE_MEMORY_RELEASE_HPP
+
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+/// Head-release page-run helpers (pure, unit tested). Pages are tracked as
+/// bits in 64-bit words (low order bit is the lowest page of the word). A
+/// release candidate page is clean (not dirty), cold (not recently written)
+/// and not already released. Candidacy is limited to full pages below the
+/// logical page count.
+/// ---------------------------------------------------------------------------
+
+/// Candidate bits given page-state words.
+constexpr uint64_t release_candidates(uint64_t dirty, uint64_t hot,
+ uint64_t released) NOEXCEPT
+{
+ using namespace system;
+ return bit_not(bit_or(dirty, bit_or(hot, released)));
+}
+
+/// Retain candidacy below the page count, for the word starting at page
+/// 'first' (a full word of candidacy above the count clears to zero).
+constexpr uint64_t release_below(uint64_t candidates, size_t pages,
+ size_t first) NOEXCEPT
+{
+ using namespace system;
+ constexpr auto bits = to_bits(sizeof(uint64_t));
+
+ if (pages <= first)
+ return zero;
+
+ if (pages - first >= bits)
+ return candidates;
+
+ return bit_and(candidates, unmask_right(pages - first));
+}
+
+/// Bits for pages [lo, hi) clamped to the word of 64 pages starting at page
+/// 'first', empty if the ranges do not intersect.
+constexpr uint64_t page_mask(size_t lo, size_t hi, size_t first) NOEXCEPT
+{
+ using namespace system;
+ constexpr auto bits = to_bits(sizeof(uint64_t));
+ const auto begin = std::max(lo, first);
+ const auto end = std::min(hi, first + bits);
+
+ if (begin >= end)
+ return zero;
+
+ const auto high = (end - first >= bits) ? bit_all :
+ unmask_right(end - first);
+
+ return bit_and(high, mask_right(begin - first));
+}
+
+/// The maximal run of set bits at or above 'page' within 'pages', as the
+/// right-open page range [first, second), empty (equal) if none.
+inline std::pair next_run(const uint64_t* words, size_t pages,
+ size_t page) NOEXCEPT
+{
+ using namespace system;
+ constexpr auto bits = to_bits(sizeof(uint64_t));
+
+ // Find first set bit at or above page.
+ while ((page < pages) && !get_right(words[page / bits], page % bits))
+ ++page;
+
+ // Find first clear bit above start.
+ auto end = page;
+ while ((end < pages) && get_right(words[end / bits], end % bits))
+ ++end;
+
+ return { page, end };
+}
+
+/// The maximal run of set bits containing 'page' within 'pages', as the
+/// right-open page range [first, second), empty (equal) if page is not set.
+inline std::pair bit_run(const uint64_t* words, size_t pages,
+ size_t page) NOEXCEPT
+{
+ using namespace system;
+ constexpr auto bits = to_bits(sizeof(uint64_t));
+
+ if ((page >= pages) || !get_right(words[page / bits], page % bits))
+ return { page, page };
+
+ auto start = page;
+ while (!is_zero(start) &&
+ get_right(words[sub1(start) / bits], sub1(start) % bits))
+ --start;
+
+ auto end = add1(page);
+ while ((end < pages) && get_right(words[end / bits], end % bits))
+ ++end;
+
+ return { start, end };
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/memory/settings.hpp b/include/bitcoin/database/memory/settings.hpp
index 5ba490ece..ca152f7c0 100644
--- a/include/bitcoin/database/memory/settings.hpp
+++ b/include/bitcoin/database/memory/settings.hpp
@@ -20,31 +20,11 @@
#define LIBBITCOIN_DATABASE_MEMORY_SETTINGS_HPP
#include
+#include
namespace libbitcoin {
namespace database {
-/// Expected read pattern of mapped storage, guiding kernel page advice. This
-/// is independent of the write pattern (structural): bodies are appended
-/// sequentially but read randomly by validation, so advising the kernel from
-/// the write pattern invites eviction of the read set under memory pressure.
-enum class advice : uint8_t
-{
- /// Let the operating system decide (mixed or unpredictable access).
- normal,
-
- /// Random access (suppresses read ahead), preloaded (small maps only).
- random,
-
- /// One pass access (allows the kernel to free pages behind).
- sequential,
-
- /// Random access (suppresses read ahead) without preload, for maps too
- /// large to reside: scattered reads otherwise fault a full read-ahead
- /// window each, and that manufactured cache displaces the head set.
- scattered
-};
-
/// Storage tuning consumed by memory map construction: the base of table
/// configuration, as network settings bases are derived by server services.
/// Future staging tunables land here without constructor signature changes.
diff --git a/include/bitcoin/database/memory/utilities.hpp b/include/bitcoin/database/memory/utilities.hpp
index 22a8b84d7..2ca4ee2f7 100644
--- a/include/bitcoin/database/memory/utilities.hpp
+++ b/include/bitcoin/database/memory/utilities.hpp
@@ -19,9 +19,6 @@
#ifndef LIBBITCOIN_DATABASE_MEMORY_UTILITIES_HPP
#define LIBBITCOIN_DATABASE_MEMORY_UTILITIES_HPP
-#include
-#include
-#include
#include
namespace libbitcoin {
@@ -33,12 +30,6 @@ BCD_API size_t page_size() NOEXCEPT;
/// The bytes of physical memory, zero if failed.
BCD_API uint64_t system_memory() NOEXCEPT;
-/// Head buckets from expected elements and installed memory, scaled linearly
-/// between the contested and uncontested load factors (in tenths). Hashmaps
-/// only; zero expected is not derived.
-BCD_API uint32_t derive_buckets(uint64_t expected, uint32_t low,
- uint32_t high) NOEXCEPT;
-
/// The bytes of unused physical memory, zero if failed. Scarcity precedes
/// pressure: clean file cache is reclaimable, so the pressure level does not
/// raise while free memory exhausts.
@@ -57,90 +48,8 @@ BCD_API size_t system_pressure() NOEXCEPT;
/// if failed or the platform provides no source.
BCD_API uint64_t system_compressed() NOEXCEPT;
-// TODO: move all below to release.hpp, remove compound ternaries, delint.
-
-/// Head-release page-run helpers (pure, unit tested). Pages are tracked as
-/// bits in 64-bit words (low order bit is the lowest page of the word). A
-/// release candidate page is clean (not dirty), cold (not recently written)
-/// and not already released. Candidacy is limited to full pages below the
-/// logical page count.
-/// ---------------------------------------------------------------------------
-
-/// Candidate bits given page-state words.
-constexpr uint64_t release_candidates(uint64_t dirty, uint64_t hot,
- uint64_t released) NOEXCEPT
-{
- using namespace system;
- return bit_not(bit_or(dirty, bit_or(hot, released)));
-}
-
-/// Retain candidacy below the page count, for the word starting at page
-/// 'first' (a full word of candidacy above the count clears to zero).
-constexpr uint64_t release_below(uint64_t candidates, size_t pages,
- size_t first) NOEXCEPT
-{
- using namespace system;
- return (pages <= first) ? zero :
- (pages - first >= to_bits(sizeof(uint64_t))) ? candidates :
- bit_and(candidates, unmask_right(pages - first));
-}
-
-/// Bits for pages [lo, hi) clamped to the word of 64 pages starting at page
-/// 'first', empty if the ranges do not intersect.
-constexpr uint64_t page_mask(size_t lo, size_t hi, size_t first) NOEXCEPT
-{
- using namespace system;
- constexpr auto bits = to_bits(sizeof(uint64_t));
- const auto begin = std::max(lo, first);
- const auto end = std::min(hi, first + bits);
- return (begin >= end) ? zero : bit_and(
- (end - first >= bits) ? bit_all :
- unmask_right(end - first),
- mask_right(begin - first));
-}
-
-/// The maximal run of set bits at or above 'page' within 'pages', as the
-/// right-open page range [first, second), empty (equal) if none.
-inline std::pair next_run(const uint64_t* words, size_t pages,
- size_t page) NOEXCEPT
-{
- using namespace system;
- constexpr auto bits = to_bits(sizeof(uint64_t));
-
- // Find first set bit at or above page.
- while ((page < pages) && !get_right(words[page / bits], page % bits))
- ++page;
-
- // Find first clear bit above start.
- auto end = page;
- while ((end < pages) && get_right(words[end / bits], end % bits))
- ++end;
-
- return { page, end };
-}
-
-/// The maximal run of set bits containing 'page' within 'pages', as the
-/// right-open page range [first, second), empty (equal) if page is not set.
-inline std::pair bit_run(const uint64_t* words, size_t pages,
- size_t page) NOEXCEPT
-{
- using namespace system;
- constexpr auto bits = to_bits(sizeof(uint64_t));
-
- if ((page >= pages) || !get_right(words[page / bits], page % bits))
- return { page, page };
-
- auto start = page;
- while (!is_zero(start) &&
- get_right(words[sub1(start) / bits], sub1(start) % bits))
- --start;
-
- auto end = add1(page);
- while ((end < pages) && get_right(words[end / bits], end % bits))
- ++end;
-
- return { start, end };
-}
+/// Number of cores, no less than one.
+BCD_API size_t cores() NOEXCEPT;
} // namespace database
} // namespace libbitcoin
diff --git a/include/bitcoin/database/primitives/hashhead.hpp b/include/bitcoin/database/primitives/hashhead.hpp
index 317eb8d4b..10d1150ac 100644
--- a/include/bitcoin/database/primitives/hashhead.hpp
+++ b/include/bitcoin/database/primitives/hashhead.hpp
@@ -56,6 +56,10 @@ class hashhead
/// Optimal filter k-value for expected load factor.
static size_t optimal_k(size_t count, size_t buckets) NOEXCEPT;
+ /// Buckets for expected elements, scaled by memory between load factors.
+ static uint32_t derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT;
+
/// Create from empty head file (not thread safe).
bool create() NOEXCEPT;
diff --git a/include/bitcoin/database/primitives/hashmap.hpp b/include/bitcoin/database/primitives/hashmap.hpp
index 2c07e6d9d..c7e16389e 100644
--- a/include/bitcoin/database/primitives/hashmap.hpp
+++ b/include/bitcoin/database/primitives/hashmap.hpp
@@ -67,6 +67,10 @@ class hashmap
/// Hash table bucket count.
size_t buckets() const NOEXCEPT;
+ /// Head buckets for expected elements (hashhead::derive_buckets).
+ static uint32_t derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT;
+
/// Filter selections, derived at construct, overridden by set_filter_k.
size_t filter_k() const NOEXCEPT;
diff --git a/include/bitcoin/database/primitives/hashmaps.hpp b/include/bitcoin/database/primitives/hashmaps.hpp
index 30fdce7c8..10d9f203a 100644
--- a/include/bitcoin/database/primitives/hashmaps.hpp
+++ b/include/bitcoin/database/primitives/hashmaps.hpp
@@ -76,6 +76,10 @@ class hashmaps
/// Hash table bucket count.
size_t buckets() const NOEXCEPT;
+ /// Head buckets for expected elements (hashhead::derive_buckets).
+ static uint32_t derive_buckets(uint64_t expected, uint32_t low,
+ uint32_t high) NOEXCEPT;
+
/// Filter selections, derived at construct, overridden by set_filter_k.
size_t filter_k() const NOEXCEPT;
diff --git a/include/bitcoin/database/primitives/headmap.hpp b/include/bitcoin/database/primitives/headmap.hpp
index f1a7feff0..66ab6dcb7 100644
--- a/include/bitcoin/database/primitives/headmap.hpp
+++ b/include/bitcoin/database/primitives/headmap.hpp
@@ -88,6 +88,10 @@ class headmap
/// Return link at index, terminal if at or above count.
Link at(size_t index) const NOEXCEPT;
+ /// Head accessor (shared lock), for at(ptr, index) over many indexes.
+ memory get_memory() const NOEXCEPT;
+ Link at(const memory& ptr, size_t index) const NOEXCEPT;
+
/// Append link at count into reserved capacity (single writer).
bool push(const Link& link) NOEXCEPT;
diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp
index 689508e22..9217b6d5d 100644
--- a/include/bitcoin/database/query.hpp
+++ b/include/bitcoin/database/query.hpp
@@ -24,6 +24,7 @@
#include
#include
#include
+#include
namespace libbitcoin {
namespace database {
@@ -70,6 +71,8 @@ class query
/// Get a transactor object.
transactor get_transactor() const NOEXCEPT;
+ /// The store.
+
/// Get first fault code, or disk_full if none and full, or success.
code get_code() const NOEXCEPT;
@@ -719,12 +722,12 @@ class query
histories& out, const hash_digest& key, size_t limit=max_size_t,
bool turbo=false) const NOEXCEPT;
- /// Electrum queries (unspents, deduped, electrum sort).
- code get_unconfirmed_unspent(const stopper& cancel, unspents& out,
+ /// Electrum queries (unspent_outputs, deduped, electrum sort).
+ code get_unconfirmed_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo=false) const NOEXCEPT;
- code get_confirmed_unspent(const stopper& cancel, unspents& out,
+ code get_confirmed_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo=false) const NOEXCEPT;
- code get_unspent(const stopper& cancel, unspents& out,
+ code get_unspent(const stopper& cancel, unspent_outputs& out,
const hash_digest& key, bool turbo=false) const NOEXCEPT;
/// Balance queries (universal, unconfirmed conflict resolution arbitrary).
@@ -748,9 +751,9 @@ class query
uint32_t index) const NOEXCEPT;
/// Unspent queries.
- unspent get_tx_unspent(const output_link& link) const NOEXCEPT;
- unspent get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT;
- unspent get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEPT;
+ unspent_output get_tx_unspent(const output_link& link) const NOEXCEPT;
+ unspent_output get_tx_confirmed_unspent(const output_link& link) const NOEXCEPT;
+ unspent_output get_tx_unconfirmed_unspent(const output_link& link) const NOEXCEPT;
/// Filters.
/// -----------------------------------------------------------------------
@@ -783,14 +786,26 @@ class query
code get_unspent_totals(const stopper& cancel, unspent_totals& out,
const header_links& branch, bool turbo=false) const NOEXCEPT;
- /// Visit each unspent coin over the branch (administrative scan), in
- /// canonical (txid, index) order as required for utxo set serialization,
- /// otherwise unordered (ordering requires a full materialized sort).
- template
- code get_unspent_coins(const stopper& cancel, unspent_totals& out,
- const Visitor& visit, const header_links& branch, bool ordered,
+ /// Unspent totals and bitcoind muhash set commitment (order invariant).
+ code get_unspent_muhash(const stopper& cancel, unspent_totals& out,
+ hash_digest& digest, const header_links& branch,
+ bool turbo=false) const NOEXCEPT;
+
+ /// Unspent totals and bitcoind serialized set commitment (canonical
+ /// (txid, index) order, a serial stream).
+ code get_unspent_serialized(const stopper& cancel, unspent_totals& out,
+ hash_digest& digest, const header_links& branch,
bool turbo=false) const NOEXCEPT;
+ /// Unspent coins with scripts hashing to the keys, and the set size.
+ code get_unspent_matches(const stopper& cancel, unspent_coins& out,
+ size_t& txouts, const std::unordered_set& keys,
+ const header_links& branch, bool turbo=false) const NOEXCEPT;
+
+ /// The block is a bip30 exception (duplicated coinbase, if configured).
+ bool is_bip30_exception(bool& out,
+ const header_link& link) const NOEXCEPT;
+
protected:
/// Network
/// -----------------------------------------------------------------------
@@ -986,21 +1001,11 @@ class query
histories& out, const tx_links& links, Functor&& functor) NOEXCEPT;
template
static code parallel_unspent_transform(const stopper& cancel, bool turbo,
- unspents& out, const output_links& outs, Functor&& functor) NOEXCEPT;
+ unspent_outputs& out, const output_links& outs, Functor&& functor) NOEXCEPT;
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, unspent_totals& out,
- 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;
- template
- code visit_unspent(const Visitor& visit, const stopper& cancel,
- const difference_set<>::entries& survivors) const NOEXCEPT;
// Not thread safe.
size_t get_fork_() const NOEXCEPT;
diff --git a/include/bitcoin/database/settings.hpp b/include/bitcoin/database/settings.hpp
index 8099e0260..855b25773 100644
--- a/include/bitcoin/database/settings.hpp
+++ b/include/bitcoin/database/settings.hpp
@@ -20,8 +20,8 @@
#define LIBBITCOIN_DATABASE_SETTINGS_HPP
#include
-#include
-#include
+#include
+#include
#include
diff --git a/include/bitcoin/database/tables/archives/output.hpp b/include/bitcoin/database/tables/archives/output.hpp
index 6739b6b84..95c81ec08 100644
--- a/include/bitcoin/database/tables/archives/output.hpp
+++ b/include/bitcoin/database/tables/archives/output.hpp
@@ -148,6 +148,31 @@ struct output
bool match{};
};
+ /// Unstreamed, writes full output directly to the sink.
+ struct write_script
+ : public schema::output
+ {
+ inline bool from_data(memory::iterator start) NOEXCEPT
+ {
+ using namespace system;
+
+ // Skip parent fk, read the value and script size.
+ const auto* position = std::next(start, tx::size);
+ value = unsafe_from_variable(position);
+ const auto script_size = unsafe_from_variable(position);
+ length = possible_narrow_cast(script_size);
+
+ sink.write_8_bytes_little_endian(value);
+ sink.write_variable(length);
+ sink.write_bytes(position, length);
+ return true;
+ }
+
+ bytewriter& sink;
+ uint64_t value{};
+ size_t length{};
+ };
+
/// Reuse an instance across gets to amortize the script allocation.
struct get_coin
: public schema::output
@@ -161,7 +186,7 @@ struct output
inline bool from_data(reader& source) NOEXCEPT
{
using namespace system;
- source.skip_bytes(tx::size);
+ parent_fk = source.read_little_endian();
value = source.read_variable();
script.resize(possible_narrow_cast(
source.read_variable()));
@@ -169,6 +194,7 @@ struct output
return source;
}
+ tx::integer parent_fk{};
uint64_t value{};
system::data_chunk script{};
};
@@ -190,7 +216,7 @@ struct output
script_size = possible_narrow_cast(
source.read_variable());
- // bitcoind's utxo set exclusion (op_return lead or oversized).
+ // bitcoind's unspent 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));
@@ -222,6 +248,30 @@ struct output
uint64_t value{};
};
+ struct get_parent_coin
+ : public schema::output
+ {
+ inline link count() const NOEXCEPT
+ {
+ BC_ASSERT(false);
+ return {};
+ }
+
+ inline bool from_data(reader& source) NOEXCEPT
+ {
+ using namespace system;
+ parent_fk = source.read_little_endian();
+ value = source.read_variable();
+ script_size = possible_narrow_cast(
+ source.read_variable());
+ return source;
+ }
+
+ tx::integer parent_fk{};
+ uint64_t value{};
+ size_t script_size{};
+ };
+
struct get_parent
: public schema::output
{
diff --git a/include/bitcoin/database/tables/archives/outs.hpp b/include/bitcoin/database/tables/archives/outs.hpp
index 6b3c70abe..79f6d031f 100644
--- a/include/bitcoin/database/tables/archives/outs.hpp
+++ b/include/bitcoin/database/tables/archives/outs.hpp
@@ -56,12 +56,50 @@ struct outs_address
/// Outs column: output fk records (rows aligned with the address spine).
struct outs_puts
{
+ using ix = linkage;
using out = schema::output::link;
using tx = schema::transaction::link;
using link = schema::outs::link;
using output_links = std::vector;
+ using excluded_outputs = std::vector>;
static constexpr auto width = schema::outs::size;
static constexpr auto suffix = "puts"_t;
+ static constexpr auto offset = out::bits;
+ static_assert(offset < to_bits(out::size));
+
+ static constexpr out::integer merge(bool excluded,
+ out::integer out_fk) NOEXCEPT
+ {
+ BC_ASSERT_MSG(!system::get_right(out_fk, offset), "overflow");
+ return system::set_right(out_fk, offset, excluded);
+ }
+
+ static constexpr out::integer to_out_fk(out::integer merged) NOEXCEPT
+ {
+ return system::set_right(merged, offset, false);
+ }
+
+ static constexpr bool is_excluded(out::integer merged) NOEXCEPT
+ {
+ return system::get_right(merged, offset);
+ }
+
+ static inline bool is_excluded(const system::chain::output& out) NOEXCEPT
+ {
+ using namespace system;
+ const auto& script = out.script();
+ const auto size = script.serialized_size(false);
+ return (size > chain::max_script_size) || (!is_zero(size) &&
+ (script.ops().front().code() == chain::opcode::op_return));
+ }
+
+ static inline bool peek_excluded(auto& source, size_t bytes) NOEXCEPT
+ {
+ using namespace system;
+ constexpr auto op_return = to_value(chain::opcode::op_return);
+ return (bytes > chain::max_script_size) ||
+ (!is_zero(bytes) && (source.peek_byte() == op_return));
+ }
struct record
: public schema::outs
@@ -75,7 +113,7 @@ struct outs_puts
{
std::ranges::for_each(out_fks, [&](auto& fk) NOEXCEPT
{
- fk = source.read_little_endian();
+ fk = to_out_fk(source.read_little_endian());
});
BC_ASSERT(!source || source.get_read_position() == count() * out::size);
@@ -108,13 +146,41 @@ struct outs_puts
inline bool from_data(reader& source) NOEXCEPT
{
- out_fk = source.read_little_endian();
+ out_fk = to_out_fk(source.read_little_endian());
return source;
}
out::integer out_fk{};
};
+ struct get_excluded
+ : public schema::outs
+ {
+ inline link count() const NOEXCEPT
+ {
+ return system::possible_narrow_cast(number);
+ }
+
+ inline bool from_data(reader& source) NOEXCEPT
+ {
+ excluded.clear();
+ for (ix::integer index{}; index < number; ++index)
+ {
+ const auto merged = source.read_little_endian();
+
+ if (is_excluded(merged))
+ excluded.emplace_back(index, to_out_fk(merged));
+ }
+
+ return source;
+ }
+
+ // Output count is assigned by the caller, from the tx record.
+ ix::integer number{};
+ excluded_outputs excluded{};
+ };
+
struct put_ref
: public schema::outs
{
@@ -133,7 +199,8 @@ struct outs_puts
const auto& outs = *tx_.outputs_ptr();
std::ranges::for_each(outs, [&](const auto& out) NOEXCEPT
{
- sink.write_little_endian(out_fk);
+ const auto merged = merge(is_excluded(*out), out_fk);
+ sink.write_little_endian(merged);
// Calculate next corresponding output fk from serialized size.
// (variable_size(value) + (value + script)) - (value - parent)
@@ -165,10 +232,12 @@ struct outs_puts
read::bytes::fast source{ stream };
for (size_t out{}; out < tx_.outputs(); ++out)
{
- sink.write_little_endian(out_fk);
const auto value = source.read_8_bytes_little_endian();
const auto bytes = source.read_size();
+ const auto excluded = peek_excluded(source, bytes);
source.skip_bytes(bytes);
+ const auto merged = merge(excluded, out_fk);
+ sink.write_little_endian(merged);
out_fk += tx::size + variable_size(value)
+ variable_size(bytes) + bytes;
}
@@ -197,8 +266,10 @@ struct outs
/// Column element aliases.
using record = outs_puts::record;
using get_output = outs_puts::get_output;
+ using get_excluded = outs_puts::get_excluded;
using put_ref = outs_puts::put_ref;
using put_view = outs_puts::put_view;
+ using excluded_outputs = outs_puts::excluded_outputs;
/// The output fk column (rows aligned with the address spine).
column puts{ *this };
diff --git a/include/bitcoin/database/tables/archives/transaction.hpp b/include/bitcoin/database/tables/archives/transaction.hpp
index 6373b4a5e..125b30a7b 100644
--- a/include/bitcoin/database/tables/archives/transaction.hpp
+++ b/include/bitcoin/database/tables/archives/transaction.hpp
@@ -266,6 +266,19 @@ struct transaction
bool coinbase{};
};
+ struct get_hash
+ : public schema::transaction
+ {
+ inline bool from_data(reader& source) NOEXCEPT
+ {
+ source.rewind_bytes(sk);
+ hash = source.read_hash();
+ return source;
+ }
+
+ key hash{};
+ };
+
struct get_version
: public schema::transaction
{
diff --git a/include/bitcoin/database/tables/archives/txs.hpp b/include/bitcoin/database/tables/archives/txs.hpp
index 2ac8b3677..9b0d40e18 100644
--- a/include/bitcoin/database/tables/archives/txs.hpp
+++ b/include/bitcoin/database/tables/archives/txs.hpp
@@ -23,7 +23,7 @@
#include
#include
#include
-#include
+#include
#include
#include
#include
diff --git a/include/bitcoin/database/types/envelope.hpp b/include/bitcoin/database/tables/envelope.hpp
similarity index 97%
rename from include/bitcoin/database/types/envelope.hpp
rename to include/bitcoin/database/tables/envelope.hpp
index c87b07937..34392732e 100644
--- a/include/bitcoin/database/types/envelope.hpp
+++ b/include/bitcoin/database/tables/envelope.hpp
@@ -16,8 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-#ifndef LIBBITCOIN_DATABASE_TYPES_ENVELOPE_HPP
-#define LIBBITCOIN_DATABASE_TYPES_ENVELOPE_HPP
+#ifndef LIBBITCOIN_DATABASE_TABLES_ENVELOPE_HPP
+#define LIBBITCOIN_DATABASE_TABLES_ENVELOPE_HPP
#include
#include
diff --git a/include/bitcoin/database/tables/schema.hpp b/include/bitcoin/database/tables/schema.hpp
index 55a6ac5bf..29dc7e195 100644
--- a/include/bitcoin/database/tables/schema.hpp
+++ b/include/bitcoin/database/tables/schema.hpp
@@ -20,7 +20,7 @@
#define LIBBITCOIN_DATABASE_TABLES_SCHEMA_HPP
#include
-#include
+#include
#include
#define TABLE_COLUMN(table, bytes) \
@@ -153,7 +153,7 @@ struct output
{
static constexpr size_t sk = zero;
static constexpr size_t pk = schema::put;
- using link = linkage;
+ using link = linkage; // reduced for outs merge.
static constexpr size_t minsize =
schema::transaction::pk + // parent->tx (address navigation)
one + // value (variable)
@@ -232,6 +232,7 @@ struct outs
static constexpr size_t pk = schema::outs_;
using link = linkage;
static constexpr size_t minsize =
+ ////schema::bit + // excluded (merged into output fk)
schema::output::pk;
static constexpr size_t minrow = minsize;
static constexpr size_t size = minsize;
diff --git a/include/bitcoin/database/tables/tables.hpp b/include/bitcoin/database/tables/tables.hpp
index fe1177c2f..01dc35130 100644
--- a/include/bitcoin/database/tables/tables.hpp
+++ b/include/bitcoin/database/tables/tables.hpp
@@ -19,6 +19,7 @@
#ifndef LIBBITCOIN_DATABASE_TABLES_TABLES_HPP
#define LIBBITCOIN_DATABASE_TABLES_TABLES_HPP
+#include
#include
#include
#include
diff --git a/include/bitcoin/database/types/constants.hpp b/include/bitcoin/database/types/constants.hpp
new file mode 100644
index 000000000..742084e3e
--- /dev/null
+++ b/include/bitcoin/database/types/constants.hpp
@@ -0,0 +1,51 @@
+/**
+ * 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_CONSTANTS_HPP
+#define LIBBITCOIN_DATABASE_TYPES_CONSTANTS_HPP
+
+#include
+
+namespace libbitcoin {
+namespace database {
+
+/// The two bip30 exceptions, coinbases duplicating (overwriting) an original.
+struct bip30
+{
+ static constexpr size_t first_original = 91722;
+ static constexpr size_t first_exception = 91880;
+ static constexpr size_t second_original = 91812;
+ static constexpr size_t second_exception = 91842;
+
+ static inline const system::chain::checkpoint first
+ {
+ "00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721",
+ first_exception
+ };
+
+ static inline const system::chain::checkpoint second
+ {
+ "00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec",
+ second_exception
+ };
+};
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/types/difference_set.hpp b/include/bitcoin/database/types/difference_set.hpp
index e7ed7e284..0a1bb27fd 100644
--- a/include/bitcoin/database/types/difference_set.hpp
+++ b/include/bitcoin/database/types/difference_set.hpp
@@ -19,85 +19,75 @@
#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
+/// Multiset symmetric difference over a dense element domain. Each
+/// presentation of an element toggles its membership, so even multiplicities
+/// cancel and the residue is the set. One bit per element, in domain order.
+/// Toggle is thread safe, enumeration (word) is not concurrent with toggle.
class difference_set
{
public:
DELETE_COPY_MOVE(difference_set);
- using entry = std::pair;
- using entries = std::vector;
+ using word = uint64_t;
+ static constexpr auto word_bits = bits