diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..ae8a7f6 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-D__cpp_concepts=202002L] diff --git a/.gitignore b/.gitignore index 06787ee..6a66733 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,6 @@ *.dylib *.dll -# Fortran module files -*.mod -*.smod - # Compiled Static libraries *.lai *.la @@ -34,12 +30,14 @@ # IDE auxiliaries .idea -/build +/build* /build-ex /cmake-build-* - /.build /.venv /.vcpkg /.build* +.vscode + +CMakeUserPresets.json /vcpkg_installed diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d413dc2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "clangd.arguments": [ + "--compile-commands-dir=build-clang-20-debug" + ], + "makefile.configureOnOpen": false +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 31a6fd3..83a5ccf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,21 +6,36 @@ cmake_minimum_required(VERSION 3.25) -option(TESTING "Build and run test suite" ON) -if (TESTING) - list(APPEND VCPKG_MANIFEST_FEATURES test) -endif () +option(MORUM_BUILD_TESTS "Build tests" ON) +option(MORUM_BUILD_BENCHMARKS "Build benchmarks" ON) +option(MORUM_BUILD_TRACY "Enable Tracy profiler" OFF) -set(CMAKE_CXX_STANDARD 20) +if (MORUM_BUILD_TESTS) + list(APPEND VCPKG_MANIFEST_FEATURES "test") +endif() +if (MORUM_BUILD_BENCHMARKS) + list(APPEND VCPKG_MANIFEST_FEATURES "benchmark") +endif() +if (MORUM_BUILD_TRACY) + list(APPEND VCPKG_MANIFEST_FEATURES "tracy") +endif() + +project(morum + VERSION 0.1.0 + LANGUAGES C CXX +) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API ON) -project(cpp-jam - VERSION 0.0.1 - LANGUAGES CXX -) +add_compile_options(-Wall -Wextra) + +if ((CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options(-flto=thin) +endif() if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") if(DEFINED VCPKG_TARGET_TRIPLET AND VCPKG_TARGET_TRIPLET) @@ -61,20 +76,28 @@ include(GNUInstallDirs) message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") message(STATUS "Boost_DIR: ${Boost_DIR}") -find_package(Python3 REQUIRED) - -find_package(PkgConfig REQUIRED) -pkg_check_modules(libb2 REQUIRED IMPORTED_TARGET GLOBAL libb2) +option(MORUM_ASAN "Enable address sanitizer" OFF) +option(MORUM_TSAN "Enable address sanitizer" OFF) +option(MORUM_UBSAN "Enable address sanitizer" OFF) +option(MORUM_TRACE "Enable tracing" OFF) -find_package(Boost CONFIG REQUIRED COMPONENTS algorithm outcome program_options) +set(Python3_FIND_VIRTUALENV ONLY) +find_package(Python3 REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS algorithm program_options outcome) find_package(fmt CONFIG REQUIRED) find_package(yaml-cpp CONFIG REQUIRED) -find_package(qdrvm-crates CONFIG REQUIRED) find_package(scale CONFIG REQUIRED) find_package(soralog CONFIG REQUIRED) find_package(Boost.DI CONFIG REQUIRED) find_package(qtils CONFIG REQUIRED) find_package(prometheus-cpp CONFIG REQUIRED) +find_package(RocksDB CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) +find_package(qdrvm-crates CONFIG REQUIRED) + +find_library(BLAKE2_LIB blake2b) +add_library(blake2b STATIC IMPORTED) +set_property(TARGET blake2b PROPERTY IMPORTED_LOCATION ${BLAKE2_LIB}) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options(-fmodules-ts) @@ -88,14 +111,50 @@ target_include_directories(headers INTERFACE $ ) -add_subdirectory(src) +if (MORUM_ASAN) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif() + +if (MORUM_UBSAN) + add_compile_options(-fsanitize=undefined -fno-sanitize-recovery=undefined) + add_link_options(-fsanitize=undefined) +endif() +if (MORUM_TSAN) + add_compile_options(-fsanitize=thread) + add_link_options(-fsanitize=thread) +endif() + +if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + option(QTILS_ASSERT "Enable asserts" OFF) +else() + option(QTILS_ASSERT "Enable asserts" ON) +endif() -if (TESTING) - enable_testing() +add_subdirectory(src) - find_package(GTest CONFIG REQUIRED) - set(GTEST_DEPS GTest::gtest_main) +if(MORUM_BUILD_BENCHMARKS) + find_package(benchmark CONFIG REQUIRED) + add_subdirectory(benchmark) +endif() - add_subdirectory(test-vectors) - add_subdirectory(tests) +if(MORUM_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + set(GTEST_DEPS GTest::gtest_main) + + enable_testing() + + function (morum_add_test TEST_NAME TEST_SRC) + add_executable(${TEST_NAME} ${TEST_SRC}) + target_include_directories(${TEST_NAME} + PUBLIC + ${CMAKE_SOURCE_DIR}/include + ) + add_compile_definitions(${TEST_NAME} PUBLIC MORUM_TEST_BUILD) + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME} WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/test_bin") + set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/test_bin") + endfunction() + + add_subdirectory(test-vectors) + add_subdirectory(test) endif () diff --git a/CMakePresets.json b/CMakePresets.json index 2f03c14..6024146 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -2,14 +2,39 @@ "version": 2, "configurePresets": [ { - "name": "default", + "name": "vcpkg", + "hidden": true, "generator": "Ninja", - "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", - "CMAKE_BUILD_TYPE": "Debug", "VCPKG_OVERLAY_PORTS": "${sourceDir}/vcpkg-overlay" } + }, + { + "name": "dev", + "inherits": "vcpkg", + "binaryDir": "${sourceDir}/build-dev", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "QTILS_ASSERT": "ON", + "MORUM_BUILD_TRACY": "ON", + "MORUM_ASAN": "OFF", + "MORUM_UBSAN": "OFF", + "MORUM_TSAN": "OFF" + } + }, + { + "name": "debug", + "inherits": "vcpkg", + "binaryDir": "${sourceDir}/build-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "MORUM_BUILD_TRACY": "ON", + "MORUM_BUILD_TESTS": "ON", + "MORUM_ASAN": "OFF", + "MORUM_UBSAN": "OFF", + "MORUM_TSAN": "OFF" + } } ] -} +} \ No newline at end of file diff --git a/CODESTYLE.md b/CODESTYLE.md index 4427a7b..6253afa 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include diff --git a/Makefile b/Makefile index d795d33..a75a563 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ configure: @echo "=== Configuring..." export PATH="$$HOME/.cargo/bin:$$PATH" && \ source $$HOME/.cargo/env 2>/dev/null || true && \ - VCPKG_ROOT=$(VCPKG) cmake --preset=default -DPython3_EXECUTABLE="$(VENV)/bin/python3" -B $(BUILD) $(PROJECT) + VCPKG_ROOT=$(VCPKG) cmake --preset=debug -DPython3_EXECUTABLE="$(VENV)/bin/python3" -B $(BUILD) $(PROJECT) build: @echo "=== Building..." diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt new file mode 100644 index 0000000..6c3b84e --- /dev/null +++ b/benchmark/CMakeLists.txt @@ -0,0 +1,3 @@ + +add_executable(set_get_benchmark merkle_tree/set_get.cpp) +target_link_libraries(set_get_benchmark benchmark::benchmark merkle_tree) diff --git a/benchmark/merkle_tree/set_get.cpp b/benchmark/merkle_tree/set_get.cpp new file mode 100644 index 0000000..58bff5a --- /dev/null +++ b/benchmark/merkle_tree/set_get.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +constexpr unsigned seed = 42; +static std::mt19937_64 rand_engine{seed}; + +template +void fill_random(R &&span) { + static std::uniform_int_distribution dist; + + for (auto &byte : span) { + byte = dist(rand_engine); + } +} + +morum::Hash32 random_hash() { + morum::Hash32 hash; + fill_random(hash); + return hash; +} + +morum::ByteVector random_vector(size_t min_size = 1, size_t max_size = 128) { + std::uniform_int_distribution dist(min_size, max_size); + size_t size = dist(rand_engine); + + morum::ByteVector v(size); + fill_random(v); + return v; +} + +std::unique_ptr trie_db; +morum::Hash32 last_root{}; + +static void BM_SetGet(benchmark::State &state) { + rand_engine.seed(42); + constexpr int INSERTION_NUM = 1000; + + for (auto _ : state) { + ZoneNamedN(loop_zone, "loop", true); + + auto tree = trie_db->load_tree(last_root).value().value(); + + std::vector> insertions; + for (int i = 0; i < INSERTION_NUM; i++) { + insertions.emplace_back(random_hash(), random_vector()); + } + { + ZoneNamedN(setter_zone, "set", true); + for (auto &[k, v] : insertions) { + tree->set(k, morum::ByteVector{v}).value(); + } + } + { + ZoneNamedN(getter_zone, "get", true); + + for (auto &[k, v] : insertions) { + tree->get(k).value(); + } + } + { + ZoneNamedN(calculate_hash_zone, "calculate_hash", true); + trie_db->get_root_and_store(*tree).value(); + } + + FrameMark; + } +} + +BENCHMARK(BM_SetGet); + +template +struct FinalAction { + ~FinalAction() { + f(); + } + + F f; +}; + +int main(int argc, char **argv) { + char arg0_default[] = "benchmark"; + char *args_default = arg0_default; + if (!argv) { + argc = 1; + argv = &args_default; + } + + auto db = std::shared_ptr{morum::open_db("./test_db").value()}; + FinalAction cleanup{[]() { std::filesystem::remove_all("./test_db"); }}; + + trie_db = std::make_unique(db); + + auto tree = trie_db->empty_tree(); + constexpr int INSERTION_NUM = 5000; + { + ZoneNamedN(setter_zone, "initial insertions", true); + + for (int i = 0; i < INSERTION_NUM; i++) { + tree->set(random_hash(), morum::ByteVector{random_vector()}).value(); + } + } + last_root = trie_db->get_root_and_store(*tree).value(); + ::benchmark::Initialize(&argc, argv); + if (::benchmark::ReportUnrecognizedArguments(argc, argv)) { + return 1; + } + ::benchmark::RunSpecifiedBenchmarks(); + ::benchmark::Shutdown(); + return 0; +} diff --git a/include/morum/archive_backend.hpp b/include/morum/archive_backend.hpp new file mode 100644 index 0000000..f7c914a --- /dev/null +++ b/include/morum/archive_backend.hpp @@ -0,0 +1,126 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include + +namespace morum { + + /** + * Paged vector to make less allocations for nodes and keep them pinned in + * memory when the vector is resized to avoid reference invalidation. + * Has nothing to do with NOMT. + */ + class FlatPagedNodeStorage final : public NodeStorage { + public: + static constexpr size_t PageSizeBytes = 4096; + static constexpr size_t MaxNodesInPage = 4096 / sizeof(TreeNode); + static_assert(MaxNodesInPage == 32); + + qtils::OptionalRef get(NodeId idx) override { + QTILS_ASSERT_LESS(idx / MaxNodesInPage, data.size()); + QTILS_ASSERT_LESS( + idx % MaxNodesInPage, metadata[idx / MaxNodesInPage].node_num); + return data[idx / MaxNodesInPage]->nodes[idx % MaxNodesInPage].node; + } + + qtils::OptionalRef get(const Hash32 &hash) override { + auto it = hash_to_id.find(hash); + if (it != hash_to_id.end()) { + return get(it->second); + } + return std::nullopt; + } + + template + NodeId allocate(Args &&...args) { + if (metadata.empty() || metadata.back().node_num == MaxNodesInPage) { + data.emplace_back(std::make_unique()); + metadata.emplace_back(PageMetadata{.node_num = 0}); + } + std::construct_at( + &data.back()->nodes[metadata.back().node_num], + Page::UninitNode{std::forward(args)...}); + metadata.back().node_num++; + return ((data.size() - 1) * MaxNodesInPage) + + (metadata.back().node_num - 1); + } + + virtual NodeId store(const TreeNode &node) override { + return allocate(node); + } + + bool empty() const override { + return metadata.empty(); + } + + void reserve_nodes(size_t nodes) override { + metadata.reserve(nodes / MaxNodesInPage); + data.reserve(nodes / MaxNodesInPage); + } + + private: + struct Page { + union UninitNode { + UninitNode() : empty{} {} + + template + UninitNode(Args &&...args) : node{std::forward(args)...} {} + + struct { + } empty; + TreeNode node; + }; + // can actually store two leaves in place of one branch + std::array nodes; + }; + + struct PageMetadata { + uint16_t node_num; + }; + + std::vector metadata; + std::vector> data; + std::unordered_map hash_to_id; + }; + + class ArchiveNodeLoader final : public NodeLoader { + public: + explicit ArchiveNodeLoader(std::shared_ptr node_storage) + : node_storage{std::move(node_storage)} {} + + std::expected, StorageError> load( + qtils::BitSpan<>, const Hash32 &hash) const override; + + private: + std::shared_ptr node_storage; + }; + + class ArchiveTrieDb { + public: + explicit ArchiveTrieDb( + std::shared_ptr> storage); + + std::expected>, StorageError> + load_tree(const Hash32 &root_hash) const; + + std::unique_ptr empty_tree() const; + + std::expected get_root_and_store( + const MerkleTree &tree); + + private: + std::shared_ptr> storage_; + std::shared_ptr node_storage_; + std::shared_ptr value_storage_; + }; + +} // namespace morum diff --git a/include/morum/calculate_root_in_memory.hpp b/include/morum/calculate_root_in_memory.hpp new file mode 100644 index 0000000..fea14a6 --- /dev/null +++ b/include/morum/calculate_root_in_memory.hpp @@ -0,0 +1,55 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +namespace morum { + + template + concept StateDictionary = std::ranges::random_access_range + && requires(std::ranges::range_value_t value) { + { + std::get<0>(value) + } -> std::convertible_to; + { + std::get<1>(value) + } -> std::convertible_to; + }; + + template + Hash32 calculate_root_in_memory(State &&state, size_t depth = 0) { + if (std::ranges::empty(state)) { + return ZeroHash32; + } + if (std::ranges::begin(state) + 1 == std::ranges::end(state)) { + auto kv = std::ranges::begin(state); + auto &&[k, v] = *kv; + ByteArray<31> partial_key; + std::copy_n(k.begin(), 31, partial_key.begin()); + return blake2b_256(serialize_leaf(partial_key, v)); + } + auto right_subtree_it = std::ranges::lower_bound( + state, std::ranges::range_value_t{}, [depth](auto value, auto) { + // bit #depth (starting from lsb) is 0 + return (std::get<0>(value)[depth / 8] & (0x01 << (depth % 8))) == 0; + }); + + auto left_root = calculate_root_in_memory( + std::ranges::subrange(std::ranges::begin(state), right_subtree_it), + depth + 1); + auto right_root = calculate_root_in_memory( + std::ranges::subrange(right_subtree_it, std::ranges::end(state)), + depth + 1); + + return blake2b_256(serialize_branch(left_root, right_root)); + } + +} // namespace morum \ No newline at end of file diff --git a/include/morum/codec.hpp b/include/morum/codec.hpp new file mode 100644 index 0000000..61ae0e9 --- /dev/null +++ b/include/morum/codec.hpp @@ -0,0 +1,132 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace morum { + + template + concept OutStream = true; + + template + concept InStream = true; + + template + concept Encodable = true; + + struct VectorStream { + void append(uint8_t b) { + data.push_back(b); + } + + std::vector data; + }; + + template + struct ArrayStream { + void append(uint8_t b) { + QTILS_ASSERT_LESS(current, N); + data[current] = b; + current++; + } + + std::array data; + size_t current = 0; + }; + + struct SpanStream { + uint8_t read_byte() { + QTILS_ASSERT_LESS(current, data.size_bytes()); + return data[current++]; + } + + std::span data; + size_t current = 0; + }; + + template + requires(sizeof...(Ts) > 1) + void encode(S &stream, const Ts &...tuple) { + (encode(stream, tuple), ...); + } + + template + consteval void encode(S &stream, const std::tuple &tuple) { + for (int i = 0; i < tuple.size(); i++) { + encode(stream, std::get(tuple)); + } + } + + template + void encode_integer_fixed(S &stream, I integer, size_t len) { + for (size_t i = 0; i < len; i++) { + stream.append(integer % 256); + integer /= 256; + } + } + + template + void decode_integer_fixed(S &stream, I &integer, size_t len) { + integer = 0; + for (size_t i = 0; i < len; i++) { + uint8_t byte = stream.read_byte(); + integer <<= 8; + integer |= static_cast(byte); + } + } + + template + void encode_integer_29bit(S &stream, I integer) { + QTILS_ASSERT(integer < (1 << 29)); + + if (integer >= (1ul << 21)) { + stream.append(256 - 32 + integer / (1ul << 24)); + encode_integer_fixed(stream, integer % (1 << 24), 3); + } + int l = 3; + if (integer == 0) { + l = 0; + } else { + auto power = 1ul << 21; + while (integer < power) { + l--; + power >>= 8; + } + } + stream.append(256 - (1 << (8 - l)) + integer / (1ul << (8 * l))); + encode_integer_fixed(stream, integer % (1ul << (8 * l)), l); + } + + template + void encode(S &stream, I integer) { + if (integer >= (1ul << 56)) { + stream.append(255); + encode_integer_fixed(stream, integer, 8); + } + int l = 8; + if (integer == 0) { + l = 0; + } else { + auto power = 1ul << 56; + while (integer < power) { + l--; + power >>= 8; + } + } + stream.append(256 - (1 << (8 - l)) + integer / (1ul << (8 * l))); + encode_integer_fixed(stream, integer % (1ul << (8 * l)), l); + } +} // namespace morum diff --git a/include/morum/common.hpp b/include/morum/common.hpp new file mode 100644 index 0000000..232ba0d --- /dev/null +++ b/include/morum/common.hpp @@ -0,0 +1,135 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef MORUM_ENABLE_TRACE + +#define MORUM_TRACE(msg, ...) \ + fmt::println("{}:{}: {}", \ + std::source_location::current().file_name(), \ + std::source_location::current().line(), \ + fmt::format(msg __VA_OPT__(, ) __VA_ARGS__)); + +namespace morum { + template + struct ScopeTimer { + using Clock = std::chrono::steady_clock; + + ~ScopeTimer() { + if (!over) { + auto now = Clock::now(); + MORUM_TRACE("Scope {} ended: {}", Name..., now - start); + } + } + + void tick() { + auto now = Clock::now(); + MORUM_TRACE("Scope {} ticked: {}", Name..., now - start); + } + + void end() { + auto now = Clock::now(); + MORUM_TRACE("Scope {} ended: {}", Name..., now - start); + over = true; + } + + Clock::time_point start; + bool over; + }; +} // namespace morum + +#define MORUM_START_SCOPE(name) ScopeTimer morum_##name##_scope_timer_; +#define MORUM_TICK_SCOPE(name) morum_##name##_scope_timer_.tick(); +#define MORUM_END_SCOPE(name) morum_##name##_scope_timer_.end(); + +#else + +#define MORUM_TRACE(...) +#define MORUM_START_SCOPE(...) +#define MORUM_TICK_SCOPE(...) +#define MORUM_END_SCOPE(...) + +#endif + +namespace morum { + + using ByteVector = std::vector; + + template + using ByteArray = std::array; + + using Hash32 = ByteArray<32>; + + constexpr Hash32 operator~(Hash32 h) { + Hash32 res; + for (size_t i = 0; i < h.size(); ++i) { + res[i] = ~h[i]; + } + return res; + } + + struct StorageError { + std::string message; + std::source_location origin = std::source_location::current(); + }; + +} // namespace morum + +template <> +struct std::formatter { + template + constexpr ParseContext::iterator parse(ParseContext &ctx) { + auto it = ctx.begin(); + return it; + } + + template + FmtContext::iterator format(const morum::StorageError &e, + FmtContext &ctx) const { + auto out = ctx.out(); + fmt::format_to(out, + "From {}:{} - {}\n", + e.origin.file_name(), + e.origin.line(), + e.message); + return out; + } +}; + +template <> +struct std::hash { + std::size_t operator()(const morum::Hash32 &s) const noexcept { + // good enough for hash maps, not meant for security + std::size_t res; + std::copy_n(s.data(), sizeof(res), reinterpret_cast(&res)); + return res; + } +}; + +namespace morum { + + inline constexpr const Hash32 ZeroHash32{{}}; + + Hash32 blake2b_256(qtils::ByteSpan bytes); + + inline unsigned char reverse_bits(unsigned char b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; + } +} // namespace morum \ No newline at end of file diff --git a/include/morum/db.hpp b/include/morum/db.hpp new file mode 100644 index 0000000..3b20134 --- /dev/null +++ b/include/morum/db.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include + +#include + +namespace rocksdb { + class DB; + class ColumnFamilyHandle; +} // namespace rocksdb + +namespace morum { + + /** + * An interface class for various database back-ends. + */ + class KeyValueStorage { + public: + virtual ~KeyValueStorage() = default; + + virtual std::expected write( + qtils::ByteSpan key, qtils::ByteSpan value) = 0; + + virtual std::expected, StorageError> read( + qtils::ByteSpan key) const = 0; + + virtual std::expected, StorageError> read_to( + qtils::ByteSpan key, qtils::ByteSpanMut value) const = 0; + + virtual std::expected remove( + qtils::ByteSpan key) const = 0; + + /** + * Batches are supposed to provide a way to write several entries + * atomically. + */ + class Batch { + public: + virtual ~Batch() = default; + + virtual std::expected write( + qtils::ByteSpan key, qtils::ByteSpan value) = 0; + virtual std::expected remove(qtils::ByteSpan key) = 0; + }; + + virtual std::unique_ptr start_batch() = 0; + virtual std::expected write_batch( + std::unique_ptr batch) = 0; + }; + + template + class ColumnFamilyStorage { + public: + virtual ~ColumnFamilyStorage() = default; + + virtual std::shared_ptr get_column_family( + ColumnFamilyId) = 0; + + /** + * Batches are supposed to provide a way to write several entries + * atomically. + */ + class Batch { + public: + virtual ~Batch() = default; + + virtual std::expected write( + ColumnFamilyId cf, qtils::ByteSpan key, qtils::ByteSpan value) = 0; + + virtual std::expected remove( + ColumnFamilyId cf, qtils::ByteSpan key) = 0; + }; + + virtual std::unique_ptr start_batch() = 0; + virtual std::expected write_batch( + std::unique_ptr batch) = 0; + }; + + enum class ColumnFamilyId { + DEFAULT, + TREE_NODE, + TREE_VALUE, + FLAT_KV, + TREE_PAGE, + }; + + std::string_view to_string_nocheck(ColumnFamilyId family); + std::optional to_string(ColumnFamilyId family); + + inline std::array column_families() { + return { + ColumnFamilyId::DEFAULT, + ColumnFamilyId::TREE_NODE, + ColumnFamilyId::TREE_VALUE, + ColumnFamilyId::FLAT_KV, + ColumnFamilyId::TREE_PAGE, + }; + } + + class RocksDb final : public ColumnFamilyStorage, + public std::enable_shared_from_this { + public: + using Batch = ColumnFamilyStorage::Batch; + + ~RocksDb() override; + + virtual std::shared_ptr get_column_family( + ColumnFamilyId) override; + + virtual std::unique_ptr start_batch() override; + virtual std::expected write_batch( + std::unique_ptr batch) override; + + private: + friend std::expected, StorageError> open_db( + const std::filesystem::path &path); + friend class RocksDbColumnFamily; + friend class RocksDbBatch; + + RocksDb( + rocksdb::DB *db, std::vector &&handles); + rocksdb::DB *db; + std::vector handles; + }; + + std::expected, StorageError> open_db( + const std::filesystem::path &path); + + class RocksDbColumnFamily final : public KeyValueStorage { + public: + RocksDbColumnFamily(std::shared_ptr db, ColumnFamilyId family); + + virtual std::expected write( + qtils::ByteSpan key, qtils::ByteSpan value) override; + + virtual std::expected, StorageError> read( + qtils::ByteSpan key) const override; + + virtual std::expected, StorageError> read_to( + qtils::ByteSpan key, qtils::ByteSpanMut value) const override; + + virtual std::expected remove( + qtils::ByteSpan key) const override; + + virtual std::unique_ptr start_batch() override; + + virtual std::expected write_batch( + std::unique_ptr batch) override; + + private: + std::shared_ptr db; + rocksdb::ColumnFamilyHandle *handle; + }; + +} // namespace morum \ No newline at end of file diff --git a/include/morum/merkle_tree.hpp b/include/morum/merkle_tree.hpp new file mode 100644 index 0000000..00e090b --- /dev/null +++ b/include/morum/merkle_tree.hpp @@ -0,0 +1,240 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace morum { + + class KeyValueStorage; + + // used to store trie keys in an std::map, for example + struct TrieKeyOrder { + bool operator()(const morum::Hash32 &a, const morum::Hash32 &b) const { + for (size_t i = 0; i < 32; i++) { + unsigned char a_bits = reverse_bits(a[i]); + unsigned char b_bits = reverse_bits(b[i]); + if (a_bits < b_bits) { + return true; + } + if (a_bits > b_bits) { + return false; + } + } + return false; + } + }; + + /** + * An interface to load nodes from + */ + class NodeLoader { + public: + virtual std::expected, StorageError> load( + qtils::BitSpan<> path, const Hash32 &hash) const = 0; + }; + + class NoopNodeLoader final : public NodeLoader { + public: + virtual std::expected, StorageError> load( + qtils::BitSpan<>, const Hash32 &) const override { + return std::nullopt; + } + }; + + class NodeStorage { + public: + virtual ~NodeStorage() = default; + + virtual qtils::OptionalRef get(NodeId id) = 0; + virtual qtils::OptionalRef get(const Hash32 &hash) = 0; + + virtual NodeId store(const TreeNode &) = 0; + + virtual bool empty() const = 0; + + virtual void reserve_nodes(size_t nodes) = 0; + }; + + class MerkleTree { + private: + qtils::OptionalRef get_root(); + + qtils::OptionalRef get_root() const; + + // value hash or embedded value + using CachedValue = std::variant>; + + public: + explicit MerkleTree(const TreeNode &root, + std::unique_ptr node_storage, + std::shared_ptr node_loader) + : nodes_{std::move(node_storage)}, loader_{std::move(node_loader)} { + QTILS_ASSERT(nodes_ != nullptr); + QTILS_ASSERT(loader_ != nullptr); + [[maybe_unused]] auto root_id = nodes_->store(root); + QTILS_ASSERT(root_id == 0); + } + + explicit MerkleTree(std::unique_ptr node_storage, + std::shared_ptr node_loader) + : nodes_{std::move(node_storage)}, loader_{std::move(node_loader)} { + QTILS_ASSERT(nodes_ != nullptr); + QTILS_ASSERT(loader_ != nullptr); + } + + std::expected set( + const Hash32 &key, ByteVector &&value); + + std::expected, StorageError> get( + const Hash32 &key) const; + + std::expected exists(const Hash32 &key) const; + + bool empty() const { + return nodes_->empty(); + } + + struct ValueWithPath { + qtils::ByteSpan key; + qtils::ByteSpan value; + + qtils::ByteArray<32> path_storage; + qtils::BitSpan<> path; + }; + + static void empty_visitor( + const TreeNode &, qtils::ByteSpan, qtils::ByteSpan, qtils::BitSpan<>) {} + + template > Visitor = decltype(empty_visitor)> + Hash32 calculate_hash(const Visitor &visitor = empty_visitor) const { + if (empty()) { + return ZeroHash32; + } + Hash32 hash; + if (get_root()->is_leaf()) { + auto serialized = serialize_leaf(get_root()->as_leaf().get_key(), + get_root()->as_leaf().hash_or_value()); + hash = blake2b_256(serialized); + visitor(*get_root(), serialized, hash, qtils::BitSpan<>{}); + } else { + ByteArray path_storage; + hash = calculate_hash( + *get_root(), visitor, qtils::BitSpan{path_storage, 0, 0}); + } + return hash; + } + + using ValueHash = Hash32; + + // if the value is cached, it means it's 'dirty' and should be written to + // the DB + std::optional get_cached_value( + const ValueHash &hash) const { + auto it = value_cache_.find(hash); + if (it != value_cache_.end()) { + return it->second; + } + return std::nullopt; + } + + private: + std::expected, StorageError> get_child_idx( + Branch &branch, int8_t bit, qtils::BitSpan<> path) const; + + std::expected get_value( + Leaf::HashOrValue hash_or_value) const; + + struct NodeWithPath { + size_t node_idx; + qtils::BitSpan<> path; + }; + // find the lowest existing branch on the path described by key + std::expected, StorageError> find_parent_branch( + const Hash32 &key); + + std::expected, StorageError> find_leaf( + const Hash32 &key) const; + + size_t create_leaf_node(const Hash32 &key, qtils::Bytes &&value); + + void replace_leaf_with_branch( + size_t path_len, size_t old_leaf_idx, size_t new_leaf_idx); + + template > Visitor> + Hash32 calculate_hash(const TreeNode &n, + const Visitor &visitor, + qtils::BitSpan path) const { + Hash32 hash; + if (n.is_branch()) { + auto calculate_child_hash = [this, visitor, path]( + Branch &n, uint8_t bit) mutable { + Hash32 hash; + if (auto hash_opt = n.get_child_hash(bit); hash_opt.has_value()) { + hash = *hash_opt; + } else if (auto idx = n.get_child_idx(bit); idx.has_value()) { + path.end_bit++; + path.set_bit(path.end_bit - 1, bit); + hash = calculate_hash(*nodes_->get(*idx), visitor, path); + path.end_bit--; + n.set_child(bit, hash); + } else { + hash = ZeroHash32; + n.set_child(bit, hash); + } + return hash; + }; + // branch won't be mutated per se, only calculated hashes would be + // cached in it + auto &branch = const_cast(n.as_branch()); + Hash32 left_t = calculate_child_hash(branch, 0); + Hash32 right_t = calculate_child_hash(branch, 1); + auto serialized = serialize_branch(left_t, right_t); + hash = blake2b_256(serialized); + visitor(n, serialized, hash, qtils::BitSpan<>{path}); + } else { + auto serialized = + serialize_leaf(n.as_leaf().get_key(), n.as_leaf().hash_or_value()); + hash = blake2b_256(serialized); + visitor(n, serialized, hash, qtils::BitSpan<>{path}); + } + return hash; + } + + // mutable because we may need to fetch a node from disk to memory in + // const methods + mutable std::shared_ptr nodes_; + mutable std::shared_ptr loader_; + mutable std::shared_ptr value_storage_; + mutable std::unordered_map value_cache_; + }; + +} // namespace morum diff --git a/include/morum/nomt_backend.hpp b/include/morum/nomt_backend.hpp new file mode 100644 index 0000000..9257bf9 --- /dev/null +++ b/include/morum/nomt_backend.hpp @@ -0,0 +1,214 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace morum { + + /** + * This branch representation doesn't store any additional data contrary to + * Branch. + */ + struct RawBranch { + // mind that the left hash must have the first bit set to 0 + Hash32 left; + Hash32 right; + + const Hash32 &get_child(uint8_t bit) const { + return bit ? right : left; + } + + bool has_child(uint8_t bit) const { + return get_child(bit) != ZeroHash32; + } + }; + + /** + * This node representation doesn't store any additional data contrary to + * TreeNode. + */ + union RawNode { + std::monostate unitialized; + Leaf leaf; + RawBranch branch; + + bool is_branch() const { + return (reinterpret_cast(this)[0] & 1) == 0; + } + + bool is_leaf() const { + return (reinterpret_cast(this)[0] & 1) == 1; + } + }; + + static_assert(sizeof(RawNode) == 64); + + // size of a page in which nodes are stored on disk + static constexpr size_t DISK_PAGE_SIZE = 4096; + static constexpr size_t PAGE_NODES = DISK_PAGE_SIZE / sizeof(RawNode) - 2; + // depth of a rootless subtree stored in a page + static constexpr size_t PAGE_LEVELS = + __builtin_ctz(DISK_PAGE_SIZE / sizeof(RawNode)) - 1; + + struct Page { + // unused space + uint8_t metadata[sizeof(RawNode) * 2]; + // i.e. for DISK_PAGE_SIZE 4096: 32 + 16 + 8 + 4 + 2 = 62 + std::array nodes; + + // follows branch nodes and returns nullopt if at any point in path the + // corresponding child is absent + qtils::OptionalRef get_node(qtils::BitSpan<> path); + + // ignores branch nodes and just return where the corresponding node is + // supposed to be placed + RawNode &get_node_unchecked(qtils::BitSpan<> path); + + qtils::FixedByteSpanMut as_bytes() { + return qtils::FixedByteSpanMut{ + reinterpret_cast(this), DISK_PAGE_SIZE}; + } + }; + static_assert(std::is_standard_layout_v); + static_assert(sizeof(Page) == DISK_PAGE_SIZE); + + inline TreeNode raw_node_to_node(const RawNode &raw_node) { + if (raw_node.is_leaf()) { + return raw_node.leaf; + } else { + return morum::Branch{raw_node.branch.left, raw_node.branch.right}; + } + } + + class NearlyOptimalNodeStorage + : public std::enable_shared_from_this { + public: + explicit NearlyOptimalNodeStorage(std::shared_ptr storage) + : storage_{storage} { + QTILS_ASSERT(storage != nullptr); + } + + std::expected, StorageError> load_root_node() const; + std::expected, StorageError> load_terminal_page( + qtils::BitSpan<> path) const; + std::expected, StorageError> load_page_direct( + qtils::BitSpan<> path) const; + + static size_t get_node_idx(qtils::BitSpan<> path) { + size_t offset{}; + for (uint8_t bit : path) { + offset |= bit; + offset <<= 1; + } + offset >>= 1; + return (1 << (path.size_bits() - 1)) + offset; + } + + struct PageKeyHash { + size_t operator()(const ByteArray<33> &page_key) const { + size_t result; + std::copy_n(page_key.begin(), + sizeof(size_t), + reinterpret_cast(&result)); + return result; + } + }; + + struct WriteBatch { + WriteBatch(std::shared_ptr storage) + : page_storage{storage} {} + + std::expected set( + qtils::BitSpan<> path, const RawNode &node); + + std::expected, StorageError> load( + [[maybe_unused]] const Hash32 &hash, qtils::BitSpan<> path); + + std::shared_ptr page_storage; + std::unordered_map, Page, PageKeyHash> page_cache; + std::optional root_node; + }; + + std::unique_ptr start_writing() { + return std::make_unique(shared_from_this()); + } + + std::expected submit_batch( + std::unique_ptr batch, + ColumnFamilyStorage::Batch &db_batch); + + private: + static qtils::FixedByteVector get_page_key( + qtils::BitSpan<> path); + + std::shared_ptr storage_; + }; + + class NomtDb { + public: + using Clock = std::chrono::steady_clock; + + struct Metrics { + size_t pages_loaded{}; + size_t pages_stored{}; + size_t nodes_loaded{}; + size_t values_loaded{}; + size_t nodes_stored{}; + size_t values_stored{}; + + Clock::duration page_load_duration{}; + Clock::duration page_store_duration{}; + Clock::duration page_batch_write_duration{}; + Clock::duration value_batch_write_duration{}; + }; + + explicit NomtDb( + std::shared_ptr> storage) + : storage_{storage}, + page_storage_{std::make_shared( + storage->get_column_family(ColumnFamilyId::TREE_PAGE))} { + QTILS_ASSERT(storage_ != nullptr); + } + + void reset_metrics() { + metrics_ = Metrics{}; + } + + const Metrics &get_metrics() const { + return metrics_; + } + + std::expected< + std::optional>, + StorageError> + start_writing_tree() const { + return std::make_unique( + page_storage_); + } + + std::expected>, StorageError> + load_tree(const Hash32 &root) const; + + std::unique_ptr empty_tree() const; + + std::expected get_root_and_store( + const MerkleTree &tree); + + private: + std::shared_ptr> storage_; + std::shared_ptr page_storage_; + + mutable Metrics metrics_{}; + }; + +} // namespace morum diff --git a/include/morum/tree_node.hpp b/include/morum/tree_node.hpp new file mode 100644 index 0000000..1648cea --- /dev/null +++ b/include/morum/tree_node.hpp @@ -0,0 +1,353 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace morum { + + struct TreeNode; + + // an identifier for a tree node in memory + using NodeId = uint64_t; + + /** + * An extended tree branch, where a child may be stored as either a subtree + * hashe or a NodeId. Children stored by NodeId are supposedly loaded to RAM + * because they are located on modified paths and need to be re-hashed. + */ + struct Branch { + friend class MerkleTree; + friend TreeNode deserialize_node(qtils::ByteSpan bytes); + + static constexpr NodeId NoId = static_cast(-1); + static constexpr Hash32 NoHash = []() { + auto h = ~ZeroHash32; + h[0] &= 0xFE; // first bit is always zero for branch nodes + return h; + }(); + + explicit Branch(const Hash32 &left, std::nullopt_t) + : left_hash{left}, right_hash{NoHash}, left_idx{NoId}, right_idx{NoId} { + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + + explicit Branch(std::nullopt_t, const Hash32 &right) + : left_hash{NoHash}, + right_hash{right}, + left_idx{NoId}, + right_idx{NoId} {} + + explicit Branch(NodeId left, std::nullopt_t) + : left_hash{NoHash}, + right_hash{NoHash}, + left_idx{left}, + right_idx{NoId} {} + + explicit Branch(std::nullopt_t, NodeId right) + : left_hash{NoHash}, + right_hash{NoHash}, + left_idx{NoId}, + right_idx{right} {} + + explicit Branch(const Hash32 &left, const Hash32 &right) + : left_hash{left}, right_hash{right}, left_idx{NoId}, right_idx{NoId} { + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + + explicit Branch(qtils::OptionalRef left, + qtils::OptionalRef right) + : left_hash{left ? *left : NoHash}, + right_hash{right ? *right : NoHash}, + left_idx{NoId}, + right_idx{NoId} { + if (left_hash != NoHash) { + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + } + + explicit Branch(NodeId left, NodeId right) + : left_hash{NoHash}, + right_hash{NoHash}, + left_idx{left}, + right_idx{right} {} + + explicit Branch(std::pair with_idx) + : left_hash{with_idx.first == 0 ? with_idx.second : NoHash}, + right_hash{with_idx.first == 1 ? with_idx.second : NoHash}, + left_idx{NoId}, + right_idx{NoId} { + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + + explicit Branch(std::pair with_idx) + : left_hash{NoHash}, + right_hash{NoHash}, + left_idx{with_idx.first == 0 ? with_idx.second : NoId}, + right_idx{with_idx.first == 1 ? with_idx.second : NoId} {} + + Branch() + : left_hash{NoHash}, + right_hash{NoHash}, + left_idx{NoId}, + right_idx{NoId} {} + + qtils::OptionalRef get_left_hash() const { + if (left_hash != NoHash && left_hash != ZeroHash32) { + return left_hash; + } + return std::nullopt; + } + + qtils::OptionalRef get_right_hash() const { + if (right_hash != NoHash && right_hash != ZeroHash32) { + return right_hash; + } + return std::nullopt; + } + + const Hash32 &get_left_hash_raw() const { + return left_hash; + } + + const Hash32 &get_right_hash_raw() const { + return right_hash; + } + + qtils::OptionalRef get_child_hash(uint8_t bit) const { + return (bit ? get_right_hash() : get_left_hash()); + } + + std::optional get_left_idx() const { + if (left_idx != NoId) { + return left_idx; + } + return std::nullopt; + } + + std::optional get_right_idx() const { + if (right_idx != NoId) { + return right_idx; + } + return std::nullopt; + } + + std::optional get_child_idx(uint8_t bit) const { + return (bit ? get_right_idx() : get_left_idx()); + } + + void set_left(const Hash32 &hash) { + left_hash = hash; + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + + void set_right(const Hash32 &hash) { + right_hash = hash; + } + + void set_left(qtils::FixedByteSpan<32> hash) { + std::ranges::copy(hash, left_hash.begin()); + left_hash[0] &= 0xFE; // the first bit in a branch must be zero + } + + void set_right(qtils::FixedByteSpan<32> hash) { + std::ranges::copy(hash, right_hash.begin()); + } + + void set_child(uint8_t bit, qtils::FixedByteSpan<32> hash) { + if (bit == 0) { + set_left(hash); + } else { + set_right(hash); + } + } + + void reset_child_hash(uint8_t bit) { + if (bit == 0) { + left_hash = NoHash; + } else { + right_hash = NoHash; + } + } + + void set_left(NodeId idx) { + left_idx = idx; + } + + void set_right(NodeId idx) { + right_idx = idx; + } + + void set_child(uint8_t bit, NodeId idx) { + if (bit == 0) { + left_idx = idx; + } else { + right_idx = idx; + } + } + + private: + Hash32 left_hash; + Hash32 right_hash; + NodeId left_idx; + NodeId right_idx; + ByteArray<42> + padding; // so that branch's size is a multiple of leaf's size + }; + + using HashRef = std::reference_wrapper; + + /** + * A tree leaf + */ + class Leaf { + public: + // values with size <= 32 are not hashed and embedded + using HashOrValue = std::variant; + + enum class Type : unsigned char { + EmbeddedValue = 0b01, + HashedValue = 0b11 + }; + template + struct Tag {}; + + using EmbeddedTag = Tag; + using HashedTag = Tag; + + Leaf(Tag, + const ByteArray<31> &key, + qtils::ByteSpan value) + : type{Type::EmbeddedValue}, + value_size{static_cast(value.size_bytes())}, + key{key}, + value{} { + QTILS_ASSERT_LESS_EQ(value.size_bytes(), 32); + std::copy_n(value.begin(), value.size_bytes(), this->value.begin()); + } + + Leaf(Tag, + const qtils::ByteArray<31> &key, + qtils::FixedByteSpan<32> value) + : type{Type::HashedValue}, value_size{}, key{key} { + std::ranges::copy(value, this->value.begin()); + } + + HashOrValue hash_or_value() const { + switch (type) { + case Type::HashedValue: + return std::ref(value); + case Type::EmbeddedValue: { + return qtils::ByteSpan{value.data(), value_size}; + } + } + std::unreachable(); + } + + const qtils::ByteArray<32> &raw_value() const { + return value; + } + + const qtils::ByteArray<31> &get_key() const { + return key; + } + + private: + Type type : 2; + uint8_t value_size : 6; + + qtils::ByteArray<31> key; + qtils::ByteArray<32> value; + }; + + template + uint8_t get_bit(R &&r, size_t bit_idx) { + if constexpr (std::ranges::sized_range) { + QTILS_ASSERT_GREATER(std::ranges::size(r) * 8, bit_idx); + } + return (r[bit_idx / 8] >> (bit_idx % 8)) & 0x1; + } + + /** + * A tree node is either a leaf or a branch. + */ + struct TreeNode { + public: + TreeNode(const Branch &branch) : node{.branch = branch} {} + TreeNode(const Leaf &leaf) : node{.leaf = leaf} {} + + bool is_branch() const { + // first bit of a node denotes its type + return (reinterpret_cast(&node)[0] & 1) == 0; + } + + bool is_leaf() const { + // first bit of a node denotes its type + return (reinterpret_cast(&node)[0] & 1) == 1; + } +#if defined(__cpp_explicit_this_parameter) \ + && __cpp_explicit_this_parameter >= 202110L + + auto &&as_branch(this auto &self) { + QTILS_ASSERT(self.is_branch()); + return qtils::cxx23::forward_like(self.node.branch); + } + + auto &&as_leaf(this auto &self) { + QTILS_ASSERT(self.is_leaf()); + return qtils::cxx23::forward_like(self.node.leaf); + } +#else + + auto &as_branch() { + QTILS_ASSERT(is_branch()); + return node.branch; + } + + const auto &as_branch() const { + QTILS_ASSERT(is_branch()); + return node.branch; + } + + auto &as_leaf() { + QTILS_ASSERT(is_leaf()); + return node.leaf; + } + + const auto &as_leaf() const { + QTILS_ASSERT(is_leaf()); + return node.leaf; + } + +#endif + private: + // first bit of a node denotes its type + union { + Leaf leaf; + Branch branch; + } node; + }; + + // branches are larger because they store additional information required for + static_assert(sizeof(Branch) == 128); + static_assert(sizeof(Leaf) == 64); + static_assert(sizeof(TreeNode) == 128); + + ByteArray<64> serialize_leaf( + const ByteArray<31> &key, const Leaf::HashOrValue &value); + + ByteArray<64> serialize_branch(const Hash32 &left, const Hash32 &right); + + TreeNode deserialize_node(qtils::ByteSpan bytes); + +} // namespace morum diff --git a/scripts/asn1.py b/scripts/asn1.py index f39a121..2ccb71d 100755 --- a/scripts/asn1.py +++ b/scripts/asn1.py @@ -187,11 +187,11 @@ def asn_sequence_of(t): if type(size) is int: return "qtils::ByteArr<%u>" % size else: - return "::jam::ConfigVec" % c_dash(size) - return "qtils::ByteVec" + return "::morum::ConfigVec" % c_dash(size) + return "qtils::Bytes" if fixed: if isinstance(size, str): - return "::jam::ConfigVec<%s, Config::Field::%s>" % (T, c_dash(size)) + return "::morum::ConfigVec<%s, Config::Field::%s>" % (T, c_dash(size)) return "std::array<%s, %s>" % (T, c_dash(size)) return "std::vector<%s>" % T @@ -499,7 +499,7 @@ def write(self, name: str): def constants(): g = GenConstants( - "jam::test_vectors", + "morum::test_vectors", "jam-types-asn", ) g.write() @@ -507,7 +507,7 @@ def constants(): def types(): g = GenCommonTypes( - "jam::test_vectors", + "morum::test_vectors", "jam-types-asn", ) g.write() @@ -515,7 +515,7 @@ def types(): def history(): g = GenSpecialTypes( - "jam::test_vectors", + "morum::test_vectors", "history", "history/history", "HistoryModule", @@ -525,7 +525,7 @@ def history(): def safrole(): g = GenSpecialTypes( - "jam::test_vectors", + "morum::test_vectors", "safrole", "safrole/safrole", "SafroleModule", @@ -535,7 +535,7 @@ def safrole(): def disputes(): g = GenSpecialTypes( - "jam::test_vectors", + "morum::test_vectors", "disputes", "disputes/disputes", "DisputesModule", @@ -545,7 +545,7 @@ def disputes(): def authorizations(): g = GenSpecialTypes( - "jam::test_vectors", + "morum::test_vectors", "authorizations", "authorizations/authorizations", "AuthorizationsModule", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45ee63f..a27f112 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -26,6 +26,9 @@ add_subdirectory(metrics) # Clocks and time subsystem add_subdirectory(clock) +# Merkle tree DB +add_subdirectory(merkle_tree) + # Subscription Engine subsystem add_subdirectory(se) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index c1b37f0..5f5e4af 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -6,24 +6,23 @@ set(BUILD_VERSION_CPP "${CMAKE_BINARY_DIR}/generated/app/build_version.cpp") set(GET_VERSION_SCRIPT "${CMAKE_SOURCE_DIR}/scripts/get_version.sh") -add_custom_command( - OUTPUT ${BUILD_VERSION_CPP} - COMMAND echo "// Auto-generated file" > ${BUILD_VERSION_CPP} - COMMAND echo "#include " >> ${BUILD_VERSION_CPP} - COMMAND echo "namespace jam {" >> ${BUILD_VERSION_CPP} - COMMAND echo " const std::string &buildVersion() {" >> ${BUILD_VERSION_CPP} - COMMAND printf " static const std::string buildVersion(\"" >> ${BUILD_VERSION_CPP} - COMMAND ${GET_VERSION_SCRIPT} >> ${BUILD_VERSION_CPP} - COMMAND echo "\");" >> ${BUILD_VERSION_CPP} - COMMAND echo " return buildVersion;" >> ${BUILD_VERSION_CPP} - COMMAND echo " }" >> ${BUILD_VERSION_CPP} - COMMAND echo "}" >> ${BUILD_VERSION_CPP} - COMMENT "Generate build_version.cpp" - DEPENDS ${GET_VERSION_SCRIPT} - VERBATIM +execute_process( + COMMAND ${GET_VERSION_SCRIPT} + OUTPUT_VARIABLE VERSION_SCRIPT_OUTPUT +) +file( + WRITE ${BUILD_VERSION_CPP} + "// Auto-generated file\n" + "#include \n" + "namespace morum {\n" + " const std::string &buildVersion() {\n" + " static const std::string buildVersion(\"${VERSION_SCRIPT_OUTPUT}\");\n" + " return buildVersion;\n" + " }\n" + "}" ) add_library(build_version - ${CMAKE_BINARY_DIR}/generated/app/build_version.cpp + ${BUILD_VERSION_CPP} ) add_library(app_configuration SHARED configuration.cpp) diff --git a/src/app/application.hpp b/src/app/application.hpp index 907bf61..1a94fb5 100644 --- a/src/app/application.hpp +++ b/src/app/application.hpp @@ -8,7 +8,7 @@ #include -namespace jam::app { +namespace morum::app { /// @class Application - JAM-application interface class Application : private Singleton { @@ -19,4 +19,4 @@ namespace jam::app { virtual void run() = 0; }; -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/build_version.hpp b/src/app/build_version.hpp index 15908cf..8290d6b 100644 --- a/src/app/build_version.hpp +++ b/src/app/build_version.hpp @@ -8,7 +8,7 @@ #include -namespace jam { +namespace morum { /** * @returns String indicating current build version. Might to contain: tag, * number of commits from tag to fork, commit branch and number of commits @@ -16,4 +16,4 @@ namespace jam { * @note Definition is generating by cmake */ const std::string &buildVersion(); -} // namespace jam +} // namespace morum diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp index 9c9ab1a..6e3294d 100644 --- a/src/app/configuration.cpp +++ b/src/app/configuration.cpp @@ -5,7 +5,7 @@ #include "app/configuration.hpp" -namespace jam::app { +namespace morum::app { Configuration::Configuration() : version_("undefined"), @@ -35,4 +35,4 @@ namespace jam::app { return metrics_; } -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index 9e335bc..2f516e8 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -12,7 +12,7 @@ #include #include -namespace jam::app { +namespace morum::app { class Configuration final : Singleton { public: using Endpoint = boost::asio::ip::tcp::endpoint; @@ -45,4 +45,4 @@ namespace jam::app { MetricsConfig metrics_; }; -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index b2b9ce6..d0a595e 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -20,8 +20,8 @@ using Endpoint = boost::asio::ip::tcp::endpoint; -OUTCOME_CPP_DEFINE_CATEGORY(jam::app, Configurator::Error, e) { - using E = jam::app::Configurator::Error; +OUTCOME_CPP_DEFINE_CATEGORY(morum::app, Configurator::Error, e) { + using E = morum::app::Configurator::Error; switch (e) { case E::CliArgsParseFailed: return "CLI Arguments parse failed"; @@ -69,7 +69,7 @@ namespace { } } // namespace -namespace jam::app { +namespace morum::app { Configurator::Configurator(int argc, const char **argv, const char **env) : argc_(argc), argv_(argv), env_(env) { @@ -140,13 +140,13 @@ namespace jam::app { } if (vm.contains("help")) { - std::cout << "JAM-node version " << buildVersion() << '\n'; + std::cout << "Morum version " << buildVersion() << '\n'; std::cout << cli_options_ << '\n'; return true; } if (vm.contains("version")) { - std::cout << "JAM-node version " << buildVersion() << '\n'; + std::cout << "Morum version " << buildVersion() << '\n'; return true; } @@ -416,8 +416,8 @@ namespace jam::app { } fail = false; - find_argument( - cli_values_map_, "prometheus_port", [&](const uint16_t &value) { + find_argument( + cli_values_map_, "prometheus-port", [&](const uint32_t &value) { if (value > 0 and value <= 65535) { config_->metrics_.endpoint = {config_->metrics_.endpoint.address(), static_cast(value)}; @@ -444,4 +444,4 @@ namespace jam::app { return outcome::success(); } -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/configurator.hpp b/src/app/configurator.hpp index e403bb5..7cac3a7 100644 --- a/src/app/configurator.hpp +++ b/src/app/configurator.hpp @@ -18,11 +18,11 @@ namespace soralog { class Logger; } // namespace soralog -namespace jam::app { +namespace morum::app { class Configuration; -} // namespace jam::app +} // namespace morum::app -namespace jam::app { +namespace morum::app { class Configurator final { public: @@ -74,6 +74,6 @@ namespace jam::app { boost::program_options::variables_map cli_values_map_; }; -} // namespace jam::app +} // namespace morum::app -OUTCOME_HPP_DECLARE_ERROR(jam::app, Configurator::Error); +OUTCOME_HPP_DECLARE_ERROR(morum::app, Configurator::Error); diff --git a/src/app/impl/application_impl.cpp b/src/app/impl/application_impl.cpp index d17649d..1891a4f 100644 --- a/src/app/impl/application_impl.cpp +++ b/src/app/impl/application_impl.cpp @@ -18,7 +18,7 @@ #include "metrics/metrics.hpp" #include "se/impl/subscription_manager.hpp" -namespace jam::app { +namespace morum::app { SeHolder::SeHolder(SePtr se) : se_(std::move(se)) {} @@ -43,7 +43,7 @@ namespace jam::app { metrics_registry_(metrics::createRegistry()) { // Metric for exposing name and version of node metrics::GaugeHelper( - "jam_build_info", + "morum_build_info", "A metric with a constant '1' value labeled by name, version", std::map{ {"name", app_config_->nodeName()}, @@ -65,7 +65,7 @@ namespace jam::app { state_manager_->atShutdown([this] { watchdog_->stop(); }); // Metric storing start time - metrics::GaugeHelper("jam_process_start_time_seconds", + metrics::GaugeHelper("morum_process_start_time_seconds", "UNIX timestamp of the moment the process started") ->set(system_clock_->nowSec()); @@ -76,4 +76,4 @@ namespace jam::app { watchdog_thread.join(); } -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/impl/application_impl.hpp b/src/app/impl/application_impl.hpp index 73dcf5e..458305d 100644 --- a/src/app/impl/application_impl.hpp +++ b/src/app/impl/application_impl.hpp @@ -15,34 +15,34 @@ #include "app/application.hpp" #include "se/subscription_fwd.hpp" -namespace jam { +namespace morum { class Watchdog; -} // namespace jam +} // namespace morum -namespace jam::app { +namespace morum::app { class Configuration; class StateManager; -} // namespace jam::app +} // namespace morum::app -namespace jam::clock { +namespace morum::clock { class SystemClock; -} // namespace jam::clock +} // namespace morum::clock namespace soralog { class Logger; } // namespace soralog -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log -namespace jam::metrics { +namespace morum::metrics { class Registry; class Gauge; class Exposer; -} // namespace jam::metrics +} // namespace morum::metrics -namespace jam::app { +namespace morum::app { /** * @brief RAII holder for subscription engine management @@ -99,4 +99,4 @@ namespace jam::app { std::unique_ptr metrics_registry_; }; -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/impl/state_manager_impl.cpp b/src/app/impl/state_manager_impl.cpp index 2d3b4b9..22a4cae 100644 --- a/src/app/impl/state_manager_impl.cpp +++ b/src/app/impl/state_manager_impl.cpp @@ -11,7 +11,7 @@ #include "log/logger.hpp" -namespace jam::app { +namespace morum::app { std::weak_ptr StateManagerImpl::wp_to_myself; std::atomic_bool StateManagerImpl::shutting_down_signals_enabled{false}; @@ -281,4 +281,4 @@ namespace jam::app { state_.store(State::ShuttingDown); cv_.notify_one(); } -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/impl/state_manager_impl.hpp b/src/app/impl/state_manager_impl.hpp index 9f39388..967af78 100644 --- a/src/app/impl/state_manager_impl.hpp +++ b/src/app/impl/state_manager_impl.hpp @@ -19,11 +19,11 @@ namespace soralog { class Logger; } // namespace soralog -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log -namespace jam::app { +namespace morum::app { class StateManagerImpl final : Singleton, @@ -82,4 +82,4 @@ namespace jam::app { std::queue shutdown_; }; -} // namespace jam::app +} // namespace morum::app diff --git a/src/app/impl/watchdog.hpp b/src/app/impl/watchdog.hpp index b1f6e07..edfb338 100644 --- a/src/app/impl/watchdog.hpp +++ b/src/app/impl/watchdog.hpp @@ -27,9 +27,9 @@ namespace soralog { class Logger; } // namespace soralog -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log #ifdef __APPLE__ @@ -64,7 +64,7 @@ inline uint64_t getPlatformThreadId() { #endif -namespace jam { +namespace morum { constexpr auto kWatchdogDefaultTimeout = std::chrono::minutes{15}; @@ -183,4 +183,4 @@ namespace jam { std::unordered_map threads_; std::atomic_bool stopped_ = false; }; -} // namespace jam +} // namespace morum diff --git a/src/app/state_manager.hpp b/src/app/state_manager.hpp index cdfe277..8481d30 100644 --- a/src/app/state_manager.hpp +++ b/src/app/state_manager.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::app { +namespace morum::app { // Concepts that check if an object has a method that is called by app state // manager. Deliberately avoid checking that the method returns bool, @@ -127,4 +127,4 @@ namespace jam::app { explicit AppStateException(std::string message) : std::runtime_error("Wrong workflow at " + std::move(message)) {} }; -} // namespace jam::app +} // namespace morum::app diff --git a/src/clock/clock.hpp b/src/clock/clock.hpp index 9ddd3ce..d4493e3 100644 --- a/src/clock/clock.hpp +++ b/src/clock/clock.hpp @@ -8,7 +8,7 @@ #include -namespace jam::clock { +namespace morum::clock { /** * An interface for a clock @@ -63,4 +63,4 @@ namespace jam::clock { */ class SteadyClock : public virtual Clock {}; -} // namespace jam::clock +} // namespace morum::clock diff --git a/src/clock/impl/clock_impl.cpp b/src/clock/impl/clock_impl.cpp index 57a97e4..7205829 100644 --- a/src/clock/impl/clock_impl.cpp +++ b/src/clock/impl/clock_impl.cpp @@ -6,7 +6,7 @@ #include "clock/impl/clock_impl.hpp" -namespace jam::clock { +namespace morum::clock { template typename Clock::TimePoint ClockImpl::now() const { @@ -30,4 +30,4 @@ namespace jam::clock { template class ClockImpl; template class ClockImpl; -} // namespace jam::clock +} // namespace morum::clock diff --git a/src/clock/impl/clock_impl.hpp b/src/clock/impl/clock_impl.hpp index 904ea65..d24e9fd 100644 --- a/src/clock/impl/clock_impl.hpp +++ b/src/clock/impl/clock_impl.hpp @@ -8,7 +8,7 @@ #include "clock/clock.hpp" -namespace jam::clock { +namespace morum::clock { template class ClockImpl : virtual public Clock { @@ -23,4 +23,4 @@ namespace jam::clock { class SteadyClockImpl : public SteadyClock, public ClockImpl {}; -} // namespace jam::clock +} // namespace morum::clock diff --git a/src/crypto/bandersnatch.hpp b/src/crypto/bandersnatch.hpp index bf80220..f5e3916 100644 --- a/src/crypto/bandersnatch.hpp +++ b/src/crypto/bandersnatch.hpp @@ -12,7 +12,7 @@ #include #include -namespace jam::crypto::bandersnatch { +namespace morum::crypto::bandersnatch { using Output = qtils::ByteArr; using Public = qtils::ByteArr; using RingCommitment = qtils::ByteArr; @@ -91,4 +91,4 @@ namespace jam::crypto::bandersnatch { RingVerifier Ring::verifier(const RingCommitment &commitment) const { return RingVerifier{*this, commitment}; } -} // namespace jam::bandersnatch +} // namespace morum::bandersnatch diff --git a/src/crypto/blake.hpp b/src/crypto/blake.hpp index 1d9f285..8cfb9e5 100644 --- a/src/crypto/blake.hpp +++ b/src/crypto/blake.hpp @@ -10,7 +10,7 @@ #include #include -namespace jam::crypto { +namespace morum::crypto { struct Blake { using Hash = qtils::ByteArr<32>; blake2b_state state; @@ -31,4 +31,4 @@ namespace jam::crypto { return Blake{}.update(input).hash(); } }; -} // namespace jam +} // namespace morum diff --git a/src/crypto/ed25519.hpp b/src/crypto/ed25519.hpp index d765335..3b24e0c 100644 --- a/src/crypto/ed25519.hpp +++ b/src/crypto/ed25519.hpp @@ -7,7 +7,7 @@ #include #include -namespace jam::crypto::ed25519 { +namespace morum::crypto::ed25519 { using Secret = qtils::ByteArr; using Public = qtils::ByteArr; using KeyPair = qtils::ByteArr; @@ -34,4 +34,4 @@ namespace jam::crypto::ed25519 { message.size_bytes()); return res == ED25519_RESULT_OK; } -} // namespace jam::ed25519 +} // namespace morum::ed25519 diff --git a/src/crypto/keccak.hpp b/src/crypto/keccak.hpp index 5cf086a..1d47334 100644 --- a/src/crypto/keccak.hpp +++ b/src/crypto/keccak.hpp @@ -10,7 +10,7 @@ * Keccak hash */ -namespace jam::crypto { +namespace morum::crypto { struct Keccak { using Hash32 = qtils::ByteArr<32>; uint64_t state[5][5] = {}; @@ -110,4 +110,4 @@ namespace jam::crypto { return Keccak{}.update(input).hash(); } }; -} // namespace jam +} // namespace morum diff --git a/src/executable/jam_node.cpp b/src/executable/jam_node.cpp index 60023ee..bfe0532 100644 --- a/src/executable/jam_node.cpp +++ b/src/executable/jam_node.cpp @@ -31,22 +31,22 @@ namespace { "Run with `--help' argument to print usage\n"; } - using jam::app::Application; - using jam::app::Configuration; - using jam::injector::NodeInjector; - using jam::log::LoggingSystem; + using morum::app::Application; + using morum::app::Configuration; + using morum::injector::NodeInjector; + using morum::log::LoggingSystem; int run_node(std::shared_ptr logsys, std::shared_ptr appcfg) { auto injector = std::make_unique(logsys, appcfg); // Load modules - std::deque> loaders; + std::deque> loaders; { auto logger = logsys->getLogger("Modules", "jam"); const std::string path(appcfg->modulesDir()); - jam::modules::ModuleLoader module_loader(path); + morum::modules::ModuleLoader module_loader(path); auto modules_res = module_loader.get_modules(); if (modules_res.has_error()) { SL_CRITICAL(logger, "Failed to load modules from path: {}", path); @@ -98,7 +98,7 @@ namespace { } // namespace int main(int argc, const char **argv, const char **env) { - soralog::util::setThreadName("jam-node"); + soralog::util::setThreadName("morum-node"); qtils::FinalAction flush_std_streams_at_exit([] { std::cout.flush(); @@ -118,7 +118,7 @@ int main(int argc, const char **argv, const char **env) { } auto app_configurator = - std::make_unique(argc, argv, env); + std::make_unique(argc, argv, env); // Parse CLI args for help, version and config if (auto res = app_configurator->step1(); res.has_value()) { @@ -152,7 +152,7 @@ int main(int argc, const char **argv, const char **env) { return EXIT_FAILURE; } - std::make_shared(std::move(logging_system)); + std::make_shared(std::move(logging_system)); }); // Parse remaining args @@ -166,7 +166,7 @@ int main(int argc, const char **argv, const char **env) { // Setup config auto configuration = ({ - auto logger = logging_system->getLogger("Configurator", "jam"); + auto logger = logging_system->getLogger("Configurator", "morum"); auto config_res = app_configurator->calculateConfig(logger); if (config_res.has_error()) { @@ -205,7 +205,7 @@ int main(int argc, const char **argv, const char **env) { } } - auto logger = logging_system->getLogger("Main", jam::log::defaultGroupName); + auto logger = logging_system->getLogger("Main", morum::log::defaultGroupName); SL_INFO(logger, "All components are stopped"); logger->flush(); diff --git a/src/injector/bind_by_lambda.hpp b/src/injector/bind_by_lambda.hpp index 2ffde14..7839a9b 100644 --- a/src/injector/bind_by_lambda.hpp +++ b/src/injector/bind_by_lambda.hpp @@ -8,7 +8,7 @@ #include -namespace jam::injector { +namespace morum::injector { /** * Creates single instance per injector. * @@ -66,4 +66,4 @@ namespace jam::injector { auto bind_by_lambda(const F &f) { return boost::di::core::dependency{f}; } -} // namespace jam::injector +} // namespace morum::injector diff --git a/src/injector/dont_inject.hpp b/src/injector/dont_inject.hpp index b5bdaf9..6531935 100644 --- a/src/injector/dont_inject.hpp +++ b/src/injector/dont_inject.hpp @@ -9,10 +9,10 @@ // Prevent implicit injection. // Will generate "unresolved symbol" error during linking. // Not using `= delete` as it would break injector SFINAE. -#define DONT_INJECT(T) explicit T(::jam::injector::DontInjectHelper, ...); +#define DONT_INJECT(T) explicit T(::morum::injector::DontInjectHelper, ...); -namespace jam::injector { +namespace morum::injector { struct DontInjectHelper { explicit DontInjectHelper() = default; }; -} // namespace jam::injector +} // namespace morum::injector diff --git a/src/injector/lazy.hpp b/src/injector/lazy.hpp index f0391b0..ec003dc 100644 --- a/src/injector/lazy.hpp +++ b/src/injector/lazy.hpp @@ -8,7 +8,7 @@ #include -namespace jam { +namespace morum { template using Lazy = boost::di::extension::lazy; @@ -25,4 +25,4 @@ namespace jam { template using LazyUPtr = boost::di::extension::lazy>; -} // namespace jam +} // namespace morum diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 213b247..bfae5d3 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -37,7 +37,7 @@ namespace { namespace di = boost::di; namespace fs = std::filesystem; - using namespace jam; // NOLINT + using namespace morum; // NOLINT template auto useConfig(C c) { @@ -89,7 +89,7 @@ namespace { } } // namespace -namespace jam::injector { +namespace morum::injector { class NodeInjectorImpl { public: using Injector = @@ -149,4 +149,4 @@ namespace jam::injector { } return std::unique_ptr(loader.release()); } -} // namespace jam::injector +} // namespace morum::injector diff --git a/src/injector/node_injector.hpp b/src/injector/node_injector.hpp index 7fd7ed2..1b6042e 100644 --- a/src/injector/node_injector.hpp +++ b/src/injector/node_injector.hpp @@ -10,24 +10,24 @@ #include "se/subscription.hpp" -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log -namespace jam::app { +namespace morum::app { class Configuration; class Application; -} // namespace jam::app +} // namespace morum::app -namespace jam::loaders { +namespace morum::loaders { class Loader; -} // namespace jam::loaders +} // namespace morum::loaders -namespace jam::modules { +namespace morum::modules { class Module; -} // namespace jam::modules +} // namespace morum::modules -namespace jam::injector { +namespace morum::injector { /** * Dependency injector for a universal node. Provides all major components @@ -46,4 +46,4 @@ namespace jam::injector { std::shared_ptr pimpl_; }; -} // namespace jam::injector +} // namespace morum::injector diff --git a/src/log/logger.cpp b/src/log/logger.cpp index 2bb1540..5916b1b 100644 --- a/src/log/logger.cpp +++ b/src/log/logger.cpp @@ -13,8 +13,8 @@ #include "log/logger.hpp" -OUTCOME_CPP_DEFINE_CATEGORY(jam::log, Error, e) { - using E = jam::log::Error; +OUTCOME_CPP_DEFINE_CATEGORY(morum::log, Error, e) { + using E = morum::log::Error; switch (e) { case E::WRONG_LEVEL: return "Unknown level"; @@ -26,7 +26,7 @@ OUTCOME_CPP_DEFINE_CATEGORY(jam::log, Error, e) { return "Unknown log::Error"; } -namespace jam::log { +namespace morum::log { outcome::result str2lvl(std::string_view str) { if (str == "trace") { @@ -68,7 +68,7 @@ namespace jam::log { for (auto &chunk : cfg) { if (auto res = str2lvl(chunk); res.has_value()) { auto level = res.value(); - logging_system_->setLevelOfGroup(jam::log::defaultGroupName, level); + logging_system_->setLevelOfGroup(morum::log::defaultGroupName, level); continue; } @@ -102,4 +102,4 @@ namespace jam::log { } } -} // namespace jam::log +} // namespace morum::log diff --git a/src/log/logger.hpp b/src/log/logger.hpp index ec61827..cb349b4 100644 --- a/src/log/logger.hpp +++ b/src/log/logger.hpp @@ -20,7 +20,7 @@ #include "injector/dont_inject.hpp" #include "utils/ctor_limiters.hpp" -namespace jam::log { +namespace morum::log { using soralog::Level; using Logger = qtils::SharedRef; @@ -31,7 +31,7 @@ namespace jam::log { void setLoggingSystem(std::weak_ptr logging_system); - inline static std::string defaultGroupName{"jam"}; + inline static std::string defaultGroupName{"morum"}; class LoggingSystem : public Singleton { public: @@ -84,6 +84,6 @@ namespace jam::log { std::shared_ptr logging_system_; }; -} // namespace jam::log +} // namespace morum::log -OUTCOME_HPP_DECLARE_ERROR(jam::log, Error); +OUTCOME_HPP_DECLARE_ERROR(morum::log, Error); diff --git a/src/merkle_tree/CMakeLists.txt b/src/merkle_tree/CMakeLists.txt new file mode 100644 index 0000000..efae9e9 --- /dev/null +++ b/src/merkle_tree/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library(merkle_tree merkle_tree.cpp db.cpp archive_backend.cpp nomt_backend.cpp) +target_include_directories(merkle_tree + PUBLIC + $ +) +target_link_libraries(merkle_tree PUBLIC blake2b) +add_library(morum::merkle_tree ALIAS merkle_tree) +if (MORUM_TRACE) + add_compile_definitions(merkle_tree PUBLIC MORUM_ENABLE_TRACE) +endif() +if (QTILS_ASSERT) + add_compile_definitions(merkle_tree PUBLIC QTILS_ENABLE_ASSERT) +endif() +target_link_libraries(merkle_tree + PUBLIC qtils::qtils + PRIVATE RocksDB::rocksdb +) diff --git a/src/merkle_tree/archive_backend.cpp b/src/merkle_tree/archive_backend.cpp new file mode 100644 index 0000000..be43748 --- /dev/null +++ b/src/merkle_tree/archive_backend.cpp @@ -0,0 +1,92 @@ +#include + +#include + +namespace morum { + + std::expected, StorageError> ArchiveNodeLoader::load( + qtils::BitSpan<>, const Hash32 &hash) const { + Hash32 hash_copy = hash; + hash_copy[0] &= 0xFE; + ByteArray + node_data; // in-memory branches contain non-serializable metadata, + // so Leaf's size is used + QTILS_UNWRAP(auto res, node_storage->read_to(hash_copy, node_data)); + if (!res) { + return std::nullopt; + } + QTILS_ASSERT_EQ(*res, sizeof(Leaf)); + return deserialize_node(node_data); + } + + ArchiveTrieDb::ArchiveTrieDb( + std::shared_ptr> storage) + : storage_{storage}, + node_storage_{storage_->get_column_family(ColumnFamilyId::TREE_NODE)}, + value_storage_{storage_->get_column_family(ColumnFamilyId::TREE_VALUE)} { + QTILS_ASSERT(storage_ != nullptr); + QTILS_ASSERT(node_storage_ != nullptr); + QTILS_ASSERT(value_storage_ != nullptr); + } + + std::expected>, StorageError> + ArchiveTrieDb::load_tree(const Hash32 &root_hash) const { + auto root_bytes_res = node_storage_->read(root_hash); + QTILS_ASSERT_HAS_VALUE(root_bytes_res); + QTILS_ASSERT(root_bytes_res->has_value()); + auto root_bytes = **root_bytes_res; + auto root = morum::deserialize_node(root_bytes); + return std::make_unique(root, + std::make_unique(), + std::make_shared(node_storage_)); + } + + std::unique_ptr ArchiveTrieDb::empty_tree() const { + return std::make_unique( + std::make_unique(), + std::make_shared()); + } + + std::expected ArchiveTrieDb::get_root_and_store( + const MerkleTree &tree) { + auto batch = storage_->start_batch(); + + auto hash = tree.calculate_hash([&](const morum::TreeNode &n, + qtils::ByteSpan serialized, + qtils::ByteSpan hash, + qtils::BitSpan<>) { + morum::Hash32 hash_copy; + std::ranges::copy(hash, hash_copy.begin()); + hash_copy[0] &= 0xFE; + [[maybe_unused]] auto res = + batch->write(ColumnFamilyId::TREE_NODE, hash_copy, serialized); + QTILS_ASSERT(res); + if (n.is_leaf()) { + // cached_nodes_.emplace(hash_copy, n); + } else { + auto &branch = n.as_branch(); + // original node may contain child node ids which are not persistent + morum::Branch b{branch.get_left_hash(), branch.get_right_hash()}; + } + if (n.is_leaf()) { + auto h_or_v = n.as_leaf().hash_or_value(); + if (auto *hash = std::get_if(&h_or_v); hash) { + if (auto value_opt = tree.get_cached_value(hash->get()); + value_opt) { + batch + ->write(ColumnFamilyId::TREE_VALUE, + hash->get(), + value_opt.value()) + .value(); + } + } + } + }); + hash[0] &= 0xFE; + + [[maybe_unused]] auto res = storage_->write_batch(std::move(batch)); + QTILS_ASSERT_HAS_VALUE(res); + + return hash; + } +} // namespace morum \ No newline at end of file diff --git a/src/merkle_tree/db.cpp b/src/merkle_tree/db.cpp new file mode 100644 index 0000000..3827771 --- /dev/null +++ b/src/merkle_tree/db.cpp @@ -0,0 +1,244 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace morum { + + template + std::expected wrap_status(T &&t, + const rocksdb::Status &status) { + if (!status.ok()) { + return std::unexpected(StorageError{status.ToString()}); + } + return t; + } + + std::expected wrap_status(const rocksdb::Status &status) { + if (!status.ok()) { + return std::unexpected(StorageError{status.ToString()}); + } + return {}; + } + + std::string_view to_string_nocheck(ColumnFamilyId family) { + switch (family) { + case ColumnFamilyId::DEFAULT: + return rocksdb::kDefaultColumnFamilyName; + case ColumnFamilyId::TREE_NODE: + return "tree_node"; + case ColumnFamilyId::TREE_VALUE: + return "tree_value"; + case ColumnFamilyId::FLAT_KV: + return "flat_kv"; + case ColumnFamilyId::TREE_PAGE: + return "tree_page"; + } + std::unreachable(); + } + + std::optional to_string(ColumnFamilyId family) { + switch (family) { + case ColumnFamilyId::DEFAULT: + return rocksdb::kDefaultColumnFamilyName; + case ColumnFamilyId::TREE_NODE: + return "tree_node"; + case ColumnFamilyId::TREE_VALUE: + return "tree_value"; + case ColumnFamilyId::FLAT_KV: + return "flat_kv"; + case ColumnFamilyId::TREE_PAGE: + return "tree_page"; + } + return std::nullopt; + } + + std::expected, StorageError> open_db( + const std::filesystem::path &path) { + rocksdb::DB *db{}; + rocksdb::Options options; + options.create_if_missing = true; + options.create_missing_column_families = true; + // options.statistics = rocksdb::CreateDBStatistics(); + // options.stats_dump_period_sec = 1; + // options.persist_stats_to_disk = true; + + std::vector handles; + std::vector desc; + [[maybe_unused]] int family_idx = 0; + for (auto family : column_families()) { + // proper order is important because ColumnFamilyId serves as an index in + // a vector of column family handles + QTILS_ASSERT_EQ(family_idx++, static_cast(family)); + desc.emplace_back(std::string{to_string_nocheck(family)}, + rocksdb::ColumnFamilyOptions{}); + } + + QTILS_UNWRAP_void(wrap_status( + rocksdb::DB::Open(options, path.c_str(), desc, &handles, &db))); + + family_idx = 0; + for ([[maybe_unused]] auto &handle : handles) { + [[maybe_unused]] auto name = + to_string(static_cast(family_idx++)); + QTILS_ASSERT(name.has_value()); + QTILS_ASSERT_EQ(handle->GetName(), *name); + } + return std::unique_ptr{new RocksDb{db, std::move(handles)}}; + } + + class RocksDbBatch final : public RocksDb::Batch, + public RocksDbColumnFamily::Batch { + public: + RocksDbBatch(std::shared_ptr db, + rocksdb::ColumnFamilyHandle *default_cf) + : db{db}, default_cf{default_cf} { + QTILS_ASSERT(db != nullptr); + QTILS_ASSERT(default_cf != nullptr); + } + + virtual ~RocksDbBatch() = default; + + std::expected write(qtils::ByteSpan key, + qtils::ByteSpan value) override { + return wrap_status( + batch.Put(default_cf, qtils::byte2str(key), qtils::byte2str(value))); + } + + std::expected remove(qtils::ByteSpan key) override { + return wrap_status(batch.Delete(default_cf, qtils::byte2str(key))); + } + + std::expected write(ColumnFamilyId cf, + qtils::ByteSpan key, + qtils::ByteSpan value) override { + return wrap_status(batch.Put(db->handles[static_cast(cf)], + qtils::byte2str(key), + qtils::byte2str(value))); + } + + std::expected remove(ColumnFamilyId cf, + qtils::ByteSpan key) override { + return wrap_status(batch.Delete(db->handles[static_cast(cf)], + qtils::byte2str(key))); + } + + std::shared_ptr db; + rocksdb::ColumnFamilyHandle *default_cf; + rocksdb::WriteBatch batch; + }; + + RocksDb::RocksDb(rocksdb::DB *db, + std::vector &&handles) + : db{db}, handles{std::move(handles)} { + QTILS_ASSERT(db != nullptr); + } + + RocksDb::~RocksDb() { + for (auto &handle : handles) { + auto status = db->DestroyColumnFamilyHandle(handle); + QTILS_ASSERT(status.ok()); + } + auto status = db->Close(); + QTILS_ASSERT(status.ok()); + delete db; + } + + std::shared_ptr RocksDb::get_column_family( + ColumnFamilyId family) { + return std::make_shared(shared_from_this(), family); + } + + std::unique_ptr RocksDb::start_batch() { + return std::make_unique( + shared_from_this(), + handles.at(std::to_underlying(ColumnFamilyId::DEFAULT))); + } + + std::expected RocksDb::write_batch( + std::unique_ptr batch) { + auto rocks_batch = dynamic_cast(batch.get()); + QTILS_ASSERT(rocks_batch != nullptr); + + return wrap_status(db->Write(rocksdb::WriteOptions{}, &rocks_batch->batch)); + } + + RocksDbColumnFamily::RocksDbColumnFamily(std::shared_ptr db, + ColumnFamilyId family) + : db{db}, handle{db->handles.at(std::to_underlying(family))} {} + + std::expected RocksDbColumnFamily::write( + qtils::ByteSpan key, qtils::ByteSpan value) { + rocksdb::WriteBatch updates; + QTILS_UNWRAP_void(wrap_status(updates.Put(handle, + rocksdb::Slice{qtils::byte2str(key)}, + rocksdb::Slice{qtils::byte2str(value)}))); + QTILS_UNWRAP_void( + wrap_status(db->db->Write(rocksdb::WriteOptions{}, &updates))); + return {}; + } + + std::expected, StorageError> + RocksDbColumnFamily::read(qtils::ByteSpan key) const { + std::string value; + auto status = db->db->Get(rocksdb::ReadOptions{}, + handle, + rocksdb::Slice{qtils::byte2str(key)}, + &value); + if (status.IsNotFound()) { + return std::nullopt; + } + QTILS_UNWRAP_void(wrap_status(status)); + return ByteVector{value.begin(), value.end()}; + } + + std::expected, StorageError> + RocksDbColumnFamily::read_to(qtils::ByteSpan key, + qtils::ByteSpanMut value) const { + std::string res; + auto status = db->db->Get(rocksdb::ReadOptions{}, + handle, + rocksdb::Slice{qtils::byte2str(key)}, + &res); + if (status.IsNotFound()) { + return std::nullopt; + } + QTILS_UNWRAP_void(wrap_status(status)); + std::copy_n(res.begin(), std::min(res.size(), value.size()), value.begin()); + return res.size(); + } + + std::expected RocksDbColumnFamily::remove( + qtils::ByteSpan key) const { + QTILS_UNWRAP_void(wrap_status( + db->db->Delete(rocksdb::WriteOptions{}, handle, qtils::byte2str(key)))); + return {}; + } + + std::unique_ptr + RocksDbColumnFamily::start_batch() { + return std::make_unique(db, handle); + } + + std::expected RocksDbColumnFamily::write_batch( + std::unique_ptr batch) { + auto rocks_batch = dynamic_cast(batch.get()); + QTILS_ASSERT(rocks_batch != nullptr); + + return wrap_status( + db->db->Write(rocksdb::WriteOptions{}, &rocks_batch->batch)); + } + +} // namespace morum diff --git a/src/merkle_tree/merkle_tree.cpp b/src/merkle_tree/merkle_tree.cpp new file mode 100644 index 0000000..e804650 --- /dev/null +++ b/src/merkle_tree/merkle_tree.cpp @@ -0,0 +1,384 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace morum { + + template + constexpr T div_round_up(T a, T b) { + return (a + b - 1) / b; + } + + Hash32 blake2b_256(qtils::ByteSpan bytes) { + Hash32 res; + blake2(res.data(), res.size(), bytes.data(), bytes.size(), nullptr, 0); + return res; + } + + qtils::ByteArray<64> serialize_leaf( + const qtils::ByteArray<31> &key, const Hash32 &value_hash) { + qtils::ByteArray<64> bytes{}; + bytes[0] = 0x3; + std::copy_n(key.begin(), 31, bytes.begin() + 1); + std::copy_n(value_hash.begin(), value_hash.size(), bytes.begin() + 32); + return bytes; + } + + qtils::ByteArray<64> serialize_leaf( + const qtils::ByteArray<31> &key, qtils::ByteSpan value) { + qtils::ByteArray<64> bytes{}; + Hash32 value_hash; + if (value.size() <= 32) { + bytes[0] = 0x01 | (value.size() << 2); + std::copy_n(value.begin(), value.size(), value_hash.begin()); + std::fill_n(value_hash.begin() + value.size(), 32 - value.size(), 0); + } else { + bytes[0] = 0x3; + value_hash = blake2b_256(value); + } + std::copy_n(key.begin(), 31, bytes.begin() + 1); + std::copy_n(value_hash.begin(), value_hash.size(), bytes.begin() + 32); + return bytes; + } + + qtils::ByteArray<64> serialize_leaf( + const qtils::ByteArray<31> &key, const Leaf::HashOrValue &value) { + if (std::holds_alternative(value)) { + return serialize_leaf(key, std::get(value).get()); + } + return serialize_leaf(key, std::get(value)); + } + + qtils::ByteArray<64> serialize_branch( + const Hash32 &left, const Hash32 &right) { + qtils::ByteArray<64> bytes{}; + bytes[0] = 0xFE & left[0]; + std::copy_n(left.begin() + 1, 31, bytes.begin() + 1); + std::copy_n(right.begin(), 32, bytes.begin() + 32); + return bytes; + } + + TreeNode deserialize_node(qtils::ByteSpan bytes) { + qtils::BitSpan<> bits{bytes}; + if (bits[0] == 0) { + Branch branch; + if (!std::ranges::equal(bytes.subspan(0, 32), ZeroHash32)) { + branch.set_left(bytes.subspan<0, 32>()); + } + if (!std::ranges::equal(bytes.subspan(32, 32), ZeroHash32)) { + branch.set_right(bytes.subspan<32, 32>()); + } + return branch; + } + qtils::ByteArray<31> key; + std::ranges::copy(bytes.subspan(1, 31), key.begin()); + if (bits[1] == 0) { + size_t value_size = bytes[0] >> 3; + return Leaf{Leaf::EmbeddedTag{}, key, bytes.subspan(32, value_size)}; + } + return Leaf{Leaf::HashedTag{}, key, bytes.subspan<32, 32>()}; + } + + qtils::OptionalRef MerkleTree::get_root() { + if (nodes_->empty()) { + return std::nullopt; + } + return nodes_->get(0); + } + + qtils::OptionalRef MerkleTree::get_root() const { + if (nodes_->empty()) { + return std::nullopt; + } + return nodes_->get(0); + } + + std::expected MerkleTree::set( + const Hash32 &key, ByteVector &&value) { + MORUM_TRACE("Set {}", BitSpan<>{key}); + if (empty()) { + MORUM_TRACE("New root"); + [[maybe_unused]] auto id = create_leaf_node(key, std::move(value)); + QTILS_ASSERT(id == 0); + return {}; + } + QTILS_UNWRAP(const auto &parent_opt, find_parent_branch(key)); + if (!parent_opt) { + // parent must be a leaf root + QTILS_ASSERT(get_root()->is_leaf()); + MORUM_TRACE("New root branch"); + + size_t new_leaf = create_leaf_node(key, std::move(value)); + replace_leaf_with_branch(0, 0, new_leaf); + + return {}; + } + auto &[parent_idx, path] = *parent_opt; + MORUM_TRACE("Parent path {}", path); + + qtils::BitSpan<> path_left{key, path.end_bit, key.size() * 8}; + auto new_leaf_idx = create_leaf_node(key, std::move(value)); + QTILS_UNWRAP(auto old_leaf_opt, + get_child_idx(nodes_->get(parent_idx)->as_branch(), + path[path.end_bit - 1], + path)); + if (old_leaf_opt) { + MORUM_TRACE("Replace leaf at bit {}, idx {} with idx {}", + path[path.end_bit - 1], + *old_leaf_opt, + new_leaf_idx); + replace_leaf_with_branch( + path_left.start_bit /*+ 1*/, *old_leaf_opt, new_leaf_idx); + } else { + MORUM_TRACE( + "New leaf at bit {}, idx {}", path[path.end_bit - 1], new_leaf_idx); + nodes_->get(parent_idx) + ->as_branch() + .set_child(path[path.end_bit - 1], new_leaf_idx); + } + + NodeId current_id = 0; + TreeNode *current = &*nodes_->get(current_id); + size_t i = 0; + while (current->is_branch()) { + auto bit = get_bit(key, i); + current->as_branch().reset_child_hash(bit); + ++i; + auto id = current->as_branch().get_child_idx(bit).value(); + current_id = id; + current = &nodes_->get(id).value(); + } + return {}; + } + + std::expected, StorageError> MerkleTree::get( + const Hash32 &key) const { + QTILS_UNWRAP(const auto &leaf_id_opt, find_leaf(key)); + if (!leaf_id_opt) { + return std::nullopt; + } + auto leaf_id = *leaf_id_opt; + auto &leaf = nodes_->get(leaf_id)->as_leaf(); + QTILS_UNWRAP(const auto &value, get_value(leaf.hash_or_value())); + + return value; + } + + std::expected MerkleTree::exists( + const Hash32 &key) const { + QTILS_UNWRAP(const auto &leaf_opt, find_leaf(key)); + return leaf_opt.has_value(); + } + + std::expected, StorageError> MerkleTree::get_child_idx( + Branch &branch, int8_t bit, qtils::BitSpan<> path) const { + QTILS_ASSERT(bit == 0 || bit == 1); + if (auto idx = branch.get_child_idx(bit); idx.has_value()) { + return *idx; + } + if (auto hash = branch.get_child_hash(bit); hash.has_value()) { + QTILS_UNWRAP(auto node_opt, loader_->load(path, *hash)); + if (!node_opt) { + return std::unexpected(StorageError{fmt::format( + "Node hash {} path {} not found", qtils::Hex{*hash}, path)}); + } + auto node_id = nodes_->store(*node_opt); + branch.set_child(bit, node_id); + return node_id; + } + + return std::nullopt; + } + + std::expected MerkleTree::get_value( + Leaf::HashOrValue hash_or_value) const { + if (auto *embedded_val = std::get_if(&hash_or_value); + embedded_val) { + return qtils::ByteSpan{embedded_val->data(), embedded_val->size()}; + } + QTILS_ASSERT(std::holds_alternative(hash_or_value)); + auto &hash = std::get(hash_or_value).get(); + if (auto it = value_cache_.find(hash); it != value_cache_.end()) { + return it->second; + } + QTILS_UNWRAP(const auto & value_opt, value_storage_->read(hash)); + if (!value_opt) { + return std::unexpected(StorageError{"Value missing"}); + } + auto [res, success] = value_cache_.emplace(hash, std::move(*value_opt)); + QTILS_ASSERT(success); + return res->second; + } + + std::expected, StorageError> + MerkleTree::find_parent_branch(const Hash32 &key) { + qtils::BitSpan<> path{key, 0, 0}; + if (empty()) { + return std::nullopt; + } + // SAFETY: the check that tree is not empty is above + auto &root = *get_root(); + if (root.is_leaf()) { + return std::nullopt; + } + TreeNode *current = &root; + size_t current_idx{}; + while (current->is_branch()) { + auto bit = get_bit(key, path.size_bits()); + path.end_bit++; + QTILS_UNWRAP(const auto & child_opt, + get_child_idx(const_cast(current->as_branch()), bit, path)); + if (!child_opt || nodes_->get(*child_opt)->is_leaf()) { + MORUM_TRACE("parent is {}, path {}", current_idx, path); + return NodeWithPath{current_idx, path}; + } + current = &*nodes_->get(*child_opt); + current_idx = *child_opt; + } + std::unreachable(); + } + + std::expected, StorageError> MerkleTree::find_leaf( + const Hash32 &key) const { + if (empty()) { + return std::nullopt; + } + // SAFETY: check that tree is not empty above + auto &root = *get_root(); + if (root.is_leaf()) { + // TODO: what if the missing byte mismatches? + if (std::ranges::equal( + root.as_leaf().get_key(), qtils::ByteSpan{key}.subspan(0, 31))) { + return 0; + } + return std::nullopt; + } + const TreeNode *current = &root; + NodeId current_id = 0; + size_t path_len = 0; + while (current->is_branch()) { + QTILS_UNWRAP(const auto & child_opt, + get_child_idx(const_cast(current->as_branch()), + get_bit(key, path_len), + qtils::BitSpan<>{key, 0, path_len})); + path_len++; + if (!child_opt) { + return std::nullopt; + } + current_id = *child_opt; + current = &*nodes_->get(*child_opt); + } + if (!current->is_leaf()) { + return std::nullopt; + } + auto &leaf = current->as_leaf(); + + // TODO: what if the dropped byte is different? + if (std::ranges::equal( + leaf.get_key(), qtils::ByteSpan{key}.subspan(0, 31))) { + return current_id; + } + return std::nullopt; + } + + size_t MerkleTree::create_leaf_node(const Hash32 &key, qtils::Bytes &&value) { + ByteArray<31> key_part = + qtils::array_from_span<31>(qtils::ByteSpan{key}.subspan(0, 31)); + if (value.size() > 32) { + Hash32 hash = blake2b_256(value); + value_cache_[hash] = std::move(value); + MORUM_TRACE("insert value with hash {} to value cache", hash); + return nodes_->store(Leaf{Leaf::HashedTag{}, key_part, hash}); + } + return nodes_->store(Leaf{Leaf::EmbeddedTag{}, + key_part, + qtils::ByteSpan{value.data(), value.size()}}); + } + + void MerkleTree::replace_leaf_with_branch( + size_t path_len, size_t old_leaf_idx, size_t new_leaf_idx) { + Leaf &old_leaf = nodes_->get(old_leaf_idx)->as_leaf(); + Leaf &new_leaf = nodes_->get(new_leaf_idx)->as_leaf(); + + ByteArray<31> old_key = old_leaf.get_key(); + qtils::BitSpan<> old_key_bits{old_key}; + qtils::BitSpan<> new_key_bits{new_leaf.get_key()}; + size_t common_key_len = std::distance(old_key_bits.begin(), + std::ranges::mismatch(old_key_bits, new_key_bits).in1); + MORUM_TRACE( + "replace leaf #{} with leaf #{}, path len {}, common key len {}", + old_leaf_idx, + new_leaf_idx, + path_len, + common_key_len); + // size of the key part in a node + if (common_key_len == sizeof(old_key)) { + // leaf keys are identical + old_leaf = new_leaf; + return; + } + QTILS_ASSERT_GREATER_EQ(common_key_len, path_len); + + size_t new_old_leaf_idx = nodes_->store(old_leaf); + MORUM_TRACE("move old leaf to #{}", new_old_leaf_idx); + + auto [left_idx, right_idx] = old_key_bits[common_key_len] == 0 + ? std::make_pair(new_old_leaf_idx, new_leaf_idx) + : std::make_pair(new_leaf_idx, new_old_leaf_idx); + + MORUM_TRACE("new left leaf is #{}, right leaf is #{}", left_idx, right_idx); + + QTILS_ASSERT_LESS(path_len + 1, old_key_bits.size_bits()); + + // add intermediate branches + *nodes_->get(old_leaf_idx) = TreeNode{Branch{}}; + MORUM_TRACE("new branch #{} in place of old leaf", old_leaf_idx); + + NodeId current_branch = old_leaf_idx; + for (size_t i = path_len; i < common_key_len; i++) { + auto new_node_idx = nodes_->store(Branch{}); + nodes_->get(current_branch) + ->as_branch() + .set_child(old_key_bits[i], new_node_idx); + MORUM_TRACE("new branch #{} parent #{}'s child {}", + new_node_idx, + current_branch, + old_key_bits[i]); + current_branch = new_node_idx; + } + auto ¤t_as_branch = nodes_->get(current_branch)->as_branch(); + current_as_branch.set_child(0, left_idx); + current_as_branch.set_child(1, right_idx); + MORUM_TRACE("set branch #{} children to #{} and #{}", + current_branch, + left_idx, + right_idx); + } +} // namespace morum diff --git a/src/merkle_tree/nomt_backend.cpp b/src/merkle_tree/nomt_backend.cpp new file mode 100644 index 0000000..4144194 --- /dev/null +++ b/src/merkle_tree/nomt_backend.cpp @@ -0,0 +1,310 @@ +#include + +#include + +namespace morum { + + qtils::OptionalRef Page::get_node(qtils::BitSpan<> path) { + QTILS_ASSERT_GREATER(path.size_bits(), 0); + QTILS_ASSERT(path.size_bits() <= PAGE_LEVELS); + RawNode *current = &nodes[path[0]]; + size_t offset{path[0]}; + for (size_t level = 1; level < path.size_bits(); ++level) { + auto bit = path[level]; + if (current->is_leaf() || !current->branch.has_child(bit)) { + return std::nullopt; + } + offset = offset | (bit << (level)); + size_t level_offset = (1 << (level + 1)) - 2; + current = &nodes[level_offset + offset]; + } + return *current; + } + + // ignores branch nodes and just return where the corresponding node is + // supposed to be placed + RawNode &Page::get_node_unchecked(qtils::BitSpan<> path) { + QTILS_ASSERT_GREATER(path.size_bits(), 0); + QTILS_ASSERT(path.size_bits() <= PAGE_LEVELS); + size_t offset = path.get_as_byte(0, path.size_bits()); + size_t level_offset = (1 << path.size_bits()) - 2; + return nodes[level_offset + offset]; + } + + std::expected, StorageError> + NearlyOptimalNodeStorage::load_root_node() const { + RawNode root{}; + QTILS_UNWRAP(auto exists, + storage_->read_to({}, + qtils::ByteSpanMut{ + reinterpret_cast(&root), sizeof(RawNode)})); + if (exists) { + return root; + } + return std::nullopt; + } + + std::expected, StorageError> + NearlyOptimalNodeStorage::load_terminal_page(qtils::BitSpan<> path) const { + std::optional previous_page; + for (size_t depth = 0; depth <= path.size_bits() / PAGE_LEVELS; depth++) { + QTILS_UNWRAP(auto opt_page, + load_page_direct(path.subspan(0, depth * PAGE_LEVELS))); + if (!opt_page) { + if (previous_page) { + MORUM_TRACE( + "load terminal page, at path {}, found at depth {} pages/{} " + "bit", + path, + depth, + depth * PAGE_LEVELS); + } else { + MORUM_TRACE("load terminal page, at path {}, not found", + path, + depth, + depth * PAGE_LEVELS); + } + + return previous_page; + } + auto &page = *opt_page; + previous_page = page; + auto node = page.get_node(path.subspan(depth * PAGE_LEVELS, PAGE_LEVELS)); + if (!node || node->is_leaf()) { + MORUM_TRACE( + "load terminal page, at path {}, found at depth {} pages/{} bit", + path, + depth, + depth * PAGE_LEVELS); + + return page; + } + } + QTILS_ASSERT( + !"unreachable: all paths must end with a leaf node or a null child of a branch node"); + std::unreachable(); + } + + std::expected, StorageError> + NearlyOptimalNodeStorage::load_page_direct(qtils::BitSpan<> path) const { + if (path.size_bits() == 0) { + return std::nullopt; + } + std::expected, StorageError> page{}; + + auto key = get_page_key(path); + QTILS_UNWRAP(auto res, + storage_->read_to(key.span(), + qtils::ByteSpanMut{ + reinterpret_cast(&**page), sizeof(Page)})); + if (!res) { + MORUM_TRACE("load page directly, at path {}, key {}, none found", + path, + key.span()); + return std::nullopt; + } + QTILS_ASSERT(res.value() == sizeof(Page)); + MORUM_TRACE("load page directly, at path {}, key {}, found successfully", + path, + key.span()); + return page; + } + + std::expected NearlyOptimalNodeStorage::WriteBatch::set( + qtils::BitSpan<> path, const RawNode &node) { + if (path.size_bits() == 0) { + root_node = node; + return {}; + } + qtils::OptionalRef page{}; + Page page_data{}; + auto [key, _] = get_page_key(path); + if (auto it = page_cache.find(key); it != page_cache.end()) { + page = it->second; + } else { + QTILS_UNWRAP(auto page_opt, page_storage->load_page_direct(path)); + if (page_opt) { + page_data = *page_opt; + } + page = page_data; + page_cache.emplace(key, page_data); + } + auto offset = PAGE_LEVELS * ((path.size_bits() - 1) / PAGE_LEVELS); + page->get_node_unchecked(path.subspan(offset, path.size_bits() - offset)) = + node; + return {}; + } + + std::expected, StorageError> + NearlyOptimalNodeStorage::WriteBatch::load( + [[maybe_unused]] const Hash32 &hash, qtils::BitSpan<> path) { + if (path.size_bits() == 0) { + QTILS_UNWRAP(auto root_opt, page_storage->load_root_node()); + if (!root_opt) { + MORUM_TRACE("load root node, not found"); + return std::nullopt; + } + MORUM_TRACE("load root node, success"); + return *root_opt; + } + QTILS_UNWRAP(auto page_opt, page_storage->load_terminal_page(path)); + if (page_opt) { + auto raw_node = page_opt->get_node(path); + if (!raw_node) { + MORUM_TRACE("load node hash {} path {}, not found in page", hash, path); + return std::nullopt; + } + MORUM_TRACE("load node hash {} path {}, success", hash, path); + return *raw_node; + } + MORUM_TRACE("load node hash {} path {}, page not found", hash, path); + return std::nullopt; + } + + std::expected NearlyOptimalNodeStorage::submit_batch( + std::unique_ptr batch, + ColumnFamilyStorage::Batch &db_batch) { + QTILS_ASSERT(batch != nullptr); + if (batch->root_node) { + if (batch->root_node->is_branch()) { + QTILS_UNWRAP_void(db_batch.write(ColumnFamilyId::TREE_PAGE, + qtils::ByteSpan{}, + serialize_branch(batch->root_node->branch.left, + batch->root_node->branch.right))); + } else { + QTILS_UNWRAP_void(db_batch.write(ColumnFamilyId::TREE_PAGE, + qtils::ByteSpan{}, + serialize_leaf(batch->root_node->leaf.get_key(), + batch->root_node->leaf.hash_or_value()))); + } + } + + for (auto &[key, page] : batch->page_cache) { + QTILS_UNWRAP_void(db_batch.write(ColumnFamilyId::TREE_PAGE, + qtils::ByteSpan{key.data(), key[0]}, + page.as_bytes())); + } + return {}; + } + + qtils::FixedByteVector + NearlyOptimalNodeStorage::get_page_key(qtils::BitSpan<> path) { + path = path.subspan(0, (path.size_bits() / PAGE_LEVELS) * PAGE_LEVELS); + qtils::FixedByteVector key_storage{}; + key_storage.data[0] = path.size_bits(); + std::copy(path.begin(), path.end(), key_storage.data.begin() + 1); + key_storage.size = path.size_bits() / 8 + 1; + return key_storage; + } + + std::expected>, StorageError> + NomtDb::load_tree(const Hash32 &root) const { + auto storage = std::make_unique(); + class NomtNodeLoader final : public NodeLoader { + public: + NomtNodeLoader(const NomtDb &db, FlatPagedNodeStorage &storage) + : db{db}, storage{storage} {} + + virtual std::expected, StorageError> load( + qtils::BitSpan<> path, const Hash32 &hash_copy) const override { + Hash32 hash = hash_copy; + hash[0] &= 0xFE; + if (path.size_bits() == 0) { + QTILS_UNWRAP(auto root_opt, db.page_storage_->load_root_node()); + if (!root_opt) { + MORUM_TRACE("load root node, not found"); + return std::nullopt; + } + MORUM_TRACE("load root node, success"); + db.metrics_.nodes_loaded++; + + return raw_node_to_node(*root_opt); + } + QTILS_UNWRAP(auto page_opt, db.page_storage_->load_terminal_page(path)); + if (page_opt) { + auto raw_node = page_opt->get_node(path); + if (!raw_node) { + MORUM_TRACE( + "load node hash {} path {}, not found in page", hash, path); + return std::nullopt; + } + MORUM_TRACE("load node hash {} path {}, success", hash, path); + db.metrics_.nodes_loaded++; + return raw_node_to_node(*raw_node); + } + MORUM_TRACE("load node hash {} path {}, page not found", hash, path); + return std::nullopt; + } + + const NomtDb &db; + FlatPagedNodeStorage &storage; + }; + auto loader = std::make_shared(*this, *storage); + QTILS_UNWRAP(auto root_node_opt, loader->load(qtils::BitSpan<>{}, root)); + if (!root_node_opt) { + return std::nullopt; + } + return std::make_unique( + std::move(storage), std::make_shared()); + } + + std::unique_ptr NomtDb::empty_tree() const { + auto storage = std::make_unique(); + + return std::make_unique( + std::move(storage), std::make_shared()); + } + + std::expected NomtDb::get_root_and_store( + const MerkleTree &tree) { + auto page_batch = page_storage_->start_writing(); + auto total_batch = storage_->start_batch(); + + auto hash = tree.calculate_hash([&](const TreeNode &n, + qtils::ByteSpan, + qtils::ByteSpan hash, + qtils::BitSpan<> path) { + morum::Hash32 hash_copy; + std::ranges::copy(hash, hash_copy.begin()); + hash_copy[0] &= 0xFE; + RawNode raw_node{}; + if (n.is_leaf()) { + raw_node.leaf = n.as_leaf(); + } else { + raw_node.branch.left = n.as_branch().get_left_hash_raw(); + raw_node.branch.right = n.as_branch().get_right_hash_raw(); + QTILS_ASSERT(raw_node.branch.left != Branch::NoHash); + QTILS_ASSERT(raw_node.branch.right != Branch::NoHash); + } + MORUM_TRACE("store node hash {} path {}", hash, path); + [[maybe_unused]] auto res = page_batch->set(path, raw_node); + QTILS_ASSERT(res); + + if (n.is_leaf()) { + auto h_or_v = n.as_leaf().hash_or_value(); + if (auto *hash = std::get_if(&h_or_v); hash) { + if (auto value_opt = tree.get_cached_value(hash->get()); value_opt) { + total_batch + ->write( + ColumnFamilyId::TREE_VALUE, hash->get(), value_opt.value()) + .value(); + } + } + } + }); + hash[0] &= 0xFE; + + auto page_batch_start = Clock::now(); + [[maybe_unused]] auto res = + page_storage_->submit_batch(std::move(page_batch), *total_batch); + QTILS_ASSERT_HAS_VALUE(res); + metrics_.page_batch_write_duration += Clock::now() - page_batch_start; + + auto value_batch_start = Clock::now(); + [[maybe_unused]] auto res2 = storage_->write_batch(std::move(total_batch)); + QTILS_ASSERT_HAS_VALUE(res2); + metrics_.value_batch_write_duration += Clock::now() - value_batch_start; + + return hash; + } +} // namespace morum \ No newline at end of file diff --git a/src/metrics/exposer.hpp b/src/metrics/exposer.hpp index 6a2f307..485638a 100644 --- a/src/metrics/exposer.hpp +++ b/src/metrics/exposer.hpp @@ -13,7 +13,7 @@ #include "handler.hpp" -namespace jam::metrics { +namespace morum::metrics { /** * @brief an http server interface to expose metrics on request with custom @@ -59,4 +59,4 @@ namespace jam::metrics { std::shared_ptr handler_; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/handler.hpp b/src/metrics/handler.hpp index a3844dc..d2b87a6 100644 --- a/src/metrics/handler.hpp +++ b/src/metrics/handler.hpp @@ -10,7 +10,7 @@ #include "metrics/session.hpp" -namespace jam::metrics { +namespace morum::metrics { class Registry; class Session; diff --git a/src/metrics/histogram_timer.hpp b/src/metrics/histogram_timer.hpp index 446cc78..a0b6ba0 100644 --- a/src/metrics/histogram_timer.hpp +++ b/src/metrics/histogram_timer.hpp @@ -10,7 +10,7 @@ #include "metrics/registry.hpp" #include "qtils/final_action.hpp" -namespace jam::metrics { +namespace morum::metrics { inline std::vector exponentialBuckets(double start, double factor, size_t count) { @@ -79,4 +79,4 @@ namespace jam::metrics { return std::make_optional(qtils::MovableFinalAction(manual())); } }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/exposer_impl.cpp b/src/metrics/impl/exposer_impl.cpp index 26d07d9..3277f49 100644 --- a/src/metrics/impl/exposer_impl.cpp +++ b/src/metrics/impl/exposer_impl.cpp @@ -16,7 +16,7 @@ #include "metrics/metrics.hpp" #include "utils/tuner.hpp" -namespace jam::metrics { +namespace morum::metrics { ExposerImpl::ExposerImpl(std::shared_ptr logsys, std::shared_ptr state_manager, std::shared_ptr config, @@ -39,10 +39,10 @@ namespace jam::metrics { bool ExposerImpl::prepare() { BOOST_ASSERT(config_->metrics().enabled == true); try { - acceptor_ = jam::api::acceptOnFreePort( + acceptor_ = morum::api::acceptOnFreePort( context_, config_->metrics().endpoint, - jam::api::kDefaultPortTolerance, + morum::api::kDefaultPortTolerance, logger_); } catch (const boost::wrapexcept &exception) { SL_CRITICAL( @@ -119,4 +119,4 @@ namespace jam::metrics { acceptor_->async_accept(new_session_->socket(), std::move(on_accept)); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/exposer_impl.hpp b/src/metrics/impl/exposer_impl.hpp index 4d5624b..4980beb 100644 --- a/src/metrics/impl/exposer_impl.hpp +++ b/src/metrics/impl/exposer_impl.hpp @@ -10,20 +10,20 @@ #include -namespace jam::app { +namespace morum::app { class StateManager; class Configuration; -} // namespace jam::app +} // namespace morum::app namespace soralog { class Logger; } // namespace soralog -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log -namespace jam::metrics { +namespace morum::metrics { class ExposerImpl : public Exposer, public std::enable_shared_from_this { @@ -56,4 +56,4 @@ namespace jam::metrics { std::unique_ptr thread_; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/metrics_watcher.cpp b/src/metrics/impl/metrics_watcher.cpp index 61e7377..31fd858 100644 --- a/src/metrics/impl/metrics_watcher.cpp +++ b/src/metrics/impl/metrics_watcher.cpp @@ -11,7 +11,7 @@ #include "log/logger.hpp" #include "metrics/registry.hpp" -namespace jam::metrics { +namespace morum::metrics { namespace fs = std::filesystem; MetricsWatcher::MetricsWatcher( @@ -23,7 +23,7 @@ namespace jam::metrics { BOOST_ASSERT(state_manager); // Metric for exposing current storage size - constexpr auto storageSizeMetricName = "jam_storage_size"; + constexpr auto storageSizeMetricName = "morum_storage_size"; metrics_registry_->registerGaugeFamily( storageSizeMetricName, "Consumption of disk space by storage"); metric_storage_size_ = @@ -89,4 +89,4 @@ namespace jam::metrics { return outcome::success(total_size); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/metrics_watcher.hpp b/src/metrics/impl/metrics_watcher.hpp index 6150cbb..39909fe 100644 --- a/src/metrics/impl/metrics_watcher.hpp +++ b/src/metrics/impl/metrics_watcher.hpp @@ -14,11 +14,11 @@ #include "metrics/metrics.hpp" #include "qtils/outcome.hpp" -namespace jam::metrics { +namespace morum::metrics { class Registry; } -namespace jam::metrics { +namespace morum::metrics { class MetricsWatcher final { public: @@ -43,4 +43,4 @@ namespace jam::metrics { metrics::Gauge *metric_storage_size_; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/handler_impl.cpp b/src/metrics/impl/prometheus/handler_impl.cpp index 655664f..98bd5ad 100644 --- a/src/metrics/impl/prometheus/handler_impl.cpp +++ b/src/metrics/impl/prometheus/handler_impl.cpp @@ -35,7 +35,7 @@ std::vector CollectMetrics( return collected_metrics; } -namespace jam::metrics { +namespace morum::metrics { PrometheusHandler::PrometheusHandler( std::shared_ptr logsys) @@ -101,4 +101,4 @@ namespace jam::metrics { }); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/handler_impl.hpp b/src/metrics/impl/prometheus/handler_impl.hpp index 866758e..85e18e2 100644 --- a/src/metrics/impl/prometheus/handler_impl.hpp +++ b/src/metrics/impl/prometheus/handler_impl.hpp @@ -14,7 +14,7 @@ #include "log/logger.hpp" #include "metrics/handler.hpp" -namespace jam::metrics { +namespace morum::metrics { class PrometheusHandler : public Handler { public: @@ -42,4 +42,4 @@ namespace jam::metrics { std::vector> collectables_; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/metrics_impl.cpp b/src/metrics/impl/prometheus/metrics_impl.cpp index 5ea2076..c716ea1 100644 --- a/src/metrics/impl/prometheus/metrics_impl.cpp +++ b/src/metrics/impl/prometheus/metrics_impl.cpp @@ -11,7 +11,7 @@ #include #include -namespace jam::metrics { +namespace morum::metrics { PrometheusCounter::PrometheusCounter(prometheus::Counter &m) : m_(m) {} void PrometheusCounter::inc() { @@ -55,4 +55,4 @@ namespace jam::metrics { void PrometheusHistogram::observe(const double value) { m_.Observe(value); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/metrics_impl.hpp b/src/metrics/impl/prometheus/metrics_impl.hpp index 2e84390..0c384bd 100644 --- a/src/metrics/impl/prometheus/metrics_impl.hpp +++ b/src/metrics/impl/prometheus/metrics_impl.hpp @@ -15,7 +15,7 @@ namespace prometheus { class Histogram; } // namespace prometheus -namespace jam::metrics { +namespace morum::metrics { class PrometheusCounter : public Counter { friend class PrometheusRegistry; @@ -66,4 +66,4 @@ namespace jam::metrics { void observe(const double value) override; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/registry_impl.cpp b/src/metrics/impl/prometheus/registry_impl.cpp index 97fbda0..f21bc17 100644 --- a/src/metrics/impl/prometheus/registry_impl.cpp +++ b/src/metrics/impl/prometheus/registry_impl.cpp @@ -9,7 +9,7 @@ #include "metrics/handler.hpp" #include "metrics/registry.hpp" -namespace jam::metrics { +namespace morum::metrics { std::unique_ptr createRegistry() { return std::make_unique(); @@ -79,4 +79,4 @@ namespace jam::metrics { return registerMetric(name, labels, q, max_age, age_buckets); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/prometheus/registry_impl.hpp b/src/metrics/impl/prometheus/registry_impl.hpp index 21a5a85..624f5f3 100644 --- a/src/metrics/impl/prometheus/registry_impl.hpp +++ b/src/metrics/impl/prometheus/registry_impl.hpp @@ -21,7 +21,7 @@ #include "metrics/impl/prometheus/metrics_impl.hpp" -namespace jam::metrics { +namespace morum::metrics { class Handler; namespace { @@ -164,4 +164,4 @@ namespace jam::metrics { } }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/session_impl.cpp b/src/metrics/impl/session_impl.cpp index 841bac7..e133758 100644 --- a/src/metrics/impl/session_impl.cpp +++ b/src/metrics/impl/session_impl.cpp @@ -8,7 +8,7 @@ #include "log/logger.hpp" -namespace jam::metrics { +namespace morum::metrics { SessionImpl::SessionImpl(std::shared_ptr logsys, Context &context, @@ -97,4 +97,4 @@ namespace jam::metrics { logger_->error("error occurred: {}, message: {}", ec, message); } -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/impl/session_impl.hpp b/src/metrics/impl/session_impl.hpp index b077427..168ec38 100644 --- a/src/metrics/impl/session_impl.hpp +++ b/src/metrics/impl/session_impl.hpp @@ -13,11 +13,11 @@ #include "log/logger.hpp" #include "metrics/session.hpp" -namespace jam::log { +namespace morum::log { class LoggingSystem; -} // namespace jam::log +} // namespace morum::log -namespace jam::metrics { +namespace morum::metrics { class SessionImpl : public Session, public std::enable_shared_from_this { @@ -104,9 +104,7 @@ namespace jam::metrics { */ void reportError(boost::system::error_code ec, std::string_view message); - static constexpr boost::string_view kServerName = "JAM-Node"; - - std::shared_ptr logger_; + static constexpr boost::string_view kServerName = "Morum-Node"; /// Strand to ensure the connection's handlers are not called concurrently. boost::asio::strand strand_; @@ -121,6 +119,8 @@ namespace jam::metrics { */ std::unique_ptr parser_; ///< http parser + + std::shared_ptr logger_; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/metrics.hpp b/src/metrics/metrics.hpp index 4ef39e3..f319862 100644 --- a/src/metrics/metrics.hpp +++ b/src/metrics/metrics.hpp @@ -8,7 +8,7 @@ #include "metrics/registry.hpp" -namespace jam::metrics { +namespace morum::metrics { // the function recommended to use to create a registry of the chosen // implementation std::unique_ptr createRegistry(); @@ -109,4 +109,4 @@ namespace jam::metrics { */ virtual void observe(const double value) = 0; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/metrics/registry.hpp b/src/metrics/registry.hpp index 6660968..accb5fc 100644 --- a/src/metrics/registry.hpp +++ b/src/metrics/registry.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::metrics { +namespace morum::metrics { class Counter; class Gauge; diff --git a/src/metrics/session.hpp b/src/metrics/session.hpp index 72b7eb2..1aff65d 100644 --- a/src/metrics/session.hpp +++ b/src/metrics/session.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::metrics { +namespace morum::metrics { /** * @brief session interface for OpenMetrics service @@ -69,4 +69,4 @@ namespace jam::metrics { virtual void respond(Response message) = 0; }; -} // namespace jam::metrics +} // namespace morum::metrics diff --git a/src/scale/jam_scale.hpp b/src/scale/jam_scale.hpp index 39ea213..b5f81fb 100644 --- a/src/scale/jam_scale.hpp +++ b/src/scale/jam_scale.hpp @@ -8,7 +8,7 @@ #include -namespace jam { +namespace morum { using scale::decode; using scale::encode; @@ -38,4 +38,4 @@ namespace jam { return std::move(value); } -} // namespace jam +} // namespace morum diff --git a/src/utils/ctor_limiters.hpp b/src/utils/ctor_limiters.hpp index 9d07a9c..bfe7d7c 100644 --- a/src/utils/ctor_limiters.hpp +++ b/src/utils/ctor_limiters.hpp @@ -11,7 +11,7 @@ #include -namespace jam { +namespace morum { class NonCopyable { public: @@ -70,4 +70,4 @@ namespace jam { inline static std::atomic_flag exists{false}; }; -} // namespace jam +} // namespace morum diff --git a/src/utils/retain_if.hpp b/src/utils/retain_if.hpp index 1db1ae9..913d947 100644 --- a/src/utils/retain_if.hpp +++ b/src/utils/retain_if.hpp @@ -9,7 +9,7 @@ #include #include -namespace jam { +namespace morum { template void retain_if(std::vector &v, auto &&predicate) { @@ -30,4 +30,4 @@ namespace jam { } } -} // namespace jam +} // namespace morum diff --git a/src/utils/tuner.cpp b/src/utils/tuner.cpp index a59569f..7a8e036 100644 --- a/src/utils/tuner.cpp +++ b/src/utils/tuner.cpp @@ -8,7 +8,7 @@ #include "log/logger.hpp" -namespace jam::api { +namespace morum::api { std::unique_ptr acceptOnFreePort( std::shared_ptr context, @@ -37,4 +37,4 @@ namespace jam::api { } } -} // namespace jam::api +} // namespace morum::api diff --git a/src/utils/tuner.hpp b/src/utils/tuner.hpp index 59e9729..4b630cb 100644 --- a/src/utils/tuner.hpp +++ b/src/utils/tuner.hpp @@ -14,7 +14,7 @@ namespace soralog { class Logger; } // namespace soralog -namespace jam::api { +namespace morum::api { constexpr uint16_t kDefaultPortTolerance = 10; @@ -27,4 +27,4 @@ namespace jam::api { uint16_t port_tolerance, const std::shared_ptr &logger); -} // namespace jam::api +} // namespace morum::api diff --git a/test-vectors/authorizations/authorizations.hpp b/test-vectors/authorizations/authorizations.hpp index ce27c7b..91e5233 100644 --- a/test-vectors/authorizations/authorizations.hpp +++ b/test-vectors/authorizations/authorizations.hpp @@ -9,8 +9,8 @@ #include #include -namespace jam::authorizations { - namespace types = jam::test_vectors; +namespace morum::authorizations { + namespace types = morum::test_vectors; auto asSet(auto &&r) { return std::set(r.begin(), r.end()); @@ -106,4 +106,4 @@ namespace jam::authorizations { return {new_state, qtils::Empty{}}; } -} // namespace jam::authorizations +} // namespace morum::authorizations diff --git a/test-vectors/authorizations/vectors.hpp b/test-vectors/authorizations/vectors.hpp index bc01f3e..7fb840f 100644 --- a/test-vectors/authorizations/vectors.hpp +++ b/test-vectors/authorizations/vectors.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::test_vectors::authorizations { +namespace morum::test_vectors::authorizations { struct Vectors : test_vectors::VectorsT { std::string_view type; @@ -28,4 +28,4 @@ namespace jam::test_vectors::authorizations { }; } }; -} // namespace jam::test_vectors::authorizations +} // namespace morum::test_vectors::authorizations diff --git a/test-vectors/common.hpp b/test-vectors/common.hpp index 74097dc..4721d67 100644 --- a/test-vectors/common.hpp +++ b/test-vectors/common.hpp @@ -18,7 +18,7 @@ * Common functions used in tests */ -namespace jam { +namespace morum { template qtils::ByteArr first_bytes(qtils::BytesIn bytes) { if (bytes.size() < N) { @@ -82,4 +82,4 @@ namespace jam { inline auto mathcal_H_K(qtils::BytesIn m) { return crypto::Keccak::hash(m); } -} // namespace jam +} // namespace morum diff --git a/test-vectors/config-types.hpp b/test-vectors/config-types.hpp index 1a1cae9..71a8199 100644 --- a/test-vectors/config-types.hpp +++ b/test-vectors/config-types.hpp @@ -12,7 +12,7 @@ #include #include -namespace jam { +namespace morum { template class ConfigVec : public std::vector { public: @@ -53,4 +53,4 @@ namespace jam { } } }; -} // namespace jam +} // namespace morum diff --git a/test-vectors/disputes/CMakeLists.txt b/test-vectors/disputes/CMakeLists.txt index 5c734dd..d9f93f4 100644 --- a/test-vectors/disputes/CMakeLists.txt +++ b/test-vectors/disputes/CMakeLists.txt @@ -13,8 +13,8 @@ find_package(qdrvm-crates CONFIG REQUIRED) add_test_vector(disputes tiny full) add_test_vector_libraries(disputes - PkgConfig::libb2 - schnorrkel::schnorrkel ark_vrf::ark_vrf + blake2b + schnorrkel::schnorrkel + PkgConfig::libb2 ) - diff --git a/test-vectors/disputes/disputes.hpp b/test-vectors/disputes/disputes.hpp index 01e5fe3..06eaa25 100644 --- a/test-vectors/disputes/disputes.hpp +++ b/test-vectors/disputes/disputes.hpp @@ -20,8 +20,8 @@ #include #include -namespace jam::disputes { - namespace types = jam::test_vectors; +namespace morum::disputes { + namespace types = morum::test_vectors; auto asSet(auto &&r) { return std::set(r.begin(), r.end()); @@ -70,14 +70,14 @@ namespace jam::disputes { template MultimapGroups(const M &) -> MultimapGroups; - inline bool ed25519_verify(const jam::crypto::ed25519::Signature &sig, - const jam::crypto::ed25519::Public &pub, + inline bool ed25519_verify(const morum::crypto::ed25519::Signature &sig, + const morum::crypto::ed25519::Public &pub, qtils::BytesIn X, const types::WorkReportHash &work_report) { qtils::ByteVec payload; qtils::append(payload, X); qtils::append(payload, work_report); - return jam::crypto::ed25519::verify(sig, payload, pub); + return morum::crypto::ed25519::verify(sig, payload, pub); } // [GP 0.4.5 I.4.5] @@ -516,4 +516,4 @@ namespace jam::disputes { .offenders_mark = {offenders_mark.begin(), offenders_mark.end()}, }}}; } -} // namespace jam::disputes +} // namespace morum::disputes diff --git a/test-vectors/disputes/vectors.hpp b/test-vectors/disputes/vectors.hpp index 469d98e..0810d0d 100644 --- a/test-vectors/disputes/vectors.hpp +++ b/test-vectors/disputes/vectors.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::test_vectors::disputes { +namespace morum::test_vectors::disputes { struct Vectors : test_vectors::VectorsT { std::string_view type; @@ -28,4 +28,4 @@ namespace jam::test_vectors::disputes { }; } }; -} // namespace jam::test_vectors::disputes +} // namespace morum::test_vectors::disputes diff --git a/test-vectors/history/history.hpp b/test-vectors/history/history.hpp index 4472f4b..a01ba02 100644 --- a/test-vectors/history/history.hpp +++ b/test-vectors/history/history.hpp @@ -12,8 +12,8 @@ #include #include -namespace jam::history { - namespace types = jam::test_vectors; +namespace morum::history { + namespace types = morum::test_vectors; /** * The size of recent history, in blocks @@ -69,4 +69,4 @@ namespace jam::history { }); return std::make_pair(types::history::State{.beta = beta_tick}, types::history::Output{}); } -} // namespace jam::history +} // namespace morum::history diff --git a/test-vectors/history/vectors.hpp b/test-vectors/history/vectors.hpp index a97834b..6702abd 100644 --- a/test-vectors/history/vectors.hpp +++ b/test-vectors/history/vectors.hpp @@ -10,7 +10,7 @@ #include #include -namespace jam::test_vectors::history { +namespace morum::test_vectors::history { struct Vectors : test_vectors::VectorsT { Vectors() : VectorsT{Config{}} { this->list(std::filesystem::path{"history/data"}); @@ -20,4 +20,4 @@ namespace jam::test_vectors::history { return {std::make_shared()}; } }; -} // namespace jam::test_vectors_history +} // namespace morum::test_vectors_history diff --git a/test-vectors/safrole/safrole.hpp b/test-vectors/safrole/safrole.hpp index b8d2ea7..780f01b 100644 --- a/test-vectors/safrole/safrole.hpp +++ b/test-vectors/safrole/safrole.hpp @@ -16,8 +16,8 @@ #include #include -namespace jam::safrole { - namespace types = jam::test_vectors; +namespace morum::safrole { + namespace types = morum::test_vectors; using BandersnatchSignature = decltype(types::TicketEnvelope::signature); @@ -380,4 +380,4 @@ namespace jam::safrole { }}, }; } -} // namespace jam::safrole +} // namespace morum::safrole diff --git a/test-vectors/safrole/vectors.hpp b/test-vectors/safrole/vectors.hpp index f10befe..17820c2 100644 --- a/test-vectors/safrole/vectors.hpp +++ b/test-vectors/safrole/vectors.hpp @@ -11,7 +11,7 @@ #include #include -namespace jam::test_vectors::safrole { +namespace morum::test_vectors::safrole { struct Vectors : test_vectors::VectorsT { std::string_view type; @@ -28,4 +28,4 @@ namespace jam::test_vectors::safrole { }; } }; -} // namespace jam::test_vectors::safrole +} // namespace morum::test_vectors::safrole diff --git a/test-vectors/vectors.hpp b/test-vectors/vectors.hpp index b5da179..2e763fc 100644 --- a/test-vectors/vectors.hpp +++ b/test-vectors/vectors.hpp @@ -21,25 +21,25 @@ /** * Common functions for test vectors */ -#define GTEST_VECTORS(VectorName, NsPart) \ - struct VectorName##Test \ - : jam::test_vectors::TestT {}; \ - INSTANTIATE_TEST_SUITE_P( \ - VectorName, \ - VectorName##Test, \ - testing::ValuesIn([] { \ - using T = jam::test_vectors::NsPart::Vectors; \ - std::vector, std::filesystem::path>> \ - params; \ - for (auto &vectors : T::vectors()) { \ - for (auto &path : vectors->paths) { \ - params.emplace_back(vectors, path); \ - } \ - } \ - return params; \ - }()), \ - [](auto &&info) { \ - return jam::test_vectors::getTestName(std::get<1>(info.param)); \ +#define GTEST_VECTORS(VectorName, NsPart) \ + struct VectorName##Test \ + : morum::test_vectors::TestT {}; \ + INSTANTIATE_TEST_SUITE_P( \ + VectorName, \ + VectorName##Test, \ + testing::ValuesIn([] { \ + using T = morum::test_vectors::NsPart::Vectors; \ + std::vector, std::filesystem::path>> \ + params; \ + for (auto &vectors : T::vectors()) { \ + for (auto &path : vectors->paths) { \ + params.emplace_back(vectors, path); \ + } \ + } \ + return params; \ + }()), \ + [](auto &&info) { \ + return morum::test_vectors::getTestName(std::get<1>(info.param)); \ }); /** @@ -48,31 +48,31 @@ * @when transition with `input` * @then get expected `post_state` and `output` */ -#define GTEST_VECTORS_TEST_TRANSITION(VectorName, NsPart) \ - TEST_P(VectorName##Test, Transition) { \ - using jam::test_vectors::getTestLabel; \ - fmt::println("Test transition for '{}'\n", getTestLabel(path)); \ - \ - ASSERT_OUTCOME_SUCCESS(raw_data, qtils::readBytes(path)); \ - \ - ASSERT_OUTCOME_SUCCESS( \ - testcase, \ - (jam::decode_with_config( \ - raw_data, vectors.config))); \ - \ - auto [state, output] = jam::NsPart::transition( \ - vectors.config, testcase.pre_state, testcase.input); \ - Indent indent{1}; \ - EXPECT_EQ(state, testcase.post_state) \ - << "Actual and expected states are differ"; \ - if (state != testcase.post_state) { \ - diff_m(indent, state, testcase.post_state, "state"); \ - } \ - EXPECT_EQ(output, testcase.output) \ - << "Actual and expected outputs are differ"; \ - if (output != testcase.output) { \ - diff_m(indent, output, testcase.output, "output"); \ - } \ +#define GTEST_VECTORS_TEST_TRANSITION(VectorName, NsPart) \ + TEST_P(VectorName##Test, Transition) { \ + using morum::test_vectors::getTestLabel; \ + fmt::println("Test transition for '{}'\n", getTestLabel(path)); \ + \ + ASSERT_OUTCOME_SUCCESS(raw_data, qtils::readBytes(path)); \ + \ + ASSERT_OUTCOME_SUCCESS( \ + testcase, \ + (morum::decode_with_config( \ + raw_data, vectors.config))); \ + \ + auto [state, output] = morum::NsPart::transition( \ + vectors.config, testcase.pre_state, testcase.input); \ + Indent indent{1}; \ + EXPECT_EQ(state, testcase.post_state) \ + << "Actual and expected states are differ"; \ + if (state != testcase.post_state) { \ + diff_m(indent, state, testcase.post_state, "state"); \ + } \ + EXPECT_EQ(output, testcase.output) \ + << "Actual and expected outputs are differ"; \ + if (output != testcase.output) { \ + diff_m(indent, output, testcase.output, "output"); \ + } \ } /** @@ -81,33 +81,33 @@ * @when decode it and encode back * @then `actual` result has the same value as `original` */ -#define GTEST_VECTORS_TEST_REENCODE(VectorName, NsPart) \ - TEST_P(VectorName##Test, Reencode) { \ - using jam::test_vectors::getTestLabel; \ - fmt::println("Test reencode for '{}'\n", getTestLabel(path)); \ - \ - ASSERT_OUTCOME_SUCCESS(raw_data, qtils::readBytes(path)); \ - const auto &original = raw_data; \ - \ - ASSERT_OUTCOME_SUCCESS( \ - decoded, \ - (jam::decode_with_config( \ - original, vectors.config))); \ - \ - ASSERT_OUTCOME_SUCCESS( \ - reencoded, (jam::encode_with_config(decoded, vectors.config))); \ - \ - EXPECT_EQ(reencoded, original); \ +#define GTEST_VECTORS_TEST_REENCODE(VectorName, NsPart) \ + TEST_P(VectorName##Test, Reencode) { \ + using morum::test_vectors::getTestLabel; \ + fmt::println("Test reencode for '{}'\n", getTestLabel(path)); \ + \ + ASSERT_OUTCOME_SUCCESS(raw_data, qtils::readBytes(path)); \ + const auto &original = raw_data; \ + \ + ASSERT_OUTCOME_SUCCESS( \ + decoded, \ + (morum::decode_with_config( \ + original, vectors.config))); \ + \ + ASSERT_OUTCOME_SUCCESS( \ + reencoded, (morum::encode_with_config(decoded, vectors.config))); \ + \ + EXPECT_EQ(reencoded, original); \ } -namespace jam::test_vectors { +namespace morum::test_vectors { inline const std::filesystem::path dir = std::filesystem::path{PROJECT_SOURCE_DIR} / "test-vectors/jamtestvectors"; auto getTestLabel(const std::filesystem::path &path) { std::filesystem::path label; auto rel_path = - std::filesystem::relative(path.parent_path(), jam::test_vectors::dir); + std::filesystem::relative(path.parent_path(), morum::test_vectors::dir); auto it = rel_path.begin(); if (it != rel_path.end()) { ++it; @@ -176,4 +176,4 @@ namespace jam::test_vectors { const T &vectors = *this->GetParam().first; const std::filesystem::path &path = this->GetParam().second; }; -} // namespace jam::test_vectors +} // namespace morum::test_vectors diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..2d6a667 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,16 @@ + +morum_add_test(codec_test merkle_tree/codec_test.cpp) +target_link_libraries(codec_test qtils::qtils) + +morum_add_test(compatibility_test merkle_tree/compat_test.cpp) +target_include_directories(compatibility_test + PUBLIC + third_party/json/single_include +) +target_link_libraries(compatibility_test merkle_tree nlohmann_json::nlohmann_json qtils::qtils) + +morum_add_test(tree_test merkle_tree/tree_test.cpp) +target_link_libraries(tree_test merkle_tree RocksDB::rocksdb) + +morum_add_test(primitives_test merkle_tree/primitives_test.cpp) +target_link_libraries(primitives_test merkle_tree) diff --git a/test/merkle_tree/codec_test.cpp b/test/merkle_tree/codec_test.cpp new file mode 100644 index 0000000..9c28f56 --- /dev/null +++ b/test/merkle_tree/codec_test.cpp @@ -0,0 +1,17 @@ +#include + +#include +#include + +int main() { + for (unsigned long i : + {0ul, 1ul, 2ul, 3ul, 1ul << 28, 1ul << 29, 1ul << 25, 1ul << 63}) { + morum::VectorStream stream; + morum::encode(stream, i); + std::cout << std::dec << i << " -> "; + for (auto b : stream.data) { + std::cout << std::hex << (int)b; + } + std::cout << "\n"; + } +} diff --git a/test/merkle_tree/compat_test.cpp b/test/merkle_tree/compat_test.cpp new file mode 100644 index 0000000..f9ec411 --- /dev/null +++ b/test/merkle_tree/compat_test.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +uint8_t unhex(char c) { + if (isdigit(c)) { + return c - '0'; + } + return c - 'a' + 10; +} + +char hex(uint8_t i) { + QTILS_ASSERT_LESS(i, 16); + if (i < 10) { + return i + '0'; + } + return i + 'a' - 10; +} + +template It> +void unhex(std::string_view hex, It it) { + QTILS_ASSERT_EQ(hex.size() % 2, 0); + for (size_t i = 0; i < hex.size(); i += 2) { + *it = unhex(hex[i + 1]) | (unhex(hex[i]) << 4); + it++; + } +} + +std::string hex(std::span bytes) { + std::string s(bytes.size() * 2, 0); + for (size_t i = 0; i < bytes.size(); i++) { + s[i * 2] = hex(bytes[i] >> 4); + s[i * 2 + 1] = hex(bytes[i] & 0x0F); + } + return s; +} + +int main() { + auto file_path = + std::filesystem::path(std::source_location::current().file_name()) + .parent_path() + / "../../test-vectors/jamtestvectors/trie/trie.json"; + std::ifstream test_cases_file{file_path}; + if (!test_cases_file.good()) { + std::cerr + << "Test vectors are not found; Try running git submodule update --init.\n"; + return -1; + } + auto test_cases = nlohmann::json::parse(test_cases_file); + + // to start from nth case if needed + size_t start_from{0}; + size_t test_idx{start_from}; + int status = 0; + for (auto test_case : test_cases | std::views::drop(start_from)) { + morum::MerkleTree tree{std::make_unique(), + std::make_shared()}; + std::vector> state; + for (auto &[k, v] : test_case["input"].items()) { + morum::Hash32 key; + unhex(k, static_cast(key.data())); + auto value_str = v.get(); + morum::ByteVector value(value_str.size() / 2); + unhex(value_str, value.data()); + state.push_back(std::pair{key, value}); + [[maybe_unused]] auto res = + tree.set(key, morum::ByteVector{value}).has_value(); + QTILS_ASSERT(res); + } + std::ranges::sort(state, + morum::TrieKeyOrder{}, + [](const auto &pair) -> const morum::Hash32 & { return pair.first; }); + for (auto &[k, v] : state) { + std::print(std::cerr, "{}\n", hex(k)); + } + morum::Hash32 expected_root; + auto expected_output_str = test_case["output"].get(); + unhex(expected_output_str, expected_root.data()); + auto root = morum::calculate_root_in_memory(state); + std::cerr << "#" << test_idx << " - "; + if (!std::ranges::equal(root, expected_root)) { + std::cerr << "fail: " << hex({root.data(), 32}) + << " != " << expected_output_str << "\n"; + status = -1; + } else { + std::cerr << "success\n"; + } + QTILS_ASSERT_RANGE_EQ(tree.calculate_hash(), expected_root); + test_idx++; + } + return status; +} diff --git a/test/merkle_tree/primitives_test.cpp b/test/merkle_tree/primitives_test.cpp new file mode 100644 index 0000000..6c30d40 --- /dev/null +++ b/test/merkle_tree/primitives_test.cpp @@ -0,0 +1,84 @@ +#include + +#include +#include +#include + +#include +#include +#include + +void test_node_consistency( + morum::Page &page, qtils::BitSpan<> path, morum::RawNode node) { + for (size_t level = 1; level < path.size_bits(); ++level) { + auto bit = path[level]; + auto &node = page.get_node_unchecked(path.subspan(0, level)); + node.branch = + morum::RawBranch{bit ? morum::ZeroHash32 : morum::Hash32{0, 1}, + bit ? morum::Hash32{1} : morum::ZeroHash32}; + } + auto &node_place = page.get_node_unchecked(path); + node_place = node; + [[maybe_unused]] auto placed_node = page.get_node(path); + QTILS_ASSERT_HAS_VALUE(placed_node); + QTILS_ASSERT((placed_node->is_branch() && node.is_branch()) + || (placed_node->is_leaf() && node.is_leaf())); + QTILS_ASSERT_RANGE_EQ((morum::ByteSpan{reinterpret_cast(&node), + sizeof(morum::RawNode)}), + (morum::ByteSpan{ + reinterpret_cast(&*placed_node), sizeof(morum::RawNode)})); +} + +int main() { + qtils::ByteArray<5> array{0x01, 0x23, 0x45, 0x67, 0x89}; + qtils::BitSpan<> span{array}; + QTILS_ASSERT_EQ(span.get_as_byte(0, 8), 0x01); + QTILS_ASSERT_EQ(span.get_as_byte(8, 8), 0x23); + QTILS_ASSERT_EQ(span.get_as_byte(32, 8), 0x89); + QTILS_ASSERT_EQ(span.get_as_byte(0, 4), 0x1); + QTILS_ASSERT_EQ(span.get_as_byte(4, 4), 0x0); + QTILS_ASSERT_EQ(span.get_as_byte(4, 8), 0x30); + QTILS_ASSERT_EQ(span.get_as_byte(12, 3), 0x2); + + qtils::ByteArray<32> path{0b0000'0000}; + morum::Page page{}; + auto node1 = page.get_node(qtils::BitSpan<>{path, 0, 1}); + QTILS_ASSERT_HAS_VALUE(node1); + node1->branch = morum::RawBranch{ + morum::ZeroHash32, morum::blake2b_256(qtils::ByteArray<4>{0, 1, 2, 3})}; + auto &node2 = page.get_node_unchecked(qtils::BitSpan<>{path, 0, 1}); + node2.leaf = + morum::Leaf{morum::Leaf::EmbeddedTag{}, qtils::ByteArray<31>{0xAB}, {}}; + [[maybe_unused]] auto node3 = page.get_node(qtils::BitSpan<>{path, 0, 1}); + QTILS_ASSERT_HAS_VALUE(node3); + QTILS_ASSERT(node3->is_leaf()); + QTILS_ASSERT_EQ(node3->leaf.get_key()[0], 0xAB); + + for (size_t depth = 1; depth < 6; depth++) { + morum::Page page{}; + morum::ByteArray<32> path{0b0000'0000}; + test_node_consistency(page, + qtils::BitSpan<>{path, 0, depth}, + morum::RawNode{ + .leaf = morum::Leaf{ + morum::Leaf::EmbeddedTag{}, morum::ByteArray<31>{0xAB}, {}}}); + } + for (size_t depth = 1; depth < 6; depth++) { + morum::Page page{}; + qtils::ByteArray<32> path{0b1111'1111}; + test_node_consistency(page, + qtils::BitSpan<>{path, 0, depth}, + morum::RawNode{ + .leaf = morum::Leaf{ + morum::Leaf::EmbeddedTag{}, morum::ByteArray<31>{0xAB}, {}}}); + } + for (size_t depth = 1; depth < 6; depth++) { + morum::Page page{}; + qtils::ByteArray<32> path{0b1010'1010}; + test_node_consistency(page, + qtils::BitSpan<>{path, 0, depth}, + morum::RawNode{ + .leaf = morum::Leaf{ + morum::Leaf::EmbeddedTag{}, morum::ByteArray<31>{0xAB}, {}}}); + } +} diff --git a/test/merkle_tree/tree_test.cpp b/test/merkle_tree/tree_test.cpp new file mode 100644 index 0000000..b69c9e9 --- /dev/null +++ b/test/merkle_tree/tree_test.cpp @@ -0,0 +1,398 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +void *operator new(std::size_t count) { + auto ptr = malloc(count); + TracyAlloc(ptr, count); + return ptr; +} +void operator delete(void *ptr) noexcept { + TracyFree(ptr); + free(ptr); +} +void operator delete(void *ptr, std::size_t) noexcept { + TracyFree(ptr); + free(ptr); +} + +constexpr unsigned seed = 42; +static std::mt19937_64 rand_engine{seed}; + +template +void fill_random(R &&span) { + static std::uniform_int_distribution dist; + + for (auto &byte : span) { + byte = dist(rand_engine); + } +} + +morum::Hash32 random_hash() { + morum::Hash32 hash; + fill_random(hash); + return hash; +} + +morum::ByteVector random_vector(size_t min_size = 1, size_t max_size = 128) { + std::uniform_int_distribution dist(min_size, max_size); + size_t size = dist(rand_engine); + + morum::ByteVector v(size); + fill_random(v); + return v; +} + +template +struct NiceDuration : Duration { + NiceDuration(): Duration() {} + NiceDuration(Duration d): Duration{d} {} +}; + +template +NiceDuration(const Duration&) -> NiceDuration; + +template +struct fmt::formatter> { + template + constexpr ParseContext::iterator parse(ParseContext &ctx) { + auto it = ctx.begin(); + return it; + } + + template + FmtContext::iterator format( + NiceDuration dur, FmtContext &ctx) const { + auto out = ctx.out(); + + if (auto n = std::chrono::duration_cast(dur).count(); + n > 10) { + fmt::format_to(out, "{}s", n); + return out; + } + if (auto n = + std::chrono::duration_cast(dur).count(); + n > 10) { + fmt::format_to(out, "{}ms", n); + return out; + } + if (auto n = + std::chrono::duration_cast(dur).count(); + n > 10) { + fmt::format_to(out, "{}us", n); + return out; + } + fmt::format_to(out, "{}ns", dur.count()); + return out; + } +}; + +int main() { + auto db = std::shared_ptr{morum::open_db("/tmp/qdrvm-test-db").value()}; + + rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex); + + auto tree_db = std::make_shared( + db, morum::ColumnFamilyId::TREE_NODE); + auto value_db = std::make_shared( + db, morum::ColumnFamilyId::TREE_VALUE); + auto page_db = std::make_shared( + db, morum::ColumnFamilyId::TREE_PAGE); + morum::RocksDbColumnFamily flat_db{db, morum::ColumnFamilyId::FLAT_KV}; + + using Clock = std::chrono::steady_clock; + using Dur = NiceDuration; + struct PerStepStats { + Dur node_reads_duration{}; + Dur value_reads_duration{}; + Dur writes_in_batch_duration{}; + Dur batch_write_duration{}; + Dur total_duration{}; + size_t new_nodes_written{}; + size_t new_values_written{}; + uint64_t new_values_size{}; + }; + + std::vector stats{}; + + constexpr int STEPS_NUM = 10; + constexpr int INSERTION_NUM = 10000; + + stats.resize(STEPS_NUM); + + morum::Hash32 previous_root{}; + + auto db_stats = rocksdb::CreateDBStatistics(); + + std::unordered_map cached_nodes; + + struct TotalTime { + Clock::duration nomt{}; + Clock::duration archive{}; + }; + std::vector totals(STEPS_NUM); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // NOMT + ///////////////////////////////////////////////////////////////////////////////////////////////// + { + morum::NomtDb nomt{db}; + + std::vector> insertions; + + for (int step = 0; step < STEPS_NUM; step++) { + auto total_start = Clock::now(); + std::unique_ptr tree; + + if (previous_root == morum::Hash32{}) { + tree = nomt.empty_tree(); + } else { + tree = std::move(nomt.load_tree(previous_root)->value()); + } + if (step > 0) { + // check that values inserted on the previous step are accessible + for (auto &[k, v] : insertions) { + auto res_opt = tree->get(k); + QTILS_ASSERT_HAS_VALUE(res_opt); + QTILS_ASSERT(res_opt.value().has_value()); + QTILS_ASSERT_RANGE_EQ(res_opt.value().value(), v); + } + insertions.clear(); + } + insertions.reserve(INSERTION_NUM); + for (int i = 0; i < INSERTION_NUM; i++) { + insertions.emplace_back(random_hash(), random_vector()); + } + for (auto &[k, v] : insertions) { + [[maybe_unused]] auto res = tree->set(k, morum::ByteVector{v}); + QTILS_ASSERT_HAS_VALUE(res); + } + for (auto &[k, v] : insertions) { + auto res_opt = tree->get(k); + QTILS_ASSERT_HAS_VALUE(res_opt); + QTILS_ASSERT(res_opt.value().has_value()); + QTILS_ASSERT_RANGE_EQ(res_opt.value().value(), v); + } + previous_root = nomt.get_root_and_store(*tree).value(); + fmt::println("{} - {} - total_duration: {}", + step, + qtils::Hex{previous_root}, + NiceDuration(Clock::now() - total_start)); + totals[step].nomt = Clock::now() - total_start; + } + } + + rand_engine.seed(42); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // Archive + ///////////////////////////////////////////////////////////////////////////////////////////////// + + previous_root = morum::ZeroHash32; + morum::ArchiveTrieDb archive_db{db}; + for (int step = 0; step < STEPS_NUM; step++) { + ZoneNamedN(loop_zone, "loop", true); + rocksdb::get_perf_context()->Reset(); + rocksdb::get_iostats_context()->Reset(); + auto total_start = Clock::now(); + + std::unique_ptr tree; + if (previous_root == morum::ZeroHash32) { + tree = archive_db.empty_tree(); + } else { + tree = archive_db.load_tree(previous_root).value().value(); + } + std::vector> insertions; + for (int i = 0; i < INSERTION_NUM; i++) { + insertions.emplace_back(random_hash(), random_vector()); + } + { + ZoneNamedN(setter_zone, "set", true); + for (auto &[k, v] : insertions) { + [[maybe_unused]] auto res = tree->set(k, morum::ByteVector{v}); + QTILS_ASSERT_HAS_VALUE(res); + } + } + { + ZoneNamedN(getter_zone, "get", true); + + for (auto &[k, v] : insertions) { + auto res_opt = tree->get(k); + QTILS_ASSERT_HAS_VALUE(res_opt); + QTILS_ASSERT(res_opt.value().has_value()); + QTILS_ASSERT_RANGE_EQ(res_opt.value().value(), v); + } + } + auto node_batch = tree_db->start_batch(); + auto value_batch = value_db->start_batch(); + morum::Hash32 hash; + { + ZoneNamedN(calculate_hash_zone, "calculate_hash", true); + hash = tree->calculate_hash([&](const morum::TreeNode &n, + qtils::ByteSpan serialized, + qtils::ByteSpan hash, + qtils::BitSpan<>) { + morum::Hash32 hash_copy; + std::ranges::copy(hash, hash_copy.begin()); + hash_copy[0] &= 0xFE; + auto start = Clock::now(); + [[maybe_unused]] auto res = node_batch->write(hash_copy, serialized); + if (n.is_leaf()) { + cached_nodes.emplace(hash_copy, n); + } else { + auto &branch = n.as_branch(); + QTILS_ASSERT(!branch.get_left_idx().has_value() + || branch.get_left_hash().has_value() + == branch.get_left_idx().has_value()); + QTILS_ASSERT(!branch.get_right_idx().has_value() + || branch.get_right_hash().has_value() + == branch.get_right_idx().has_value()); + // original node may contain child node ids which are not persistent + morum::Branch b{branch.get_left_hash(), branch.get_right_hash()}; + cached_nodes.emplace(hash_copy, b); + } + QTILS_ASSERT(res); + stats[step].new_nodes_written++; + if (n.is_leaf()) { + auto h_or_v = n.as_leaf().hash_or_value(); + if (auto *hash = std::get_if(&h_or_v); hash) { + if (auto value_opt = tree->get_cached_value(hash->get()); + value_opt) { + [[maybe_unused]] auto res = + value_batch->write(hash->get(), value_opt.value()); + stats[step].new_values_written++; + stats[step].new_values_size += value_opt.value().size_bytes(); + QTILS_ASSERT(res); + } + } + } + stats[step].writes_in_batch_duration += Clock::now() - start; + + [[maybe_unused]] auto deserialized = + morum::deserialize_node(serialized); + if (n.is_branch()) { + QTILS_ASSERT(deserialized.is_branch()); + QTILS_ASSERT_EQ(n.as_branch().get_left_hash(), + deserialized.as_branch().get_left_hash()); + QTILS_ASSERT_EQ(n.as_branch().get_right_hash(), + deserialized.as_branch().get_right_hash()); + } else { + QTILS_ASSERT(deserialized.is_leaf()); + } + }); + } + hash[0] &= 0xFE; + previous_root = hash; + + auto start = Clock::now(); + [[maybe_unused]] auto res = tree_db->write_batch(std::move(node_batch)); + QTILS_ASSERT_HAS_VALUE(res); + [[maybe_unused]] auto res2 = value_db->write_batch(std::move(value_batch)); + QTILS_ASSERT_HAS_VALUE(res2); + stats[step].batch_write_duration = Dur{Clock::now() - start}; + + stats[step].total_duration += Clock::now() - total_start; + + fmt::println( + "\r{} - {}, {}, {} nodes and {} values written, {} bytes of values, " + "{} " + "bytes of nodes", + step, + qtils::Hex{hash}, + stats[step].total_duration, + stats[step].new_nodes_written, + stats[step].new_values_written, + stats[step].new_values_size, + stats[step].new_nodes_written * sizeof(morum::Leaf)); + fmt::println("total_duration: {}", stats[step].total_duration); + totals[step].archive = stats[step].total_duration; + + fmt::println( + "{}", "========================================================="); + FrameMark; + } + PerStepStats avg{}; + PerStepStats max{}; + PerStepStats min{ + {Dur::max()}, + {Dur::max()}, + {Dur::max()}, + {Dur::max()}, + {Dur::max()}, + }; + + for (auto &stat : stats) { + avg.total_duration += stat.total_duration; + avg.batch_write_duration += stat.batch_write_duration; + avg.node_reads_duration += stat.node_reads_duration; + avg.value_reads_duration += stat.value_reads_duration; + avg.writes_in_batch_duration += stat.writes_in_batch_duration; + + max.total_duration = std::max(stat.total_duration, max.total_duration); + max.batch_write_duration = + std::max(stat.batch_write_duration, max.batch_write_duration); + max.node_reads_duration = + std::max(stat.node_reads_duration, max.node_reads_duration); + max.value_reads_duration = + std::max(stat.value_reads_duration, max.value_reads_duration); + max.writes_in_batch_duration = + std::max(stat.writes_in_batch_duration, max.writes_in_batch_duration); + + min.total_duration = std::min(stat.total_duration, min.total_duration); + min.batch_write_duration = + std::min(stat.batch_write_duration, min.batch_write_duration); + min.node_reads_duration = + std::min(stat.node_reads_duration, min.node_reads_duration); + min.value_reads_duration = + std::min(stat.value_reads_duration, min.value_reads_duration); + min.writes_in_batch_duration = + std::min(stat.writes_in_batch_duration, min.writes_in_batch_duration); + } + avg.total_duration /= stats.size(); + avg.batch_write_duration /= stats.size(); + avg.node_reads_duration /= stats.size(); + avg.value_reads_duration /= stats.size(); + avg.writes_in_batch_duration /= stats.size(); + + for (int i = 0; i < STEPS_NUM; i++) { + fmt::println("#{}: NOMT {} vs Archive {}: {:.1f}%", + i, + NiceDuration(totals[i].nomt), + NiceDuration(totals[i].archive), + 100.0 * (double)totals[i].nomt.count() + / (double)totals[i].archive.count()); + } + db.reset(); +} diff --git a/vcpkg-overlay/blake2/CMakeLists.txt b/vcpkg-overlay/blake2/CMakeLists.txt new file mode 100644 index 0000000..54a3126 --- /dev/null +++ b/vcpkg-overlay/blake2/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.28) +project(blake2 VERSION 1.0.0 LANGUAGES C) + +include(GNUInstallDirs) + +if (CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64) + set(VECTOR_EXT sse) +elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL arm64) + set(VECTOR_EXT neon) +else() + message(AUTHOR_WARNING "Unexpected architecture ${CMAKE_SYSTEM_PROCESSOR}, will use unoptimized implementation.") + set(VECTOR_EXT ref) +endif() + +add_library(blake2b ${VECTOR_EXT}/blake2b.c) +target_compile_options(blake2b PRIVATE -O3 -Wall -Wextra -std=c89 -pedantic -Wno-long-long) + +install(TARGETS blake2b) +install(FILES "${CMAKE_SOURCE_DIR}/${VECTOR_EXT}/blake2.h" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/vcpkg-overlay/blake2/portfile.cmake b/vcpkg-overlay/blake2/portfile.cmake new file mode 100644 index 0000000..d16da9a --- /dev/null +++ b/vcpkg-overlay/blake2/portfile.cmake @@ -0,0 +1,13 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO BLAKE2/BLAKE2 + REF ed1974ea83433eba7b2d95c5dcd9ac33cb847913 + SHA512 5c4bad10659ef2d3eec2b4ba1a13b45055910e28d4c843a1e39cba9999bfcd8232a58e7aa44d603b434cc87255fdaed7aa3de5fedd57738c69775b9569bd271d +) + +file(COPY "${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt" DESTINATION ${SOURCE_PATH}) +vcpkg_cmake_configure(SOURCE_PATH ${SOURCE_PATH}) +vcpkg_cmake_install() +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/COPYING") diff --git a/vcpkg-overlay/blake2/vcpkg.json b/vcpkg-overlay/blake2/vcpkg.json new file mode 100644 index 0000000..a45ba8e --- /dev/null +++ b/vcpkg-overlay/blake2/vcpkg.json @@ -0,0 +1,14 @@ +{ + "name": "blake2", + "version": "1.0.0", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/vcpkg-overlay/kagome-crates/portfile.cmake b/vcpkg-overlay/kagome-crates/portfile.cmake deleted file mode 100644 index 8cbe9ae..0000000 --- a/vcpkg-overlay/kagome-crates/portfile.cmake +++ /dev/null @@ -1,25 +0,0 @@ -vcpkg_check_linkage(ONLY_STATIC_LIBRARY) -# Force rebuild to debug bandersnatch_vrfs issue -vcpkg_from_github( - OUT_SOURCE_PATH SOURCE_PATH - REPO qdrvm/kagome-crates - REF c4a2c5ea9c7b2fda8623066591593a35dc47b927 - SHA512 1c5ae38aa64ac4dca2c37f950785bfdc588127cf2d2a2386744dee911983c7d3944c3d441d709c7eaaa39e41f6786a2b8f11d86b315805c7d4f443533a8e3fac - HEAD_REF main -) - -vcpkg_cmake_configure( - SOURCE_PATH "${SOURCE_PATH}" - OPTIONS - "-DQDRVM_BIND_CRATES=schnorrkel;ark_vrf" -) - -vcpkg_cmake_build() - -vcpkg_cmake_install() - -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") -file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") - -# Use vcpkg_cmake_config_fixup to properly move config files -vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/qdrvm-crates PACKAGE_NAME qdrvm-crates) diff --git a/vcpkg-overlay/qdrvm-crates/portfile.cmake b/vcpkg-overlay/qdrvm-crates/portfile.cmake new file mode 100644 index 0000000..0caad39 --- /dev/null +++ b/vcpkg-overlay/qdrvm-crates/portfile.cmake @@ -0,0 +1,19 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO qdrvm/kagome-crates + REF fa437487127bfdc78f15266bb1fddaf0c913d8cf + SHA512 c186c8f274aa0ec49fc33ebb4aff3b0cc1dc287d48c1ca5408fc0de38302e5e0d405c83849a580739b10456e2f4f5da7f6441cf24777a4e004276de74d0f4a0b +) +vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}" OPTIONS "-DQDRVM_BIND_CRATES=bandersnatch_vrfs;schnorrkel;ark_vrf") +vcpkg_cmake_install() +vcpkg_cmake_config_fixup(PACKAGE_NAME "qdrvm-crates" CONFIG_PATH "lib/cmake/qdrvm-crates" NO_PREFIX_CORRECTION) + +# since config file is moved to share/ +#vcpkg_replace_string( +# "${CURRENT_PACKAGES_DIR}/share/qdrvm-crates/qdrvm-cratesConfig.cmake" +# "{CMAKE_CURRENT_LIST_DIR}/../../../" +# "{CMAKE_CURRENT_LIST_DIR}/../../" +#) + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") diff --git a/vcpkg-overlay/kagome-crates/vcpkg.json b/vcpkg-overlay/qdrvm-crates/vcpkg.json similarity index 84% rename from vcpkg-overlay/kagome-crates/vcpkg.json rename to vcpkg-overlay/qdrvm-crates/vcpkg.json index 6104ca7..4686377 100644 --- a/vcpkg-overlay/kagome-crates/vcpkg.json +++ b/vcpkg-overlay/qdrvm-crates/vcpkg.json @@ -1,5 +1,5 @@ { - "name": "kagome-crates", + "name": "qdrvm-crates", "version": "1.0.2", "dependencies": [ { "name": "vcpkg-cmake", "host": true }, diff --git a/vcpkg-overlay/qtils/portfile.cmake b/vcpkg-overlay/qtils/portfile.cmake index e8351ca..4d411a5 100644 --- a/vcpkg-overlay/qtils/portfile.cmake +++ b/vcpkg-overlay/qtils/portfile.cmake @@ -5,7 +5,9 @@ vcpkg_from_github( REF refs/tags/v0.1.3 SHA512 09e759f82ce273b602ec851ed7b5bb5fe1e0471a35a9874d04190946a38b8cf5dcea0923af91db798c53c01970c7bfe3452fb956efd66e067ecbec3e0c99fb02 ) -vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") +vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + -DHUNTER_ENABLED=OFF) vcpkg_cmake_install() vcpkg_cmake_config_fixup(PACKAGE_NAME "qtils") file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") diff --git a/vcpkg.json b/vcpkg.json index 43afc63..bdbe386 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,21 +1,64 @@ { - "name": "cpp-jam", - "version": "0.0.1", + "name": "morum", + "version": "0.1.0", + "features": { + "test": { + "description": "Build tests", + "dependencies": [ + { + "name": "gtest", + "version>=": "1.15.2" + } + ] + }, + "benchmark": { + "description": "Build benchmarks", + "dependencies": [ + { + "name": "benchmark", + "version>=": "1.9.0" + } + ] + }, + "tracy": { + "description": "Enable tracy", + "dependencies": [ + { + "name": "tracy", + "version>=": "0.11.1" + } + ] + } + }, "dependencies": [ "qtils", "scale", "fmt", "soralog", - "kagome-crates", - "libb2", + "qdrvm-crates", + "blake2", "boost-di", "boost-program-options", "boost-asio", "boost-beast", "prometheus-cpp", + "qtils", + { + "name": "yaml-cpp", + "version>=": "0.8.0#3" + }, + { + "name": "fmt", + "version>=": "10.2.1#2" + }, + { + "name": "rocksdb", + "version>=": "10.1.3" + }, + { + "name": "nlohmann-json", + "version>=": "3.11.3" + }, "ftxui" - ], - "features": { - "test": { "description": "Test", "dependencies": ["gtest"]} - } + ] }