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()