diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am
index 7ad027ea6..b6238fb5f 100644
--- a/builds/gnu/Makefile.am
+++ b/builds/gnu/Makefile.am
@@ -109,6 +109,7 @@ include_bitcoin_database_impl_primitives_HEADERS = \
${srcdir}/../../include/bitcoin/database/impl/primitives/hashhead.ipp \
${srcdir}/../../include/bitcoin/database/impl/primitives/hashmap.ipp \
${srcdir}/../../include/bitcoin/database/impl/primitives/hashmaps.ipp \
+ ${srcdir}/../../include/bitcoin/database/impl/primitives/headmap.ipp \
${srcdir}/../../include/bitcoin/database/impl/primitives/iterator.ipp \
${srcdir}/../../include/bitcoin/database/impl/primitives/keys.ipp \
${srcdir}/../../include/bitcoin/database/impl/primitives/linkage.ipp \
@@ -249,6 +250,7 @@ include_bitcoin_database_primitives_HEADERS = \
${srcdir}/../../include/bitcoin/database/primitives/hashhead.hpp \
${srcdir}/../../include/bitcoin/database/primitives/hashmap.hpp \
${srcdir}/../../include/bitcoin/database/primitives/hashmaps.hpp \
+ ${srcdir}/../../include/bitcoin/database/primitives/headmap.hpp \
${srcdir}/../../include/bitcoin/database/primitives/iterator.hpp \
${srcdir}/../../include/bitcoin/database/primitives/keys.hpp \
${srcdir}/../../include/bitcoin/database/primitives/linkage.hpp \
@@ -371,6 +373,7 @@ test_libbitcoin_database_test_SOURCES = \
${srcdir}/../../test/primitives/hashhead.cpp \
${srcdir}/../../test/primitives/hashmap.cpp \
${srcdir}/../../test/primitives/hashmaps.cpp \
+ ${srcdir}/../../test/primitives/headmap.cpp \
${srcdir}/../../test/primitives/iterator.cpp \
${srcdir}/../../test/primitives/keys.cpp \
${srcdir}/../../test/primitives/linkage.cpp \
diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj
index c2876a2ba..c9a26d1e5 100644
--- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.vcxproj
@@ -139,6 +139,7 @@
+
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 7e98fb279..5cbbc61a5 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
@@ -117,6 +117,9 @@
src\primitives
+
+ src\primitives
+
src\primitives
diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
index ddeb8a1e7..8dafce85b 100644
--- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj
@@ -171,6 +171,7 @@
+
@@ -236,6 +237,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
index 5f5bd1c99..9a99d8c69 100644
--- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.vcxproj.filters
@@ -230,6 +230,9 @@
include\bitcoin\database\primitives
+
+ include\bitcoin\database\primitives
+
include\bitcoin\database\primitives
@@ -421,6 +424,9 @@
include\bitcoin\database\impl\primitives
+
+ include\bitcoin\database\impl\primitives
+
include\bitcoin\database\impl\primitives
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 d073839e6..70424600b 100644
--- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
@@ -139,6 +139,7 @@
+
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 7e98fb279..5cbbc61a5 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
@@ -117,6 +117,9 @@
src\primitives
+
+ src\primitives
+
src\primitives
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
index 5463e47ab..9222bd248 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
@@ -171,6 +171,7 @@
+
@@ -236,6 +237,7 @@
+
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
index 5f5bd1c99..9a99d8c69 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj.filters
@@ -230,6 +230,9 @@
include\bitcoin\database\primitives
+
+ include\bitcoin\database\primitives
+
include\bitcoin\database\primitives
@@ -421,6 +424,9 @@
include\bitcoin\database\impl\primitives
+
+ include\bitcoin\database\impl\primitives
+
include\bitcoin\database\impl\primitives
diff --git a/include/bitcoin/database.hpp b/include/bitcoin/database.hpp
index c093da66e..b80167d39 100644
--- a/include/bitcoin/database.hpp
+++ b/include/bitcoin/database.hpp
@@ -48,6 +48,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp
index 6558274ce..d2578d20c 100644
--- a/include/bitcoin/database/impl/memory/mmap_staging.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp
@@ -514,7 +514,7 @@ bool CLASS::lazy_install_() NOEXCEPT
// Commit the remainder to the commitment target and populate the tail.
const auto target = std::max(page_ceiling(to_width(
- capacity_.load())), page_ceiling(logical));
+ to_commitment())), page_ceiling(logical));
if ((target > floor) && (mmap_commit(std::next(memory_map_[zero], floor),
target - floor, headroom_) == fail))
{
@@ -538,7 +538,7 @@ bool CLASS::lazy_install_() NOEXCEPT
bit_all : unmask_right(flags - first));
}
- engaged_.store(true);
+ lazy_.store(true);
// Attribute the anonymous span for diagnostics (smaps decomposition).
mmap_name(std::next(memory_map_[zero], floor), reserved - floor,
diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp
index 649f03a11..66ae99071 100644
--- a/include/bitcoin/database/impl/memory/mmap_storage.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp
@@ -205,7 +205,7 @@ void CLASS::prepare(size_t STAGING_ONLY(offset),
// in flight across passes; the count persists until mark.
writers_.fetch_add(one);
- if (!engaged_.load(relaxed))
+ if (!engaged_.load(relaxed) && !lazy_.load(relaxed))
return;
// Declare intent before the write (sequentially consistent, pairing with
diff --git a/include/bitcoin/database/impl/primitives/headmap.ipp b/include/bitcoin/database/impl/primitives/headmap.ipp
new file mode 100644
index 000000000..fc2393cea
--- /dev/null
+++ b/include/bitcoin/database/impl/primitives/headmap.ipp
@@ -0,0 +1,213 @@
+/**
+ * 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_PRIMITIVES_HEADMAP_IPP
+#define LIBBITCOIN_DATABASE_PRIMITIVES_HEADMAP_IPP
+
+#include
+
+namespace libbitcoin {
+namespace database {
+
+TEMPLATE
+CLASS::headmap(storage& head) NOEXCEPT
+ : file_(head)
+{
+}
+
+// not thread safe
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+bool CLASS::create() NOEXCEPT
+{
+ // An empty head is a zero count.
+ return is_zero(head_size());
+}
+
+TEMPLATE
+bool CLASS::close() NOEXCEPT
+{
+ // Count is the head logical size.
+ return true;
+}
+
+TEMPLATE
+bool CLASS::backup(bool) NOEXCEPT
+{
+ // Count is the head logical size.
+ return true;
+}
+
+TEMPLATE
+bool CLASS::restore() NOEXCEPT
+{
+ // Content is fully restored with the head, so there is nothing to prune.
+ return verify();
+}
+
+TEMPLATE
+bool CLASS::verify() const NOEXCEPT
+{
+ return is_zero(head_size() % bucket_size);
+}
+
+// sizing
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+size_t CLASS::head_size() const NOEXCEPT
+{
+ return file_.size();
+}
+
+TEMPLATE
+size_t CLASS::body_size() const NOEXCEPT
+{
+ // There is no body (all content is in the head).
+ return zero;
+}
+
+TEMPLATE
+Link CLASS::count() const NOEXCEPT
+{
+ return position_to_link(head_size());
+}
+
+TEMPLATE
+bool CLASS::truncate(const Link& count) NOEXCEPT
+{
+ if (count.is_terminal())
+ return false;
+
+ return file_.truncate(link_to_position(count));
+}
+
+TEMPLATE
+bool CLASS::reserve(const Link& count) NOEXCEPT
+{
+ if (count.is_terminal())
+ return false;
+
+ return file_.reserve(link_to_position(count));
+}
+
+// error condition
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+code CLASS::get_fault() const NOEXCEPT
+{
+ return file_.get_fault();
+}
+
+TEMPLATE
+size_t CLASS::get_space() const NOEXCEPT
+{
+ return file_.get_space();
+}
+
+TEMPLATE
+code CLASS::reload() NOEXCEPT
+{
+ return error::success;
+}
+
+// query interface
+// ----------------------------------------------------------------------------
+
+TEMPLATE
+Link CLASS::at(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 ptr = file_.get(link_to_position(index));
+ if (!ptr)
+ return {};
+
+ const auto raw = ptr.data();
+ 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
+{
+ using namespace system;
+ if (link.is_terminal())
+ return false;
+
+ const auto position = head_size();
+
+ // Dispose accessor (lock) before allocate.
+ {
+ const auto ptr = file_.get_capacity(position);
+ if (!ptr || (ptr.size() < possible_narrow_sign_cast(
+ bucket_size)))
+ return false;
+
+ // Writes into unpublished capacity.
+ file_.prepare(position, bucket_size);
+ const auto raw = ptr.data();
+ cell value = link.value;
+ if constexpr (aligned)
+ {
+ // Writes full padded word.
+ auto& head = *pointer_cast>(raw);
+ head.store(value, std::memory_order_relaxed);
+ }
+ else
+ {
+ auto& head = cell_array(raw);
+
+ mutex_.lock();
+ head = cell_array(value);
+ mutex_.unlock();
+ }
+
+ file_.mark(position, bucket_size);
+ }
+
+ // Publication of the pre-written bucket is its allocation.
+ return file_.allocate(bucket_size) != storage::eof;
+}
+
+} // namespace database
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/database/impl/primitives/nomap.ipp b/include/bitcoin/database/impl/primitives/nomap.ipp
index dbd2d30b5..30f04e7d1 100644
--- a/include/bitcoin/database/impl/primitives/nomap.ipp
+++ b/include/bitcoin/database/impl/primitives/nomap.ipp
@@ -120,13 +120,6 @@ bool CLASS::drop() NOEXCEPT
return body_.truncate(0) && backup();
}
-TEMPLATE
-bool CLASS::reserve(const Link& size) NOEXCEPT
-{
- // Not writer-writer thread safe (two writers may share reserve).
- return body_.reserve(size);
-}
-
TEMPLATE
Link CLASS::allocate(const Link& size) NOEXCEPT
{
@@ -321,27 +314,6 @@ inline Link CLASS::put_link(const Element& element) NOEXCEPT
return put_link(link, element) ? link : Link{};
}
-// NOT THREAD SAFE (used only for height index with writer ordering).
-TEMPLATE
-template >
-inline bool CLASS::commit(const Element& element) NOEXCEPT
-{
- // Zero allocation provides link of next (presumably reserved) element.
- const auto link = body_.allocate(0);
-
- // Write element into reserved but unallocated space.
- if (!put(body_.get_capacity(link), element))
- return false;
-
- // Allocate reserved and written element (exposes logically).
- if (body_.allocate(element.count()).is_terminal())
- return false;
-
- // Publication of the pre-written element is its completion.
- body_.complete(link, element.count());
- return true;
-}
-
} // namespace database
} // namespace libbitcoin
diff --git a/include/bitcoin/database/impl/query/confirmed.ipp b/include/bitcoin/database/impl/query/confirmed.ipp
index ee12f0c4a..5e84b4394 100644
--- a/include/bitcoin/database/impl/query/confirmed.ipp
+++ b/include/bitcoin/database/impl/query/confirmed.ipp
@@ -78,9 +78,7 @@ bool CLASS::is_candidate_header(const header_link& link) const NOEXCEPT
if (height.is_terminal())
return false;
- table::height::record candidate{};
- return store_.candidate.get(height, candidate) &&
- (candidate.header_fk == link);
+ return store_.candidate.at(height) == link;
}
TEMPLATE
@@ -91,9 +89,7 @@ bool CLASS::is_confirmed_block(const header_link& link) const NOEXCEPT
if (height.is_terminal())
return false;
- table::height::record confirmed{};
- return store_.confirmed.get(height, confirmed) &&
- (confirmed.header_fk == link);
+ return store_.confirmed.at(height) == link;
}
TEMPLATE
diff --git a/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp b/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp
index 77fd96a07..de5d475ad 100644
--- a/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp
+++ b/include/bitcoin/database/impl/query/consensus/consensus_forks.ipp
@@ -34,9 +34,7 @@ height_link CLASS::get_confirmed_height(const header_link& link) const NOEXCEPT
if (const auto height = get_height(link); !height.is_terminal())
{
// The block is confirmed (by height).
- table::height::record confirmed{};
- if (store_.confirmed.get(height, confirmed) &&
- (confirmed.header_fk == link))
+ if (store_.confirmed.at(height) == link)
return height;
}
diff --git a/include/bitcoin/database/impl/query/height.ipp b/include/bitcoin/database/impl/query/height.ipp
index 345fc2a88..4a8e3d17b 100644
--- a/include/bitcoin/database/impl/query/height.ipp
+++ b/include/bitcoin/database/impl/query/height.ipp
@@ -35,7 +35,7 @@ bool CLASS::push_candidate(const header_link& link) NOEXCEPT
if (link.is_terminal())
return false;
- // Reserve-commit to ensure disk full safety and deferred access.
+ // Reserve-push to ensure disk full safety and deferred access.
if (!store_.candidate.reserve(one))
return false;
@@ -43,8 +43,7 @@ bool CLASS::push_candidate(const header_link& link) NOEXCEPT
const auto scope = get_transactor();
// Clean single allocation failure (e.g. disk full).
- const table::height::record candidate{ {}, link };
- return store_.candidate.put(candidate);
+ return store_.candidate.push(link);
// ========================================================================
}
@@ -77,7 +76,7 @@ bool CLASS::push_confirmed(const header_link& link, bool strong) NOEXCEPT
if (strong && !store_.txs.at(to_txs(link), txs))
return false;
- // Reserve-commit to ensure disk full safety and deferred access.
+ // Reserve-push to ensure disk full safety and deferred access.
if (!store_.confirmed.reserve(one))
return false;
@@ -88,8 +87,7 @@ bool CLASS::push_confirmed(const header_link& link, bool strong) NOEXCEPT
if (strong && !set_strong(link, txs.number, txs.coinbase_fk, true))
return false;
- const table::height::record confirmed{ {}, link };
- return store_.confirmed.commit(confirmed);
+ return store_.confirmed.push(link);
// ========================================================================
}
diff --git a/include/bitcoin/database/impl/query/locator.ipp b/include/bitcoin/database/impl/query/locator.ipp
index 20ce1c1e2..d61b42cff 100644
--- a/include/bitcoin/database/impl/query/locator.ipp
+++ b/include/bitcoin/database/impl/query/locator.ipp
@@ -117,10 +117,8 @@ size_t CLASS::get_locator_start(const hashes& locator) const NOEXCEPT
continue;
const auto height = get_height(link);
- table::height::record confirmed{};
- if (store_.confirmed.get(height, confirmed) &&
- confirmed.header_fk == link)
- return height;
+ if (store_.confirmed.at(height) == link)
+ return height;
}
return zero;
diff --git a/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp b/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp
index a071bbe7c..34fa89cc5 100644
--- a/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp
+++ b/include/bitcoin/database/impl/query/navigate/navigate_natural.ipp
@@ -32,31 +32,13 @@ namespace database {
TEMPLATE
inline header_link CLASS::to_candidate(size_t height) const NOEXCEPT
{
- using namespace system;
- using link = table::height::header::integer;
- if (height >= store_.candidate.count())
- return {};
-
- table::height::record index{};
- if (!store_.candidate.get(possible_narrow_cast(height), index))
- return {};
-
- return index.header_fk;
+ return store_.candidate.at(height);
}
TEMPLATE
inline header_link CLASS::to_confirmed(size_t height) const NOEXCEPT
{
- using namespace system;
- using link = table::height::header::integer;
- if (height >= store_.confirmed.count())
- return {};
-
- table::height::record index{};
- if (!store_.confirmed.get(possible_narrow_cast(height), index))
- return {};
-
- return index.header_fk;
+ return store_.confirmed.at(height);
}
TEMPLATE
diff --git a/include/bitcoin/database/impl/store/store.ipp b/include/bitcoin/database/impl/store/store.ipp
index f19897a33..4eef24afb 100644
--- a/include/bitcoin/database/impl/store/store.ipp
+++ b/include/bitcoin/database/impl/store/store.ipp
@@ -31,75 +31,72 @@ CLASS::store(const settings& config) NOEXCEPT
// Archive.
// ------------------------------------------------------------------------
- header_head_(head(config.path / schema::dir::heads, schema::archive::header), head_settings, random),
+ header_head_(head(config.path / schema::dir::heads, schema::archive::header), head_settings(config.header), random),
header_body_(body(config.path, schema::archive::header), config.header, sequential, staged),
- input_head_(head(config.path / schema::dir::heads, schema::archive::input), head_settings, sequential),
+ input_head_(head(config.path / schema::dir::heads, schema::archive::input), head_settings(config.input), sequential),
input_body_(body(config.path, schema::archive::input), config.input, sequential, staged),
- output_head_(head(config.path / schema::dir::heads, schema::archive::output), head_settings, sequential),
+ output_head_(head(config.path / schema::dir::heads, schema::archive::output), head_settings(config.output), sequential),
output_body_(body(config.path, schema::archive::output), config.output, sequential, staged),
- ins_head_(head(config.path / schema::dir::heads, schema::archive::ins), head_settings, random),
+ ins_head_(head(config.path / schema::dir::heads, schema::archive::ins), head_settings(config.ins), random),
ins_body_(body(config.path, schema::archive::ins), config.ins, sequential, staged),
- outs_head_(head(config.path / schema::dir::heads, schema::archive::outs), head_settings, random),
+ outs_head_(head(config.path / schema::dir::heads, schema::archive::outs), head_settings(config.outs), random),
outs_body_(body(config.path, schema::archive::outs), config.outs, sequential, staged),
- tx_head_(head(config.path / schema::dir::heads, schema::archive::tx), head_settings, random),
+ tx_head_(head(config.path / schema::dir::heads, schema::archive::tx), head_settings(config.tx), random),
tx_body_(body(config.path, schema::archive::tx), config.tx, sequential, staged),
- txs_head_(head(config.path / schema::dir::heads, schema::archive::txs), head_settings, random),
+ txs_head_(head(config.path / schema::dir::heads, schema::archive::txs), head_settings(config.txs), random),
txs_body_(body(config.path, schema::archive::txs), config.txs, sequential, staged),
// Indexes.
// ------------------------------------------------------------------------
- candidate_head_(head(config.path / schema::dir::heads, schema::indexes::candidate), head_settings, sequential),
- candidate_body_(body(config.path, schema::indexes::candidate), config.candidate, sequential, staged),
+ candidate_head_(head(config.path / schema::dir::heads, schema::indexes::candidate), head_settings(config.candidate), sequential),
+ confirmed_head_(head(config.path / schema::dir::heads, schema::indexes::confirmed), head_settings(config.confirmed), sequential),
- confirmed_head_(head(config.path / schema::dir::heads, schema::indexes::confirmed), head_settings, sequential),
- confirmed_body_(body(config.path, schema::indexes::confirmed), config.confirmed, sequential, staged),
-
- strong_tx_head_(head(config.path / schema::dir::heads, schema::indexes::strong_tx), head_settings, random),
+ strong_tx_head_(head(config.path / schema::dir::heads, schema::indexes::strong_tx), head_settings(config.strong_tx), random),
strong_tx_body_(body(config.path, schema::indexes::strong_tx), config.strong_tx, sequential, staged),
// Caches.
// ------------------------------------------------------------------------
// TODO: body not random, but keep in memory.
- ecdsa_head_(head(config.path / schema::dir::heads, schema::caches::ecdsa), head_settings, sequential),
+ ecdsa_head_(head(config.path / schema::dir::heads, schema::caches::ecdsa), head_settings(config.ecdsa), sequential),
ecdsa_body_(body(config.path, schema::caches::ecdsa), config.ecdsa, sequential, staged),
// TODO: body not random, but keep in memory.
- schnorr_head_(head(config.path / schema::dir::heads, schema::caches::schnorr), head_settings, sequential),
+ schnorr_head_(head(config.path / schema::dir::heads, schema::caches::schnorr), head_settings(config.schnorr), sequential),
schnorr_body_(body(config.path, schema::caches::schnorr), config.schnorr, sequential, staged),
- silent_head_(head(config.path / schema::dir::heads, schema::caches::silent), head_settings, sequential),
+ silent_head_(head(config.path / schema::dir::heads, schema::caches::silent), head_settings(config.silent), sequential),
silent_body_(body(config.path, schema::caches::silent), config.silent, sequential, staged),
- duplicate_head_(head(config.path / schema::dir::heads, schema::caches::duplicate), head_settings, random),
+ duplicate_head_(head(config.path / schema::dir::heads, schema::caches::duplicate), head_settings(config.duplicate), random),
duplicate_body_(body(config.path, schema::caches::duplicate), config.duplicate, sequential, staged),
- prevalid_head_(head(config.path / schema::dir::heads, schema::caches::prevalid), head_settings, sequential),
+ prevalid_head_(head(config.path / schema::dir::heads, schema::caches::prevalid), head_settings(config.prevalid), sequential),
prevalid_body_(body(config.path, schema::caches::prevalid), config.prevalid, sequential, staged),
- prevout_head_(head(config.path / schema::dir::heads, schema::caches::prevout), head_settings, random),
+ prevout_head_(head(config.path / schema::dir::heads, schema::caches::prevout), head_settings(config.prevout), random),
prevout_body_(body(config.path, schema::caches::prevout), config.prevout, sequential, staged),
- validated_bk_head_(head(config.path / schema::dir::heads, schema::caches::validated_bk), head_settings, random),
+ validated_bk_head_(head(config.path / schema::dir::heads, schema::caches::validated_bk), head_settings(config.validated_bk), random),
validated_bk_body_(body(config.path, schema::caches::validated_bk), config.validated_bk, sequential, staged),
- validated_tx_head_(head(config.path / schema::dir::heads, schema::caches::validated_tx), head_settings, random),
+ validated_tx_head_(head(config.path / schema::dir::heads, schema::caches::validated_tx), head_settings(config.validated_tx), random),
validated_tx_body_(body(config.path, schema::caches::validated_tx), config.validated_tx, sequential, staged),
// Optionals.
// ------------------------------------------------------------------------
- filter_bk_head_(head(config.path / schema::dir::heads, schema::optionals::filter_bk), head_settings, random),
+ filter_bk_head_(head(config.path / schema::dir::heads, schema::optionals::filter_bk), head_settings(config.filter_bk), random),
filter_bk_body_(body(config.path, schema::optionals::filter_bk), config.filter_bk, sequential, staged),
- filter_tx_head_(head(config.path / schema::dir::heads, schema::optionals::filter_tx), head_settings, random),
+ filter_tx_head_(head(config.path / schema::dir::heads, schema::optionals::filter_tx), head_settings(config.filter_tx), random),
filter_tx_body_(body(config.path, schema::optionals::filter_tx), config.filter_tx, sequential, staged),
// Locks.
@@ -119,8 +116,8 @@ CLASS::store(const settings& config) NOEXCEPT
tx(tx_head_, tx_body_, config.tx.buckets),
txs(txs_head_, txs_body_, config.txs.buckets),
- candidate(candidate_head_, candidate_body_),
- confirmed(confirmed_head_, confirmed_body_),
+ candidate(candidate_head_),
+ confirmed(confirmed_head_),
strong_tx(strong_tx_head_, strong_tx_body_, config.strong_tx.buckets),
ecdsa(ecdsa_head_, ecdsa_body_),
diff --git a/include/bitcoin/database/impl/store/store_backup.ipp b/include/bitcoin/database/impl/store/store_backup.ipp
index fe33a1ac2..061087734 100644
--- a/include/bitcoin/database/impl/store/store_backup.ipp
+++ b/include/bitcoin/database/impl/store/store_backup.ipp
@@ -100,7 +100,16 @@ code CLASS::backup(const event_handler& handler, bool prune) NOEXCEPT
}
// Rename /temporary to /primary (atomic).
- return file::rename_ex(temporary, primary);
+ if ((ec = file::rename_ex(temporary, primary))) return ec;
+
+ // Delete the rotated /secondary, superseded by the new /primary.
+ if (file::is_directory(secondary))
+ {
+ if ((ec = file::clear_directory_ex(secondary))) return ec;
+ ec = file::remove_ex(secondary);
+ }
+
+ return ec;
}
} // namespace database
diff --git a/include/bitcoin/database/impl/store/store_create.ipp b/include/bitcoin/database/impl/store/store_create.ipp
index 3b17cf911..55d352790 100644
--- a/include/bitcoin/database/impl/store/store_create.ipp
+++ b/include/bitcoin/database/impl/store/store_create.ipp
@@ -44,6 +44,28 @@ code CLASS::create(const event_handler& handler) NOEXCEPT
return error::flush_lock;
}
+ auto ec = create_load(handler);
+
+ if (ec)
+ {
+ /* code */ unload_close(handler);
+
+ // unlock errors override ec.
+ if (!flush_lock_.try_unlock()) ec = error::flush_unlock;
+ if (!process_lock_.try_unlock()) ec = error::process_unlock;
+ /* bool */ file::clear_directory(configuration_.path);
+ /* bool */ file::remove(configuration_.path);
+ }
+
+ // process and flush locks remain open until close().
+ transactor_mutex_.unlock();
+ return ec;
+}
+
+// protected
+TEMPLATE
+code CLASS::create_load(const event_handler& handler) NOEXCEPT
+{
const auto create = [&handler](code& ec, const auto& file,
table_t table) NOEXCEPT
{
@@ -73,9 +95,7 @@ code CLASS::create(const event_handler& handler) NOEXCEPT
create(ec, txs_body_, table_t::txs_body);
create(ec, candidate_head_, table_t::candidate_head);
- create(ec, candidate_body_, table_t::candidate_body);
create(ec, confirmed_head_, table_t::confirmed_head);
- create(ec, confirmed_body_, table_t::confirmed_body);
create(ec, strong_tx_head_, table_t::strong_tx_head);
create(ec, strong_tx_body_, table_t::strong_tx_body);
@@ -139,19 +159,6 @@ code CLASS::create(const event_handler& handler) NOEXCEPT
populate(ec, filter_bk, table_t::filter_bk_table);
populate(ec, filter_tx, table_t::filter_tx_table);
- if (ec)
- {
- /* code */ unload_close(handler);
-
- // unlock errors override ec.
- if (!flush_lock_.try_unlock()) ec = error::flush_unlock;
- if (!process_lock_.try_unlock()) ec = error::process_unlock;
- /* bool */ file::clear_directory(configuration_.path);
- /* bool */ file::remove(configuration_.path);
- }
-
- // process and flush locks remain open until close().
- transactor_mutex_.unlock();
return ec;
}
diff --git a/include/bitcoin/database/impl/store/store_open_load.ipp b/include/bitcoin/database/impl/store/store_open_load.ipp
index 9dfaa3ef2..7da415a3a 100644
--- a/include/bitcoin/database/impl/store/store_open_load.ipp
+++ b/include/bitcoin/database/impl/store/store_open_load.ipp
@@ -54,9 +54,7 @@ code CLASS::open_load(const event_handler& handler) NOEXCEPT
open(ec, txs_body_, table_t::txs_body);
open(ec, candidate_head_, table_t::candidate_head);
- open(ec, candidate_body_, table_t::candidate_body);
open(ec, confirmed_head_, table_t::confirmed_head);
- open(ec, confirmed_body_, table_t::confirmed_body);
open(ec, strong_tx_head_, table_t::strong_tx_head);
open(ec, strong_tx_body_, table_t::strong_tx_body);
@@ -107,9 +105,7 @@ code CLASS::open_load(const event_handler& handler) NOEXCEPT
load(ec, txs_body_, table_t::txs_body);
load(ec, candidate_head_, table_t::candidate_head);
- load(ec, candidate_body_, table_t::candidate_body);
load(ec, confirmed_head_, table_t::confirmed_head);
- load(ec, confirmed_body_, table_t::confirmed_body);
load(ec, strong_tx_head_, table_t::strong_tx_head);
load(ec, strong_tx_body_, table_t::strong_tx_body);
diff --git a/include/bitcoin/database/impl/store/store_prune.ipp b/include/bitcoin/database/impl/store/store_prune.ipp
index f3f1d1f0f..c8bb9a272 100644
--- a/include/bitcoin/database/impl/store/store_prune.ipp
+++ b/include/bitcoin/database/impl/store/store_prune.ipp
@@ -58,6 +58,7 @@ code CLASS::prune(const event_handler& handler) NOEXCEPT
{
// Snapshot with nullified head and zero body count.
// The 'prune' parameter signals to not reset body count.
+ // Success deletes the pre-prune snapshot (trim strands nothing).
ec = snapshot(handler, true);
// If the pruning fails here the snapshot remains valid.
diff --git a/include/bitcoin/database/impl/store/store_reload.ipp b/include/bitcoin/database/impl/store/store_reload.ipp
index a8f930dd3..7abd7daa9 100644
--- a/include/bitcoin/database/impl/store/store_reload.ipp
+++ b/include/bitcoin/database/impl/store/store_reload.ipp
@@ -69,9 +69,7 @@ code CLASS::reload(const event_handler& handler) NOEXCEPT
reload(ec, txs_body_, table_t::txs_body);
reload(ec, candidate_head_, table_t::candidate_head);
- reload(ec, candidate_body_, table_t::candidate_body);
reload(ec, confirmed_head_, table_t::confirmed_head);
- reload(ec, confirmed_body_, table_t::confirmed_body);
reload(ec, strong_tx_head_, table_t::strong_tx_head);
reload(ec, strong_tx_body_, table_t::strong_tx_body);
diff --git a/include/bitcoin/database/impl/store/store_report.ipp b/include/bitcoin/database/impl/store/store_report.ipp
index b3adaa9c2..5f1e27281 100644
--- a/include/bitcoin/database/impl/store/store_report.ipp
+++ b/include/bitcoin/database/impl/store/store_report.ipp
@@ -48,8 +48,6 @@ void CLASS::report(const error_handler& handler) const NOEXCEPT
report(outs_body_, table_t::outs_body);
report(tx_body_, table_t::tx_body);
report(txs_body_, table_t::txs_body);
- report(candidate_body_, table_t::candidate_body);
- report(confirmed_body_, table_t::confirmed_body);
report(strong_tx_body_, table_t::strong_tx_body);
report(ecdsa_body_, table_t::ecdsa_body);
report(schnorr_body_, table_t::schnorr_body);
@@ -83,9 +81,7 @@ code CLASS::get_fault() const NOEXCEPT
if ((ec = txs_head_.get_fault())) return ec;
if ((ec = txs_body_.get_fault())) return ec;
if ((ec = candidate_head_.get_fault())) return ec;
- if ((ec = candidate_body_.get_fault())) return ec;
if ((ec = confirmed_head_.get_fault())) return ec;
- if ((ec = confirmed_body_.get_fault())) return ec;
if ((ec = strong_tx_head_.get_fault())) return ec;
if ((ec = strong_tx_body_.get_fault())) return ec;
if ((ec = ecdsa_head_.get_fault())) return ec;
@@ -136,9 +132,7 @@ size_t CLASS::get_space() const NOEXCEPT
space(txs_head_);
space(txs_body_);
space(candidate_head_);
- space(candidate_body_);
space(confirmed_head_);
- space(confirmed_body_);
space(strong_tx_head_);
space(strong_tx_body_);
space(ecdsa_head_);
diff --git a/include/bitcoin/database/impl/store/store_restore.ipp b/include/bitcoin/database/impl/store/store_restore.ipp
index 465d97814..bb26f9513 100644
--- a/include/bitcoin/database/impl/store/store_restore.ipp
+++ b/include/bitcoin/database/impl/store/store_restore.ipp
@@ -77,17 +77,24 @@ code CLASS::restore(const event_handler& handler) NOEXCEPT
}
else
{
- ec = error::missing_snapshot;
+ // A corrupted store with no snapshot is unrecoverable, so recreate.
+ ec = create_load(handler);
+
+ if (ec)
+ {
+ /* code */ unload_close(handler);
+
+ // unlock errors override ec.
+ // on failure flush_lock is left in place (store corrupt).
+ if (!process_lock_.try_unlock()) ec = error::process_unlock;
+ }
+
+ // store is open after successful recreate but not otherwise.
+ transactor_mutex_.unlock();
+ return ec;
}
- // BUGBUG: height indexes are not append only, resulting in failure to
- // recover and allowing insidious integrity failure after recovery,
- // specifically orphaned header links in the index due to write below snap.
- // Regarding height index recovery there are two criteria for the fix.
- // (1) we must accept the existing body size if smaller than head indicates
- // (reset head), and (2) prune the body (with head indicator) to highest
- // contiguous set of links that do not exceed the header table count
- // (orphaned by snapshot restore). This requires a scan upon restore.
+ // Height index (headmap) content is captured/restored with the heads.
const auto restore = [&handler](code& ec, auto& logical,
table_t table) NOEXCEPT
{
@@ -152,6 +159,13 @@ code CLASS::restore(const event_handler& handler) NOEXCEPT
/* code */ unload_close(handler);
}
+ if (!ec && file::is_directory(secondary))
+ {
+ // Delete crash remnant /secondary, superseded by restored /primary.
+ /* bool */ file::clear_directory(secondary);
+ /* bool */ file::remove(secondary);
+ }
+
if (ec)
{
// unlock errors override ec.
diff --git a/include/bitcoin/database/impl/store/store_snapshot.ipp b/include/bitcoin/database/impl/store/store_snapshot.ipp
index d7814a325..f5e5c9962 100644
--- a/include/bitcoin/database/impl/store/store_snapshot.ipp
+++ b/include/bitcoin/database/impl/store/store_snapshot.ipp
@@ -53,8 +53,6 @@ code CLASS::snapshot(const event_handler& handler, bool prune) NOEXCEPT
flush(ec, tx_body_, table_t::tx_body);
flush(ec, txs_body_, table_t::txs_body);
- flush(ec, candidate_body_, table_t::candidate_body);
- flush(ec, confirmed_body_, table_t::confirmed_body);
flush(ec, strong_tx_body_, table_t::strong_tx_body);
flush(ec, ecdsa_body_, table_t::ecdsa_body);
diff --git a/include/bitcoin/database/impl/store/store_tables.ipp b/include/bitcoin/database/impl/store/store_tables.ipp
index c62745d0d..f6914f244 100644
--- a/include/bitcoin/database/impl/store/store_tables.ipp
+++ b/include/bitcoin/database/impl/store/store_tables.ipp
@@ -55,10 +55,8 @@ const CLASS::table_map CLASS::tables
// Indexes.
{ table_t::candidate_table, "candidate_table" },
{ table_t::candidate_head, "candidate_head" },
- { table_t::candidate_body, "candidate_body" },
{ table_t::confirmed_table, "confirmed_table" },
{ table_t::confirmed_head, "confirmed_head" },
- { table_t::confirmed_body, "confirmed_body" },
{ table_t::strong_tx_table, "strong_tx_table" },
{ table_t::strong_tx_head, "strong_tx_head" },
{ table_t::strong_tx_body, "strong_tx_body" },
diff --git a/include/bitcoin/database/impl/store/store_unload_close.ipp b/include/bitcoin/database/impl/store/store_unload_close.ipp
index ad419a319..f1fd20a58 100644
--- a/include/bitcoin/database/impl/store/store_unload_close.ipp
+++ b/include/bitcoin/database/impl/store/store_unload_close.ipp
@@ -54,9 +54,7 @@ code CLASS::unload_close(const event_handler& handler) NOEXCEPT
unload(ec, txs_body_, table_t::txs_body);
unload(ec, candidate_head_, table_t::candidate_head);
- unload(ec, candidate_body_, table_t::candidate_body);
unload(ec, confirmed_head_, table_t::confirmed_head);
- unload(ec, confirmed_body_, table_t::confirmed_body);
unload(ec, strong_tx_head_, table_t::strong_tx_head);
unload(ec, strong_tx_body_, table_t::strong_tx_body);
@@ -107,9 +105,7 @@ code CLASS::unload_close(const event_handler& handler) NOEXCEPT
close(ec, txs_body_, table_t::txs_body);
close(ec, candidate_head_, table_t::candidate_head);
- close(ec, candidate_body_, table_t::candidate_body);
close(ec, confirmed_head_, table_t::confirmed_head);
- close(ec, confirmed_body_, table_t::confirmed_body);
close(ec, strong_tx_head_, table_t::strong_tx_head);
close(ec, strong_tx_body_, table_t::strong_tx_body);
diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp
index 03fc56f92..331480f8e 100644
--- a/include/bitcoin/database/memory/mmap.hpp
+++ b/include/bitcoin/database/memory/mmap.hpp
@@ -479,6 +479,9 @@ class mmap
// Set when the first head page releases (gates the prepare fast path).
std::atomic_bool engaged_{};
+ // Set when a head loads released (gates prepare only, never the drain).
+ std::atomic_bool lazy_{};
+
// Writers between prepare and mark (unaged, unlike intent bits), so a
// release pass cannot settle under a preempted in-flight write.
std::atomic writers_{};
diff --git a/include/bitcoin/database/primitives/headmap.hpp b/include/bitcoin/database/primitives/headmap.hpp
new file mode 100644
index 000000000..f1a7feff0
--- /dev/null
+++ b/include/bitcoin/database/primitives/headmap.hpp
@@ -0,0 +1,154 @@
+/**
+ * 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_PRIMITIVES_HEADMAP_HPP
+#define LIBBITCOIN_DATABASE_PRIMITIVES_HEADMAP_HPP
+
+#include
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace database {
+
+/// A head-only array of links indexed by position (no body, no count cell).
+/// The storage logical size is the count, providing count atomicity and
+/// bucket publication as with bodies. Content restores with the heads.
+template = true>
+class headmap
+{
+public:
+ DELETE_COPY_MOVE_DESTRUCT(headmap);
+
+ using link = Link;
+
+ headmap(storage& head) NOEXCEPT;
+
+ /// Setup, not thread safe.
+ /// -----------------------------------------------------------------------
+
+ bool create() NOEXCEPT;
+ bool close() NOEXCEPT;
+ bool backup(bool=false) NOEXCEPT;
+ bool restore() NOEXCEPT;
+ bool verify() const NOEXCEPT;
+
+ /// Sizing.
+ /// -----------------------------------------------------------------------
+
+ /// Head file bytes.
+ size_t head_size() const NOEXCEPT;
+
+ /// Body file bytes (zero, no body).
+ size_t body_size() const NOEXCEPT;
+
+ /// Count of buckets (links).
+ Link count() const NOEXCEPT;
+
+ /// Reduce count as specified (truncated buckets are rewritten by push).
+ bool truncate(const Link& count) NOEXCEPT;
+
+ /// Reserve additional count buckets to guard against disk full.
+ /// Not writer-writer thread safe.
+ bool reserve(const Link& count) NOEXCEPT;
+
+ /// Errors.
+ /// -----------------------------------------------------------------------
+
+ /// Get the fault condition.
+ code get_fault() const NOEXCEPT;
+
+ /// Get the space required to clear the disk full condition.
+ size_t get_space() const NOEXCEPT;
+
+ /// Resume from disk full condition (nothing to reload, no body).
+ code reload() NOEXCEPT;
+
+ /// Query interface.
+ /// -----------------------------------------------------------------------
+
+ /// Return link at index, terminal if at or above count.
+ Link at(size_t index) const NOEXCEPT;
+
+ /// Append link at count into reserved capacity (single writer).
+ bool push(const Link& link) NOEXCEPT;
+
+private:
+ using cell = unsigned_type;
+ static constexpr size_t bucket_size = Cell;
+ static constexpr bool aligned = (bucket_size == sizeof(cell));
+ static_assert(is_nonzero(Link::size));
+ static_assert(std::atomic::is_always_lock_free);
+
+ INLINE static auto& cell_array(memory::iterator it) NOEXCEPT
+ {
+ return system::unsafe_array_cast(it);
+ }
+
+ template
+ INLINE static auto& cell_array(Integral& value) NOEXCEPT
+ {
+ return cell_array(system::pointer_cast(&value));
+ }
+
+ INLINE static constexpr Link to_link(cell value) NOEXCEPT
+ {
+ using namespace system;
+ return possible_narrow_cast(
+ bit_and| (Link::terminal, value));
+ }
+
+ // Byte offset of bucket index within head file.
+ // [[bucket[0]...bucket[count-1]]]
+ INLINE static constexpr size_t link_to_position(size_t index) NOEXCEPT
+ {
+ BC_ASSERT(!system::is_multiply_overflow(index, bucket_size));
+ return index * bucket_size;
+ }
+
+ INLINE static constexpr Link position_to_link(size_t position) NOEXCEPT
+ {
+ using namespace system;
+ return possible_narrow_cast(
+ floored_divide(position, bucket_size));
+ }
+
+ // These are thread safe.
+ storage& file_;
+ mutable std::shared_mutex mutex_{};
+};
+
+template
+using head_map = headmap;
+
+} // namespace database
+} // namespace libbitcoin
+
+#define TEMPLATE template If>
+#define CLASS headmap
+
+#include
+
+#undef CLASS
+#undef TEMPLATE
+
+#endif
diff --git a/include/bitcoin/database/primitives/nomap.hpp b/include/bitcoin/database/primitives/nomap.hpp
index d0ad12bd8..98f35ccc5 100644
--- a/include/bitcoin/database/primitives/nomap.hpp
+++ b/include/bitcoin/database/primitives/nomap.hpp
@@ -79,12 +79,6 @@ class nomap
/// Drop the table (truncate to zero and update head size).
bool drop() NOEXCEPT;
- /// Reserve additional count or slab to guard against disk full.
- /// This is necessary for no-maps that are publicly-indexed (e.g. heights).
- /// Not writer-writer thread safe. Link must be put (or discarded) before
- /// any subsequent element is reserved or put, or will overwrite.
- bool reserve(const Link& size) NOEXCEPT;
-
/// Allocate count records or slab bytes at returned link (follow with put).
Link allocate(const Link& size) NOEXCEPT;
@@ -141,11 +135,6 @@ class nomap
template = true>
Link put_link(const Element& element) NOEXCEPT;
- /// NOT THREAD SAFE (used only for height index with writer ordering).
- /// Write element to reserved next position and commit to logical space.
- template = true>
- bool commit(const Element& element) NOEXCEPT;
-
private:
static constexpr auto is_slab = (Size == max_size_t);
using body = database::body, Size>;
diff --git a/include/bitcoin/database/primitives/primitives.hpp b/include/bitcoin/database/primitives/primitives.hpp
index 597e478d1..794cde0c8 100644
--- a/include/bitcoin/database/primitives/primitives.hpp
+++ b/include/bitcoin/database/primitives/primitives.hpp
@@ -37,6 +37,7 @@
#include
#include
#include
+#include
#include
#include
diff --git a/include/bitcoin/database/settings.hpp b/include/bitcoin/database/settings.hpp
index 9c36d7ee8..0d5f1117a 100644
--- a/include/bitcoin/database/settings.hpp
+++ b/include/bitcoin/database/settings.hpp
@@ -86,8 +86,8 @@ struct BCD_API settings
/// Indexes.
/// -----------------------------------------------------------------------
- simple_table candidate{};
- simple_table confirmed{};
+ bucket_table candidate{};
+ bucket_table confirmed{};
bucket_table strong_tx{};
/// Caches.
diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp
index d3af4628c..ca4408a5a 100644
--- a/include/bitcoin/database/store.hpp
+++ b/include/bitcoin/database/store.hpp
@@ -19,6 +19,7 @@
#ifndef LIBBITCOIN_DATABASE_STORE_HPP
#define LIBBITCOIN_DATABASE_STORE_HPP
+#include
#include
#include
#include
@@ -114,6 +115,7 @@ class store
using path = std::filesystem::path;
/// Method helpers.
+ code create_load(const event_handler& handler) NOEXCEPT;
code open_load(const event_handler& handler) NOEXCEPT;
code unload_close(const event_handler& handler) NOEXCEPT;
code backup(const event_handler& handler, bool prune=false) NOEXCEPT;
@@ -157,13 +159,11 @@ class store
/// Indexes.
/// -----------------------------------------------------------------------
- // array
+ // headmap
Storage candidate_head_;
- Storage candidate_body_;
- // array
+ // headmap
Storage confirmed_head_;
- Storage confirmed_body_;
// record hashmap
Storage strong_tx_head_;
@@ -230,11 +230,25 @@ class store
static constexpr bool random = true;
static constexpr bool sequential = false;
- // Heads are minimally allocated with no expansion (heads size to their
- // configured buckets at table creation) and randomly probed (the advice
- // preserves optimal classic mapped reads; staged heads are anonymous).
- static constexpr storage_settings head_settings{ .size = 1, .rate = 0,
- .access = advice::random };
+ // Heads share the table growth rate.
+ static constexpr storage_settings head_settings(
+ const settings::simple_table& table, uint64_t minimum=one) NOEXCEPT
+ {
+ return
+ {
+ .size = std::max(one, minimum),
+ .rate = table.rate,
+ .access = advice::random
+ };
+ }
+
+ template
+ static constexpr storage_settings head_settings(
+ const settings::bucket_table& table) NOEXCEPT
+ {
+ return head_settings(table,
+ system::ceilinged_multiply(table.buckets, Cell));
+ }
// Bodies are append-only, so stage writes in anonymous memory where the
// staging backend is built (heads update in place and remain resident).
diff --git a/include/bitcoin/database/tables/indexes/height.hpp b/include/bitcoin/database/tables/indexes/height.hpp
index 99725e505..34709b2f1 100644
--- a/include/bitcoin/database/tables/indexes/height.hpp
+++ b/include/bitcoin/database/tables/indexes/height.hpp
@@ -27,42 +27,11 @@ namespace libbitcoin {
namespace database {
namespace table {
-/// height is an array of header fk records.
+/// height is a head-only array of header fks indexed by height (no body).
struct height
- : public no_map
+ : public head_map
{
- using header = schema::header::link;
- using no_map::nomap;
-
- struct record
- : public schema::height
- {
- static constexpr link count() NOEXCEPT
- {
- return 1;
- }
-
- inline bool from_data(reader& source) NOEXCEPT
- {
- header_fk = source.read_little_endian();
- BC_ASSERT(!source || source.get_read_position() == minrow);
- return source;
- }
-
- inline bool to_data(flipper& sink) const NOEXCEPT
- {
- sink.write_little_endian(header_fk);
- BC_ASSERT(!sink || sink.get_write_position() == minrow);
- return sink;
- }
-
- inline bool operator==(const record& other) const NOEXCEPT
- {
- return header_fk == other.header_fk;
- }
-
- header::integer header_fk{};
- };
+ using head_map::headmap;
};
} // namespace table
diff --git a/include/bitcoin/database/tables/schema.hpp b/include/bitcoin/database/tables/schema.hpp
index cb12f81bf..0bddef3cb 100644
--- a/include/bitcoin/database/tables/schema.hpp
+++ b/include/bitcoin/database/tables/schema.hpp
@@ -69,7 +69,7 @@ constexpr size_t doubles_ = 4; // doubles bucket (no actual keys).
/// Archive tables.
/// -----------------------------------------------------------------------
-// size_t `cell` sets the hashmap bucket size (minimum size of link type).
+// size_t `cell` sets the hashmap/headmap bucket size (minimum link size).
// bool `align` causes arraymap bucket size to be expanded to nearest word.
// Memory fencing used (vs. mutex) when array/hashmap bucket is word sized.
@@ -257,7 +257,7 @@ struct txs
/// Index tables.
/// ---------------------------------------------------------------------------
-// array (candidate and confirmed)
+// headmap (candidate and confirmed)
struct height
{
static constexpr size_t pk = schema::header::pk;
@@ -266,11 +266,12 @@ struct height
schema::header::pk;
static constexpr size_t minrow = minsize;
static constexpr size_t size = minsize;
+ static constexpr size_t cell = sizeof(unsigned_type);
static constexpr auto suffix = "height"_t;
- ////static constexpr link count() NOEXCEPT { return 1; }
static_assert(minsize == 3u);
static_assert(minrow == 3u);
static_assert(link::size == 3u);
+ static_assert(cell == 4u);
};
// record hashmap
diff --git a/include/bitcoin/database/tables/table.hpp b/include/bitcoin/database/tables/table.hpp
index 092f9aba5..4b2f00c60 100644
--- a/include/bitcoin/database/tables/table.hpp
+++ b/include/bitcoin/database/tables/table.hpp
@@ -57,10 +57,8 @@ enum class table_t
/// Indexes.
candidate_table,
candidate_head,
- candidate_body,
confirmed_table,
confirmed_head,
- confirmed_body,
strong_tx_table,
strong_tx_head,
strong_tx_body,
diff --git a/test/mocks/chunk_store.hpp b/test/mocks/chunk_store.hpp
index b18ec579b..cd754f89f 100644
--- a/test/mocks/chunk_store.hpp
+++ b/test/mocks/chunk_store.hpp
@@ -120,21 +120,11 @@ class chunk_store
return candidate_head_.buffer();
}
- system::data_chunk& candidate_body() NOEXCEPT
- {
- return candidate_body_.buffer();
- }
-
system::data_chunk& confirmed_head() NOEXCEPT
{
return confirmed_head_.buffer();
}
- system::data_chunk& confirmed_body() NOEXCEPT
- {
- return confirmed_body_.buffer();
- }
-
system::data_chunk& strong_tx_head() NOEXCEPT
{
return strong_tx_head_.buffer();
diff --git a/test/mocks/map_store.hpp b/test/mocks/map_store.hpp
index 631287ce9..45a5d29e9 100644
--- a/test/mocks/map_store.hpp
+++ b/test/mocks/map_store.hpp
@@ -144,21 +144,11 @@ class map_store
return candidate_head_.file();
}
- inline const path& candidate_body_file() const NOEXCEPT
- {
- return candidate_body_.file();
- }
-
inline const path& confirmed_head_file() const NOEXCEPT
{
return confirmed_head_.file();
}
- inline const path& confirmed_body_file() const NOEXCEPT
- {
- return confirmed_body_.file();
- }
-
inline const path& strong_tx_head_file() const NOEXCEPT
{
return strong_tx_head_.file();
diff --git a/test/primitives/headmap.cpp b/test/primitives/headmap.cpp
new file mode 100644
index 000000000..a7c3e91c7
--- /dev/null
+++ b/test/primitives/headmap.cpp
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include "../test.hpp"
+#include "../mocks/chunk_storage.hpp"
+
+BOOST_AUTO_TEST_SUITE(headmap_tests)
+
+using namespace system;
+
+using link = linkage<2>;
+using test_headmap = headmap;
+using small_link = linkage<3>;
+using unaligned_headmap = headmap;
+
+BOOST_AUTO_TEST_CASE(headmap__create__empty__expected)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ test_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.verify());
+ BOOST_REQUIRE(head_file.empty());
+ BOOST_REQUIRE_EQUAL(instance.head_size(), zero);
+ BOOST_REQUIRE_EQUAL(instance.body_size(), zero);
+ BOOST_REQUIRE_EQUAL(instance.count(), 0u);
+ BOOST_REQUIRE(instance.at(0).is_terminal());
+}
+
+BOOST_AUTO_TEST_CASE(headmap__push__reserved__expected_at)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ test_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(!instance.push(link::terminal));
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(0x1122_u16));
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(0x3344_u16));
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(instance.at(0), 0x1122_u16);
+ BOOST_REQUIRE_EQUAL(instance.at(1), 0x3344_u16);
+ BOOST_REQUIRE(instance.at(2).is_terminal());
+ BOOST_REQUIRE_EQUAL(head_file, base16_chunk("22114433"));
+}
+
+BOOST_AUTO_TEST_CASE(headmap__push__unreserved__false)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ test_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(!instance.push(0x1122_u16));
+ BOOST_REQUIRE_EQUAL(instance.count(), 0u);
+}
+
+BOOST_AUTO_TEST_CASE(headmap__truncate__push__rewrites_bucket)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ test_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.reserve(2u));
+ BOOST_REQUIRE(instance.push(0x1122_u16));
+ BOOST_REQUIRE(instance.push(0x3344_u16));
+ BOOST_REQUIRE(!instance.truncate(3u));
+ BOOST_REQUIRE(instance.truncate(1u));
+ BOOST_REQUIRE_EQUAL(instance.count(), 1u);
+ BOOST_REQUIRE(instance.at(1).is_terminal());
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(0x5566_u16));
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(instance.at(1), 0x5566_u16);
+}
+
+BOOST_AUTO_TEST_CASE(headmap__unaligned__push_at_truncate__expected)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ unaligned_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.reserve(2u));
+ BOOST_REQUIRE(instance.push(0x00112233_u32));
+ BOOST_REQUIRE(instance.push(0x00445566_u32));
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(instance.at(0), 0x00112233_u32);
+ BOOST_REQUIRE_EQUAL(instance.at(1), 0x00445566_u32);
+ BOOST_REQUIRE(instance.at(2).is_terminal());
+ BOOST_REQUIRE_EQUAL(head_file, base16_chunk("332211665544"));
+ BOOST_REQUIRE(instance.truncate(1u));
+ BOOST_REQUIRE(instance.at(1).is_terminal());
+}
+
+BOOST_AUTO_TEST_CASE(headmap__setup__lifecycle__expected)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ test_headmap instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(0x1122_u16));
+ BOOST_REQUIRE(instance.close());
+ BOOST_REQUIRE(instance.backup());
+ BOOST_REQUIRE(instance.restore());
+ BOOST_REQUIRE(instance.verify());
+ BOOST_REQUIRE_EQUAL(instance.count(), 1u);
+ BOOST_REQUIRE(!instance.get_fault());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/primitives/nomap.cpp b/test/primitives/nomap.cpp
index 84ee1cfd7..80a9d5ca0 100644
--- a/test/primitives/nomap.cpp
+++ b/test/primitives/nomap.cpp
@@ -441,67 +441,6 @@ BOOST_AUTO_TEST_CASE(nomap__slab_get__file_excess__false)
BOOST_REQUIRE(!instance.get_fault());
}
-// reserve/commit
-// ----------------------------------------------------------------------------
-
-BOOST_AUTO_TEST_CASE(nomap__record_reserve__terminal__false_empty)
-{
- data_chunk head_file{};
- data_chunk body_file{};
- test::chunk_storage head_store{ head_file };
- test::chunk_storage body_store{ body_file };
- record_table instance{ head_store, body_store };
- BOOST_REQUIRE(!instance.reserve(link5::terminal));
- BOOST_REQUIRE_EQUAL(instance.count(), zero);
-}
-
-BOOST_AUTO_TEST_CASE(nomap__record_reserve__zero__true_empty)
-{
- data_chunk head_file{};
- data_chunk body_file{};
- test::chunk_storage head_store{ head_file };
- test::chunk_storage body_store{ body_file };
- record_table instance{ head_store, body_store };
- BOOST_REQUIRE(instance.reserve(0));
- BOOST_REQUIRE_EQUAL(instance.count(), zero);
-}
-
-BOOST_AUTO_TEST_CASE(nomap__record_reserve__value__true_expected_capacity)
-{
- data_chunk head_file{};
- data_chunk body_file{};
- test::chunk_storage head_store{ head_file };
- test::chunk_storage body_store{ body_file };
- record_table instance{ head_store, body_store };
- BOOST_REQUIRE(instance.reserve(2));
- BOOST_REQUIRE_EQUAL(instance.count(), zero);
- BOOST_REQUIRE_EQUAL(instance.capacity(), 2u * record4::size);
- BOOST_REQUIRE_EQUAL(body_file.size(), 2u * record4::size);
-}
-
-BOOST_AUTO_TEST_CASE(nomap__record_reserve__commit__expected)
-{
- data_chunk head_file{};
- data_chunk body_file{};
- test::chunk_storage head_store{ head_file };
- test::chunk_storage body_store{ body_file };
- nomap instance{ head_store, body_store };
- BOOST_REQUIRE_EQUAL(instance.capacity(), zero);
- BOOST_REQUIRE(body_file.empty());
- BOOST_REQUIRE(instance.reserve(1));
- BOOST_REQUIRE_EQUAL(instance.capacity(), big_record::size);
- BOOST_REQUIRE_EQUAL(instance.count(), zero);
- BOOST_REQUIRE(instance.commit(big_record{ 0xa1b2c3d4_u32 }));
- BOOST_REQUIRE_EQUAL(instance.capacity(), big_record::size);
- BOOST_REQUIRE_EQUAL(body_file.size(), big_record::size);
- BOOST_REQUIRE_EQUAL(instance.count(), one);
-
- big_record record{};
- BOOST_REQUIRE(instance.get(0, record));
- BOOST_REQUIRE_EQUAL(record.value, 0xa1b2c3d4_u32);
- BOOST_REQUIRE(!instance.get_fault());
-}
-
// record create/close/backup/restore/verify
// ----------------------------------------------------------------------------
diff --git a/test/query/extent.cpp b/test/query/extent.cpp
index 16690a6fd..5ffb9c5fe 100644
--- a/test/query/extent.cpp
+++ b/test/query/extent.cpp
@@ -1,172 +1,172 @@
-/**
- * Copyright (c) 2011-2026 libbitcoin developers
- *
- * This file is part of libbitcoin.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-#include "../test.hpp"
-#include "../mocks/blocks.hpp"
-#include "../mocks/chunk_store.hpp"
-
-BOOST_FIXTURE_TEST_SUITE(query_extent_tests, test::directory_setup_fixture)
-
-BOOST_AUTO_TEST_CASE(query__is_full__chunk_store__false)
-{
- const settings configuration{};
- test::chunk_store store{ configuration };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!query.is_full());
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__body_sizes__genesis__expected)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
-
- BOOST_REQUIRE_EQUAL(query.header_body_size(), schema::header::minrow);
- BOOST_REQUIRE_EQUAL(query.output_body_size(), 81u);
- BOOST_REQUIRE_EQUAL(query.input_body_size(), 79u);
- BOOST_REQUIRE_EQUAL(query.ins_body_size(), schema::ins::minrow + schema::ins_sequence::minrow);
- BOOST_REQUIRE_EQUAL(query.outs_body_size(), schema::address::minrow + schema::outs::minrow);
- BOOST_REQUIRE_EQUAL(query.txs_body_size(), schema::txs::minrow + one + schema::flags);
- BOOST_REQUIRE_EQUAL(query.tx_body_size(), schema::transaction::minrow);
-
- BOOST_REQUIRE_EQUAL(query.candidate_body_size(), schema::height::minrow);
- BOOST_REQUIRE_EQUAL(query.confirmed_body_size(), schema::height::minrow);
- BOOST_REQUIRE_EQUAL(query.strong_tx_body_size(), schema::strong_tx::minrow);
- BOOST_REQUIRE_EQUAL(query.ecdsa_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.schnorr_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.silent_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.duplicate_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.prevalid_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.prevout_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.validated_bk_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.validated_tx_body_size(), zero);
- BOOST_REQUIRE_EQUAL(query.filter_bk_body_size(), schema::filter_bk::minrow);
- BOOST_REQUIRE_EQUAL(query.filter_tx_body_size(), 5u);
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__buckets__genesis__expected)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
-
- BOOST_REQUIRE_EQUAL(query.header_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.ins_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.outs_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.txs_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.tx_buckets(), 128u);
-
- BOOST_REQUIRE_EQUAL(query.strong_tx_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.duplicate_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.prevout_buckets(), 128);
- BOOST_REQUIRE_EQUAL(query.validated_tx_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.validated_bk_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.filter_tx_buckets(), 128u);
- BOOST_REQUIRE_EQUAL(query.filter_bk_buckets(), 128u);
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__records__genesis__expected)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
-
- BOOST_REQUIRE_EQUAL(query.header_records(), one);
- BOOST_REQUIRE_EQUAL(query.ins_records(), one);
- BOOST_REQUIRE_EQUAL(query.outs_records(), one);
- BOOST_REQUIRE_EQUAL(query.tx_records(), one);
-
- BOOST_REQUIRE_EQUAL(query.candidate_records(), one);
- BOOST_REQUIRE_EQUAL(query.confirmed_records(), one);
- BOOST_REQUIRE_EQUAL(query.strong_tx_records(), one);
- BOOST_REQUIRE_EQUAL(query.ecdsa_records(), zero);
- BOOST_REQUIRE_EQUAL(query.schnorr_records(), zero);
- BOOST_REQUIRE_EQUAL(query.silent_records(), zero);
- BOOST_REQUIRE_EQUAL(query.duplicate_records(), zero);
- BOOST_REQUIRE_EQUAL(query.prevalid_records(), zero);
- BOOST_REQUIRE_EQUAL(query.filter_bk_records(), one);
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__input_output_count__genesis__expected)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
-
- BOOST_REQUIRE_EQUAL(query.input_count(0), one);
- BOOST_REQUIRE_EQUAL(query.output_count(0), one);
- BOOST_REQUIRE_EQUAL(query.put_counts(0).first, one);
- BOOST_REQUIRE_EQUAL(query.put_counts(0).second, one);
-
- BOOST_REQUIRE_EQUAL(query.input_count(1), zero);
- BOOST_REQUIRE_EQUAL(query.output_count(1), zero);
- BOOST_REQUIRE_EQUAL(query.put_counts(1).first, zero);
- BOOST_REQUIRE_EQUAL(query.put_counts(1).second, zero);
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__optionals_enabled__default__true)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
- BOOST_REQUIRE(query.address_enabled());
- BOOST_REQUIRE(query.filter_enabled());
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__address_enabled__disabled__false)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- settings.outs.buckets = 0;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
- BOOST_REQUIRE(!query.address_enabled());
- BOOST_REQUIRE(query.filter_enabled());
-}
-
-BOOST_AUTO_TEST_CASE(query_extent__filter_enabled__disabled__false)
-{
- settings settings{};
- settings.path = TEST_DIRECTORY;
- settings.filter_tx.buckets = 0;
- test::chunk_store store{ settings };
- test::query_accessor query{ store };
- BOOST_REQUIRE(!store.create(test::events_handler));
- BOOST_REQUIRE(query.initialize(test::genesis));
- BOOST_REQUIRE(query.address_enabled());
- BOOST_REQUIRE(!query.filter_enabled());
-}
-
-BOOST_AUTO_TEST_SUITE_END()
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include "../test.hpp"
+#include "../mocks/blocks.hpp"
+#include "../mocks/chunk_store.hpp"
+
+BOOST_FIXTURE_TEST_SUITE(query_extent_tests, test::directory_setup_fixture)
+
+BOOST_AUTO_TEST_CASE(query__is_full__chunk_store__false)
+{
+ const settings configuration{};
+ test::chunk_store store{ configuration };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!query.is_full());
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__body_sizes__genesis__expected)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+
+ BOOST_REQUIRE_EQUAL(query.header_body_size(), schema::header::minrow);
+ BOOST_REQUIRE_EQUAL(query.output_body_size(), 81u);
+ BOOST_REQUIRE_EQUAL(query.input_body_size(), 79u);
+ BOOST_REQUIRE_EQUAL(query.ins_body_size(), schema::ins::minrow + schema::ins_sequence::minrow);
+ BOOST_REQUIRE_EQUAL(query.outs_body_size(), schema::address::minrow + schema::outs::minrow);
+ BOOST_REQUIRE_EQUAL(query.txs_body_size(), schema::txs::minrow + one + schema::flags);
+ BOOST_REQUIRE_EQUAL(query.tx_body_size(), schema::transaction::minrow);
+
+ BOOST_REQUIRE_EQUAL(query.candidate_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.confirmed_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.strong_tx_body_size(), schema::strong_tx::minrow);
+ BOOST_REQUIRE_EQUAL(query.ecdsa_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.schnorr_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.silent_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.duplicate_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.prevalid_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.prevout_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.validated_bk_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.validated_tx_body_size(), zero);
+ BOOST_REQUIRE_EQUAL(query.filter_bk_body_size(), schema::filter_bk::minrow);
+ BOOST_REQUIRE_EQUAL(query.filter_tx_body_size(), 5u);
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__buckets__genesis__expected)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+
+ BOOST_REQUIRE_EQUAL(query.header_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.ins_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.outs_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.txs_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.tx_buckets(), 128u);
+
+ BOOST_REQUIRE_EQUAL(query.strong_tx_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.duplicate_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.prevout_buckets(), 128);
+ BOOST_REQUIRE_EQUAL(query.validated_tx_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.validated_bk_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.filter_tx_buckets(), 128u);
+ BOOST_REQUIRE_EQUAL(query.filter_bk_buckets(), 128u);
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__records__genesis__expected)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+
+ BOOST_REQUIRE_EQUAL(query.header_records(), one);
+ BOOST_REQUIRE_EQUAL(query.ins_records(), one);
+ BOOST_REQUIRE_EQUAL(query.outs_records(), one);
+ BOOST_REQUIRE_EQUAL(query.tx_records(), one);
+
+ BOOST_REQUIRE_EQUAL(query.candidate_records(), one);
+ BOOST_REQUIRE_EQUAL(query.confirmed_records(), one);
+ BOOST_REQUIRE_EQUAL(query.strong_tx_records(), one);
+ BOOST_REQUIRE_EQUAL(query.ecdsa_records(), zero);
+ BOOST_REQUIRE_EQUAL(query.schnorr_records(), zero);
+ BOOST_REQUIRE_EQUAL(query.silent_records(), zero);
+ BOOST_REQUIRE_EQUAL(query.duplicate_records(), zero);
+ BOOST_REQUIRE_EQUAL(query.prevalid_records(), zero);
+ BOOST_REQUIRE_EQUAL(query.filter_bk_records(), one);
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__input_output_count__genesis__expected)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+
+ BOOST_REQUIRE_EQUAL(query.input_count(0), one);
+ BOOST_REQUIRE_EQUAL(query.output_count(0), one);
+ BOOST_REQUIRE_EQUAL(query.put_counts(0).first, one);
+ BOOST_REQUIRE_EQUAL(query.put_counts(0).second, one);
+
+ BOOST_REQUIRE_EQUAL(query.input_count(1), zero);
+ BOOST_REQUIRE_EQUAL(query.output_count(1), zero);
+ BOOST_REQUIRE_EQUAL(query.put_counts(1).first, zero);
+ BOOST_REQUIRE_EQUAL(query.put_counts(1).second, zero);
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__optionals_enabled__default__true)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+ BOOST_REQUIRE(query.address_enabled());
+ BOOST_REQUIRE(query.filter_enabled());
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__address_enabled__disabled__false)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ settings.outs.buckets = 0;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+ BOOST_REQUIRE(!query.address_enabled());
+ BOOST_REQUIRE(query.filter_enabled());
+}
+
+BOOST_AUTO_TEST_CASE(query_extent__filter_enabled__disabled__false)
+{
+ settings settings{};
+ settings.path = TEST_DIRECTORY;
+ settings.filter_tx.buckets = 0;
+ test::chunk_store store{ settings };
+ test::query_accessor query{ store };
+ BOOST_REQUIRE(!store.create(test::events_handler));
+ BOOST_REQUIRE(query.initialize(test::genesis));
+ BOOST_REQUIRE(query.address_enabled());
+ BOOST_REQUIRE(!query.filter_enabled());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/settings.cpp b/test/settings.cpp
index 7e0289dd9..fd94ff8a3 100644
--- a/test/settings.cpp
+++ b/test/settings.cpp
@@ -51,9 +51,9 @@ BOOST_AUTO_TEST_CASE(settings__construct__default__expected)
BOOST_REQUIRE_EQUAL(configuration.txs.rate, 5u);
// Indexes.
- BOOST_REQUIRE_EQUAL(configuration.candidate.size, 1u);
+ BOOST_REQUIRE_EQUAL(configuration.candidate.buckets, 128u);
BOOST_REQUIRE_EQUAL(configuration.candidate.rate, 5u);
- BOOST_REQUIRE_EQUAL(configuration.confirmed.size, 1u);
+ BOOST_REQUIRE_EQUAL(configuration.confirmed.buckets, 128u);
BOOST_REQUIRE_EQUAL(configuration.confirmed.rate, 5u);
BOOST_REQUIRE_EQUAL(configuration.strong_tx.buckets, 128u);
BOOST_REQUIRE_EQUAL(configuration.strong_tx.size, 1u);
diff --git a/test/store/store.cpp b/test/store/store.cpp
index 1e2eae0ee..de2d2c829 100644
--- a/test/store/store.cpp
+++ b/test/store/store.cpp
@@ -133,9 +133,7 @@ BOOST_AUTO_TEST_CASE(store__paths__default_configuration__expected)
/// Index.
BOOST_REQUIRE_EQUAL(instance.candidate_head_file(), "bitcoin/heads/index_candidate.head");
- BOOST_REQUIRE_EQUAL(instance.candidate_body_file(), "bitcoin/index_candidate.data");
BOOST_REQUIRE_EQUAL(instance.confirmed_head_file(), "bitcoin/heads/index_confirmed.head");
- BOOST_REQUIRE_EQUAL(instance.confirmed_body_file(), "bitcoin/index_confirmed.data");
BOOST_REQUIRE_EQUAL(instance.strong_tx_head_file(), "bitcoin/heads/index_strong.head");
BOOST_REQUIRE_EQUAL(instance.strong_tx_body_file(), "bitcoin/index_strong.data");
diff --git a/test/store/store_backup.cpp b/test/store/store_backup.cpp
index d2b17e77a..64d8dc6bc 100644
--- a/test/store/store_backup.cpp
+++ b/test/store/store_backup.cpp
@@ -54,7 +54,7 @@ BOOST_AUTO_TEST_CASE(store__backup__primary_loaded__success)
BOOST_REQUIRE(!instance.backup_());
BOOST_REQUIRE(!instance.close(test::events));
BOOST_REQUIRE(test::folder(configuration.path / schema::dir::primary));
- BOOST_REQUIRE(test::folder(configuration.path / schema::dir::secondary));
+ BOOST_REQUIRE(!test::folder(configuration.path / schema::dir::secondary));
}
BOOST_AUTO_TEST_CASE(store__backup__primary_secondary_loaded__success)
@@ -68,7 +68,7 @@ BOOST_AUTO_TEST_CASE(store__backup__primary_secondary_loaded__success)
BOOST_REQUIRE(!instance.backup_());
BOOST_REQUIRE(!instance.close(test::events));
BOOST_REQUIRE(test::folder(configuration.path / schema::dir::primary));
- BOOST_REQUIRE(test::folder(configuration.path / schema::dir::secondary));
+ BOOST_REQUIRE(!test::folder(configuration.path / schema::dir::secondary));
}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/store/store_prune.cpp b/test/store/store_prune.cpp
index 765b66812..185315689 100644
--- a/test/store/store_prune.cpp
+++ b/test/store/store_prune.cpp
@@ -37,6 +37,30 @@ BOOST_AUTO_TEST_CASE(store__prune__initialized__success)
BOOST_REQUIRE(!instance.create(test::events));
BOOST_REQUIRE(query_.initialize(test::genesis));
BOOST_REQUIRE(!instance.prune(test::events));
+
+ // The retained snapshot is post-prune (the trim strands nothing).
+ BOOST_REQUIRE(test::folder(configuration.path / schema::dir::primary));
+ BOOST_REQUIRE(!test::folder(configuration.path / schema::dir::secondary));
+ BOOST_REQUIRE_EQUAL(query_.prevout_body_size(), zero);
+ BOOST_REQUIRE(!instance.close(test::events));
+}
+
+BOOST_AUTO_TEST_CASE(store__prune__faulted_restore__success)
+{
+ settings configuration{};
+ configuration.path = TEST_DIRECTORY;
+ store instance{ configuration };
+ query> query_{ instance };
+ BOOST_REQUIRE(!instance.create(test::events));
+ BOOST_REQUIRE(query_.initialize(test::genesis));
+ BOOST_REQUIRE(!instance.prune(test::events));
+ BOOST_REQUIRE(!instance.close(test::events));
+
+ // Simulate power fault (flush lock persists), restore from post-prune.
+ BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
+ BOOST_REQUIRE(!instance.restore(test::events));
+ BOOST_REQUIRE(query_.is_initialized());
+ BOOST_REQUIRE_EQUAL(query_.prevout_body_size(), zero);
BOOST_REQUIRE(!instance.close(test::events));
}
diff --git a/test/store/store_restore.cpp b/test/store/store_restore.cpp
index bad56f477..5d198fd9b 100644
--- a/test/store/store_restore.cpp
+++ b/test/store/store_restore.cpp
@@ -54,40 +54,35 @@ BOOST_AUTO_TEST_CASE(store__restore__no_flush_lock__flush_lock)
BOOST_REQUIRE_EQUAL(instance.restore(test::events), error::flush_lock);
}
-BOOST_AUTO_TEST_CASE(store__restore__missing_snapshot__missing_snapshot_locks)
+BOOST_AUTO_TEST_CASE(store__restore__missing_snapshot__recreate_success)
{
settings configuration{};
configuration.path = TEST_DIRECTORY;
test::map_store instance{ configuration };
BOOST_REQUIRE(test::create(instance.process_lock_file()));
BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
- BOOST_REQUIRE_EQUAL(instance.restore(test::events), error::missing_snapshot);
- BOOST_REQUIRE(test::exists(instance.flush_lock_file()));
- BOOST_REQUIRE(!test::exists(instance.process_lock_file()));
-}
-
-BOOST_AUTO_TEST_CASE(store__restore__success__success_locks)
-{
- settings configuration{};
- configuration.path = TEST_DIRECTORY;
- test::map_store instance{ configuration };
- BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
- BOOST_REQUIRE(instance.restore(test::events));
+ BOOST_REQUIRE(!instance.restore(test::events));
BOOST_REQUIRE(test::exists(instance.flush_lock_file()));
- BOOST_REQUIRE(!test::exists(instance.process_lock_file()));
+ BOOST_REQUIRE(test::exists(instance.process_lock_file()));
BOOST_REQUIRE(instance.transactor_mutex().try_lock());
+ instance.transactor_mutex().unlock();
+ BOOST_REQUIRE(!instance.close(test::events));
}
-BOOST_AUTO_TEST_CASE(store__restore__no_backups__missing_snapshot_locks)
+BOOST_AUTO_TEST_CASE(store__restore__no_backups__recreate_success_locks)
{
settings configuration{};
configuration.path = TEST_DIRECTORY;
test::map_store instance{ configuration };
BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
- BOOST_REQUIRE_EQUAL(instance.restore_(), error::missing_snapshot);
+ BOOST_REQUIRE(!instance.restore_());
BOOST_REQUIRE(test::exists(instance.flush_lock_file()));
- BOOST_REQUIRE(!test::exists(instance.process_lock_file()));
+ BOOST_REQUIRE(test::exists(instance.process_lock_file()));
BOOST_REQUIRE(instance.transactor_mutex().try_lock());
+ instance.transactor_mutex().unlock();
+ BOOST_REQUIRE(!instance.close(test::events));
+ BOOST_REQUIRE(!test::exists(instance.flush_lock_file()));
+ BOOST_REQUIRE(!test::exists(instance.process_lock_file()));
}
// The lock is process-exclusive in linux/macOS, globally in win32.
@@ -215,4 +210,25 @@ BOOST_AUTO_TEST_CASE(store__restore__snapshot__success_unlocks)
BOOST_REQUIRE(!test::exists(instance.process_lock_file()));
}
+BOOST_AUTO_TEST_CASE(store__restore__secondary_remnant__deleted)
+{
+ settings configuration{};
+ configuration.path = TEST_DIRECTORY;
+ const auto primary = configuration.path / schema::dir::primary;
+ const auto secondary = configuration.path / schema::dir::secondary;
+
+ test::map_store instance{ configuration };
+ BOOST_REQUIRE(!instance.create(test::events));
+ BOOST_REQUIRE(!instance.snapshot(test::events));
+ BOOST_REQUIRE(!instance.close(test::events));
+
+ // Simulate crash between backup promotion and /secondary deletion.
+ BOOST_REQUIRE(file::copy_directory(primary, secondary));
+ BOOST_REQUIRE(test::create(test::flush_lock_file(configuration.path)));
+ BOOST_REQUIRE(!instance.restore(test::events));
+ BOOST_REQUIRE(test::folder(primary));
+ BOOST_REQUIRE(!test::folder(secondary));
+ BOOST_REQUIRE(!instance.close(test::events));
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/tables/indexes/height.cpp b/test/tables/indexes/height.cpp
index 8d8faadc2..e40186896 100644
--- a/test/tables/indexes/height.cpp
+++ b/test/tables/indexes/height.cpp
@@ -22,60 +22,79 @@
BOOST_AUTO_TEST_SUITE(height_tests)
using namespace system;
-const table::height::record in1{ {}, 0x12345678 };
-const table::height::record in2{ {}, 0xabcdef12 };
-const table::height::record out1{ {}, 0x00345678 };
-const table::height::record out2{ {}, 0x00cdef12 };
+const table::height::link link1{ 0x00345678 };
+const table::height::link link2{ 0x004def12 };
const auto expected_head = base16_chunk
(
- "000000"
-);
-const auto closed_head = base16_chunk
-(
- "020000"
-);
-const auto expected_body = base16_chunk
-(
- "785634" // header_fk1
- "12efcd" // header_fk2
+ "78563400" // bucket[0]
+ "12ef4d00" // bucket[1]
);
-BOOST_AUTO_TEST_CASE(height__put__two__expected)
+BOOST_AUTO_TEST_CASE(height__create__empty__expected)
{
- test::chunk_storage head_store{};
- test::chunk_storage body_store{};
- table::height instance{ head_store, body_store };
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ table::height instance{ head_store };
BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.verify());
+ BOOST_REQUIRE(head_file.empty());
+ BOOST_REQUIRE_EQUAL(instance.count(), 0u);
+ BOOST_REQUIRE_EQUAL(instance.body_size(), zero);
+ BOOST_REQUIRE(instance.at(0).is_terminal());
+}
- table::height::link link1{};
- BOOST_REQUIRE(instance.put_link(link1, in1));
- BOOST_REQUIRE_EQUAL(link1, 0u);
-
- table::height::link link2{};
- BOOST_REQUIRE(instance.put_link(link2, in2));
- BOOST_REQUIRE_EQUAL(link2, 1u);
-
- BOOST_REQUIRE_EQUAL(head_store.buffer(), expected_head);
- BOOST_REQUIRE_EQUAL(body_store.buffer(), expected_body);
+BOOST_AUTO_TEST_CASE(height__push__two__expected)
+{
+ data_chunk head_file{};
+ test::chunk_storage head_store{ head_file };
+ table::height instance{ head_store };
+ BOOST_REQUIRE(instance.create());
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(link1));
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(link2));
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(head_file, expected_head);
BOOST_REQUIRE(instance.close());
- BOOST_REQUIRE_EQUAL(head_store.buffer(), closed_head);
+ BOOST_REQUIRE_EQUAL(head_file, expected_head);
}
-BOOST_AUTO_TEST_CASE(height__get__two__expected)
+BOOST_AUTO_TEST_CASE(height__at__two__expected)
{
- auto head = expected_head;
- auto body = expected_body;
- test::chunk_storage head_store{ head };
- test::chunk_storage body_store{ body };
- table::height instance{ head_store, body_store };
- BOOST_REQUIRE_EQUAL(head_store.buffer(), expected_head);
- BOOST_REQUIRE_EQUAL(body_store.buffer(), expected_body);
+ auto head_file = expected_head;
+ test::chunk_storage head_store{ head_file };
+ table::height instance{ head_store };
+ BOOST_REQUIRE(instance.verify());
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(instance.at(0), link1);
+ BOOST_REQUIRE_EQUAL(instance.at(1), link2);
+ BOOST_REQUIRE(instance.at(2).is_terminal());
+}
- table::height::record out{};
- BOOST_REQUIRE(instance.get(0u, out));
- BOOST_REQUIRE(out == out1);
- BOOST_REQUIRE(instance.get(1u, out));
- BOOST_REQUIRE(out == out2);
+BOOST_AUTO_TEST_CASE(height__truncate__push__rewrites_bucket)
+{
+ auto head_file = expected_head;
+ test::chunk_storage head_store{ head_file };
+ table::height instance{ head_store };
+ BOOST_REQUIRE(!instance.truncate(3u));
+ BOOST_REQUIRE(instance.truncate(1u));
+ BOOST_REQUIRE_EQUAL(instance.count(), 1u);
+ BOOST_REQUIRE(instance.at(1).is_terminal());
+ BOOST_REQUIRE(instance.reserve(1u));
+ BOOST_REQUIRE(instance.push(link1));
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(instance.at(1), link1);
+}
+
+BOOST_AUTO_TEST_CASE(height__restore__expected__idempotent)
+{
+ auto head_file = expected_head;
+ test::chunk_storage head_store{ head_file };
+ table::height instance{ head_store };
+ BOOST_REQUIRE(instance.backup());
+ BOOST_REQUIRE(instance.restore());
+ BOOST_REQUIRE_EQUAL(instance.count(), 2u);
+ BOOST_REQUIRE_EQUAL(head_file, expected_head);
}
BOOST_AUTO_TEST_SUITE_END()
| | |