From f9e6a912ee75573d4e549d4055abd78332dbc39e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 17 Apr 2026 13:54:10 -0500 Subject: [PATCH 1/2] chainbase_bench: put undo_index in segment, add scenarios The original benchmark stack-allocated the undo_index. offset_node_base packs tree pointers as signed 42-bit offsets (<<2 => 8 TiB reach) relative to each node, so when the index header lives outside the segment the nodes live in, the node-to-header distance can exceed 8 TiB and the packed offset truncates, producing bogus parent pointers that SIGSEGV during AVL rebalancing. Effect was ASLR-dependent, showed up as ~50% crash rate. Mirror libraries/chaindb/test/undo_index.cpp's `undo_index_in_segment` wrapper so the index (and its header) lives alongside its nodes. Also split the driver into three scenarios targeted at specific optimization candidates, switch pinnable_mapped_file to heap mode with an 8 GiB region, and give stopwatch a label so per-scenario timings print meaningfully. --- libraries/chaindb/benchmark/bench.cpp | 180 +++++++++++++++++++++----- 1 file changed, 148 insertions(+), 32 deletions(-) diff --git a/libraries/chaindb/benchmark/bench.cpp b/libraries/chaindb/benchmark/bench.cpp index 313d44d768..ca4fa0784d 100644 --- a/libraries/chaindb/benchmark/bench.cpp +++ b/libraries/chaindb/benchmark/bench.cpp @@ -31,35 +31,78 @@ class test_allocator : public test_allocator_base { using shared_string = chainbase::shared_string; +// undo_index's 42-bit packed offsets between a node and the index's header limit +// the addressable separation to ~8 TiB. Allocating the index on the stack while +// nodes live in a kernel-chosen mmap region can exceed that range and produce +// truncated/bogus parent offsets that SIGSEGV during tree rebalancing. +// Construct the index inside the segment so the header sits next to the nodes. +template +struct undo_index_in_segment { + using undo_index_t = chainbase::undo_index; + + template + undo_index_in_segment(A& alloc) : segment_manager(*alloc.get_segment_manager()) { + p = segment_manager.template construct("")(alloc); + } + + ~undo_index_in_segment() { + if (p) segment_manager.destroy_ptr(p); + } + + undo_index_t* operator->() { return p; } + undo_index_t& operator*() { return *p; } + + chainbase::segment_manager& segment_manager; + undo_index_t* p = nullptr; + + undo_index_in_segment(const undo_index_in_segment&) = delete; + undo_index_in_segment& operator=(const undo_index_in_segment&) = delete; +}; + struct elem_t { template - elem_t(C&& c, A&& a) : str(a) { + elem_t(C&& c, A&&) { c(*this); } - + friend std::ostream& operator<<(std::ostream& os, const elem_t& e) { os << '[' << e.id << ", " << e.val << ']'; return os; } - + uint64_t id; uint64_t val; shared_string str; }; +// Two-index element. id is primary, name is a secondary indexed key. +// Modifies touch only `val` (non-indexed) so post_modify walk is pure overhead. +struct elem2_t { + template + elem2_t(C&& c, A&&) { c(*this); } + + uint64_t id; + uint64_t name; // indexed but never mutated + uint64_t val; // not indexed, bumped on every modify +}; + +struct by_id {}; +struct by_name {}; + template -struct stopwatch -{ - stopwatch() { _start = clock::now(); } +struct stopwatch { + stopwatch(const char* label) : _label(label) { _start = clock::now(); } ~stopwatch() { using duration_t = std::chrono::duration; point end = clock::now(); float elapsed = std::chrono::duration_cast(end - _start).count(); - printf("Bench time %14.2fs\n", elapsed / 1000); + printf("%-55s %10.2f s\n", _label, elapsed / 1000); + fflush(stdout); } using clock = std::chrono::high_resolution_clock; using point = std::chrono::time_point; point _start; + const char* _label; }; template @@ -70,34 +113,107 @@ struct key_impl { template using fn = boost::multi_index::member template using key = typename key_impl::template fn; +// ---- Scenario 1: original mixed find/modify/emplace/remove, single index, no sessions. +static void scenario_original(chainbase::segment_manager* mgr) { + constexpr size_t num_elems = 32 * 1024 * 1024; + test_allocator alloc(mgr); + undo_index_in_segment, + bmi::ordered_unique>> i0(alloc); + boost::random::mt19937 gen; + boost::random::uniform_int_distribution<> dist(1, num_elems); + + stopwatch sw("scenario_original (1 idx, no session, 32M iters)"); + for (size_t i=0; ifind(id); + if (e) { + i0->modify(*e, [old=e](elem_t& e) { e.val = old->val + 1; }); + } else { + auto& e = i0->emplace([](elem_t& e) { + e.val = 0; + e.str = "a string"; + }); + if (e.id % 5 == 0) + i0->remove(e); + } + } +} + +// ---- Scenario 2: 2 indexes, modifies only non-indexed `val`. +// Every modify runs post_modify across both indexes even though no key changes. +static void scenario_two_idx_nonkey_modify(chainbase::segment_manager* mgr) { + constexpr size_t num_inserts = 2 * 1024 * 1024; + constexpr size_t num_modifies = 16 * 1024 * 1024; + + test_allocator alloc(mgr); + undo_index_in_segment, + bmi::ordered_unique, key<&elem2_t::id>>, + bmi::ordered_unique, key<&elem2_t::name>>> idx(alloc); + + for (size_t i = 0; i < num_inserts; ++i) { + idx->emplace([&](elem2_t& e) { e.name = i * 2654435761ULL; e.val = 0; }); + } + + boost::random::mt19937 gen(42); + boost::random::uniform_int_distribution dist(0, num_inserts - 1); + + stopwatch sw("scenario_two_idx_nonkey_modify (2 idx, 16M modifies)"); + for (size_t i = 0; i < num_modifies; ++i) { + const elem2_t* e = idx->find(dist(gen)); + if (!e) continue; + idx->modify(*e, [](elem2_t& o) { ++o.val; }); + } +} + +// ---- Scenario 3: single index, many undo sessions starting/squashing. +// Exercises _undo_stack push/pop pattern (targets deque -> vector change). +static void scenario_undo_session_churn(chainbase::segment_manager* mgr) { + constexpr size_t num_inserts = 256 * 1024; + constexpr size_t num_sessions = 2 * 1024 * 1024; + constexpr size_t modifies_per_session = 4; + + test_allocator alloc(mgr); + undo_index_in_segment, + bmi::ordered_unique>> idx(alloc); + + for (size_t i = 0; i < num_inserts; ++i) { + idx->emplace([&](elem_t& e) { e.val = 0; }); + } + + boost::random::mt19937 gen(7); + boost::random::uniform_int_distribution dist(0, num_inserts - 1); + + stopwatch sw("scenario_undo_session_churn (2M sessions, squash)"); + for (size_t s = 0; s < num_sessions; ++s) { + auto session = idx->start_undo_session(true); + for (size_t m = 0; m < modifies_per_session; ++m) { + const elem_t* e = idx->find(dist(gen)); + if (e) idx->modify(*e, [](elem_t& o) { ++o.val; }); + } + session.squash(); // squash is the common hot path during apply_block + } +} + +int main(int argc, char** argv) { + // Allow selecting a single scenario by name (for stable per-scenario runs). + auto want = [&](const char* name) { + if (argc < 2) return true; + for (int i = 1; i < argc; ++i) if (std::string(argv[i]) == name) return true; + return false; + }; -int main() -{ fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; try { - constexpr size_t num_elems = 32 * 1024 * 1024; - chainbase::pinnable_mapped_file db(temp, true, 64 * num_elems, false, chainbase::pinnable_mapped_file::map_mode::mapped); - test_allocator alloc(db.get_segment_manager()); - chainbase::undo_index, bmi::ordered_unique>> i0(alloc); - boost::random::mt19937 gen; - boost::random::uniform_int_distribution<> dist(1, num_elems); - - stopwatch sw; - for (size_t i=0; ival + 1; }); - } else { - auto &e = i0.emplace([](elem_t& e) { - e.val = 0; - e.str = "a string"; - }); - if (e.id % 5 == 0) - i0.remove(e); - } - } + constexpr size_t db_size = 8ull * 1024 * 1024 * 1024; // 8 GiB + chainbase::pinnable_mapped_file db(temp, true, db_size, false, + chainbase::pinnable_mapped_file::map_mode::heap); + + if (want("original")) + scenario_original(db.get_segment_manager()); + if (want("two_idx")) + scenario_two_idx_nonkey_modify(db.get_segment_manager()); + if (want("undo")) + scenario_undo_session_churn(db.get_segment_manager()); } catch (...) { fs::remove_all(temp); throw; From 8994f8e27dbfd67ce03f998ffb44e4ac394a00b0 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 17 Apr 2026 14:27:05 -0500 Subject: [PATCH 2/2] chainbase: skip post_modify key-walk when no secondary key changed post_modify() was called for every modify() on every secondary index, doing iterator_to + two neighbor comparisons even when the modified object's key for that index hadn't moved. For tables where modifies usually touch non-indexed fields (account recv_sequence, resource state, etc.), that's pure overhead. Snapshot the secondary-index keys in modify() before invoking the modifier, then in post_modify compare each index's post-modify key to the saved pre-key; if unchanged, skip the fixup for that index entirely. The fast path is only engaged when it is provably safe: the index's key extractor is a plain `boost::multi_index::member` AND the field type F is trivially copyable AND the copy is value-owning (checked via an `is_shallow_copy` blacklist for pointers, pointer-to- member, `std::reference_wrapper`, `std::string_view`, `std::span`, and recursively through `std::array`, C arrays `T[N]`, `std::optional`, and `std::variant`). This rules out `composite_key<...>` (whose `composite_key_result` holds a reference to the source value, making the "snapshot" alias the live node) and any extractor returning a view, reference wrapper, raw pointer, or an aggregate containing one. Ineligible indexes keep the original iterator_to + neighbor-compare full walk. The revert path still uses the original key-less post_modify because after `node_ref = std::move(*backup)` the tree position may be wrong while the keys match pre_keys. Belt-and-suspenders: the trait and the `fast_path_eligible_v` alias live in `chainbase::detail` so external call sites can static_assert their own extractor types against it; a Debug-only (NDEBUG==0) runtime assertion inside the fast path re-verifies the tree-local sort invariant immediately after every skip, so any aliasing bug that slipped past the trait would fire in CI. Measured on chainbase_bench (heap mode, Release, 3 runs each): scenario_original 23.25 -> 18.96 s (-18%) scenario_two_idx_nonkey_modify 16.66 -> 12.74 s (-23%) scenario_undo_session_churn 2.53 -> 2.52 s (unchanged; 1 idx) Test coverage: - libraries/chaindb/test/undo_index.cpp - test_composite_key_modify: mutating a composite-key component correctly reorders the secondary index (regresses with an unconditional fast path). - test_composite_key_modify_undo: under an undo session, modifying a composite-key field stays sorted and undo restores ordering. - test_mixed_member_and_composite: fast-path-eligible plain member index coexists with a composite-key index; modifying a non-indexed field touches neither, modifying the composite key reorders only the composite index, modifying the plain member reorders only that index. - test_composite_key_modify_uniqueness_conflict: revert on uniqueness violation under composite_key works correctly. - unittests/chainbase_fast_path_tests.cpp: compile-time static_assert that every chainbase-backed production secondary member<> extractor (account_object.by_name, account_metadata_object.by_name, resource_object.by_owner, resource_pending_object.by_owner) passes fast_path_eligible_v. Future table changes that swap a trivially- copyable field for an aliasing or non-trivial type fail the build. chainbase_test, unit_test (sys-vm), plugin_test, contracts_unit_test (sys-vm), test_fc all pass. --- .../chaindb/include/chainbase/undo_index.hpp | 180 +++++++++++++++- libraries/chaindb/test/undo_index.cpp | 203 ++++++++++++++++++ unittests/chainbase_fast_path_tests.cpp | 54 +++++ 3 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 unittests/chainbase_fast_path_tests.cpp diff --git a/libraries/chaindb/include/chainbase/undo_index.hpp b/libraries/chaindb/include/chainbase/undo_index.hpp index 665d414513..4b02d7e8d9 100644 --- a/libraries/chaindb/include/chainbase/undo_index.hpp +++ b/libraries/chaindb/include/chainbase/undo_index.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -16,11 +17,87 @@ #include #include #include +#include +#include #include +#include +#include +#include #include +#include #include namespace chainbase { + + namespace detail { + // Types whose value-copy leaves the copy aliasing the source's storage + // (e.g. copy just duplicates a pointer, pointer+length, or wraps a + // reference). Such types are `std::is_trivially_copyable_v` but a + // snapshot still observes later mutations through the original, which + // would defeat undo_index::post_modify's pre/post-key comparison fast + // path. Not exhaustive for user-defined structs with aliasing members + // (reflection would be needed) -- when adding a new secondary index, + // verify the key field type is genuinely value-owning. + template + struct is_shallow_copy + : std::bool_constant || std::is_member_pointer_v> {}; + template + struct is_shallow_copy> : std::true_type {}; + template + struct is_shallow_copy> : std::true_type {}; + template + struct is_shallow_copy> : std::true_type {}; + // Aggregates: recurse into element / alternative types so nested aliasing + // members are caught (e.g. std::optional, + // std::variant, int*[4], std::array, N>). + template + struct is_shallow_copy> : is_shallow_copy {}; + template + struct is_shallow_copy : is_shallow_copy {}; + template + struct is_shallow_copy> : is_shallow_copy {}; + template + struct is_shallow_copy> + : std::bool_constant<(is_shallow_copy::value || ...)> {}; + template + inline constexpr bool is_shallow_copy_v = is_shallow_copy::value; + + // Trait: the fast-path key-compare in undo_index::post_modify is only + // sound when the index's key extractor is a plain + // `boost::multi_index::member` AND the field type F is trivially + // copyable AND its copy is not shallow/aliasing. Other extractors + // (`composite_key<...>` returning a `composite_key_result` that holds + // `const value_type&`, `const_mem_fun`, user-defined extractors) all + // fall through to the false primary. Exposed at namespace scope so new + // tables can static_assert their extractors against it. + template + struct fast_path_eligible_impl : std::false_type {}; + template + struct fast_path_eligible_impl> + : std::bool_constant + && !is_shallow_copy_v> {}; + template + inline constexpr bool fast_path_eligible_v = fast_path_eligible_impl::value; + + // Compile-time fingerprint so regressions in the trait fail at parse + // time (not only when a future caller trips them). + static_assert(!is_shallow_copy_v); + static_assert(!is_shallow_copy_v>); + static_assert(!is_shallow_copy_v>); + static_assert(!is_shallow_copy_v>); + static_assert( is_shallow_copy_v); + static_assert( is_shallow_copy_v); + static_assert( is_shallow_copy_v); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v>); + static_assert( is_shallow_copy_v, 3>>); + } + struct constructor_tag {}; // Adapts multi_index's idea of keys to intrusive @@ -383,12 +460,17 @@ namespace chainbase { value_type* backup = on_modify(obj); value_type& node_ref = const_cast(obj); bool success = false; + // Snapshot secondary-index keys so post_modify can skip per-index fixup + // for indexes whose key did not change. Most modifies on + // account/resource tables touch only non-indexed fields; without this + // snapshot each such modify still walks every secondary index. + auto pre_keys = extract_secondary_keys(node_ref); { auto guard0 = scope_exit{[&]{ - if(!post_modify(node_ref)) { // The object id cannot be modified + if(!post_modify(node_ref, pre_keys)) { // The object id cannot be modified if(backup) { node_ref = std::move(*backup); - bool success = post_modify(node_ref); + bool success = post_modify(node_ref); // full walk: tree position may be off (void)success; assert(success); assert(backup == &_old_values.front()); @@ -740,6 +822,100 @@ namespace chainbase { return true; } + // Extract secondary-index keys (indexes 1..N-1) into a tuple. For + // indexes whose extractor is fast-path-eligible the tuple slot holds a + // value-typed copy of the field. For ineligible indexes the slot is + // monostate; post_modify ignores that slot via the same compile-time + // guard. Index 0 is the id index, which is immutable across modify. + template + auto extract_secondary_keys(const value_type& p) const { + if constexpr (M < sizeof...(Indices)) { + using idx_t = std::tuple_element_t>; + using key_extractor = typename idx_t::key_from_value_type; + if constexpr (detail::fast_path_eligible_v) { + using extractor = get_key; + return std::tuple_cat(std::make_tuple(extractor{}(p)), + extract_secondary_keys(p)); + } else { + return std::tuple_cat(std::make_tuple(std::monostate{}), + extract_secondary_keys(p)); + } + } else { + return std::tuple<>{}; + } + } + + // post_modify with pre-key snapshot. For fast-path-eligible indexes, + // compare the saved pre-key to the current key; if unchanged, skip + // fixup entirely. Otherwise fall through to the full iterator_to + + // neighbor-compare walk that the original post_modify uses. Ineligible + // indexes always take the full walk regardless of pre_keys contents. + template + bool post_modify(value_type& p, const Keys& pre_keys) { + if constexpr (N < sizeof...(Indices)) { + using idx_t = std::tuple_element_t>; + using key_extractor = typename idx_t::key_from_value_type; + if constexpr (detail::fast_path_eligible_v) { + using extractor = get_key; + using compare_t = typename idx_t::compare_type; + auto k_now = extractor{}(p); + const auto& k_pre = std::get(pre_keys); + compare_t cmp; + if (!cmp(k_now, k_pre) && !cmp(k_pre, k_now)) { +#ifndef NDEBUG + // Paranoia (Debug only, zero cost in Release): verify the + // tree is actually still sorted around p. Catches any future + // trait regression or user-struct aliasing bug that would + // make the fast path skip when it shouldn't. For + // ordered_unique, neighbors must be strictly less/greater, + // so value_comp must return true in both directions. + auto& idx_dbg = std::get(_indices); + auto it_dbg = idx_dbg.iterator_to(p); + if (it_dbg != idx_dbg.begin()) { + auto prev_dbg = it_dbg; --prev_dbg; + assert(idx_dbg.value_comp()(*prev_dbg, p) + && "post_modify fast-path skipped but prev >= p"); + } + ++it_dbg; + if (it_dbg != idx_dbg.end()) { + assert(idx_dbg.value_comp()(p, *it_dbg) + && "post_modify fast-path skipped but p >= next"); + } +#endif + return post_modify(p, pre_keys); + } + // Key changed: fall through to the full fixup below. + } + auto& idx = std::get(_indices); + auto iter = idx.iterator_to(p); + bool fixup = false; + if (iter != idx.begin()) { + auto copy = iter; + --copy; + if (!idx.value_comp()(*copy, p)) fixup = true; + } + ++iter; + if (iter != idx.end()) { + if(!idx.value_comp()(p, *iter)) fixup = true; + } + if(fixup) { + auto iter2 = idx.iterator_to(p); + idx.erase(iter2); + if constexpr (unique) { + auto [new_pos, inserted] = idx.insert_unique(p); + if (!inserted) { + idx.insert_before(new_pos, p); + return false; + } + } else { + idx.insert_equal(p); + } + } + return post_modify(p, pre_keys); + } + return true; + } + template void erase_impl(value_type& p) { if constexpr (N < sizeof...(Indices)) { diff --git a/libraries/chaindb/test/undo_index.cpp b/libraries/chaindb/test/undo_index.cpp index c2ffaf8f27..7d796de35f 100644 --- a/libraries/chaindb/test/undo_index.cpp +++ b/libraries/chaindb/test/undo_index.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -848,6 +849,208 @@ EXCEPTION_TEST_CASE(test_modify_fail) { struct by_secondary {}; +// Regression: modifying a key component of a composite_key secondary index must +// keep that index sorted. A naive "skip post_modify when keys unchanged" fast +// path is unsound here because composite_key_result stores a reference to the +// source value, so any pre-modify snapshot evaluated lazily aliases the +// (subsequently mutated) object and compares equal to the post-modify key. +struct composite_element_t { + template + composite_element_t(C&& c, chainbase::constructor_tag) { c(*this); } + + uint64_t id; + uint64_t a; + uint64_t b; +}; + +struct by_ab {}; + +BOOST_AUTO_TEST_CASE(test_composite_key_modify) { + fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; + try { + chainbase::pinnable_mapped_file db(temp, true, 1024 * 1024, false, chainbase::pinnable_mapped_file::map_mode::mapped); + test_allocator alloc(db.get_segment_manager()); + undo_index_in_segment, + boost::multi_index::ordered_unique>, + boost::multi_index::ordered_unique, + boost::multi_index::composite_key, + key<&composite_element_t::b>>>> i0(alloc); + // id=0: (a=1, b=0) + i0->emplace([](composite_element_t& e) { e.a = 1; e.b = 0; }); + // id=1: (a=3, b=0) + i0->emplace([](composite_element_t& e) { e.a = 3; e.b = 0; }); + + // Baseline: secondary index in order (1,0), (3,0). + { + auto& idx = i0->template get(); + auto it = idx.begin(); + BOOST_TEST(it->id == 0u); + ++it; + BOOST_TEST(it->id == 1u); + } + + // Move id=0 from a=1 to a=5 so it should sort after id=1 in by_ab. + i0->modify(*i0->find(0u), [](composite_element_t& e) { e.a = 5; }); + + // After modify, by_ab must yield (3,0) then (5,0). + auto& idx = i0->template get(); + auto it = idx.begin(); + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 1u); + BOOST_TEST(it->a == 3u); + ++it; + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 0u); + BOOST_TEST(it->a == 5u); + ++it; + BOOST_TEST(it == idx.end()); + } catch ( ... ) { + fs::remove_all( temp ); + throw; + } + fs::remove_all( temp ); +} + +// Modifying a composite_key component under an active undo session must also +// leave the tree correctly sorted, and undo must restore the pre-modify value +// at its pre-modify position. +BOOST_AUTO_TEST_CASE(test_composite_key_modify_undo) { + fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; + try { + chainbase::pinnable_mapped_file db(temp, true, 1024 * 1024, false, chainbase::pinnable_mapped_file::map_mode::mapped); + test_allocator alloc(db.get_segment_manager()); + undo_index_in_segment, + boost::multi_index::ordered_unique>, + boost::multi_index::ordered_unique, + boost::multi_index::composite_key, + key<&composite_element_t::b>>>> i0(alloc); + i0->emplace([](composite_element_t& e) { e.a = 1; e.b = 0; }); // id=0 + i0->emplace([](composite_element_t& e) { e.a = 3; e.b = 0; }); // id=1 + { + auto session = i0->start_undo_session(true); + i0->modify(*i0->find(0u), [](composite_element_t& e) { e.a = 5; }); + // After modify, (3,0) then (5,0). + auto& idx = i0->template get(); + auto it = idx.begin(); + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 1u); + ++it; + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 0u); + BOOST_TEST(it->a == 5u); + // Session goes out of scope: undo. + } + // After undo: back to original (1,0) then (3,0). + auto& idx = i0->template get(); + auto it = idx.begin(); + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 0u); + BOOST_TEST(it->a == 1u); + ++it; + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 1u); + BOOST_TEST(it->a == 3u); + } catch ( ... ) { fs::remove_all(temp); throw; } + fs::remove_all(temp); +} + +// Mix of plain-member secondary index (fast-path eligible) and composite_key +// (not eligible) on the same container. Modifying a non-key field must touch +// neither; modifying the composite-key field must reorder the composite index +// while leaving the plain-member index untouched. +struct mixed_element_t { + template + mixed_element_t(C&& c, chainbase::constructor_tag) { c(*this); } + uint64_t id; + uint64_t plain; + uint64_t comp_a; + uint64_t comp_b; + uint64_t val; // not indexed +}; +struct by_plain {}; +struct by_comp {}; + +BOOST_AUTO_TEST_CASE(test_mixed_member_and_composite) { + fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; + try { + chainbase::pinnable_mapped_file db(temp, true, 1024 * 1024, false, chainbase::pinnable_mapped_file::map_mode::mapped); + test_allocator alloc(db.get_segment_manager()); + undo_index_in_segment, + boost::multi_index::ordered_unique>, + boost::multi_index::ordered_unique, key<&mixed_element_t::plain>>, + boost::multi_index::ordered_unique, + boost::multi_index::composite_key, + key<&mixed_element_t::comp_b>>>> i0(alloc); + i0->emplace([](mixed_element_t& e) { e.plain = 10; e.comp_a = 100; e.comp_b = 0; e.val = 1; }); // id=0 + i0->emplace([](mixed_element_t& e) { e.plain = 20; e.comp_a = 200; e.comp_b = 0; e.val = 2; }); // id=1 + + // (1) Modify non-indexed field: both secondary indexes unchanged. + i0->modify(*i0->find(0u), [](mixed_element_t& e) { e.val = 99; }); + { + auto& pi = i0->template get(); + auto it = pi.begin(); BOOST_TEST(it->id == 0u); ++it; BOOST_TEST(it->id == 1u); + auto& ci = i0->template get(); + auto it2 = ci.begin(); BOOST_TEST(it2->id == 0u); ++it2; BOOST_TEST(it2->id == 1u); + } + // (2) Modify composite-key component: composite index reorders, plain stays. + i0->modify(*i0->find(0u), [](mixed_element_t& e) { e.comp_a = 300; }); + { + auto& pi = i0->template get(); + auto it = pi.begin(); BOOST_TEST(it->id == 0u); ++it; BOOST_TEST(it->id == 1u); + auto& ci = i0->template get(); + auto it2 = ci.begin(); BOOST_TEST(it2->id == 1u); ++it2; BOOST_TEST(it2->id == 0u); + } + // (3) Modify plain-member key: plain index reorders, composite stays. + i0->modify(*i0->find(0u), [](mixed_element_t& e) { e.plain = 30; }); + { + auto& pi = i0->template get(); + auto it = pi.begin(); BOOST_TEST(it->id == 1u); ++it; BOOST_TEST(it->id == 0u); + auto& ci = i0->template get(); + auto it2 = ci.begin(); BOOST_TEST(it2->id == 1u); ++it2; BOOST_TEST(it2->id == 0u); + } + } catch ( ... ) { fs::remove_all(temp); throw; } + fs::remove_all(temp); +} + +// Modifying a composite-key component in a way that collides with another +// element's key must properly revert via the backup path (which uses the +// full-walk post_modify, not the pre_keys fast path). +BOOST_AUTO_TEST_CASE(test_composite_key_modify_uniqueness_conflict) { + fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; + try { + chainbase::pinnable_mapped_file db(temp, true, 1024 * 1024, false, chainbase::pinnable_mapped_file::map_mode::mapped); + test_allocator alloc(db.get_segment_manager()); + undo_index_in_segment, + boost::multi_index::ordered_unique>, + boost::multi_index::ordered_unique, + boost::multi_index::composite_key, + key<&composite_element_t::b>>>> i0(alloc); + i0->emplace([](composite_element_t& e) { e.a = 1; e.b = 0; }); // id=0 + i0->emplace([](composite_element_t& e) { e.a = 2; e.b = 0; }); // id=1 + auto session = i0->start_undo_session(true); + // Try to collide id=0 onto id=1's (a=2, b=0). modify() must detect the + // uniqueness violation and revert. + BOOST_CHECK_THROW( + i0->modify(*i0->find(0u), [](composite_element_t& e) { e.a = 2; }), + std::logic_error); + // Post-revert: both elements present at their original keys. + BOOST_TEST(i0->find(0u)->a == 1u); + BOOST_TEST(i0->find(1u)->a == 2u); + auto& idx = i0->template get(); + auto it = idx.begin(); + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 0u); + ++it; + BOOST_REQUIRE(it != idx.end()); + BOOST_TEST(it->id == 1u); + } catch ( ... ) { fs::remove_all(temp); throw; } + fs::remove_all(temp); +} + BOOST_AUTO_TEST_CASE(test_project) { fs::path temp = fs::temp_directory_path() / "pinnable_mapped_file"; try { diff --git a/unittests/chainbase_fast_path_tests.cpp b/unittests/chainbase_fast_path_tests.cpp new file mode 100644 index 0000000000..7786ab74fb --- /dev/null +++ b/unittests/chainbase_fast_path_tests.cpp @@ -0,0 +1,54 @@ +// Compile-time verification that every chainbase-backed production table's +// secondary-index `boost::multi_index::member<>` extractor passes +// chainbase::detail::fast_path_eligible_v. If a future table change replaces +// a trivially-copyable field with a shallow/aliasing or non-trivial type, +// one of these static_asserts will fail at build time, preventing a silent +// loss of the post_modify fast path (or worse, a consensus-divergent +// aliasing hazard if the exclusion trait misses a new aliasing type). +// +// Only tables registered via CHAINBASE_SET_INDEX_TYPE are covered here; +// unapplied_transaction_queue / vote_processor / wasm_interface_private use +// plain boost::multi_index_container (no undo_index, no fast path). +// +// Primary (by_id) indexes are intentionally not asserted: modify() never +// mutates the id field and post_modify starts at N=1, so by_id eligibility +// does not affect the fast path. Composite-key indexes are intentionally +// not eligible and always take the full walk (see +// test_composite_key_modify in libraries/chaindb/test/undo_index.cpp). + +#include + +#include +#include + +#include + +#include + +namespace { + namespace bmi = boost::multi_index; + using chainbase::detail::fast_path_eligible_v; + + using sysio::chain::account_object; + using sysio::chain::account_metadata_object; + using sysio::chain::account_name; + + static_assert(fast_path_eligible_v>); + static_assert(fast_path_eligible_v>); + + using sysio::chain::resource_limits::resource_object; + using sysio::chain::resource_limits::resource_pending_object; + + static_assert(fast_path_eligible_v>); + static_assert(fast_path_eligible_v>); +} + +BOOST_AUTO_TEST_SUITE(chainbase_fast_path_tests) + +// Single runtime test case so Boost.Test records something; the real work +// is the static_asserts above, which fire at build time. +BOOST_AUTO_TEST_CASE(verify_production_extractors_eligible) { + BOOST_TEST(true); +} + +BOOST_AUTO_TEST_SUITE_END()