From 8a1032ec4738e9b592b45500c1cf47c0e1820ad3 Mon Sep 17 00:00:00 2001 From: PhilWindle <60546371+PhilWindle@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:12:37 +0100 Subject: [PATCH] feat: Native Merkle Trees (#7037) This PR is the first of several where we are moving to use native implementations of Merkle Trees for performance reasons. It includes: 1. The core tree implementations for both indexed trees and append only trees. 2. The introduction of LMDB as a dependency that we retrieve from GIT and build ourselves. 3. The creation of a set of RAII wrapper objects around the LMDB concepts. 4. The creation of a committed/uncommitted store on top of LMDB used by the trees for state management --------- Co-authored-by: IlyasRidhuan Co-authored-by: Alex Gherghisan --- .github/workflows/ci.yml | 2 +- barretenberg/cpp/CMakeLists.txt | 1 + barretenberg/cpp/Earthfile | 2 +- barretenberg/cpp/cmake/lmdb.cmake | 27 + barretenberg/cpp/scripts/merkle_tree_tests.sh | 13 + barretenberg/cpp/src/CMakeLists.txt | 10 +- .../append_only_tree_bench/CMakeLists.txt | 2 +- .../append_only_tree.bench.cpp | 53 +- .../benchmark/client_ivc_bench/CMakeLists.txt | 2 +- .../indexed_tree_bench/CMakeLists.txt | 2 +- .../indexed_tree_bench/indexed_tree.bench.cpp | 64 +- .../merkle_tree_bench/CMakeLists.txt | 2 +- .../benchmark/simulator_bench/CMakeLists.txt | 2 +- .../benchmark/ultra_bench/CMakeLists.txt | 5 +- .../common/parallel_for_queued.cpp | 96 +- .../src/barretenberg/common/thread_pool.cpp | 77 ++ .../src/barretenberg/common/thread_pool.hpp | 40 + .../src/barretenberg/crypto/CMakeLists.txt | 5 +- .../crypto/merkle_tree/CMakeLists.txt | 19 +- .../append_only_tree/append_only_tree.hpp | 449 +++++-- .../append_only_tree.test.cpp | 699 ++++++++++- .../crypto/merkle_tree/array_store.hpp | 37 - .../crypto/merkle_tree/fixtures.hpp | 50 + .../merkle_tree/indexed_tree/indexed_leaf.hpp | 172 ++- .../merkle_tree/indexed_tree/indexed_tree.hpp | 785 ++++++++---- .../indexed_tree/indexed_tree.test.cpp | 1050 ++++++++++++++--- .../merkle_tree/indexed_tree/leaves_cache.cpp | 49 - .../merkle_tree/indexed_tree/leaves_cache.hpp | 27 - .../merkle_tree/lmdb_store/callbacks.cpp | 148 +++ .../merkle_tree/lmdb_store/callbacks.hpp | 71 ++ .../merkle_tree/lmdb_store/lmdb_database.cpp | 36 + .../merkle_tree/lmdb_store/lmdb_database.hpp | 33 + .../lmdb_store/lmdb_db_transaction.cpp | 12 + .../lmdb_store/lmdb_db_transaction.hpp | 23 + .../lmdb_store/lmdb_environment.cpp | 59 + .../lmdb_store/lmdb_environment.hpp | 44 + .../lmdb_store/lmdb_read_transaction.cpp | 41 + .../lmdb_store/lmdb_read_transaction.hpp | 117 ++ .../merkle_tree/lmdb_store/lmdb_store.cpp | 28 + .../merkle_tree/lmdb_store/lmdb_store.hpp | 37 + .../lmdb_store/lmdb_store.test.cpp | 536 +++++++++ .../lmdb_store/lmdb_transaction.cpp | 29 + .../lmdb_store/lmdb_transaction.hpp | 41 + .../lmdb_store/lmdb_write_transaction.cpp | 52 + .../lmdb_store/lmdb_write_transaction.hpp | 47 + .../crypto/merkle_tree/memory_tree.hpp | 13 +- .../merkle_tree/node_store/array_store.hpp | 94 ++ .../node_store/cached_tree_store.hpp | 505 ++++++++ .../merkle_tree/node_store/tree_meta.hpp | 25 + .../nullifier_tree/nullifier_leaf.hpp | 22 +- .../nullifier_tree/nullifier_memory_tree.hpp | 34 +- .../nullifier_memory_tree.test.cpp | 162 +-- .../nullifier_tree/nullifier_tree.cpp | 13 +- .../nullifier_tree/nullifier_tree.test.cpp | 17 +- .../crypto/merkle_tree/response.hpp | 101 ++ .../crypto/merkle_tree/signal.hpp | 63 + .../barretenberg/crypto/merkle_tree/types.hpp | 6 + .../cpp/src/barretenberg/messaging/header.hpp | 62 + .../barretenberg/messaging/stream_parser.hpp | 69 ++ 59 files changed, 5353 insertions(+), 929 deletions(-) create mode 100644 barretenberg/cpp/cmake/lmdb.cmake create mode 100755 barretenberg/cpp/scripts/merkle_tree_tests.sh create mode 100644 barretenberg/cpp/src/barretenberg/common/thread_pool.cpp create mode 100644 barretenberg/cpp/src/barretenberg/common/thread_pool.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/array_store.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/fixtures.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.test.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.cpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/array_store.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/tree_meta.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/response.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/signal.hpp create mode 100644 barretenberg/cpp/src/barretenberg/crypto/merkle_tree/types.hpp create mode 100644 barretenberg/cpp/src/barretenberg/messaging/header.hpp create mode 100644 barretenberg/cpp/src/barretenberg/messaging/stream_parser.hpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6588dd5fb2a..994ff3e40e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,7 +277,7 @@ jobs: # limit our parallelism to half our cores run: earthly-ci --no-output +preset-gcc - # barretenberg (prover) native and AVM (public VM) tests + # barretenberg (prover) native, AVM (public VM) and Merkle tree (world state) tests # only ran on x86 for resource reasons (memory intensive) bb-native-tests: needs: [build-images, changes] diff --git a/barretenberg/cpp/CMakeLists.txt b/barretenberg/cpp/CMakeLists.txt index 638daa5940f..22fa253f8e4 100644 --- a/barretenberg/cpp/CMakeLists.txt +++ b/barretenberg/cpp/CMakeLists.txt @@ -152,6 +152,7 @@ include(cmake/gtest.cmake) include(cmake/benchmark.cmake) include(cmake/module.cmake) include(cmake/msgpack.cmake) +include(cmake/lmdb.cmake) # We do not need to bloat barretenberg.wasm with gzip functionality in a browser context as the browser can do this if (NOT WASM) diff --git a/barretenberg/cpp/Earthfile b/barretenberg/cpp/Earthfile index bef0a1a3127..bc9cbfa2670 100644 --- a/barretenberg/cpp/Earthfile +++ b/barretenberg/cpp/Earthfile @@ -31,7 +31,7 @@ preset-release: preset-release-assert: FROM +source - RUN cmake --preset clang16-assert -Bbuild && cmake --build build --target bb && rm -rf build/{deps,lib,src} + RUN cmake --preset clang16-assert -Bbuild && cmake --build build --target bb crypto_merkle_tree_tests && rm -rf build/{deps,lib,src} SAVE ARTIFACT build/bin preset-debug: diff --git a/barretenberg/cpp/cmake/lmdb.cmake b/barretenberg/cpp/cmake/lmdb.cmake new file mode 100644 index 00000000000..ca24a99c802 --- /dev/null +++ b/barretenberg/cpp/cmake/lmdb.cmake @@ -0,0 +1,27 @@ +include(ExternalProject) + +set(LMDB_PREFIX "${CMAKE_BINARY_DIR}/_deps/lmdb") +set(LMDB_INCLUDE "${LMDB_PREFIX}/src/lmdb_repo/libraries/liblmdb") +set(LMDB_LIB "${LMDB_INCLUDE}/liblmdb.a") +set(LMDB_OBJECT "${LMDB_INCLUDE}/*.o") + +ExternalProject_Add( + lmdb_repo + PREFIX ${LMDB_PREFIX} + GIT_REPOSITORY "https://github.com/LMDB/lmdb.git" + GIT_TAG ddd0a773e2f44d38e4e31ec9ed81af81f4e4ccbb + BUILD_IN_SOURCE YES + CONFIGURE_COMMAND "" # No configure step + BUILD_COMMAND make -C libraries/liblmdb -e XCFLAGS=-fPIC liblmdb.a + INSTALL_COMMAND "" + UPDATE_COMMAND "" # No update step + BUILD_BYPRODUCTS ${LMDB_LIB} ${LMDB_INCLUDE} +) + +add_library(lmdb STATIC IMPORTED GLOBAL) +add_dependencies(lmdb lmdb_repo) +set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION ${LMDB_LIB}) + +add_library(lmdb_objects OBJECT IMPORTED GLOBAL) +add_dependencies(lmdb_objects lmdb_repo) +set_target_properties(lmdb_objects PROPERTIES IMPORTED_LOCATION ${LMDB_OBJECT}) diff --git a/barretenberg/cpp/scripts/merkle_tree_tests.sh b/barretenberg/cpp/scripts/merkle_tree_tests.sh new file mode 100755 index 00000000000..6cd44b2374c --- /dev/null +++ b/barretenberg/cpp/scripts/merkle_tree_tests.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +# run commands relative to parent directory +cd $(dirname $0)/.. + +DEFAULT_TESTS=PersistedIndexedTreeTest.*:PersistedAppendOnlyTreeTest.*:LMDBStoreTest.* +TEST=${1:-$DEFAULT_TESTS} +PRESET=${PRESET:-clang16} + +cmake --build --preset $PRESET --target crypto_merkle_tree_tests +./build/bin/crypto_merkle_tree_tests --gtest_filter=$TEST diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 924efbb9ab1..a2c8ba9eb15 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -21,6 +21,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "Compiling with memory checks.") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address") endif() + if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18) # We target clang18 and need this, eventually warning should be fixed or this will be unconditional. add_compile_options(-Wno-vla-cxx-extension) @@ -41,7 +42,7 @@ if(WASM) add_link_options(-Wl,--export-memory,--import-memory,--stack-first,-z,stack-size=1048576,--max-memory=4294967296) endif() -include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${MSGPACK_INCLUDE} ${TRACY_INCLUDE}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${MSGPACK_INCLUDE} ${TRACY_INCLUDE} ${LMDB_INCLUDE}) # I feel this should be limited to ecc, however it's currently used in headers that go across libraries, # and there currently isn't an easy way to inherit the DDISABLE_ASM parameter. @@ -87,7 +88,6 @@ add_subdirectory(barretenberg/ultra_honk) add_subdirectory(barretenberg/vm) add_subdirectory(barretenberg/wasi) - if(SMT) add_subdirectory(barretenberg/smt_verification) endif() @@ -140,7 +140,6 @@ set(BARRETENBERG_TARGET_OBJECTS $ $ $ - $ $ $ $ @@ -160,6 +159,11 @@ if(NOT DISABLE_AZTEC_VM) list(APPEND BARRETENBERG_TARGET_OBJECTS $) endif() +if(NOT WASM) + # enable merkle trees + list(APPEND BARRETENBERG_TARGET_OBJECTS $) +endif() + add_library( barretenberg STATIC diff --git a/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/CMakeLists.txt index 7270e7e9530..c2f2a6c01c5 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/CMakeLists.txt @@ -1 +1 @@ -barretenberg_module(append_only_tree_bench crypto_poseidon2 crypto_merkle_tree) +barretenberg_module(append_only_tree_bench crypto_poseidon2 crypto_pedersen_hash crypto_merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/append_only_tree.bench.cpp b/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/append_only_tree.bench.cpp index bde03dcb4ff..833ce035d1e 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/append_only_tree.bench.cpp +++ b/barretenberg/cpp/src/barretenberg/benchmark/append_only_tree_bench/append_only_tree.bench.cpp @@ -1,25 +1,46 @@ #include "barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.hpp" -#include "barretenberg/crypto/merkle_tree/array_store.hpp" +#include "barretenberg/common/thread_pool.hpp" +#include "barretenberg/crypto/merkle_tree/fixtures.hpp" #include "barretenberg/crypto/merkle_tree/hash.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/array_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" #include "barretenberg/numeric/random/engine.hpp" #include +#include +#include using namespace benchmark; using namespace bb::crypto::merkle_tree; -using Pedersen = AppendOnlyTree; -using Poseidon2 = AppendOnlyTree; +namespace { +using StoreType = CachedTreeStore; + +using Pedersen = AppendOnlyTree; +using Poseidon2 = AppendOnlyTree; const size_t TREE_DEPTH = 32; const size_t MAX_BATCH_SIZE = 128; -namespace { -auto& random_engine = bb::numeric::get_randomness(); -} // namespace - template void perform_batch_insert(TreeType& tree, const std::vector& values) { - tree.add_values(values); + Signal signal(1); + auto completion = [&](const TypedResponse&) -> void { signal.signal_level(0); }; + + tree.add_values(values, completion); + signal.wait_for_level(0); +} + +template void commit_tree(TreeType& tree) +{ + Signal signal(1); + auto completion = [&]() -> void { signal.signal_level(0); }; + tree.commit(completion); + signal.wait_for_level(0); } template void append_only_tree_bench(State& state) noexcept @@ -27,8 +48,16 @@ template void append_only_tree_bench(State& state) noexcept const size_t batch_size = size_t(state.range(0)); const size_t depth = TREE_DEPTH; - ArrayStore store(depth, 1024 * 1024); - TreeType tree = TreeType(store, depth); + std::string directory = random_temp_directory(); + std::string name = random_string(); + std::filesystem::create_directories(directory); + uint32_t num_threads = 16; + LMDBEnvironment environment = LMDBEnvironment(directory, 1024 * 1024, 2, num_threads); + + LMDBStore db(environment, name, false, false, integer_key_cmp); + StoreType store(name, depth, db); + ThreadPool workers(num_threads); + TreeType tree = TreeType(store, workers); for (auto _ : state) { state.PauseTiming(); @@ -39,6 +68,8 @@ template void append_only_tree_bench(State& state) noexcept state.ResumeTiming(); perform_batch_insert(tree, values); } + + std::filesystem::remove_all(directory); } BENCHMARK(append_only_tree_bench) ->Unit(benchmark::kMillisecond) @@ -51,4 +82,6 @@ BENCHMARK(append_only_tree_bench) ->Range(2, MAX_BATCH_SIZE) ->Iterations(1000); +} // namespace + BENCHMARK_MAIN(); \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/benchmark/client_ivc_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/client_ivc_bench/CMakeLists.txt index 2757bea5d94..2ccd992aec3 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/client_ivc_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/client_ivc_bench/CMakeLists.txt @@ -1 +1 @@ -barretenberg_module(client_ivc_bench client_ivc stdlib_honk_verifier stdlib_sha256 crypto_merkle_tree stdlib_primitives) +barretenberg_module(client_ivc_bench client_ivc stdlib_honk_verifier stdlib_sha256 stdlib_primitives) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/CMakeLists.txt index 998bbdf55de..59caee96f97 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/CMakeLists.txt @@ -1 +1 @@ -barretenberg_module(indexed_tree_bench crypto_poseidon2 crypto_merkle_tree) +barretenberg_module(indexed_tree_bench crypto_poseidon2 crypto_pedersen_hash crypto_merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/indexed_tree.bench.cpp b/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/indexed_tree.bench.cpp index 5e7c0f10abb..ae653a1dad3 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/indexed_tree.bench.cpp +++ b/barretenberg/cpp/src/barretenberg/benchmark/indexed_tree_bench/indexed_tree.bench.cpp @@ -1,27 +1,33 @@ #include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp" -#include "barretenberg/crypto/merkle_tree/array_store.hpp" +#include "barretenberg/crypto/merkle_tree/fixtures.hpp" #include "barretenberg/crypto/merkle_tree/hash.hpp" -#include "barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" #include "barretenberg/numeric/random/engine.hpp" #include +#include using namespace benchmark; using namespace bb::crypto::merkle_tree; -using Poseidon2 = IndexedTree; -using Pedersen = IndexedTree; +using StoreType = CachedTreeStore; -const size_t TREE_DEPTH = 32; -const size_t MAX_BATCH_SIZE = 128; +using Poseidon2 = IndexedTree; +using Pedersen = IndexedTree; -namespace { -auto& random_engine = bb::numeric::get_randomness(); -} // namespace +const size_t TREE_DEPTH = 40; +const size_t MAX_BATCH_SIZE = 128; -template -void perform_batch_insert(TreeType& tree, const std::vector& values, bool single_threaded) +template void add_values(TreeType& tree, const std::vector& values) { - tree.add_or_update_values(values, single_threaded); + Signal signal(1); + auto completion = [&](const auto&) -> void { signal.signal_level(0); }; + + tree.add_or_update_values(values, completion); + signal.wait_for_level(0); } template void multi_thread_indexed_tree_bench(State& state) noexcept @@ -29,17 +35,25 @@ template void multi_thread_indexed_tree_bench(State& state) const size_t batch_size = size_t(state.range(0)); const size_t depth = TREE_DEPTH; - ArrayStore store(depth, 1024 * 1024); - TreeType tree = TreeType(store, depth, batch_size); + std::string directory = random_temp_directory(); + std::string name = random_string(); + std::filesystem::create_directories(directory); + uint32_t num_threads = 16; + LMDBEnvironment environment = LMDBEnvironment(directory, 1024 * 1024, 2, num_threads); + + LMDBStore db(environment, name, false, false, integer_key_cmp); + StoreType store(name, depth, db); + ThreadPool workers(num_threads); + TreeType tree = TreeType(store, workers, batch_size); for (auto _ : state) { state.PauseTiming(); - std::vector values(batch_size); + std::vector values(batch_size); for (size_t i = 0; i < batch_size; ++i) { values[i] = fr(random_engine.get_random_uint256()); } state.ResumeTiming(); - perform_batch_insert(tree, values, false); + add_values(tree, values); } } @@ -48,17 +62,25 @@ template void single_thread_indexed_tree_bench(State& state) const size_t batch_size = size_t(state.range(0)); const size_t depth = TREE_DEPTH; - ArrayStore store(depth, 1024 * 1024); - TreeType tree = TreeType(store, depth, batch_size); + std::string directory = random_temp_directory(); + std::string name = random_string(); + std::filesystem::create_directories(directory); + uint32_t num_threads = 1; + LMDBEnvironment environment = LMDBEnvironment(directory, 1024 * 1024, 2, num_threads); + + LMDBStore db(environment, name, false, false, integer_key_cmp); + StoreType store(name, depth, db); + ThreadPool workers(num_threads); + TreeType tree = TreeType(store, workers, batch_size); for (auto _ : state) { state.PauseTiming(); - std::vector values(batch_size); + std::vector values(batch_size); for (size_t i = 0; i < batch_size; ++i) { values[i] = fr(random_engine.get_random_uint256()); } state.ResumeTiming(); - perform_batch_insert(tree, values, true); + add_values(tree, values); } } BENCHMARK(single_thread_indexed_tree_bench) @@ -84,4 +106,4 @@ BENCHMARK(multi_thread_indexed_tree_bench) ->Range(2, MAX_BATCH_SIZE) ->Iterations(1000); -BENCHMARK_MAIN(); \ No newline at end of file +BENCHMARK_MAIN(); diff --git a/barretenberg/cpp/src/barretenberg/benchmark/merkle_tree_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/merkle_tree_bench/CMakeLists.txt index f497e924d56..c5501d1016a 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/merkle_tree_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/merkle_tree_bench/CMakeLists.txt @@ -1 +1 @@ -barretenberg_module(merkle_tree_bench crypto_poseidon2 crypto_merkle_tree) +barretenberg_module(merkle_tree_bench crypto_poseidon2 crypto_pedersen_hash crypto_merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/simulator_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/simulator_bench/CMakeLists.txt index 5cad28d95dc..caff4f1c2bc 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/simulator_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/simulator_bench/CMakeLists.txt @@ -1 +1 @@ - barretenberg_module(simulator_bench stdlib_honk_verifier stdlib_sha256 crypto_merkle_tree) \ No newline at end of file + barretenberg_module(simulator_bench stdlib_honk_verifier stdlib_sha256 stdlib_pedersen_hash) \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/benchmark/ultra_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/ultra_bench/CMakeLists.txt index 2fe5a61098b..34d8e03f7b2 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/ultra_bench/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/ultra_bench/CMakeLists.txt @@ -3,6 +3,7 @@ barretenberg_module( ultra_honk stdlib_sha256 stdlib_keccak - crypto_merkle_tree - plonk + stdlib_pedersen_hash + stdlib_poseidon2 + plonk ) diff --git a/barretenberg/cpp/src/barretenberg/common/parallel_for_queued.cpp b/barretenberg/cpp/src/barretenberg/common/parallel_for_queued.cpp index acaf920fd03..82a1a02d727 100644 --- a/barretenberg/cpp/src/barretenberg/common/parallel_for_queued.cpp +++ b/barretenberg/cpp/src/barretenberg/common/parallel_for_queued.cpp @@ -1,6 +1,7 @@ #ifndef NO_MULTITHREADING #include "log.hpp" #include "thread.hpp" +#include "thread_pool.hpp" #include #include #include @@ -9,101 +10,6 @@ #include #include -namespace { -class ThreadPool { - public: - ThreadPool(size_t num_threads); - ThreadPool(const ThreadPool& other) = delete; - ThreadPool(ThreadPool&& other) = delete; - ~ThreadPool(); - - ThreadPool& operator=(const ThreadPool& other) = delete; - ThreadPool& operator=(ThreadPool&& other) = delete; - - void enqueue(const std::function& task); - void wait(); - - private: - std::vector workers; - std::queue> tasks; - std::mutex tasks_mutex; - std::condition_variable condition; - std::condition_variable finished_condition; - std::atomic tasks_running; - bool stop = false; - - void worker_loop(size_t thread_index); -}; - -ThreadPool::ThreadPool(size_t num_threads) -{ - workers.reserve(num_threads); - for (size_t i = 0; i < num_threads; ++i) { - workers.emplace_back(&ThreadPool::worker_loop, this, i); - } -} - -ThreadPool::~ThreadPool() -{ - { - std::unique_lock lock(tasks_mutex); - stop = true; - } - condition.notify_all(); - for (auto& worker : workers) { - worker.join(); - } -} - -void ThreadPool::enqueue(const std::function& task) -{ - { - std::unique_lock lock(tasks_mutex); - tasks.push(task); - } - condition.notify_one(); -} - -void ThreadPool::wait() -{ - std::unique_lock lock(tasks_mutex); - finished_condition.wait(lock, [this] { return tasks.empty() && tasks_running == 0; }); -} - -void ThreadPool::worker_loop(size_t /*unused*/) -{ - // info("created worker ", worker_num); - while (true) { - std::function task; - { - std::unique_lock lock(tasks_mutex); - condition.wait(lock, [this] { return !tasks.empty() || stop; }); - - if (tasks.empty() && stop) { - break; - } - - task = tasks.front(); - tasks.pop(); - tasks_running++; - } - // info("worker ", worker_num, " processing a task!"); - task(); - // info("task done"); - { - std::unique_lock lock(tasks_mutex); - tasks_running--; - if (tasks.empty() && tasks_running == 0) { - // info("notifying main thread"); - finished_condition.notify_all(); - } - } - // info("worker ", worker_num, " done!"); - } - // info("worker exit ", worker_num); -} -} // namespace - namespace bb { /** * A thread pooled strategey that assumed that thread pools would be more efficient than spawning threads. diff --git a/barretenberg/cpp/src/barretenberg/common/thread_pool.cpp b/barretenberg/cpp/src/barretenberg/common/thread_pool.cpp new file mode 100644 index 00000000000..588173e6919 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/common/thread_pool.cpp @@ -0,0 +1,77 @@ + +#ifndef NO_MULTITHREADING + +#include "thread_pool.hpp" +#include "barretenberg/common/log.hpp" +namespace bb { + +ThreadPool::ThreadPool(size_t num_threads) +{ + workers.reserve(num_threads); + for (size_t i = 0; i < num_threads; ++i) { + workers.emplace_back(&ThreadPool::worker_loop, this, i); + } +} + +ThreadPool::~ThreadPool() +{ + { + std::unique_lock lock(tasks_mutex); + stop = true; + } + condition.notify_all(); + for (auto& worker : workers) { + worker.join(); + } +} + +void ThreadPool::enqueue(const std::function& task) +{ + { + std::unique_lock lock(tasks_mutex); + tasks.push(task); + } + condition.notify_one(); +} + +void ThreadPool::wait() +{ + std::unique_lock lock(tasks_mutex); + finished_condition.wait(lock, [this] { return tasks.empty() && tasks_running == 0; }); +} + +void ThreadPool::worker_loop(size_t /*unused*/) +{ + // info("created worker ", worker_num); + while (true) { + std::function task; + { + std::unique_lock lock(tasks_mutex); + condition.wait(lock, [this] { return !tasks.empty() || stop; }); + + if (tasks.empty() && stop) { + break; + } + + task = tasks.front(); + tasks.pop(); + tasks_running++; + } + // info("worker ", worker_num, " processing a task!"); + task(); + // info("task done"); + { + std::unique_lock lock(tasks_mutex); + tasks_running--; + if (tasks.empty() && tasks_running == 0) { + // info("notifying main thread"); + finished_condition.notify_all(); + } + } + // info("worker ", worker_num, " done!"); + } + // info("worker exit ", worker_num); +} +} // namespace bb + +#endif diff --git a/barretenberg/cpp/src/barretenberg/common/thread_pool.hpp b/barretenberg/cpp/src/barretenberg/common/thread_pool.hpp new file mode 100644 index 00000000000..945f8a0aa3b --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/common/thread_pool.hpp @@ -0,0 +1,40 @@ +#pragma once + +#ifndef NO_MULTITHREADING + +#include +#include +#include +#include +#include +#include +#include +namespace bb { +class ThreadPool { + public: + ThreadPool(size_t num_threads); + ThreadPool(const ThreadPool& other) = delete; + ThreadPool(ThreadPool&& other) = delete; + ~ThreadPool(); + + ThreadPool& operator=(const ThreadPool& other) = delete; + ThreadPool& operator=(ThreadPool&& other) = delete; + + void enqueue(const std::function& task); + void wait(); + size_t num_threads() { return workers.size(); }; + + private: + std::vector workers; + std::queue> tasks; + std::mutex tasks_mutex; + std::condition_variable condition; + std::condition_variable finished_condition; + std::atomic tasks_running; + bool stop = false; + + void worker_loop(size_t thread_index); +}; +} // namespace bb + +#endif diff --git a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt index 1319f7bc668..f165b403102 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt @@ -11,4 +11,7 @@ add_subdirectory(sha256) add_subdirectory(ecdsa) add_subdirectory(aes128) add_subdirectory(poseidon2) -add_subdirectory(merkle_tree) \ No newline at end of file + +if (NOT WASM) + add_subdirectory(merkle_tree) +endif() diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/CMakeLists.txt index 475b8ae9829..4749a1a2021 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/CMakeLists.txt @@ -1,7 +1,16 @@ +# merkle tree is agnostic to hash function barretenberg_module( crypto_merkle_tree - stdlib_primitives - stdlib_blake3s - stdlib_pedersen_hash - crypto_poseidon2 -) \ No newline at end of file + lmdb +) + +if (NOT FUZZING) + # but the tests use pedersen and poseidon + target_link_libraries(crypto_merkle_tree_tests PRIVATE stdlib_pedersen_hash stdlib_poseidon2) + add_dependencies(crypto_merkle_tree_tests lmdb_repo) + add_dependencies(crypto_merkle_tree_test_objects lmdb_repo) +endif() + +add_dependencies(crypto_merkle_tree lmdb_repo) +add_dependencies(crypto_merkle_tree_objects lmdb_repo) + diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.hpp index 7170a9292ea..6041a9b4930 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.hpp @@ -1,137 +1,432 @@ #pragma once #include "../hash_path.hpp" +#include "../node_store//tree_meta.hpp" +#include "../response.hpp" +#include "../types.hpp" +#include "barretenberg/common/thread_pool.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/numeric/bitop/pow.hpp" +#include +#include +#include +#include +#include +#include namespace bb::crypto::merkle_tree { using namespace bb; -typedef uint256_t index_t; - /** * @brief Implements a simple append-only merkle tree - * Accepts template argument of the type of store backing the tree and the hashing policy + * All methods are asynchronous unless specified as otherwise + * Accepts template arguments of the type of store backing the tree and the hashing policy + * Accepts the store as an argument on construction as well as a thread pool instance + * Asynchronous methods are exeucted on the provided thread pool * */ template class AppendOnlyTree { public: - AppendOnlyTree(Store& store, size_t depth, uint8_t tree_id = 0); + using StoreType = Store; + + // Asynchronous methods accept these callback function types as arguments + using AppendCompletionCallback = std::function&)>; + using MetaDataCallback = std::function&)>; + using HashPathCallback = std::function&)>; + using FindLeafCallback = std::function&)>; + using GetLeafCallback = std::function&)>; + using CommitCallback = std::function; + using RollbackCallback = std::function; + + // Only construct from provided store and thread pool, no copies or moves + AppendOnlyTree(Store& store, ThreadPool& workers); AppendOnlyTree(AppendOnlyTree const& other) = delete; AppendOnlyTree(AppendOnlyTree&& other) = delete; - virtual ~AppendOnlyTree(); + AppendOnlyTree& operator=(AppendOnlyTree const& other) = delete; + AppendOnlyTree& operator=(AppendOnlyTree const&& other) = delete; + virtual ~AppendOnlyTree() = default; /** * @brief Adds a single value to the end of the tree + * @param value The value to be added + * @param on_completion Callback to be called on completion */ - virtual fr add_value(const fr& value); + virtual void add_value(const fr& value, const AppendCompletionCallback& on_completion); /** * @brief Adds the given set of values to the end of the tree + * @param values The values to be added + * @param on_completion Callback to be called on completion + */ + virtual void add_values(const std::vector& values, const AppendCompletionCallback& on_completion); + + /** + * @brief Returns the sibling path from the leaf at the given index to the root + * @param index The index at which to read the sibling path + * @param on_completion Callback to be called on completion + * @param includeUncommitted Whether to include uncommitted changes */ - virtual fr add_values(const std::vector& values); + void get_sibling_path(const index_t& index, const HashPathCallback& on_completion, bool includeUncommitted) const; /** - * @brief Returns the index of the right-most populated leaf in the tree + * @brief Get the subtree sibling path object + * + * @param subtree_depth The depth of the subtree + * @param on_completion Callback to be called on completion + * @param includeUncommitted Whether to include uncommitted changes */ - index_t size() const; + void get_subtree_sibling_path(uint32_t subtree_depth, + const HashPathCallback& on_completion, + bool includeUncommitted) const; /** - * @brief Returns the root of the tree + * @brief Get the subtree sibling path object to a leaf + * + * @param leaf_index The depth of the subtree + * @param subtree_depth The depth of the subtree + * @param on_completion Callback to be called on completion + * @param includeUncommitted Whether to include uncommitted changes */ - fr root() const; + void get_subtree_sibling_path(index_t leaf_index, + uint32_t subtree_depth, + const HashPathCallback& on_completion, + bool includeUncommitted) const; /** - * @brief Returns the depth of the tree + * @brief Returns the tree meta data + * @param includeUncommitted Whether to include uncommitted changes + * @param on_completion Callback to be called on completion */ - size_t depth() const; + void get_meta_data(bool includeUncommitted, const MetaDataCallback& on_completion) const; /** - * @brief Returns the hash path from the leaf at the given index to the root + * @brief Returns the leaf value at the provided index + * @param index The index of the leaf to be retrieved + * @param includeUncommitted Whether to include uncommitted changes + * @param on_completion Callback to be called on completion */ - fr_hash_path get_hash_path(const index_t& index) const; + void get_leaf(const index_t& index, bool includeUncommitted, const GetLeafCallback& completion) const; + + /** + * @brief Returns the index of the provided leaf in the tree + */ + void find_leaf_index(const fr& leaf, bool includeUncommitted, const FindLeafCallback& on_completion) const; + + /** + * @brief Returns the index of the provided leaf in the tree only if it exists after the index value provided + */ + void find_leaf_index_from(const fr& leaf, + index_t start_index, + bool includeUncommitted, + const FindLeafCallback& on_completion) const; + + /** + * @brief Commit the tree to the backing store + */ + void commit(const CommitCallback& on_completion); + + /** + * @brief Rollback the uncommitted changes + */ + void rollback(const RollbackCallback& on_completion); + + /** + * @brief Synchronous method to retrieve the depth of the tree + */ + uint32_t depth() const { return depth_; } protected: - fr get_element_or_zero(size_t level, const index_t& index) const; + using ReadTransaction = typename Store::ReadTransaction; + using ReadTransactionPtr = typename Store::ReadTransactionPtr; + fr get_element_or_zero(uint32_t level, const index_t& index, ReadTransaction& tx, bool includeUncommitted) const; + + void write_node(uint32_t level, const index_t& index, const fr& value); + std::pair read_node(uint32_t level, + const index_t& index, + ReadTransaction& tx, + bool includeUncommitted) const; + + void add_values_internal(std::shared_ptr> values, + fr& new_root, + index_t& new_size, + bool update_index); + + void add_values_internal(const std::vector& values, + const AppendCompletionCallback& on_completion, + bool update_index); - void write_node(size_t level, const index_t& index, const fr& value); - std::pair read_node(size_t level, const index_t& index) const; + fr_sibling_path get_subtree_sibling_path_internal(index_t leaf_index, + uint32_t subtree_depth, + ReadTransaction& tx, + bool includeUncommitted) const; Store& store_; - size_t depth_; - uint8_t tree_id_; + uint32_t depth_; + std::string name_; + uint64_t max_size_; std::vector zero_hashes_; - fr root_; - index_t size_; + ThreadPool& workers_; }; template -AppendOnlyTree::AppendOnlyTree(Store& store, size_t depth, uint8_t tree_id) +AppendOnlyTree::AppendOnlyTree(Store& store, ThreadPool& workers) : store_(store) - , depth_(depth) - , tree_id_(tree_id) + , workers_(workers) { - zero_hashes_.resize(depth + 1); + index_t stored_size = 0; + bb::fr stored_root = fr::zero(); + { + // start by reading the meta data from the backing store + ReadTransactionPtr tx = store_.create_read_transaction(); + store_.get_full_meta(stored_size, stored_root, name_, depth_, *tx, false); + } + zero_hashes_.resize(depth_ + 1); // Create the zero hashes for the tree auto current = HashingPolicy::zero_hash(); - for (size_t i = depth; i > 0; --i) { + for (size_t i = depth_; i > 0; --i) { zero_hashes_[i] = current; current = HashingPolicy::hash_pair(current, current); } zero_hashes_[0] = current; - root_ = current; + + if (stored_size == 0) { + // if the tree is empty then we want to write the initial root + store_.put_meta(0, current); + store_.commit(); + } + max_size_ = numeric::pow64(2, depth_); } -template AppendOnlyTree::~AppendOnlyTree() {} +template +void AppendOnlyTree::get_meta_data(bool includeUncommitted, + const MetaDataCallback& on_completion) const +{ + auto job = [=, this]() { + execute_and_report( + [=, this](TypedResponse& response) { + ReadTransactionPtr tx = store_.create_read_transaction(); + store_.get_meta(response.inner.size, response.inner.root, *tx, includeUncommitted); + response.inner.depth = depth_; + }, + on_completion); + }; + workers_.enqueue(job); +} -template index_t AppendOnlyTree::size() const +template +void AppendOnlyTree::get_sibling_path(const index_t& index, + const HashPathCallback& on_completion, + bool includeUncommitted) const { - return size_; + auto job = [=, this]() { + execute_and_report( + [=, this](TypedResponse& response) { + index_t current_index = index; + ReadTransactionPtr tx = store_.create_read_transaction(); + for (uint32_t level = depth_; level > 0; --level) { + bool is_right = static_cast(current_index & 0x01); + fr sibling = is_right ? get_element_or_zero(level, current_index - 1, *tx, includeUncommitted) + : get_element_or_zero(level, current_index + 1, *tx, includeUncommitted); + response.inner.path.emplace_back(sibling); + current_index >>= 1; + } + }, + on_completion); + }; + workers_.enqueue(job); } -template fr AppendOnlyTree::root() const +template +void AppendOnlyTree::get_subtree_sibling_path(const uint32_t subtree_depth, + const HashPathCallback& on_completion, + bool includeUncommitted) const { - return root_; + auto job = [=, this]() { + execute_and_report( + [=, this](TypedResponse& response) { + ReadTransactionPtr tx = store_.create_read_transaction(); + index_t index_of_next_leaf = 0; + bb::fr root; + store_.get_meta(index_of_next_leaf, root, *tx, includeUncommitted); + response.inner.path = + get_subtree_sibling_path_internal(index_of_next_leaf, subtree_depth, *tx, includeUncommitted); + }, + on_completion); + }; + workers_.enqueue(job); } -template size_t AppendOnlyTree::depth() const +template +void AppendOnlyTree::get_subtree_sibling_path(const index_t leaf_index, + const uint32_t subtree_depth, + const HashPathCallback& on_completion, + bool includeUncommitted) const { - return depth_; + auto job = [=, this]() { + execute_and_report( + [=, this](TypedResponse& response) { + ReadTransactionPtr tx = store_.create_read_transaction(); + response.inner.path = + get_subtree_sibling_path_internal(leaf_index, subtree_depth, *tx, includeUncommitted); + }, + on_completion); + }; + workers_.enqueue(job); } template -fr_hash_path AppendOnlyTree::get_hash_path(const index_t& index) const +fr_sibling_path AppendOnlyTree::get_subtree_sibling_path_internal(const index_t leaf_index, + const uint32_t subtree_depth, + ReadTransaction& tx, + bool includeUncommitted) const { - fr_hash_path path; - index_t current_index = index; + // skip the first levels, all the way to the subtree_root + index_t current_index = leaf_index >> subtree_depth; + fr_sibling_path path; + path.reserve(depth_ - subtree_depth); - for (size_t level = depth_; level > 0; --level) { - bool is_right = bool(current_index & 0x01); - fr right_value = - is_right ? get_element_or_zero(level, current_index) : get_element_or_zero(level, current_index + 1); - fr left_value = - is_right ? get_element_or_zero(level, current_index - 1) : get_element_or_zero(level, current_index); - path.push_back(std::make_pair(left_value, right_value)); + for (uint32_t level = depth_ - subtree_depth; level > 0; --level) { + bool is_right = static_cast(current_index & 0x01); + fr sibling = is_right ? get_element_or_zero(level, current_index - 1, tx, includeUncommitted) + : get_element_or_zero(level, current_index + 1, tx, includeUncommitted); + path.emplace_back(sibling); current_index >>= 1; } + return path; } -template fr AppendOnlyTree::add_value(const fr& value) +template +void AppendOnlyTree::get_leaf(const index_t& index, + bool includeUncommitted, + const GetLeafCallback& on_completion) const +{ + auto job = [=, this]() { + execute_and_report( + [=, this](TypedResponse& response) { + ReadTransactionPtr tx = store_.create_read_transaction(); + auto leaf = read_node(depth_, index, *tx, includeUncommitted); + response.success = leaf.first; + if (leaf.first) { + response.inner.leaf = leaf.second; + } + }, + on_completion); + }; + workers_.enqueue(job); +} + +template +void AppendOnlyTree::find_leaf_index(const fr& leaf, + bool includeUncommitted, + const FindLeafCallback& on_completion) const +{ + find_leaf_index_from(leaf, 0, includeUncommitted, on_completion); +} + +template +void AppendOnlyTree::find_leaf_index_from(const fr& leaf, + index_t start_index, + bool includeUncommitted, + const FindLeafCallback& on_completion) const +{ + auto job = [=, this]() -> void { + execute_and_report( + [=, this](TypedResponse& response) { + typename Store::ReadTransactionPtr tx = store_.create_read_transaction(); + std::optional leaf_index = + store_.find_leaf_index_from(leaf, start_index, *tx, includeUncommitted); + response.success = leaf_index.has_value(); + if (response.success) { + response.inner.leaf_index = leaf_index.value(); + } + }, + on_completion); + }; + workers_.enqueue(job); +} + +template +void AppendOnlyTree::add_value(const fr& value, const AppendCompletionCallback& on_completion) { - return add_values(std::vector{ value }); + add_values(std::vector{ value }, on_completion); } template -fr AppendOnlyTree::add_values(const std::vector& values) +void AppendOnlyTree::add_values(const std::vector& values, + const AppendCompletionCallback& on_completion) { - index_t index = size(); - size_t number_to_insert = values.size(); - size_t level = depth_; - std::vector hashes = values; + add_values_internal(values, on_completion, true); +} + +template +void AppendOnlyTree::add_values_internal(const std::vector& values, + const AppendCompletionCallback& on_completion, + bool update_index) +{ + std::shared_ptr> hashes = std::make_shared>(values); + auto append_op = [=, this]() -> void { + execute_and_report( + [=, this](TypedResponse& response) { + add_values_internal(hashes, response.inner.root, response.inner.size, update_index); + }, + on_completion); + }; + workers_.enqueue(append_op); +} + +template +void AppendOnlyTree::commit(const CommitCallback& on_completion) +{ + auto job = [=, this]() { execute_and_report([=, this]() { store_.commit(); }, on_completion); }; + workers_.enqueue(job); +} + +template +void AppendOnlyTree::rollback(const RollbackCallback& on_completion) +{ + auto job = [=, this]() { execute_and_report([=, this]() { store_.rollback(); }, on_completion); }; + workers_.enqueue(job); +} + +template +void AppendOnlyTree::add_values_internal(std::shared_ptr> values, + fr& new_root, + index_t& new_size, + bool update_index) +{ + + uint32_t start_level = depth_; + uint32_t level = start_level; + std::vector& hashes_local = *values; + auto number_to_insert = static_cast(hashes_local.size()); + index_t start_size = 0; + bb::fr root; + + typename Store::ReadTransactionPtr tx = store_.create_read_transaction(); + store_.get_meta(start_size, root, *tx, true); + index_t index = start_size; + new_size = start_size + number_to_insert; + + if (values->empty()) { + return; + } + + if (new_size > max_size_) { + throw std::runtime_error("Tree is full"); + } // Add the values at the leaf nodes of the tree - for (size_t i = 0; i < number_to_insert; ++i) { - write_node(level, index + i, hashes[i]); + for (uint32_t i = 0; i < number_to_insert; ++i) { + write_node(level, index + i, hashes_local[i]); + } + + // If we have been told to add these leaves to the index then do so now + if (update_index) { + for (uint32_t i = 0; i < number_to_insert; ++i) { + store_.update_index(index + i, hashes_local[i]); + } } // Hash the values as a sub tree and insert them @@ -139,52 +434,60 @@ fr AppendOnlyTree::add_values(const std::vector& value number_to_insert >>= 1; index >>= 1; --level; - for (size_t i = 0; i < number_to_insert; ++i) { - hashes[i] = HashingPolicy::hash_pair(hashes[i * 2], hashes[i * 2 + 1]); - write_node(level, index + i, hashes[i]); + for (uint32_t i = 0; i < number_to_insert; ++i) { + hashes_local[i] = HashingPolicy::hash_pair(hashes_local[i * 2], hashes_local[i * 2 + 1]); + write_node(level, index + i, hashes_local[i]); } } // Hash from the root of the sub-tree to the root of the overall tree - fr new_hash = hashes[0]; + fr new_hash = hashes_local[0]; while (level > 0) { - bool is_right = bool(index & 0x01); - fr left_hash = is_right ? get_element_or_zero(level, index - 1) : new_hash; - fr right_hash = is_right ? new_hash : get_element_or_zero(level, index + 1); + bool is_right = static_cast(index & 0x01); + fr left_hash = is_right ? get_element_or_zero(level, index - 1, *tx, true) : new_hash; + fr right_hash = is_right ? new_hash : get_element_or_zero(level, index + 1, *tx, true); new_hash = HashingPolicy::hash_pair(left_hash, right_hash); + index >>= 1; --level; - write_node(level, index, new_hash); + if (level > 0) { + write_node(level, index, new_hash); + } } - size_ += values.size(); - root_ = new_hash; - return root_; + new_root = new_hash; + store_.put_meta(new_size, new_root); } +// Retrieves the value at the given level and index or the 'zero' tree hash if not present template -fr AppendOnlyTree::get_element_or_zero(size_t level, const index_t& index) const +fr AppendOnlyTree::get_element_or_zero(uint32_t level, + const index_t& index, + ReadTransaction& tx, + bool includeUncommitted) const { - const std::pair read_data = read_node(level, index); + const std::pair read_data = read_node(level, index, tx, includeUncommitted); if (read_data.first) { return read_data.second; } - ASSERT(level > 0 && level < zero_hashes_.size()); return zero_hashes_[level]; } template -void AppendOnlyTree::write_node(size_t level, const index_t& index, const fr& value) +void AppendOnlyTree::write_node(uint32_t level, const index_t& index, const fr& value) { std::vector buf; write(buf, value); - store_.put(level, size_t(index), buf); + store_.put_node(level, index, buf); } template -std::pair AppendOnlyTree::read_node(size_t level, const index_t& index) const +std::pair AppendOnlyTree::read_node(uint32_t level, + const index_t& index, + ReadTransaction& tx, + bool includeUncommitted) const { std::vector buf; - bool available = store_.get(level, size_t(index), buf); + bool available = store_.get_node(level, index, buf, tx, includeUncommitted); if (!available) { return std::make_pair(false, fr::zero()); } @@ -192,4 +495,4 @@ std::pair AppendOnlyTree::read_node(size_t level return std::make_pair(true, value); } -} // namespace bb::crypto::merkle_tree \ No newline at end of file +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.test.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.test.cpp index e25f53ba1ca..656242cab18 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.test.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/append_only_tree/append_only_tree.test.cpp @@ -1,127 +1,696 @@ #include "append_only_tree.hpp" -#include "../array_store.hpp" +#include "../fixtures.hpp" #include "../memory_tree.hpp" #include "barretenberg/common/streams.hpp" #include "barretenberg/common/test.hpp" -#include "barretenberg/numeric/random/engine.hpp" +#include "barretenberg/common/thread_pool.hpp" +#include "barretenberg/crypto/merkle_tree/hash.hpp" +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/array_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "gtest/gtest.h" +#include +#include +#include +#include using namespace bb; using namespace bb::crypto::merkle_tree; -namespace { -auto& engine = numeric::get_debug_randomness(); -auto& random_engine = numeric::get_randomness(); -} // namespace +using Store = CachedTreeStore; +using TreeType = AppendOnlyTree; -const size_t NUM_VALUES = 1024; -static std::vector VALUES = []() { - std::vector values(NUM_VALUES); - for (size_t i = 0; i < NUM_VALUES; ++i) { - values[i] = fr(random_engine.get_random_uint256()); +class PersistedAppendOnlyTreeTest : public testing::Test { + protected: + void SetUp() override + { + // setup with 1MB max db size, 1 max database and 2 maximum concurrent readers + _directory = random_temp_directory(); + std::filesystem::create_directories(_directory); + _environment = std::make_unique(_directory, 1024, 2, 2); } - return values; -}(); - -inline void print_tree(const size_t depth, std::vector hashes, std::string const& msg) -{ - info("\n", msg); - size_t offset = 0; - for (size_t i = 0; i < depth; i++) { - info("i = ", i); - size_t layer_size = (1UL << (depth - i)); - for (size_t j = 0; j < layer_size; j++) { - info("j = ", j, ": ", hashes[offset + j]); + + void TearDown() override { std::filesystem::remove_all(_directory); } + + static std::string _directory; + + std::unique_ptr _environment; +}; + +std::string PersistedAppendOnlyTreeTest::_directory; + +void check_size(TreeType& tree, index_t expected_size, bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + EXPECT_EQ(response.inner.size, expected_size); + signal.signal_level(); + }; + tree.get_meta_data(includeUncommitted, completion); + signal.wait_for_level(); +} + +void check_root(TreeType& tree, fr expected_root, bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + EXPECT_EQ(response.inner.root, expected_root); + signal.signal_level(); + }; + tree.get_meta_data(includeUncommitted, completion); + signal.wait_for_level(); +} + +void check_sibling_path(TreeType& tree, + index_t index, + fr_sibling_path expected_sibling_path, + bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + EXPECT_EQ(response.inner.path, expected_sibling_path); + signal.signal_level(); + }; + tree.get_sibling_path(index, completion, includeUncommitted); + signal.wait_for_level(); +} + +void commit_tree(TreeType& tree) +{ + Signal signal; + auto completion = [&](const Response& response) -> void { + EXPECT_EQ(response.success, true); + signal.signal_level(); + }; + tree.commit(completion); + signal.wait_for_level(); +} + +void rollback_tree(TreeType& tree) +{ + Signal signal; + auto completion = [&](const Response& response) -> void { + EXPECT_EQ(response.success, true); + signal.signal_level(); + }; + tree.rollback(completion); + signal.wait_for_level(); +} + +void add_value(TreeType& tree, const fr& value) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + signal.signal_level(); + }; + + tree.add_value(value, completion); + signal.wait_for_level(); +} + +void add_values(TreeType& tree, const std::vector& values) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + signal.signal_level(); + }; + + tree.add_values(values, completion); + signal.wait_for_level(); +} + +void check_find_leaf_index( + TreeType& tree, const fr& leaf, index_t expected_index, bool expected_success, bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, expected_success); + if (expected_success) { + EXPECT_EQ(response.inner.leaf_index, expected_index); } - offset += layer_size; + signal.signal_level(); + }; + + tree.find_leaf_index(leaf, includeUncommitted, completion); + signal.wait_for_level(); +} + +void check_find_leaf_index_from(TreeType& tree, + const fr& leaf, + index_t start_index, + index_t expected_index, + bool expected_success, + bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, expected_success); + if (expected_success) { + EXPECT_EQ(response.inner.leaf_index, expected_index); + } + signal.signal_level(); + }; + + tree.find_leaf_index_from(leaf, start_index, includeUncommitted, completion); + signal.wait_for_level(); +} + +void check_leaf( + TreeType& tree, const fr& leaf, index_t leaf_index, bool expected_success, bool includeUncommitted = true) +{ + Signal signal; + tree.get_leaf(leaf_index, includeUncommitted, [&](const TypedResponse& response) { + EXPECT_EQ(response.success, expected_success); + if (expected_success) { + EXPECT_EQ(response.inner.leaf, leaf); + } + signal.signal_level(); + }); + signal.wait_for_level(); +} + +void check_sibling_path(fr expected_root, fr node, index_t index, fr_sibling_path sibling_path) +{ + fr left, right, hash = node; + for (size_t i = 0; i < sibling_path.size(); ++i) { + if (index % 2 == 0) { + left = hash; + right = sibling_path[i]; + } else { + left = sibling_path[i]; + right = hash; + } + + hash = Poseidon2HashPolicy::hash_pair(left, right); + index >>= 1; } + + EXPECT_EQ(hash, expected_root); } -TEST(stdlib_append_only_tree, can_create) +TEST_F(PersistedAppendOnlyTreeTest, can_create) { constexpr size_t depth = 10; - ArrayStore store(depth); - AppendOnlyTree tree(store, depth); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + EXPECT_NO_THROW(Store store(name, depth, db)); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); MemoryTree memdb(depth); - EXPECT_EQ(tree.size(), 0); - EXPECT_EQ(tree.root(), memdb.root()); + check_size(tree, 0); + check_root(tree, memdb.root()); } -TEST(stdlib_append_only_tree, can_add_value) +TEST_F(PersistedAppendOnlyTreeTest, can_only_recreate_with_same_name_and_depth) { constexpr size_t depth = 10; - ArrayStore store(depth); - AppendOnlyTree tree(store, depth); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + EXPECT_ANY_THROW(Store store_wrong_name("Wrong name", depth, db)); + EXPECT_ANY_THROW(Store store_wrong_depth(name, depth + 1, db)); +} + +TEST_F(PersistedAppendOnlyTreeTest, can_add_value_and_get_sibling_path) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); MemoryTree memdb(depth); - EXPECT_EQ(tree.size(), 0); - EXPECT_EQ(tree.root(), memdb.root()); + check_size(tree, 0); + check_root(tree, memdb.root()); memdb.update_element(0, VALUES[0]); - tree.add_value(VALUES[0]); + add_value(tree, VALUES[0]); + + check_size(tree, 1); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); +} + +TEST_F(PersistedAppendOnlyTreeTest, reports_an_error_if_tree_is_overfilled) +{ + constexpr size_t depth = 4; + std::string name = random_string(); + std::string directory = random_temp_directory(); + std::filesystem::create_directories(directory); + auto environment = std::make_unique(directory, 1024, 1, 2); + LMDBStore db(*environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); + + std::vector values; + for (uint32_t i = 0; i < 16; i++) { + values.push_back(VALUES[i]); + } + add_values(tree, values); + + Signal signal; + auto add_completion = [&](const TypedResponse& response) { + EXPECT_EQ(response.success, false); + EXPECT_EQ(response.message, "Tree is full"); + signal.signal_level(); + }; + tree.add_value(VALUES[16], add_completion); + signal.wait_for_level(); + std::filesystem::remove_all(directory); +} + +TEST_F(PersistedAppendOnlyTreeTest, errors_are_caught_and_handled) +{ + // We use a deep tree with a small amount of storage (20 * 1024) bytes + constexpr size_t depth = 16; + std::string name = random_string(); + std::string directory = random_temp_directory(); + std::filesystem::create_directories(directory); + auto environment = std::make_unique(directory, 300, 1, 2); + LMDBStore db(*environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); + MemoryTree memdb(depth); + + // check the committed data only, so we read from the db + check_size(tree, 0, false); + check_root(tree, memdb.root(), false); + + fr empty_root = memdb.root(); + + // Add lots of values to the tree + uint32_t num_values_to_add = 16 * 1024; + std::vector values(num_values_to_add, VALUES[0]); + for (uint32_t i = 0; i < num_values_to_add; i++) { + memdb.update_element(i, VALUES[0]); + } + add_values(tree, values); + + // check the uncommitted data is accurate + check_size(tree, num_values_to_add, true); + check_root(tree, memdb.root(), true); + + // trying to commit that should fail + Signal signal; + auto completion = [&](const Response& response) -> void { + EXPECT_EQ(response.success, false); + signal.signal_level(); + }; + + tree.commit(completion); + signal.wait_for_level(); + + // At this stage, the tree is still in an uncommited state despite the error + // Reading both committed and uncommitted data shold be ok + + // check the uncommitted data is accurate + check_size(tree, num_values_to_add, true); + check_root(tree, memdb.root(), true); + + // Reading committed data should still work + check_size(tree, 0, false); + check_root(tree, empty_root, false); + + // Now rollback the tree + rollback_tree(tree); + + // committed and uncommitted data should be as an empty tree + check_size(tree, 0, true); + check_root(tree, empty_root, true); + + // Reading committed data should still work + check_size(tree, 0, false); + check_root(tree, empty_root, false); + + // // Now add a single value and commit it + add_value(tree, VALUES[0]); + commit_tree(tree); + + MemoryTree memdb2(depth); + memdb2.update_element(0, VALUES[0]); + + // committed and uncommitted data should be equal to the tree with 1 item + check_size(tree, 1, true); + check_root(tree, memdb2.root(), true); + + // Reading committed data should still work + check_size(tree, 1, false); + check_root(tree, memdb2.root(), false); +} + +TEST_F(PersistedAppendOnlyTreeTest, can_commit_and_restore) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + MemoryTree memdb(depth); + { + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); + + check_size(tree, 0); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + bb::fr initial_root = memdb.root(); + fr_sibling_path initial_sibling_path = memdb.get_sibling_path(0); + memdb.update_element(0, VALUES[0]); + add_value(tree, VALUES[0]); + + // check uncommitted state + check_size(tree, 1); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + // check committed state + check_size(tree, 0, false); + check_root(tree, initial_root, false); + check_sibling_path(tree, 0, initial_sibling_path, false); + + // commit the changes + commit_tree(tree); + // now committed and uncommitted should be the same - EXPECT_EQ(tree.root(), memdb.root()); - EXPECT_EQ(tree.get_hash_path(0), memdb.get_hash_path(0)); + // check uncommitted state + check_size(tree, 1); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + // check committed state + check_size(tree, 1, false); + check_root(tree, memdb.root(), false); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), false); + } + + // Re-create the store and tree, it should be the same as how we left it + { + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + ThreadPool pool(1); + TreeType tree(store, pool); + + // check uncommitted state + check_size(tree, 1); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + // check committed state + check_size(tree, 1, false); + check_root(tree, memdb.root(), false); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), false); + } } -TEST(stdlib_append_only_tree, test_size) +TEST_F(PersistedAppendOnlyTreeTest, test_size) { constexpr size_t depth = 10; - ArrayStore store(depth); - AppendOnlyTree tree(store, depth); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); - EXPECT_EQ(tree.size(), 0ULL); + check_size(tree, 0); // Add a new non-zero leaf at index 0. - tree.add_value(30); - EXPECT_EQ(tree.size(), 1ULL); + add_value(tree, 30); + check_size(tree, 1); // Add second. - tree.add_value(10); - EXPECT_EQ(tree.size(), 2ULL); + add_value(tree, 10); + check_size(tree, 2); // Add third. - tree.add_value(20); - EXPECT_EQ(tree.size(), 3ULL); + add_value(tree, 20); + check_size(tree, 3); // Add forth but with same value. - tree.add_value(20); - EXPECT_EQ(tree.size(), 4ULL); + add_value(tree, 40); + check_size(tree, 4); } -TEST(stdlib_append_only_tree, can_add_multiple_values) +TEST_F(PersistedAppendOnlyTreeTest, test_find_leaf_index) { constexpr size_t depth = 10; - ArrayStore store(depth); - AppendOnlyTree tree(store, depth); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); + + add_value(tree, 30); + add_value(tree, 10); + add_value(tree, 20); + add_value(tree, 40); + + // check the committed state and that the uncommitted state is empty + check_find_leaf_index(tree, 10, 1, true, true); + check_find_leaf_index(tree, 10, 0, false, false); + + check_find_leaf_index(tree, 15, 0, false, true); + check_find_leaf_index(tree, 15, 0, false, false); + + check_find_leaf_index(tree, 40, 3, true, true); + check_find_leaf_index(tree, 30, 0, true, true); + check_find_leaf_index(tree, 20, 2, true, true); + + check_find_leaf_index(tree, 40, 0, false, false); + check_find_leaf_index(tree, 30, 0, false, false); + check_find_leaf_index(tree, 20, 0, false, false); + + commit_tree(tree); + + std::vector values{ 15, 18, 26, 2, 48 }; + add_values(tree, values); + + // check the now committed state + check_find_leaf_index(tree, 40, 3, true, false); + check_find_leaf_index(tree, 30, 0, true, false); + check_find_leaf_index(tree, 20, 2, true, false); + + // check the new uncommitted state + check_find_leaf_index(tree, 18, 5, true, true); + check_find_leaf_index(tree, 18, 0, false, false); + + commit_tree(tree); + + values = { 16, 4, 18, 22, 101 }; + add_values(tree, values); + + // we now have duplicate leaf 18, one committed the other not + check_find_leaf_index(tree, 18, 5, true, true); + check_find_leaf_index(tree, 18, 5, true, false); + + // verify the find index from api + check_find_leaf_index_from(tree, 18, 0, 5, true, true); + check_find_leaf_index_from(tree, 18, 6, 11, true, true); + check_find_leaf_index_from(tree, 18, 6, 0, false, false); + + commit_tree(tree); + + // add another leaf 18 + add_value(tree, 18); + + // should return the first index + check_find_leaf_index_from(tree, 18, 0, 5, true, false); + check_find_leaf_index_from(tree, 18, 0, 5, true, true); + + add_value(tree, 88); + // and another uncommitted 18 + add_value(tree, 18); + + add_value(tree, 32); + + // should return the first uncommitted + check_find_leaf_index_from(tree, 18, 12, 14, true, true); + check_find_leaf_index_from(tree, 18, 15, 16, true, true); + + // look past the last instance of this leaf + check_find_leaf_index_from(tree, 18, 17, 0, false, true); + + // look beyond the end of uncommitted + check_find_leaf_index_from(tree, 18, 18, 0, false, true); + + // look beyond the end of committed and don't include uncomitted + check_find_leaf_index_from(tree, 18, 14, 0, false, false); +} + +TEST_F(PersistedAppendOnlyTreeTest, can_add_multiple_values) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); MemoryTree memdb(depth); for (size_t i = 0; i < NUM_VALUES; ++i) { fr mock_root = memdb.update_element(i, VALUES[i]); - fr tree_root = tree.add_value(VALUES[i]); - EXPECT_EQ(mock_root, tree_root); + add_value(tree, VALUES[i]); + check_root(tree, mock_root); + + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree, i, memdb.get_sibling_path(i)); + } +} - EXPECT_EQ(memdb.get_hash_path(0), tree.get_hash_path(0)); - EXPECT_EQ(memdb.get_hash_path(i), tree.get_hash_path(i)); +TEST_F(PersistedAppendOnlyTreeTest, can_add_multiple_values_in_a_batch) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); + MemoryTree memdb(depth); + + for (size_t i = 0; i < NUM_VALUES; ++i) { + memdb.update_element(i, VALUES[i]); } + add_values(tree, VALUES); + check_size(tree, NUM_VALUES); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree, NUM_VALUES - 1, memdb.get_sibling_path(NUM_VALUES - 1)); } -TEST(stdlib_append_only_tree, can_be_filled) +TEST_F(PersistedAppendOnlyTreeTest, can_be_filled) { constexpr size_t depth = 3; - ArrayStore store(depth); - AppendOnlyTree tree(store, depth); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); MemoryTree memdb(depth); - EXPECT_EQ(tree.size(), 0); - EXPECT_EQ(tree.root(), memdb.root()); + check_size(tree, 0); + check_root(tree, memdb.root()); for (size_t i = 0; i < 8; i++) { memdb.update_element(i, VALUES[i]); - tree.add_value(VALUES[i]); + add_value(tree, VALUES[i]); } - EXPECT_EQ(tree.root(), memdb.root()); - EXPECT_EQ(tree.get_hash_path(0), memdb.get_hash_path(0)); - EXPECT_EQ(tree.get_hash_path(7), memdb.get_hash_path(7)); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree, 7, memdb.get_sibling_path(7)); +} + +TEST_F(PersistedAppendOnlyTreeTest, can_add_single_whilst_reading) +{ + constexpr size_t depth = 10; + MemoryTree memdb(depth); + fr_sibling_path initial_path = memdb.get_sibling_path(0); + memdb.update_element(0, VALUES[0]); + fr_sibling_path final_sibling_path = memdb.get_sibling_path(0); + + uint32_t num_reads = 16 * 1024; + std::vector paths(num_reads); + + { + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(8); + TreeType tree(store, pool); + + check_size(tree, 0); + + Signal signal(2); + + auto add_completion = [&](const TypedResponse&) { + signal.signal_level(1); + auto commit_completion = [&](const Response&) { signal.signal_level(0); }; + tree.commit(commit_completion); + }; + tree.add_value(VALUES[0], add_completion); + + for (size_t i = 0; i < num_reads; i++) { + auto completion = [&, i](const TypedResponse& response) { + paths[i] = response.inner.path; + }; + tree.get_sibling_path(0, completion, false); + } + signal.wait_for_level(0); + } + + for (auto& path : paths) { + EXPECT_TRUE(path == initial_path || path == final_sibling_path); + } +} + +TEST_F(PersistedAppendOnlyTreeTest, can_get_inserted_leaves) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); + + add_values(tree, { 30, 10, 20, 40 }); + + check_leaf(tree, 30, 0, true); + check_leaf(tree, 10, 1, true); + check_leaf(tree, 20, 2, true); + check_leaf(tree, 40, 3, true); + + check_leaf(tree, 0, 4, false); +} + +TEST_F(PersistedAppendOnlyTreeTest, returns_sibling_path) +{ + constexpr size_t depth = 4; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(1); + TreeType tree(store, pool); + MemoryTree memdb(depth); + + add_values(tree, { 30, 10, 20 }); + memdb.update_element(0, 30); + memdb.update_element(1, 10); + memdb.update_element(2, 20); + + tree.get_subtree_sibling_path( + 0, + [&](auto& resp) { + fr_sibling_path expected_sibling_path = memdb.get_sibling_path(3); + EXPECT_EQ(resp.inner.path, expected_sibling_path); + }, + true); + + tree.get_subtree_sibling_path( + 2, + [&](auto& resp) { + fr_sibling_path expected_sibling_path = { memdb.get_node(2, 1), memdb.get_node(1, 1) }; + EXPECT_EQ(resp.inner.path, expected_sibling_path); + }, + true); } diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/array_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/array_store.hpp deleted file mode 100644 index ade8472b1b9..00000000000 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/array_store.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include "barretenberg/stdlib/primitives/field/field.hpp" - -namespace bb::crypto::merkle_tree { - -/** - * @brief A very basic 2-d array for use as a backing store for merkle trees. - * Can store up to 'indices' nodes per row and 'levels' rows. - */ -class ArrayStore { - - public: - ArrayStore(size_t levels, size_t indices = 1024) - : map_(std::vector>>>( - levels + 1, - std::vector>>( - indices, std::pair>(false, std::vector())))) - {} - ~ArrayStore() {} - - void put(size_t level, size_t index, const std::vector& data) - { - map_[level][index] = std::make_pair(true, data); - } - bool get(size_t level, size_t index, std::vector& data) const - { - const std::pair>& slot = map_[level][index]; - if (slot.first) { - data = slot.second; - } - return slot.first; - } - - private: - std::vector>>> map_; -}; -} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/fixtures.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/fixtures.hpp new file mode 100644 index 00000000000..d824198d856 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/fixtures.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/numeric/random/engine.hpp" +#include +#include +#include + +namespace bb::crypto::merkle_tree { + +const uint32_t NUM_VALUES = 1024; +inline auto& engine = numeric::get_debug_randomness(); +inline auto& random_engine = numeric::get_randomness(); + +static std::vector VALUES = []() { + std::vector values(NUM_VALUES); + for (uint32_t i = 0; i < NUM_VALUES; ++i) { + values[i] = fr(random_engine.get_random_uint256()); + } + return values; +}(); + +inline std::string random_string() +{ + std::stringstream ss; + ss << random_engine.get_random_uint256(); + return ss.str(); +} + +inline std::string random_temp_directory() +{ + std::stringstream ss; + ss << "/tmp/lmdb/" << random_string(); + return ss.str(); +} + +inline void print_tree(const uint32_t depth, std::vector hashes, std::string const& msg) +{ + info("\n", msg); + uint32_t offset = 0; + for (uint32_t i = 0; i < depth; i++) { + info("i = ", i); + uint32_t layer_size = (1U << (depth - i)); + for (uint32_t j = 0; j < layer_size; j++) { + info("j = ", j, ": ", hashes[offset + j]); + } + offset += layer_size; + } +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp index bd5e9859cbe..796abefc1a1 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp @@ -1,25 +1,183 @@ #pragma once +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/serialize/msgpack.hpp" #include "barretenberg/stdlib/primitives/field/field.hpp" namespace bb::crypto::merkle_tree { -typedef uint256_t index_t; +struct NullifierLeafValue { + fr value; + + MSGPACK_FIELDS(value) + + NullifierLeafValue() = default; + NullifierLeafValue(const fr& v) + : value(v) + {} + NullifierLeafValue(const NullifierLeafValue& other) = default; + NullifierLeafValue(NullifierLeafValue&& other) = default; + NullifierLeafValue& operator=(const NullifierLeafValue& other) + { + if (this != &other) { + value = other.value; + } + return *this; + } + + NullifierLeafValue& operator=(NullifierLeafValue&& other) noexcept + { + if (this != &other) { + value = other.value; + } + return *this; + } + ~NullifierLeafValue() = default; + + static bool is_updateable() { return false; } + + bool operator==(NullifierLeafValue const& other) const { return value == other.value; } + + friend std::ostream& operator<<(std::ostream& os, const NullifierLeafValue& v) + { + os << "value = " << v.value; + return os; + } + + fr get_key() const { return value; } + + bool is_empty() const { return value == fr::zero(); } -struct indexed_leaf { + std::vector get_hash_inputs(fr nextValue, fr nextIndex) const + { + return std::vector({ value, nextValue, nextIndex }); + } + + operator uint256_t() const { return get_key(); } + + static NullifierLeafValue empty() { return { fr::zero() }; } + + static NullifierLeafValue padding(index_t i) { return { i }; } +}; + +struct PublicDataLeafValue { fr value; + fr slot; + + MSGPACK_FIELDS(value, slot) + + PublicDataLeafValue() = default; + PublicDataLeafValue(const fr& s, const fr& v) + : value(v) + , slot(s) + {} + PublicDataLeafValue(const PublicDataLeafValue& other) = default; + PublicDataLeafValue(PublicDataLeafValue&& other) = default; + PublicDataLeafValue& operator=(const PublicDataLeafValue& other) + { + if (this != &other) { + value = other.value; + slot = other.slot; + } + return *this; + } + + PublicDataLeafValue& operator=(PublicDataLeafValue&& other) noexcept + { + if (this != &other) { + value = other.value; + slot = other.slot; + } + return *this; + } + ~PublicDataLeafValue() = default; + + static bool is_updateable() { return true; } + + bool operator==(PublicDataLeafValue const& other) const { return value == other.value && slot == other.slot; } + + friend std::ostream& operator<<(std::ostream& os, const PublicDataLeafValue& v) + { + os << "slot = " << v.slot << " : value = " << v.value; + return os; + } + + fr get_key() const { return slot; } + + bool is_empty() const { return slot == fr::zero(); } + + std::vector get_hash_inputs(fr nextValue, fr nextIndex) const + { + return std::vector({ slot, value, nextIndex, nextValue }); + } + + operator uint256_t() const { return get_key(); } + + static PublicDataLeafValue empty() { return { fr::zero(), fr::zero() }; } + + static PublicDataLeafValue padding(index_t i) { return { i, fr::zero() }; } +}; + +template struct IndexedLeaf { + LeafType value; index_t nextIndex; fr nextValue; - bool operator==(indexed_leaf const&) const = default; + MSGPACK_FIELDS(value, nextIndex, nextValue) + + IndexedLeaf() = default; - std::ostream& operator<<(std::ostream& os) + IndexedLeaf(const LeafType& val, index_t nextIdx, fr nextVal) + : value(val) + , nextIndex(nextIdx) + , nextValue(nextVal) + {} + + IndexedLeaf(const IndexedLeaf& other) = default; + IndexedLeaf(IndexedLeaf&& other) noexcept = default; + ~IndexedLeaf() = default; + + static bool is_updateable() { return LeafType::is_updateable(); } + + bool operator==(IndexedLeaf const& other) const + { + return value == other.value && nextValue == other.nextValue && nextIndex == other.nextIndex; + } + + IndexedLeaf& operator=(IndexedLeaf const& other) { - os << "value = " << value << "\nnextIdx = " << nextIndex << "\nnextVal = " << nextValue; + if (this != &other) { + value = other.value; + nextValue = other.nextValue; + nextIndex = other.nextIndex; + } + return *this; + } + + IndexedLeaf& operator=(IndexedLeaf&& other) noexcept + { + if (this != &other) { + value = other.value; + nextValue = other.nextValue; + nextIndex = other.nextIndex; + } + return *this; + } + + friend std::ostream& operator<<(std::ostream& os, const IndexedLeaf& leaf) + { + os << leaf.value << "\nnextIdx = " << leaf.nextIndex << "\nnextVal = " << leaf.nextValue; return os; } - std::vector get_hash_inputs() const { return std::vector({ value, nextIndex, nextValue }); } + std::vector get_hash_inputs() const { return value.get_hash_inputs(nextValue, nextIndex); } + + bool is_empty() { return value.is_empty(); } + + static IndexedLeaf empty() { return { LeafType::empty(), 0, 0 }; } + + static IndexedLeaf padding(index_t i) { return { LeafType::padding(i), 0, 0 }; } }; -} // namespace bb::crypto::merkle_tree \ No newline at end of file +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp index e87b4438508..6f561553968 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.hpp @@ -1,321 +1,643 @@ #pragma once -#include "../../../common/thread.hpp" #include "../append_only_tree/append_only_tree.hpp" #include "../hash.hpp" #include "../hash_path.hpp" +#include "../signal.hpp" +#include "barretenberg/common/assert.hpp" +#include "barretenberg/common/thread_pool.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" #include "indexed_leaf.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace bb::crypto::merkle_tree { -using index_t = uint256_t; - -/** - * @brief Used in parallel insertions in the IndexedTree. Workers signal to other following workes as they move up - * the level of the tree. - * - */ -class LevelSignal { - public: - LevelSignal(size_t initial_level) - : signal_(initial_level){}; - ~LevelSignal() = default; - LevelSignal(const LevelSignal& other) - : signal_(other.signal_.load()) - {} - LevelSignal(const LevelSignal&& other) noexcept - : signal_(other.signal_.load()) - {} - - /** - * @brief Causes the thread to wait until the required level has been signalled - * @param level The required level - * - */ - void wait_for_level(size_t level) - { - size_t current_level = signal_.load(); - while (current_level > level) { - signal_.wait(current_level); - current_level = signal_.load(); - } - } - - /** - * @brief Signals that the given level has been passed - * @param level The level to be signalled - * - */ - void signal_level(size_t level) - { - signal_.store(level); - signal_.notify_all(); - } - - private: - std::atomic signal_; -}; - /** * @brief Implements a parallelised batch insertion indexed tree * Accepts template argument of the type of store backing the tree, the type of store containing the leaves and the * hashing policy - * + * All public methods are asynchronous unless marked otherwise */ -template -class IndexedTree : public AppendOnlyTree { +template class IndexedTree : public AppendOnlyTree { public: - IndexedTree(Store& store, size_t depth, size_t initial_size = 1, uint8_t tree_id = 0); + using StoreType = Store; + + // The public methods accept these function types as asynchronous callbacks + using LeafValueType = typename Store::LeafType; + using IndexedLeafValueType = typename Store::IndexedLeafValueType; + using AddCompletionCallback = std::function>&)>; + using LeafCallback = std::function>&)>; + using FindLowLeafCallback = std::function>&)>; + + IndexedTree(Store& store, ThreadPool& workers, index_t initial_size); IndexedTree(IndexedTree const& other) = delete; IndexedTree(IndexedTree&& other) = delete; - ~IndexedTree(); + ~IndexedTree() = default; + IndexedTree& operator=(const IndexedTree& other) = delete; + IndexedTree& operator=(IndexedTree&& other) = delete; /** * @brief Adds or updates a single values in the tree (updates not currently supported) - * @param value The value to be added or updated - * @returns The 'previous' hash paths of all updated values */ - fr_hash_path add_or_update_value(const fr& value); + void add_or_update_value(const LeafValueType& value, const AddCompletionCallback& completion); /** * @brief Adds or updates the given set of values in the tree (updates not currently supported) * @param values The values to be added or updated - * @param no_multithreading Performs single threaded insertion, just used whilst prototyping and benchmarking - * @returns The 'previous' hash paths of all updated values + * @param completion The callback to be triggered once the values have been added */ - std::vector add_or_update_values(const std::vector& values, bool no_multithreading = false); + void add_or_update_values(const std::vector& values, const AddCompletionCallback& completion); /** - * @brief Adds or updates a single value without returning the previous hash path - * @param value The value to be added or updated - * @returns The new root of the tree + * @brief Adds or updates the given set of values in the tree (updates not currently supported) + * @param values The values to be added or updated + * @param subtree_depth The height of the subtree to be inserted. + * @param completion The callback to be triggered once the values have been added */ - fr add_value(const fr& value) override; + void add_or_update_values(const std::vector& values, + uint32_t subtree_depth, + const AddCompletionCallback& completion); + + void get_leaf(const index_t& index, bool includeUncommitted, const LeafCallback& completion) const; /** - * @brief Adds or updates the given set of values without returning the previous hash paths - * @param values The values to be added or updated - * @returns The new root of the tree + * @brief Find the index of the provided leaf value if it exists */ - fr add_values(const std::vector& values) override; + void find_leaf_index(const LeafValueType& leaf, + bool includeUncommitted, + const AppendOnlyTree::FindLeafCallback& on_completion) const; - indexed_leaf get_leaf(const index_t& index); + /** + * @brief Find the index of the provided leaf value if it exists, only considers indexed beyond the value provided + */ + void find_leaf_index_from(const LeafValueType& leaf, + index_t start_index, + bool includeUncommitted, + const AppendOnlyTree::FindLeafCallback& on_completion) const; + + /** + * @brief Find the leaf with the value immediately lower then the value provided + */ + void find_low_leaf(const fr& leaf_key, bool includeUncommitted, const FindLowLeafCallback& on_completion) const; - using AppendOnlyTree::get_hash_path; - using AppendOnlyTree::root; - using AppendOnlyTree::depth; + using AppendOnlyTree::get_sibling_path; private: - fr update_leaf_and_hash_to_root(const index_t& index, const indexed_leaf& leaf); - fr update_leaf_and_hash_to_root(const index_t& index, - const indexed_leaf& leaf, - LevelSignal& leader, - LevelSignal& follower, - fr_hash_path& previous_hash_path); - fr append_subtree(const index_t& start_index); + using typename AppendOnlyTree::AppendCompletionCallback; + using ReadTransaction = typename Store::ReadTransaction; + using ReadTransactionPtr = typename Store::ReadTransactionPtr; + + struct Status { + std::atomic_bool success{ true }; + std::string message; + + void set_failure(const std::string& msg) + { + if (success.exchange(false)) { + message = msg; + } + } + }; + + struct LeafInsertion { + index_t low_leaf_index; + IndexedLeafValueType low_leaf, original_low_leaf; + }; + + void update_leaf_and_hash_to_root(const index_t& index, + const IndexedLeafValueType& leaf, + Signal& leader, + Signal& follower, + fr_sibling_path& previous_sibling_path, + ReadTransaction& tx); + + struct InsertionGenerationResponse { + std::shared_ptr> insertions; + std::shared_ptr> indexed_leaves; + }; + + using InsertionGenerationCallback = std::function&)>; + void generate_insertions(const std::shared_ptr>>& values_to_be_sorted, + const InsertionGenerationCallback& completion); + + struct InsertionCompletionResponse { + std::shared_ptr>> low_leaf_witness_data; + }; + + using InsertionCompletionCallback = std::function&)>; + void perform_insertions(size_t total_leaves, + std::shared_ptr> insertions, + const InsertionCompletionCallback& completion); + + struct HashGenerationResponse { + std::shared_ptr> hashes; + }; + + using HashGenerationCallback = std::function&)>; + void generate_hashes_for_appending(std::shared_ptr> leaves_to_hash, + const HashGenerationCallback& completion); using AppendOnlyTree::get_element_or_zero; using AppendOnlyTree::write_node; using AppendOnlyTree::read_node; - private: + using AppendOnlyTree::add_value; + using AppendOnlyTree::add_values; + using AppendOnlyTree::add_values_internal; + using AppendOnlyTree::store_; using AppendOnlyTree::zero_hashes_; using AppendOnlyTree::depth_; - using AppendOnlyTree::tree_id_; - using AppendOnlyTree::root_; - LeavesStore leaves_; + using AppendOnlyTree::name_; + using AppendOnlyTree::workers_; + using AppendOnlyTree::max_size_; }; -template -IndexedTree::IndexedTree(Store& store, - size_t depth, - size_t initial_size, - uint8_t tree_id) - : AppendOnlyTree(store, depth, tree_id) +template +IndexedTree::IndexedTree(Store& store, ThreadPool& workers, index_t initial_size) + : AppendOnlyTree(store, workers) { - ASSERT(initial_size > 0); - zero_hashes_.resize(depth + 1); + if (initial_size < 2) { + throw std::runtime_error("Indexed trees must have initial size > 1"); + } + zero_hashes_.resize(depth_ + 1); // Create the zero hashes for the tree - indexed_leaf zero_leaf{ 0, 0, 0 }; - auto current = HashingPolicy::hash(zero_leaf.get_hash_inputs()); - for (size_t i = depth; i > 0; --i) { + // Indexed_LeafType zero_leaf{ 0, 0, 0 }; + auto current = fr::zero(); + for (uint32_t i = depth_; i > 0; --i) { zero_hashes_[i] = current; current = HashingPolicy::hash_pair(current, current); } zero_hashes_[0] = current; + + index_t stored_size = 0; + bb::fr stored_root = fr::zero(); + { + ReadTransactionPtr tx = store_.create_read_transaction(); + std::string name; + uint32_t depth = 0; + store_.get_full_meta(stored_size, stored_root, name, depth, *tx, false); + } + + if (stored_size > 0) { + return; + } + + std::vector appended_leaves; + std::vector appended_hashes; // Inserts the initial set of leaves as a chain in incrementing value order - for (size_t i = 0; i < initial_size; ++i) { + for (uint32_t i = 0; i < initial_size; ++i) { // Insert the zero leaf to the `leaves` and also to the tree at index 0. - indexed_leaf initial_leaf = indexed_leaf{ .value = i, .nextIndex = i + 1, .nextValue = i + 1 }; - leaves_.append_leaf(initial_leaf); + bool last = i == (initial_size - 1); + IndexedLeafValueType initial_leaf = + IndexedLeafValueType(LeafValueType::padding(i), last ? 0 : i + 1, last ? 0 : i + 1); + appended_leaves.push_back(initial_leaf); + appended_hashes.push_back(HashingPolicy::hash(initial_leaf.get_hash_inputs())); + store_.set_at_index(i, initial_leaf, true); + } + + TypedResponse result; + Signal signal(1); + AppendCompletionCallback completion = [&](const TypedResponse& _result) -> void { + result = _result; + signal.signal_level(0); + }; + AppendOnlyTree::add_values_internal(appended_hashes, completion, false); + signal.wait_for_level(0); + if (!result.success) { + throw std::runtime_error("Failed to initialise tree: " + result.message); } + store_.commit(); +} - // Points the last leaf back to the first - leaves_.set_at_index( - initial_size - 1, - indexed_leaf{ .value = leaves_.get_leaf(initial_size - 1).value, .nextIndex = 0, .nextValue = 0 }, - false); - append_subtree(0); +template +void IndexedTree::get_leaf(const index_t& index, + bool includeUncommitted, + const LeafCallback& completion) const +{ + auto job = [=, this]() { + execute_and_report>( + [=, this](TypedResponse>& response) { + ReadTransactionPtr tx = store_.create_read_transaction(); + response.inner.indexed_leaf = store_.get_leaf(index, *tx, includeUncommitted); + }, + completion); + }; + workers_.enqueue(job); } -template -IndexedTree::~IndexedTree() -{} +template +void IndexedTree::find_leaf_index( + const LeafValueType& leaf, + bool includeUncommitted, + const AppendOnlyTree::FindLeafCallback& on_completion) const +{ + find_leaf_index_from(leaf, 0, includeUncommitted, on_completion); +} -template -indexed_leaf IndexedTree::get_leaf(const index_t& index) +template +void IndexedTree::find_leaf_index_from( + const LeafValueType& leaf, + index_t start_index, + bool includeUncommitted, + const AppendOnlyTree::FindLeafCallback& on_completion) const { - return leaves_.get_leaf(index); + auto job = [=, this]() -> void { + execute_and_report( + [=, this](TypedResponse& response) { + typename Store::ReadTransactionPtr tx = store_.create_read_transaction(); + std::optional leaf_index = + store_.find_leaf_index_from(leaf, start_index, *tx, includeUncommitted); + response.success = leaf_index.has_value(); + if (response.success) { + response.inner.leaf_index = leaf_index.value(); + } + }, + on_completion); + }; + workers_.enqueue(job); } -template -fr IndexedTree::add_value(const fr& value) +template +void IndexedTree::find_low_leaf(const fr& leaf_key, + bool includeUncommitted, + const FindLowLeafCallback& on_completion) const { - return add_values(std::vector{ value }); + auto job = [=, this]() { + execute_and_report>( + [=, this](TypedResponse>& response) { + typename Store::ReadTransactionPtr tx = store_.create_read_transaction(); + response.inner = store_.find_low_value(leaf_key, includeUncommitted, *tx); + }, + on_completion); + }; + + workers_.enqueue(job); } -template -fr IndexedTree::add_values(const std::vector& values) +template +void IndexedTree::add_or_update_value(const LeafValueType& value, + const AddCompletionCallback& completion) { - add_or_update_values(values); - return root(); + add_or_update_values(std::vector{ value }, 1, completion); } -template -fr_hash_path IndexedTree::add_or_update_value(const fr& value) +template +void IndexedTree::add_or_update_values(const std::vector& values, + const AddCompletionCallback& completion) { - return add_or_update_values(std::vector{ value })[0]; + add_or_update_values(values, 0, completion); } -template -std::vector IndexedTree::add_or_update_values( - const std::vector& values, bool no_multithreading) +template +void IndexedTree::add_or_update_values(const std::vector& values, + const uint32_t subtree_depth, + const AddCompletionCallback& completion) { - // The first thing we do is sort the values into descending order but maintain knowledge of their orignal order - struct { - bool operator()(const std::pair& a, const std::pair& b) const - { - return uint256_t(a.first) > uint256_t(b.first); - } - } comp; - std::vector> values_sorted(values.size()); + // We first take a copy of the leaf values and their locations within the set given to us + std::shared_ptr>> values_to_be_sorted = + std::make_shared>>(values.size()); for (size_t i = 0; i < values.size(); ++i) { - values_sorted[i] = std::make_pair(values[i], i); + (*values_to_be_sorted)[i] = std::make_pair(values[i], i); } - std::sort(values_sorted.begin(), values_sorted.end(), comp); - // Now that we have the sorted values we need to identify the leaves that need updating. - // This is performed sequentially and is stored in this 'leaf_insertion' struct - struct leaf_insertion { - index_t low_leaf_index; - indexed_leaf low_leaf; + // This is to collect some state from the asynchronous operations we are about to perform + struct IntermediateResults { + // new hashes that will be appended to the tree + std::shared_ptr> hashes_to_append; + // info about the low leaves that have been updated + std::shared_ptr>> low_leaf_witness_data; + fr_sibling_path subtree_path; + std::atomic count; + Status status; + + // We set to 2 here as we will kick off the 2 main async operations concurrently and we need to trakc thri + // completion + IntermediateResults() + : count(2) + { + // Default to success, set to false on error + status.success = true; + }; + }; + std::shared_ptr results = std::make_shared(); + + auto on_error = [=](const std::string& message) { + try { + TypedResponse> response; + response.success = false; + response.message = message; + completion(response); + } catch (std::exception&) { + } }; - std::vector insertions(values.size()); - index_t old_size = leaves_.get_size(); - - for (size_t i = 0; i < values_sorted.size(); ++i) { - fr value = values_sorted[i].first; - index_t index_of_new_leaf = index_t(values_sorted[i].second) + old_size; - - // This gives us the leaf that need updating - index_t current; - bool is_already_present; - std::tie(is_already_present, current) = leaves_.find_low_value(values_sorted[i].first); - indexed_leaf current_leaf = leaves_.get_leaf(current); + // Thsi is the final callback triggered once the leaves have been appended to the tree + auto final_completion = [=](const TypedResponse& add_data_response) { + TypedResponse> response; + response.success = add_data_response.success; + response.message = add_data_response.message; + if (add_data_response.success) { + response.inner.subtree_path = results->subtree_path; + response.inner.sorted_leaves = values_to_be_sorted; + response.inner.low_leaf_witness_data = results->low_leaf_witness_data; + response.inner.add_data_result = add_data_response.inner; + } + // Trigger the client's provided callback + completion(response); + }; - indexed_leaf new_leaf = - indexed_leaf{ .value = value, .nextIndex = current_leaf.nextIndex, .nextValue = current_leaf.nextValue }; + auto sibling_path_completion = [=, this](const TypedResponse& response) { + if (!response.success) { + results->status.set_failure(response.message); + } else { + results->subtree_path = response.inner.path; + AppendOnlyTree::add_values_internal( + (*results->hashes_to_append), final_completion, false); + } + }; - // We only handle new values being added. We don't yet handle values being updated - if (!is_already_present) { - // Update the current leaf to point it to the new leaf - current_leaf.nextIndex = index_of_new_leaf; - current_leaf.nextValue = value; + // This signals the completion of the appended hash generation + // If the low leaf updates are also completed then we will append the leaves + HashGenerationCallback hash_completion = [=, this](const TypedResponse& hashes_response) { + if (!hashes_response.success) { + results->status.set_failure(hashes_response.message); + } else { + results->hashes_to_append = hashes_response.inner.hashes; + } - leaves_.set_at_index(current, current_leaf, false); - leaves_.set_at_index(index_of_new_leaf, new_leaf, true); + if (results->count.fetch_sub(1) == 1) { + if (!results->status.success) { + on_error(results->status.message); + return; + } + AppendOnlyTree::get_subtree_sibling_path( + subtree_depth, sibling_path_completion, true); } + }; - // Capture the index and value of the updated 'low' leaf - leaf_insertion& insertion = insertions[i]; - insertion.low_leaf_index = current; - insertion.low_leaf = indexed_leaf{ .value = current_leaf.value, - .nextIndex = current_leaf.nextIndex, - .nextValue = current_leaf.nextValue }; + // This signals the completion of the low leaf updates + // If the append hash generation has also copleted then the hashes can be appended + InsertionCompletionCallback insertion_completion = + [=, this](const TypedResponse& insertion_response) { + if (!insertion_response.success) { + results->status.set_failure(insertion_response.message); + } else { + results->low_leaf_witness_data = insertion_response.inner.low_leaf_witness_data; + } + + if (results->count.fetch_sub(1) == 1) { + if (!results->status.success) { + on_error(results->status.message); + return; + } + AppendOnlyTree::get_subtree_sibling_path( + subtree_depth, sibling_path_completion, true); + } + }; + + // This signals the completion of the insertion data generation + // Here we will enqueue both the generation of the appended hashes and the low leaf updates (insertions) + InsertionGenerationCallback insertion_generation_completed = + [=, this](const TypedResponse& insertion_response) { + if (!insertion_response.success) { + on_error(insertion_response.message); + return; + } + workers_.enqueue([=, this]() { + generate_hashes_for_appending(insertion_response.inner.indexed_leaves, hash_completion); + }); + perform_insertions(values.size(), insertion_response.inner.insertions, insertion_completion); + }; + + // We start by enqueueing the insertion data generation + workers_.enqueue([=, this]() { generate_insertions(values_to_be_sorted, insertion_generation_completed); }); +} + +template +void IndexedTree::perform_insertions(size_t total_leaves, + std::shared_ptr> insertions, + const InsertionCompletionCallback& completion) +{ + auto low_leaf_witness_data = std::make_shared>>( + total_leaves, + LowLeafWitnessData{ IndexedLeafValueType::empty(), 0, fr_sibling_path(depth_, fr::zero()) }); + + // early return, no insertions to perform + if (insertions->size() == 0) { + TypedResponse response; + response.success = true; + response.inner.low_leaf_witness_data = low_leaf_witness_data; + completion(response); + return; } // We now kick off multiple workers to perform the low leaf updates // We create set of signals to coordinate the workers as the move up the tree - std::vector paths(insertions.size()); - std::vector signals; - // The first signal is set to 0. This ensure the first worker up the tree is not impeded - signals.emplace_back(size_t(0)); - // Workers will follow their leaders up the tree, being trigger by the signal in front of them - for (size_t i = 0; i < insertions.size(); ++i) { - signals.emplace_back(size_t(1 + depth_)); + std::shared_ptr> signals = std::make_shared>(); + std::shared_ptr status = std::make_shared(); + // The first signal is set to 0. This ensures the first worker up the tree is not impeded + signals->emplace_back(0); + // Workers will follow their leaders up the tree, being triggered by the signal in front of them + for (size_t i = 0; i < insertions->size(); ++i) { + signals->emplace_back(uint32_t(1 + depth_)); } - if (no_multithreading) { - // Execute the jobs in series - for (size_t i = 0; i < insertions.size(); ++i) { - leaf_insertion& insertion = insertions[i]; - update_leaf_and_hash_to_root( - insertion.low_leaf_index, insertion.low_leaf, signals[i], signals[i + 1], paths[i]); - } - } else { - // Execute the jobs in parallel - parallel_for(insertions.size(), [&](size_t i) { - leaf_insertion& insertion = insertions[i]; - update_leaf_and_hash_to_root( - insertion.low_leaf_index, insertion.low_leaf, signals[i], signals[i + 1], paths[i]); - }); + for (uint32_t i = 0; i < insertions->size(); ++i) { + auto op = [=, this]() { + LeafInsertion& insertion = (*insertions)[i]; + Signal& leaderSignal = (*signals)[i]; + Signal& followerSignal = (*signals)[i + 1]; + try { + ReadTransactionPtr tx = store_.create_read_transaction(); + auto& current_witness_data = low_leaf_witness_data->at(i); + current_witness_data.leaf = insertion.original_low_leaf; + current_witness_data.index = insertion.low_leaf_index; + current_witness_data.path.clear(); + update_leaf_and_hash_to_root(insertion.low_leaf_index, + insertion.low_leaf, + leaderSignal, + followerSignal, + current_witness_data.path, + *tx); + } catch (std::exception& e) { + status->set_failure(e.what()); + // ensure that any followers are not blocked by our failure + followerSignal.signal_level(0); + } + if (i == insertions->size() - 1) { + TypedResponse response; + response.success = status->success; + response.message = status->message; + if (response.success) { + response.inner.low_leaf_witness_data = low_leaf_witness_data; + } + completion(response); + } + }; + workers_.enqueue(op); } +} - // Now that we have updated all of the low leaves, we insert the new leaves as a subtree at the end - root_ = append_subtree(old_size); - - return paths; +template +void IndexedTree::generate_hashes_for_appending( + std::shared_ptr> leaves_to_hash, const HashGenerationCallback& completion) +{ + execute_and_report( + [=](TypedResponse& response) { + response.inner.hashes = std::make_shared>(leaves_to_hash->size(), 0); + std::vector& leaves = *leaves_to_hash; + for (uint32_t i = 0; i < leaves.size(); ++i) { + if (!leaves[i].is_empty()) { + (*response.inner.hashes)[i] = (HashingPolicy::hash(leaves[i].get_hash_inputs())); + } + } + }, + completion); } -template -fr IndexedTree::update_leaf_and_hash_to_root(const index_t& leaf_index, - const indexed_leaf& leaf) +template +void IndexedTree::generate_insertions( + const std::shared_ptr>>& values_to_be_sorted, + const InsertionGenerationCallback& on_completion) { - LevelSignal leader(0); - LevelSignal follower(0); - fr_hash_path hash_path; - return update_leaf_and_hash_to_root(leaf_index, leaf, leader, follower, hash_path); + execute_and_report( + [=, this](TypedResponse& response) { + // The first thing we do is sort the values into descending order but maintain knowledge of their + // orignal order + struct { + bool operator()(std::pair& a, std::pair& b) const + { + uint256_t aValue = a.first.get_key(); + uint256_t bValue = b.first.get_key(); + return aValue == bValue ? a.second < b.second : aValue > bValue; + } + } comp; + std::sort(values_to_be_sorted->begin(), values_to_be_sorted->end(), comp); + + std::vector>& values = *values_to_be_sorted; + + // Now that we have the sorted values we need to identify the leaves that need updating. + // This is performed sequentially and is stored in this 'leaf_insertion' struct + response.inner.insertions = std::make_shared>(); + response.inner.insertions->reserve(values.size()); + response.inner.indexed_leaves = + std::make_shared>(values.size(), IndexedLeafValueType::empty()); + index_t old_size = 0; + index_t num_leaves_to_be_inserted = values.size(); + std::set unique_values; + { + ReadTransactionPtr tx = store_.create_read_transaction(); + bb::fr old_root = fr::zero(); + store_.get_meta(old_size, old_root, *tx, true); + // Ensure that the tree is not going to be overfilled + index_t new_total_size = num_leaves_to_be_inserted + old_size; + if (new_total_size > max_size_) { + throw std::runtime_error("Tree is full"); + } + for (size_t i = 0; i < values.size(); ++i) { + std::pair& value_pair = values[i]; + size_t index_into_appended_leaves = value_pair.second; + index_t index_of_new_leaf = static_cast(index_into_appended_leaves) + old_size; + if (value_pair.first.is_empty()) { + continue; + } + fr value = value_pair.first.get_key(); + auto it = unique_values.insert(value); + if (!it.second) { + throw std::runtime_error("Duplicate key not allowed in same batch"); + } + + // This gives us the leaf that need updating + index_t current = 0; + bool is_already_present = false; + std::tie(is_already_present, current) = + store_.find_low_value(value_pair.first.get_key(), true, *tx); + // .value() throws if the low leaf does not exist + IndexedLeafValueType current_leaf = store_.get_leaf(current, *tx, true).value(); + + LeafInsertion insertion = { + .low_leaf_index = current, + .low_leaf = IndexedLeafValueType::empty(), + .original_low_leaf = current_leaf, + }; + + // Capture the index and original value of the 'low' leaf + + if (!is_already_present) { + // Update the current leaf to point it to the new leaf + IndexedLeafValueType new_leaf = + IndexedLeafValueType(value_pair.first, current_leaf.nextIndex, current_leaf.nextValue); + + current_leaf.nextIndex = index_of_new_leaf; + current_leaf.nextValue = value; + store_.set_at_index(current, current_leaf, false); + store_.set_at_index(index_of_new_leaf, new_leaf, true); + + // Update the set of leaves to append + (*response.inner.indexed_leaves)[index_into_appended_leaves] = new_leaf; + } else if (IndexedLeafValueType::is_updateable()) { + // Update the current leaf's value, don't change it's link + IndexedLeafValueType replacement_leaf = + IndexedLeafValueType(value_pair.first, current_leaf.nextIndex, current_leaf.nextValue); + store_.set_at_index(current, replacement_leaf, false); + IndexedLeafValueType empty_leaf = IndexedLeafValueType::empty(); + // don't update the index for this empty leaf + store_.set_at_index(index_of_new_leaf, empty_leaf, false); + // The set of appended leaves already has an empty leaf in the slot at index + // 'index_into_appended_leaves' + } else { + throw std::runtime_error("IndexedLeafValue is not updateable"); + } + + // capture new low leaf + insertion.low_leaf = + IndexedLeafValueType(current_leaf.value, current_leaf.nextIndex, current_leaf.nextValue); + + response.inner.insertions->push_back(insertion); + } + } + }, + on_completion); } -template -fr IndexedTree::update_leaf_and_hash_to_root(const index_t& leaf_index, - const indexed_leaf& leaf, - LevelSignal& leader, - LevelSignal& follower, - fr_hash_path& previous_hash_path) +template +void IndexedTree::update_leaf_and_hash_to_root(const index_t& leaf_index, + const IndexedLeafValueType& leaf, + Signal& leader, + Signal& follower, + fr_sibling_path& previous_sibling_path, + ReadTransaction& tx) { + auto get_node = [&](uint32_t level, index_t index) -> fr { return get_element_or_zero(level, index, tx, true); }; // We are a worker at a specific leaf index. // We are going to move up the tree and at each node/level: // 1. Wait for the level above to become 'signalled' as clear for us to write into // 2. Read the node and it's sibling // 3. Write the new node value index_t index = leaf_index; - size_t level = depth_; + uint32_t level = depth_; fr new_hash = HashingPolicy::hash(leaf.get_hash_inputs()); - // Wait until we see that our leader has cleared 'depth_ - 1' (i.e. the level above the leaves that we are about to - // write into) this ensures that our leader is not still reading the leaves - size_t leader_level = depth_ - 1; + // Wait until we see that our leader has cleared 'depth_ - 1' (i.e. the level above the leaves that we are about + // to write into) this ensures that our leader is not still reading the leaves + uint32_t leader_level = depth_ - 1; leader.wait_for_level(leader_level); // Extract the value of the leaf node and it's sibling - bool is_right = bool(index & 0x01); - // extract the current leaf hash values for the previous hash path - fr current_right_value = get_element_or_zero(level, index + (is_right ? 0 : 1)); - fr current_left_value = get_element_or_zero(level, is_right ? (index - 1) : index); - previous_hash_path.push_back(std::make_pair(current_left_value, current_right_value)); + bool is_right = static_cast(index & 0x01); // Write the new leaf hash in place write_node(level, index, new_hash); @@ -324,28 +646,23 @@ fr IndexedTree::update_leaf_and_hash_to_root( while (level > 0) { if (level > 1) { - // Level is > 1. Therefore we need to wait for our leader to have written to the level above meaning we can - // read from it - size_t level_to_read = level - 1; - leader_level = level_to_read; - + // Level is > 1. Therefore we need to wait for our leader to have written to the level above meaning we + // can read from it + leader_level = level - 1; leader.wait_for_level(leader_level); // Now read the node and it's sibling index_t index_of_node_above = index >> 1; - bool node_above_is_right = bool(index_of_node_above & 0x01); - fr above_right_value = - get_element_or_zero(level_to_read, index_of_node_above + (node_above_is_right ? 0 : 1)); - fr above_left_value = get_element_or_zero( - level_to_read, node_above_is_right ? (index_of_node_above - 1) : index_of_node_above); - previous_hash_path.push_back(std::make_pair(above_left_value, above_right_value)); + bool node_above_is_right = static_cast(index_of_node_above & 0x01); + fr above_sibling = get_node(level, node_above_is_right ? index_of_node_above - 1 : index_of_node_above + 1); } // Now that we have extracted the hash path from the row above // we can compute the new hash at that level and write it - is_right = bool(index & 0x01); - fr new_right_value = is_right ? new_hash : get_element_or_zero(level, index + 1); - fr new_left_value = is_right ? get_element_or_zero(level, index - 1) : new_hash; + is_right = static_cast(index & 0x01); + fr new_right_value = is_right ? new_hash : get_node(level, index + 1); + fr new_left_value = is_right ? get_node(level, index - 1) : new_hash; + previous_sibling_path.emplace_back(is_right ? new_left_value : new_right_value); new_hash = HashingPolicy::hash_pair(new_left_value, new_right_value); index >>= 1; --level; @@ -360,22 +677,6 @@ fr IndexedTree::update_leaf_and_hash_to_root( write_node(level, index, new_hash); follower.signal_level(level); } - return new_hash; -} - -template -fr IndexedTree::append_subtree(const index_t& start_index) -{ - index_t index = start_index; - size_t number_to_insert = size_t(index_t(leaves_.get_size()) - index); - std::vector hashes_to_append = std::vector(number_to_insert); - - for (size_t i = 0; i < number_to_insert; ++i) { - index_t index_to_insert = index + i; - hashes_to_append[i] = HashingPolicy::hash(leaves_.get_leaf(size_t(index_to_insert)).get_hash_inputs()); - } - - return AppendOnlyTree::add_values(hashes_to_append); } -} // namespace bb::crypto::merkle_tree \ No newline at end of file +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.test.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.test.cpp index d1dc13d95e5..854c38d1882 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.test.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_tree.test.cpp @@ -1,220 +1,662 @@ #include "indexed_tree.hpp" -#include "../array_store.hpp" +#include "../fixtures.hpp" #include "../hash.hpp" +#include "../node_store/array_store.hpp" #include "../nullifier_tree/nullifier_memory_tree.hpp" #include "barretenberg/common/streams.hpp" #include "barretenberg/common/test.hpp" +#include "barretenberg/common/thread_pool.hpp" +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" #include "barretenberg/numeric/random/engine.hpp" -#include "leaves_cache.hpp" +#include +#include +#include +#include +#include +#include +#include using namespace bb; using namespace bb::crypto::merkle_tree; using HashPolicy = Poseidon2HashPolicy; -namespace { -auto& engine = numeric::get_debug_randomness(); -auto& random_engine = numeric::get_randomness(); -} // namespace +using Store = CachedTreeStore; +using TreeType = IndexedTree; +using IndexedNullifierLeafType = IndexedLeaf; +using IndexedPublicDataLeafType = IndexedLeaf; -const size_t NUM_VALUES = 1024; -static std::vector VALUES = []() { - std::vector values(NUM_VALUES); - for (size_t i = 0; i < NUM_VALUES; ++i) { - values[i] = fr(random_engine.get_random_uint256()); +using CompletionCallback = TreeType::AddCompletionCallback; + +class PersistedIndexedTreeTest : public testing::Test { + protected: + void SetUp() override + { + // setup with 1MB max db size, 1 max database and 2 maximum concurrent readers + _directory = random_temp_directory(); + std::filesystem::create_directories(_directory); + _environment = std::make_unique(_directory, 1024, 2, 2); } - return values; -}(); -TEST(stdlib_indexed_tree, can_create) + void TearDown() override { std::filesystem::remove_all(_directory); } + + static std::string _directory; + + std::unique_ptr _environment; +}; + +std::string PersistedIndexedTreeTest::_directory; + +template +void check_size(IndexedTree, Poseidon2HashPolicy>& tree, + index_t expected_size, + bool includeUncommitted = true) { - ArrayStore store(10); - IndexedTree tree = IndexedTree(store, 10); - EXPECT_EQ(tree.size(), 1ULL); + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, true); + EXPECT_EQ(response.inner.size, expected_size); + signal.signal_level(); + }; + tree.get_meta_data(includeUncommitted, completion); + signal.wait_for_level(); +} - NullifierMemoryTree memdb(10); - EXPECT_EQ(memdb.root(), tree.root()); +template +fr get_root(IndexedTree, Poseidon2HashPolicy>& tree, + bool includeUncommitted = true) +{ + fr r; + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + r = response.inner.root; + signal.signal_level(); + }; + tree.get_meta_data(includeUncommitted, completion); + signal.wait_for_level(); + return r; } -TEST(stdlib_indexed_tree, test_size) +template +void check_root(IndexedTree, Poseidon2HashPolicy>& tree, + fr expected_root, + bool includeUncommitted = true) { - ArrayStore store(32); - auto db = IndexedTree(store, 32); + fr root = get_root(tree, includeUncommitted); + EXPECT_EQ(root, expected_root); +} - // We assume that the first leaf is already filled with (0, 0, 0). - EXPECT_EQ(db.size(), 1ULL); +template +fr_sibling_path get_sibling_path(IndexedTree, Poseidon2HashPolicy>& tree, + index_t index, + bool includeUncommitted = true) +{ + fr_sibling_path h; + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + h = response.inner.path; + signal.signal_level(); + }; + tree.get_sibling_path(index, completion, includeUncommitted); + signal.wait_for_level(); + return h; +} + +template +IndexedLeaf get_leaf(IndexedTree, Poseidon2HashPolicy>& tree, + index_t index, + bool includeUncommitted = true) +{ + std::optional> l; + Signal signal; + auto completion = [&](const TypedResponse>& leaf) -> void { + l = leaf.inner.indexed_leaf; + signal.signal_level(); + }; + tree.get_leaf(index, includeUncommitted, completion); + signal.wait_for_level(); + return l.value(); +} + +template +std::pair get_low_leaf(IndexedTree, Poseidon2HashPolicy>& tree, + const LeafValueType& leaf, + bool includeUncommitted = true) +{ + std::pair low_leaf_info; + Signal signal; + auto completion = [&](const auto& leaf) -> void { + low_leaf_info = leaf.inner; + signal.signal_level(); + }; + tree.find_low_leaf(leaf.get_key(), includeUncommitted, completion); + signal.wait_for_level(); + return low_leaf_info; +} - // Add a new non-zero leaf at index 1. - db.add_value(30); - EXPECT_EQ(db.size(), 2ULL); +template +void check_find_leaf_index(TreeType& tree, + const LeafValueType& leaf, + index_t expected_index, + bool expected_success, + bool includeUncommitted = true) +{ + Signal signal; + auto completion = [&](const TypedResponse& response) -> void { + EXPECT_EQ(response.success, expected_success); + if (expected_success) { + EXPECT_EQ(response.inner.leaf_index, expected_index); + } + signal.signal_level(); + }; - // Add third. - db.add_value(10); - EXPECT_EQ(db.size(), 3ULL); + tree.find_leaf_index(leaf, includeUncommitted, completion); + signal.wait_for_level(); +} - // Add forth. - db.add_value(20); - EXPECT_EQ(db.size(), 4ULL); +template +void check_sibling_path(IndexedTree, Poseidon2HashPolicy>& tree, + index_t index, + fr_sibling_path expected_sibling_path, + bool includeUncommitted = true) +{ + fr_sibling_path path = get_sibling_path(tree, index, includeUncommitted); + EXPECT_EQ(path, expected_sibling_path); } -TEST(stdlib_indexed_tree, test_get_hash_path) +template +void commit_tree(IndexedTree, Poseidon2HashPolicy>& tree) { + Signal signal; + auto completion = [&](const Response& response) -> void { + EXPECT_EQ(response.success, true); + signal.signal_level(); + }; + tree.commit(completion); + signal.wait_for_level(); +} + +template +void add_value(IndexedTree, Poseidon2HashPolicy>& tree, + const LeafValueType& value) +{ + Signal signal; + auto completion = [&](const TypedResponse>&) -> void { + signal.signal_level(); + }; + + tree.add_or_update_value(value, completion); + signal.wait_for_level(); +} + +template +void add_values(IndexedTree, Poseidon2HashPolicy>& tree, + const std::vector& values) +{ + Signal signal; + auto completion = [&](const TypedResponse>&) -> void { + signal.signal_level(); + }; + + tree.add_or_update_values(values, completion); + signal.wait_for_level(); +} + +TEST_F(PersistedIndexedTreeTest, can_create) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + EXPECT_NO_THROW(Store store(name, depth, db)); + Store store(name, depth, db); + ThreadPool workers(1); + TreeType tree = TreeType(store, workers, 2); + check_size(tree, 2); + NullifierMemoryTree memdb(10); + check_root(tree, memdb.root()); +} + +TEST_F(PersistedIndexedTreeTest, can_only_recreate_with_same_name_and_depth) +{ + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + + EXPECT_ANY_THROW(Store store_wrong_name("Wrong name", depth, db)); + EXPECT_ANY_THROW(Store store_wrong_depth(name, depth + 1, db)); +} + +TEST_F(PersistedIndexedTreeTest, test_size) +{ + index_t current_size = 2; + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); + + check_size(tree, current_size); + + // We assume that the first leaf is already filled with (0, 0, 0). + for (uint32_t i = 0; i < 4; i++) { + add_value(tree, NullifierLeafValue(VALUES[i])); + check_size(tree, ++current_size); + } +} - ArrayStore store(10); - auto db = IndexedTree(store, 10); +TEST_F(PersistedIndexedTreeTest, indexed_tree_must_have_at_least_2_initial_size) +{ + index_t current_size = 1; + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + EXPECT_THROW(TreeType(store, workers, current_size), std::runtime_error); +} + +TEST_F(PersistedIndexedTreeTest, reports_an_error_if_tree_is_overfilled) +{ + index_t current_size = 2; + ThreadPool workers(1); + constexpr size_t depth = 4; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); + + std::vector values; + for (uint32_t i = 0; i < 14; i++) { + values.emplace_back(VALUES[i]); + } + add_values(tree, values); + + Signal signal; + auto add_completion = [&](const TypedResponse>& response) { + EXPECT_EQ(response.success, false); + EXPECT_EQ(response.message, "Tree is full"); + signal.signal_level(); + }; + tree.add_or_update_value(NullifierLeafValue(VALUES[16]), add_completion); + signal.wait_for_level(); +} + +TEST_F(PersistedIndexedTreeTest, test_get_sibling_path) +{ + index_t current_size = 2; + NullifierMemoryTree memdb(10, current_size); - EXPECT_EQ(memdb.root(), db.root()); + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); - EXPECT_EQ(memdb.get_hash_path(0), db.get_hash_path(0)); + check_size(tree, current_size); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); memdb.update_element(VALUES[512]); - db.add_value(VALUES[512]); + add_value(tree, NullifierLeafValue(VALUES[512])); + + // std::cout << memdb.get_sibling_path(0) << std::endl; + // std::cout << memdb.get_hash_path(0) << std::endl; + + // std::cout << get_sibling_path(tree, 0, true) << std::endl; + // std::cout << get_sibling_path(tree, 1, true) << std::endl; + + check_size(tree, ++current_size); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree, 1, memdb.get_sibling_path(1)); - EXPECT_EQ(db.get_hash_path(0), memdb.get_hash_path(0)); + uint32_t num_to_append = 512; - for (size_t i = 0; i < 512; ++i) { + for (uint32_t i = 0; i < num_to_append; ++i) { memdb.update_element(VALUES[i]); - db.add_value(VALUES[i]); + add_value(tree, NullifierLeafValue(VALUES[i])); } + check_size(tree, num_to_append + current_size); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree, 512, memdb.get_sibling_path(512)); +} + +TEST_F(PersistedIndexedTreeTest, test_find_leaf_index) +{ + index_t initial_size = 2; + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, initial_size); + + add_value(tree, NullifierLeafValue(30)); + add_value(tree, NullifierLeafValue(10)); + add_value(tree, NullifierLeafValue(20)); + add_value(tree, NullifierLeafValue(40)); + + // check the committed state and that the uncommitted state is empty + check_find_leaf_index(tree, NullifierLeafValue(10), 1 + initial_size, true, true); + check_find_leaf_index(tree, NullifierLeafValue(10), 0, false, false); + + check_find_leaf_index(tree, NullifierLeafValue(15), 0, false, true); + check_find_leaf_index(tree, NullifierLeafValue(15), 0, false, false); + + check_find_leaf_index(tree, NullifierLeafValue(40), 3 + initial_size, true, true); + check_find_leaf_index(tree, NullifierLeafValue(30), 0 + initial_size, true, true); + check_find_leaf_index(tree, NullifierLeafValue(20), 2 + initial_size, true, true); + + check_find_leaf_index(tree, NullifierLeafValue(40), 0, false, false); + check_find_leaf_index(tree, NullifierLeafValue(30), 0, false, false); + check_find_leaf_index(tree, NullifierLeafValue(20), 0, false, false); + + commit_tree(tree); + + std::vector values{ NullifierLeafValue(15), + NullifierLeafValue(18), + NullifierLeafValue(26), + NullifierLeafValue(2), + NullifierLeafValue(48) }; + add_values(tree, values); + + // check the now committed state + check_find_leaf_index(tree, NullifierLeafValue(40), 3 + initial_size, true, false); + check_find_leaf_index(tree, NullifierLeafValue(30), 0 + initial_size, true, false); + check_find_leaf_index(tree, NullifierLeafValue(20), 2 + initial_size, true, false); + + // check the new uncommitted state + check_find_leaf_index(tree, NullifierLeafValue(18), 5 + initial_size, true, true); + check_find_leaf_index(tree, NullifierLeafValue(18), 0, false, false); + + commit_tree(tree); + + values = { NullifierLeafValue(16), NullifierLeafValue(4), NullifierLeafValue(22), NullifierLeafValue(101) }; + add_values(tree, values); - EXPECT_EQ(db.get_hash_path(512), memdb.get_hash_path(512)); + // we now have duplicate leaf 18, one committed the other not + check_find_leaf_index(tree, NullifierLeafValue(18), 5 + initial_size, true, true); + check_find_leaf_index(tree, NullifierLeafValue(18), 5 + initial_size, true, false); } -TEST(stdlib_indexed_tree, test_batch_insert) +TEST_F(PersistedIndexedTreeTest, can_commit_and_restore) { - const size_t batch_size = 16; - const size_t num_batches = 16; - size_t depth = 10; + NullifierMemoryTree memdb(10); + index_t current_size = 2; + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + + { + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); + + check_size(tree, current_size); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + add_value(tree, NullifierLeafValue(VALUES[512])); + + // Committed data should not have changed + check_size(tree, current_size, false); + check_root(tree, memdb.root(), false); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), false); + check_sibling_path(tree, 1, memdb.get_sibling_path(1), false); + + memdb.update_element(VALUES[512]); + + // Uncommitted data should have changed + check_size(tree, current_size + 1, true); + check_root(tree, memdb.root(), true); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), true); + check_sibling_path(tree, 1, memdb.get_sibling_path(1), true); + + // Now commit + commit_tree(tree); + + // Now committed data should have changed + check_size(tree, ++current_size, false); + check_root(tree, memdb.root(), false); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), false); + check_sibling_path(tree, 1, memdb.get_sibling_path(1), false); + } + + // Now restore and it should continue from where we left off + { + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); + + // check uncommitted state + check_size(tree, current_size); + check_root(tree, memdb.root()); + check_sibling_path(tree, 0, memdb.get_sibling_path(0)); + + // check committed state + check_size(tree, current_size, false); + check_root(tree, memdb.root(), false); + check_sibling_path(tree, 0, memdb.get_sibling_path(0), false); + } +} + +TEST_F(PersistedIndexedTreeTest, test_batch_insert) +{ + auto& random_engine = numeric::get_randomness(); + const uint32_t batch_size = 16; + const uint32_t num_batches = 16; + uint32_t depth = 10; + ThreadPool workers(1); + ThreadPool multi_workers(8); NullifierMemoryTree memdb(depth, batch_size); - ArrayStore store1(depth); - IndexedTree tree1 = - IndexedTree(store1, depth, batch_size); + std::string name1 = random_string(); + LMDBStore db1(*_environment, name1, false, false, integer_key_cmp); + Store store1(name1, depth, db1); + auto tree1 = TreeType(store1, workers, batch_size); - ArrayStore store2(depth); - IndexedTree tree2 = - IndexedTree(store2, depth, batch_size); + std::string name2 = random_string(); + LMDBStore db2(*_environment, name2, false, false, integer_key_cmp); + Store store2(name2, depth, db2); + auto tree2 = TreeType(store2, workers, batch_size); - EXPECT_EQ(memdb.root(), tree1.root()); - EXPECT_EQ(tree1.root(), tree2.root()); + check_root(tree1, memdb.root()); + check_root(tree2, memdb.root()); - EXPECT_EQ(memdb.get_hash_path(0), tree1.get_hash_path(0)); - EXPECT_EQ(tree1.get_hash_path(0), tree2.get_hash_path(0)); + check_sibling_path(tree1, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree2, 0, memdb.get_sibling_path(0)); - EXPECT_EQ(memdb.get_hash_path(512), tree1.get_hash_path(512)); - EXPECT_EQ(tree1.get_hash_path(512), tree2.get_hash_path(512)); + check_sibling_path(tree1, 512, memdb.get_sibling_path(512)); + check_sibling_path(tree2, 512, memdb.get_sibling_path(512)); - for (size_t i = 0; i < num_batches; i++) { - std::vector batch; - std::vector memory_tree_hash_paths; - for (size_t j = 0; j < batch_size; j++) { - batch.push_back(fr(random_engine.get_random_uint256())); - fr_hash_path path = memdb.update_element(batch[j]); - memory_tree_hash_paths.push_back(path); + for (uint32_t i = 0; i < num_batches; i++) { + std::vector batch; + std::vector memory_tree_sibling_paths; + for (uint32_t j = 0; j < batch_size; j++) { + batch.emplace_back(random_engine.get_random_uint256()); + fr_sibling_path path = memdb.update_element(batch[j].value); + memory_tree_sibling_paths.push_back(path); + } + std::shared_ptr>> tree1_low_leaf_witness_data; + std::shared_ptr>> tree2_low_leaf_witness_data; + { + Signal signal; + CompletionCallback completion = + [&](const TypedResponse>& response) { + tree1_low_leaf_witness_data = response.inner.low_leaf_witness_data; + signal.signal_level(); + }; + tree1.add_or_update_values(batch, completion); + signal.wait_for_level(); } - std::vector tree1_hash_paths = tree1.add_or_update_values(batch, true); - std::vector tree2_hash_paths = tree2.add_or_update_values(batch); - EXPECT_EQ(memdb.root(), tree1.root()); - EXPECT_EQ(tree1.root(), tree2.root()); + { + Signal signal; + CompletionCallback completion = + [&](const TypedResponse>& response) { + tree2_low_leaf_witness_data = response.inner.low_leaf_witness_data; + signal.signal_level(); + }; + tree2.add_or_update_values(batch, completion); + signal.wait_for_level(); + } + check_root(tree1, memdb.root()); + check_root(tree2, memdb.root()); - EXPECT_EQ(memdb.get_hash_path(0), tree1.get_hash_path(0)); - EXPECT_EQ(tree1.get_hash_path(0), tree2.get_hash_path(0)); + check_sibling_path(tree1, 0, memdb.get_sibling_path(0)); + check_sibling_path(tree2, 0, memdb.get_sibling_path(0)); - EXPECT_EQ(memdb.get_hash_path(512), tree1.get_hash_path(512)); - EXPECT_EQ(tree1.get_hash_path(512), tree2.get_hash_path(512)); + check_sibling_path(tree1, 512, memdb.get_sibling_path(512)); + check_sibling_path(tree2, 512, memdb.get_sibling_path(512)); - for (size_t j = 0; j < batch_size; j++) { - EXPECT_EQ(tree1_hash_paths[j], tree2_hash_paths[j]); - // EXPECT_EQ(tree1_hash_paths[j], memory_tree_hash_paths[j]); + for (uint32_t j = 0; j < batch_size; j++) { + EXPECT_EQ(tree1_low_leaf_witness_data->at(j).leaf, tree2_low_leaf_witness_data->at(j).leaf); + EXPECT_EQ(tree1_low_leaf_witness_data->at(j).index, tree2_low_leaf_witness_data->at(j).index); + EXPECT_EQ(tree1_low_leaf_witness_data->at(j).path, tree2_low_leaf_witness_data->at(j).path); } } } -fr hash_leaf(const indexed_leaf& leaf) +TEST_F(PersistedIndexedTreeTest, reports_an_error_if_batch_contains_duplicate) +{ + index_t current_size = 2; + ThreadPool workers(1); + constexpr size_t depth = 10; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); + + std::vector values; + for (uint32_t i = 0; i < 16; i++) { + values.emplace_back(VALUES[i]); + } + values[8] = values[0]; + + Signal signal; + auto add_completion = [&](const TypedResponse>& response) { + EXPECT_EQ(response.success, false); + EXPECT_EQ(response.message, "Duplicate key not allowed in same batch"); + signal.signal_level(); + }; + tree.add_or_update_values(values, add_completion); + signal.wait_for_level(); +} + +template fr hash_leaf(const IndexedLeaf& leaf) { return HashPolicy::hash(leaf.get_hash_inputs()); } -bool check_hash_path(const fr& root, const fr_hash_path& path, const indexed_leaf& leaf_value, const size_t idx) +bool verify_sibling_path(TreeType& tree, const IndexedNullifierLeafType& leaf_value, const uint32_t idx) { + fr root = get_root(tree, true); + fr_sibling_path path = get_sibling_path(tree, idx, true); auto current = hash_leaf(leaf_value); - size_t depth_ = path.size(); - size_t index = idx; - for (size_t i = 0; i < depth_; ++i) { - fr left = (index & 1) ? path[i].first : current; - fr right = (index & 1) ? current : path[i].second; + uint32_t depth_ = static_cast(path.size()); + uint32_t index = idx; + for (uint32_t i = 0; i < depth_; ++i) { + fr left = (index & 1) ? path[i] : current; + fr right = (index & 1) ? current : path[i]; current = HashPolicy::hash_pair(left, right); index >>= 1; } return current == root; } -TEST(stdlib_indexed_tree, test_indexed_memory) +IndexedNullifierLeafType create_indexed_nullifier_leaf(const fr& value, index_t nextIndex, const fr& nextValue) +{ + return IndexedNullifierLeafType{ NullifierLeafValue(value), nextIndex, nextValue }; +} + +IndexedPublicDataLeafType create_indexed_public_data_leaf(const fr& slot, + const fr& value, + index_t nextIndex, + const fr& nextValue) { + return IndexedPublicDataLeafType{ PublicDataLeafValue(slot, value), nextIndex, nextValue }; +} + +TEST_F(PersistedIndexedTreeTest, test_indexed_memory) +{ + index_t current_size = 2; + ThreadPool workers(8); // Create a depth-3 indexed merkle tree constexpr size_t depth = 3; - ArrayStore store(depth); - IndexedTree tree = - IndexedTree(store, depth, 1); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); /** * Intial state: * * index 0 1 2 3 4 5 6 7 * --------------------------------------------------------------------- - * val 0 0 0 0 0 0 0 0 - * nextIdx 0 0 0 0 0 0 0 0 + * val 1 1 0 0 0 0 0 0 + * nextIdx 1 0 0 0 0 0 0 0 * nextVal 0 0 0 0 0 0 0 0 */ - indexed_leaf zero_leaf = { 0, 0, 0 }; - EXPECT_EQ(tree.size(), 1); - EXPECT_EQ(tree.get_leaf(0), zero_leaf); + IndexedNullifierLeafType zero_leaf(NullifierLeafValue(0), 1, 1); + IndexedNullifierLeafType one_leaf(NullifierLeafValue(1), 0, 0); + check_size(tree, current_size); + EXPECT_EQ(get_leaf(tree, 0), zero_leaf); + EXPECT_EQ(get_leaf(tree, 1), one_leaf); /** * Add new value 30: * * index 0 1 2 3 4 5 6 7 * --------------------------------------------------------------------- - * val 0 30 0 0 0 0 0 0 - * nextIdx 1 0 0 0 0 0 0 0 - * nextVal 30 0 0 0 0 0 0 0 + * val 0 1 30 0 0 0 0 0 + * nextIdx 1 2 0 0 0 0 0 0 + * nextVal 1 30 0 0 0 0 0 0 */ - tree.add_value(30); - EXPECT_EQ(tree.size(), 2); - EXPECT_EQ(hash_leaf(tree.get_leaf(0)), hash_leaf({ 0, 1, 30 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(1)), hash_leaf({ 30, 0, 0 })); + add_value(tree, NullifierLeafValue(30)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_nullifier_leaf(0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_nullifier_leaf(1, 2, 30)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_nullifier_leaf(30, 0, 0)); /** * Add new value 10: * * index 0 1 2 3 4 5 6 7 * --------------------------------------------------------------------- - * val 0 30 10 0 0 0 0 0 - * nextIdx 2 0 1 0 0 0 0 0 - * nextVal 10 0 30 0 0 0 0 0 + * val 0 1 30 10 0 0 0 0 + * nextIdx 1 3 0 2 0 0 0 0 + * nextVal 1 10 0 30 0 0 0 0 */ - tree.add_value(10); - EXPECT_EQ(tree.size(), 3); - EXPECT_EQ(hash_leaf(tree.get_leaf(0)), hash_leaf({ 0, 2, 10 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(1)), hash_leaf({ 30, 0, 0 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(2)), hash_leaf({ 10, 1, 30 })); + add_value(tree, NullifierLeafValue(10)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_nullifier_leaf(0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_nullifier_leaf(1, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_nullifier_leaf(30, 0, 0)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_nullifier_leaf(10, 2, 30)); /** * Add new value 20: * * index 0 1 2 3 4 5 6 7 * --------------------------------------------------------------------- - * val 0 30 10 20 0 0 0 0 - * nextIdx 2 0 3 1 0 0 0 0 - * nextVal 10 0 20 30 0 0 0 0 + * val 0 1 30 10 20 0 0 0 + * nextIdx 1 3 0 4 2 0 0 0 + * nextVal 1 10 0 20 30 0 0 0 */ - tree.add_value(20); - EXPECT_EQ(tree.size(), 4); - EXPECT_EQ(hash_leaf(tree.get_leaf(0)), hash_leaf({ 0, 2, 10 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(1)), hash_leaf({ 30, 0, 0 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(2)), hash_leaf({ 10, 3, 20 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(3)), hash_leaf({ 20, 1, 30 })); + add_value(tree, NullifierLeafValue(20)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_nullifier_leaf(0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_nullifier_leaf(1, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_nullifier_leaf(30, 0, 0)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_nullifier_leaf(10, 4, 20)); + EXPECT_EQ(get_leaf(tree, 4), create_indexed_nullifier_leaf(20, 2, 30)); // Adding the same value must not affect anything // tree.update_element(20); @@ -229,27 +671,28 @@ TEST(stdlib_indexed_tree, test_indexed_memory) * * index 0 1 2 3 4 5 6 7 * --------------------------------------------------------------------- - * val 0 30 10 20 50 0 0 0 - * nextIdx 2 4 3 1 0 0 0 0 - * nextVal 10 50 20 30 0 0 0 0 + * val 0 1 30 10 20 50 0 0 + * nextIdx 1 3 5 4 2 0 0 0 + * nextVal 1 10 50 20 30 0 0 0 */ - tree.add_value(50); - EXPECT_EQ(tree.size(), 5); - EXPECT_EQ(hash_leaf(tree.get_leaf(0)), hash_leaf({ 0, 2, 10 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(1)), hash_leaf({ 30, 4, 50 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(2)), hash_leaf({ 10, 3, 20 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(3)), hash_leaf({ 20, 1, 30 })); - EXPECT_EQ(hash_leaf(tree.get_leaf(4)), hash_leaf({ 50, 0, 0 })); + add_value(tree, NullifierLeafValue(50)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_nullifier_leaf(0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_nullifier_leaf(1, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_nullifier_leaf(30, 5, 50)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_nullifier_leaf(10, 4, 20)); + EXPECT_EQ(get_leaf(tree, 4), create_indexed_nullifier_leaf(20, 2, 30)); + EXPECT_EQ(get_leaf(tree, 5), create_indexed_nullifier_leaf(50, 0, 0)); // Manually compute the node values - auto e000 = hash_leaf(tree.get_leaf(0)); - auto e001 = hash_leaf(tree.get_leaf(1)); - auto e010 = hash_leaf(tree.get_leaf(2)); - auto e011 = hash_leaf(tree.get_leaf(3)); - auto e100 = hash_leaf(tree.get_leaf(4)); - auto e101 = hash_leaf({ 0, 0, 0 }); - auto e110 = hash_leaf({ 0, 0, 0 }); - auto e111 = hash_leaf({ 0, 0, 0 }); + auto e000 = hash_leaf(get_leaf(tree, 0)); + auto e001 = hash_leaf(get_leaf(tree, 1)); + auto e010 = hash_leaf(get_leaf(tree, 2)); + auto e011 = hash_leaf(get_leaf(tree, 3)); + auto e100 = hash_leaf(get_leaf(tree, 4)); + auto e101 = hash_leaf(get_leaf(tree, 5)); + auto e110 = fr::zero(); + auto e111 = fr::zero(); auto e00 = HashPolicy::hash_pair(e000, e001); auto e01 = HashPolicy::hash_pair(e010, e011); @@ -262,48 +705,67 @@ TEST(stdlib_indexed_tree, test_indexed_memory) // Check the hash path at index 2 and 3 // Note: This merkle proof would also serve as a non-membership proof of values in (10, 20) and (20, 30) - fr_hash_path expected = { - std::make_pair(e000, e001), - std::make_pair(e00, e01), - std::make_pair(e0, e1), + fr_sibling_path expected = { + e001, + e01, + e1, + }; + check_sibling_path(tree, 0, expected); + expected = { + e000, + e01, + e1, }; - EXPECT_EQ(tree.get_hash_path(0), expected); - EXPECT_EQ(tree.get_hash_path(1), expected); - fr_hash_path expected2 = { - std::make_pair(e010, e011), - std::make_pair(e00, e01), - std::make_pair(e0, e1), + check_sibling_path(tree, 1, expected); + expected = { + e011, + e00, + e1, }; - EXPECT_EQ(tree.get_hash_path(2), expected2); - EXPECT_EQ(tree.get_hash_path(3), expected2); - EXPECT_EQ(tree.root(), root); + check_sibling_path(tree, 2, expected); + expected = { + e010, + e00, + e1, + }; + check_sibling_path(tree, 3, expected); + check_root(tree, root); // Check the hash path at index 6 and 7 expected = { - std::make_pair(e110, e111), - std::make_pair(e10, e11), - std::make_pair(e0, e1), + e111, + e10, + e0, + }; + check_sibling_path(tree, 6, expected); + expected = { + e110, + e10, + e0, }; - EXPECT_EQ(tree.get_hash_path(6), expected); - EXPECT_EQ(tree.get_hash_path(7), expected); + check_sibling_path(tree, 7, expected); } -TEST(stdlib_indexed_tree, test_indexed_tree) +TEST_F(PersistedIndexedTreeTest, test_indexed_tree) { + index_t current_size = 2; + ThreadPool workers(1); // Create a depth-8 indexed merkle tree - constexpr size_t depth = 8; - ArrayStore store(depth); - IndexedTree tree = - IndexedTree(store, depth, 1); + constexpr uint32_t depth = 8; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, current_size); - indexed_leaf zero_leaf = { 0, 0, 0 }; - EXPECT_EQ(tree.size(), 1); - EXPECT_EQ(hash_leaf(tree.get_leaf(0)), hash_leaf(zero_leaf)); + IndexedNullifierLeafType zero_leaf = create_indexed_nullifier_leaf(0, 1, 1); + check_size(tree, current_size); + EXPECT_EQ(hash_leaf(get_leaf(tree, 0)), hash_leaf(zero_leaf)); // Add 20 random values to the tree - for (size_t i = 0; i < 20; i++) { + for (uint32_t i = 0; i < 20; i++) { auto value = fr::random_element(); - tree.add_value(value); + add_value(tree, NullifierLeafValue(value)); + ++current_size; } auto abs_diff = [](uint256_t a, uint256_t b) { @@ -314,18 +776,266 @@ TEST(stdlib_indexed_tree, test_indexed_tree) } }; + check_size(tree, current_size); + // Check if a new random value is not a member of this tree. fr new_member = fr::random_element(); std::vector differences; - for (size_t i = 0; i < size_t(tree.size()); i++) { - uint256_t diff_hi = abs_diff(uint256_t(new_member), uint256_t(tree.get_leaf(i).value)); - uint256_t diff_lo = abs_diff(uint256_t(new_member), uint256_t(tree.get_leaf(i).value)); + for (uint32_t i = 0; i < uint32_t(21); i++) { + uint256_t diff_hi = abs_diff(uint256_t(new_member), uint256_t(get_leaf(tree, i).value.get_key())); + uint256_t diff_lo = abs_diff(uint256_t(new_member), uint256_t(get_leaf(tree, i).value.get_key())); differences.push_back(diff_hi + diff_lo); } auto it = std::min_element(differences.begin(), differences.end()); - auto index = static_cast(it - differences.begin()); + auto index = static_cast(it - differences.begin()); // Merkle proof at `index` proves non-membership of `new_member` - auto hash_path = tree.get_hash_path(index); - EXPECT_TRUE(check_hash_path(tree.root(), hash_path, tree.get_leaf(index), index)); -} \ No newline at end of file + EXPECT_TRUE(verify_sibling_path(tree, get_leaf(tree, index), index)); +} + +TEST_F(PersistedIndexedTreeTest, can_add_single_whilst_reading) +{ + constexpr size_t depth = 10; + NullifierMemoryTree memdb(10); + fr_sibling_path initial_path = memdb.get_sibling_path(0); + memdb.update_element(VALUES[0]); + fr_sibling_path final_sibling_path = memdb.get_sibling_path(0); + + uint32_t num_reads = 16 * 1024; + std::vector paths(num_reads); + + { + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + ThreadPool pool(8); + TreeType tree(store, pool, 2); + + check_size(tree, 2); + + Signal signal(2); + + auto add_completion = [&](const TypedResponse>&) { + signal.signal_level(1); + auto commit_completion = [&](const Response&) { signal.signal_level(); }; + tree.commit(commit_completion); + }; + tree.add_or_update_value(VALUES[0], add_completion); + + for (size_t i = 0; i < num_reads; i++) { + auto completion = [&, i](const TypedResponse& response) { + paths[i] = response.inner.path; + }; + tree.get_sibling_path(0, completion, false); + } + signal.wait_for_level(); + } + + // for (auto& path : paths) { + // EXPECT_TRUE(path == initial_path || path == final_sibling_path); + // } +} + +TEST_F(PersistedIndexedTreeTest, test_indexed_memory_with_public_data_writes) +{ + index_t current_size = 2; + ThreadPool workers(8); + // Create a depth-3 indexed merkle tree + constexpr size_t depth = 3; + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + CachedTreeStore store(name, depth, db); + auto tree = + IndexedTree, Poseidon2HashPolicy>(store, workers, current_size); + + /** + * Intial state: + * + * index 0 1 2 3 4 5 6 7 + * --------------------------------------------------------------------- + * slot 0 1 0 0 0 0 0 0 + * val 0 0 0 0 0 0 0 0 + * nextIdx 1 0 0 0 0 0 0 0 + * nextVal 1 0 0 0 0 0 0 0 + */ + IndexedPublicDataLeafType zero_leaf = create_indexed_public_data_leaf(0, 0, 1, 1); + IndexedPublicDataLeafType one_leaf = create_indexed_public_data_leaf(1, 0, 0, 0); + check_size(tree, current_size); + EXPECT_EQ(get_leaf(tree, 0), zero_leaf); + EXPECT_EQ(get_leaf(tree, 1), one_leaf); + + /** + * Add new slot:value 30:5: + * + * index 0 1 2 3 4 5 6 7 + * --------------------------------------------------------------------- + * slot 0 1 30 0 0 0 0 0 + * val 0 0 5 0 0 0 0 0 + * nextIdx 1 2 0 0 0 0 0 0 + * nextVal 1 30 0 0 0 0 0 0 + */ + add_value(tree, PublicDataLeafValue(30, 5)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_public_data_leaf(0, 0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_public_data_leaf(1, 0, 2, 30)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_public_data_leaf(30, 5, 0, 0)); + + /** + * Add new slot:value 10:20: + * + * index 0 1 2 3 4 5 6 7 + * --------------------------------------------------------------------- + * slot 0 1 30 10 0 0 0 0 + * val 0 0 5 20 0 0 0 0 + * nextIdx 1 3 0 2 0 0 0 0 + * nextVal 1 10 0 30 0 0 0 0 + */ + add_value(tree, PublicDataLeafValue(10, 20)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_public_data_leaf(0, 0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_public_data_leaf(1, 0, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_public_data_leaf(30, 5, 0, 0)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_public_data_leaf(10, 20, 2, 30)); + + /** + * Update value at slot 30 to 6: + * + * index 0 1 2 3 4 5 6 7 + * --------------------------------------------------------------------- + * slot 0 1 30 10 0 0 0 0 + * val 0 0 6 20 0 0 0 0 + * nextIdx 1 3 0 2 0 0 0 0 + * nextVal 1 10 0 30 0 0 0 0 + */ + add_value(tree, PublicDataLeafValue(30, 6)); + // The size still increases as we pad with an empty leaf + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_public_data_leaf(0, 0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_public_data_leaf(1, 0, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_public_data_leaf(30, 6, 0, 0)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_public_data_leaf(10, 20, 2, 30)); + EXPECT_EQ(get_leaf(tree, 4), create_indexed_public_data_leaf(0, 0, 0, 0)); + + /** + * Add new value slot:value 50:8: + * + * index 0 1 2 3 4 5 6 7 + * --------------------------------------------------------------------- + * slot 0 1 30 10 0 50 0 0 + * val 0 0 6 20 0 8 0 0 + * nextIdx 1 3 5 2 0 0 0 0 + * nextVal 1 10 50 30 0 0 0 0 + */ + add_value(tree, PublicDataLeafValue(50, 8)); + check_size(tree, ++current_size); + EXPECT_EQ(get_leaf(tree, 0), create_indexed_public_data_leaf(0, 0, 1, 1)); + EXPECT_EQ(get_leaf(tree, 1), create_indexed_public_data_leaf(1, 0, 3, 10)); + EXPECT_EQ(get_leaf(tree, 2), create_indexed_public_data_leaf(30, 6, 5, 50)); + EXPECT_EQ(get_leaf(tree, 3), create_indexed_public_data_leaf(10, 20, 2, 30)); + EXPECT_EQ(get_leaf(tree, 4), create_indexed_public_data_leaf(0, 0, 0, 0)); + EXPECT_EQ(get_leaf(tree, 5), create_indexed_public_data_leaf(50, 8, 0, 0)); + + // Manually compute the node values + auto e000 = hash_leaf(get_leaf(tree, 0)); + auto e001 = hash_leaf(get_leaf(tree, 1)); + auto e010 = hash_leaf(get_leaf(tree, 2)); + auto e011 = hash_leaf(get_leaf(tree, 3)); + auto e100 = fr::zero(); // tree doesn't hash 0 leaves! + auto e101 = hash_leaf(get_leaf(tree, 5)); + auto e110 = fr::zero(); + auto e111 = fr::zero(); + + auto e00 = HashPolicy::hash_pair(e000, e001); + auto e01 = HashPolicy::hash_pair(e010, e011); + auto e10 = HashPolicy::hash_pair(e100, e101); + auto e11 = HashPolicy::hash_pair(e110, e111); + + auto e0 = HashPolicy::hash_pair(e00, e01); + auto e1 = HashPolicy::hash_pair(e10, e11); + auto root = HashPolicy::hash_pair(e0, e1); + + fr_sibling_path expected = { + e001, + e01, + e1, + }; + check_sibling_path(tree, 0, expected); + expected = { + e000, + e01, + e1, + }; + check_sibling_path(tree, 1, expected); + expected = { + e011, + e00, + e1, + }; + check_sibling_path(tree, 2, expected); + expected = { + e010, + e00, + e1, + }; + check_sibling_path(tree, 3, expected); + check_root(tree, root); + + // Check the hash path at index 6 and 7 + expected = { + e111, + e10, + e0, + }; + check_sibling_path(tree, 6, expected); + expected = { + e110, + e10, + e0, + }; + check_sibling_path(tree, 7, expected); +} + +TEST_F(PersistedIndexedTreeTest, returns_low_leaves) +{ + // Create a depth-8 indexed merkle tree + constexpr uint32_t depth = 8; + + ThreadPool workers(1); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, 2); + + auto predecessor = get_low_leaf(tree, NullifierLeafValue(42)); + + EXPECT_EQ(predecessor.first, false); + EXPECT_EQ(predecessor.second, 1); + + add_value(tree, NullifierLeafValue(42)); + + predecessor = get_low_leaf(tree, NullifierLeafValue(42)); + // returns the current leaf since it exists already. Inserting 42 again would modify the existing leaf + EXPECT_EQ(predecessor.first, true); + EXPECT_EQ(predecessor.second, 2); +} + +TEST_F(PersistedIndexedTreeTest, duplicates) +{ + // Create a depth-8 indexed merkle tree + constexpr uint32_t depth = 8; + + ThreadPool workers(1); + std::string name = random_string(); + LMDBStore db(*_environment, name, false, false, integer_key_cmp); + Store store(name, depth, db); + auto tree = TreeType(store, workers, 2); + + add_value(tree, NullifierLeafValue(42)); + check_size(tree, 3); + + commit_tree(tree); + + add_value(tree, NullifierLeafValue(42)); + commit_tree(tree); + check_size(tree, 3); +} diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.cpp deleted file mode 100644 index cd29811455d..00000000000 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "leaves_cache.hpp" - -namespace bb::crypto::merkle_tree { - -index_t LeavesCache::get_size() const -{ - return index_t(leaves_.size()); -} - -std::pair LeavesCache::find_low_value(const fr& new_value) const -{ - std::map::const_iterator it = indices_.lower_bound(new_value); - if (it == indices_.end()) { - // there is no element >= the requested value. - // decrement the iterator to get the value preceeding the requested value - --it; - return std::make_pair(false, it->second); - } - if (it->first == uint256_t(new_value)) { - // the value is already present and the iterator points to it - return std::make_pair(true, it->second); - } - // the iterator points to the element immediately larger than the requested value - --it; - // it now points to the value less than that requested - return std::make_pair(false, it->second); -} -indexed_leaf LeavesCache::get_leaf(const index_t& index) const -{ - ASSERT(index >= 0 && index < leaves_.size()); - return leaves_[size_t(index)]; -} -void LeavesCache::set_at_index(const index_t& index, const indexed_leaf& leaf, bool add_to_index) -{ - if (index >= leaves_.size()) { - leaves_.resize(size_t(index + 1)); - } - leaves_[size_t(index)] = leaf; - if (add_to_index) { - indices_[uint256_t(leaf.value)] = index; - } -} -void LeavesCache::append_leaf(const indexed_leaf& leaf) -{ - index_t next_index = leaves_.size(); - set_at_index(next_index, leaf, true); -} - -} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.hpp deleted file mode 100644 index b38f556807e..00000000000 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/leaves_cache.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include "barretenberg/stdlib/primitives/field/field.hpp" -#include "indexed_leaf.hpp" - -namespace bb::crypto::merkle_tree { - -typedef uint256_t index_t; - -/** - * @brief Used to facilitate testing of the IndexedTree. Stores leaves in memory with an index for O(logN) retrieval of - * 'low leaves' - * - */ -class LeavesCache { - public: - index_t get_size() const; - std::pair find_low_value(const bb::fr& new_value) const; - indexed_leaf get_leaf(const index_t& index) const; - void set_at_index(const index_t& index, const indexed_leaf& leaf, bool add_to_index); - void append_leaf(const indexed_leaf& leaf); - - private: - std::map indices_; - std::vector leaves_; -}; - -} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.cpp new file mode 100644 index 00000000000..485b1bff766 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.cpp @@ -0,0 +1,148 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include "lmdb.h" +#include +#include +#include +#include +#include + +namespace bb::crypto::merkle_tree { +void throw_error(const std::string& errorString, int error) +{ + std::stringstream ss; + ss << errorString << ": " << error << " - " << mdb_strerror(error) << std::endl; + throw std::runtime_error(ss.str()); +} + +std::vector serialise_key(uint8_t key) +{ + return { key }; +} + +void deserialise_key(void* data, uint8_t& key) +{ + uint8_t* p = static_cast(data); + key = *p; +} + +std::vector serialise_key(uint64_t key) +{ + const uint8_t* p = reinterpret_cast(&key); + return std::vector(p, p + sizeof(key)); +} + +void deserialise_key(void* data, uint64_t& key) +{ + std::memcpy(&key, data, sizeof(key)); +} + +std::vector serialise_key(uint128_t key) +{ + std::vector buf(16); +#ifdef __i386__ + std::memcpy(buf.data(), key.data, 16); +#else + std::memcpy(buf.data(), &key, 16); +#endif + return buf; +} + +void deserialise_key(void* data, uint128_t& key) +{ +#ifdef __i386__ + std::memcpy(key.data, data, 16); +#else + std::memcpy(&key, data, 16); +#endif +} + +std::vector serialise_key(uint256_t key) +{ + std::vector buf(32); + std::memcpy(buf.data(), key.data, 32); + return buf; +} + +void deserialise_key(void* data, uint256_t& key) +{ + std::memcpy(key.data, data, 32); +} + +// Nodes are stored as a heap +NodeKeyType get_key_for_node(uint32_t level, index_t index) +{ + NodeKeyType key = static_cast(1) << level; + key += static_cast(index); + return key - 1; +} + +int size_cmp(const MDB_val* a, const MDB_val* b) +{ + if (a->mv_size < b->mv_size) { + return -1; + } + if (a->mv_size > b->mv_size) { + return 1; + } + return 0; +} + +std::vector mdb_val_to_vector(const MDB_val& dbVal) +{ + const uint8_t* p = static_cast(dbVal.mv_data); + return std::vector(p, p + dbVal.mv_size); +} + +/** + * Default lexicographical implementation of key comparisons used in our LMDB implementation + */ +int lexico_cmp(const MDB_val* a, const MDB_val* b) +{ + std::vector a_vector = mdb_val_to_vector(*a); + std::vector b_vector = mdb_val_to_vector(*b); + return std::lexicographical_compare(a_vector.begin(), a_vector.end(), b_vector.begin(), b_vector.end()); +} + +/** + * Integer key comparison function. + * We use this to divide the key space into discrete integer sizes + * We check the key length in bytes to establish if it is exactly + * 1. 1 byte + * 2. 8 bytes + * 3. 16 bytes + * 4. 32 bytes + * If it is one of the above sizes then we assume it is an integer value and we compare it as such + */ +int integer_key_cmp(const MDB_val* a, const MDB_val* b) +{ + // Id the keys sizes are different, sort by key size + if (a->mv_size != b->mv_size) { + return size_cmp(a, b); + } + uint64_t factor = a->mv_size / sizeof(uint64_t); + uint64_t remainder = a->mv_size % sizeof(uint64_t); + + // If the size is > 32 bytes, use default comparison + if (a->mv_size > 32) { + return lexico_cmp(a, b); + } + // If the size is not a divisible by 8 then use default comparison, unless it is 1 byte + if (a->mv_size > 1 && remainder != 0) { + return lexico_cmp(a, b); + } + + // Size is either 1, 8, 16 or 32 bytes, compare based on integer keys + using f = std::function; + static std::vector functions{ + value_cmp, value_cmp, value_cmp, lexico_cmp, value_cmp + }; + return functions[factor](a, b); +} + +void copy_to_vector(const MDB_val& dbVal, std::vector& target) +{ + std::vector temp = mdb_val_to_vector(dbVal); + target.swap(temp); +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp new file mode 100644 index 00000000000..0c9174f09ec --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/numeric/uint128/uint128.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include +#include +#include + +namespace bb::crypto::merkle_tree { +using NodeKeyType = uint128_t; +using LeafIndexKeyType = uint64_t; +using FrKeyType = uint256_t; +using MetaKeyType = uint8_t; + +void throw_error(const std::string& errorString, int error); + +int size_cmp(const MDB_val* a, const MDB_val* b); + +int lexico_cmp(const MDB_val*, const MDB_val*); + +NodeKeyType get_key_for_node(uint32_t level, index_t index); + +std::vector serialise_key(uint8_t key); +std::vector serialise_key(uint64_t key); +std::vector serialise_key(uint128_t key); +std::vector serialise_key(uint256_t key); + +void deserialise_key(void* data, uint8_t& key); +void deserialise_key(void* data, uint64_t& key); +void deserialise_key(void* data, uint128_t& key); +void deserialise_key(void* data, uint256_t& key); + +template int value_cmp(const MDB_val* a, const MDB_val* b) +{ + T lhs; + T rhs; + deserialise_key(a->mv_data, lhs); + deserialise_key(b->mv_data, rhs); + if (lhs < rhs) { + return -1; + } + if (lhs > rhs) { + return 1; + } + return 0; +} + +int integer_key_cmp(const MDB_val* a, const MDB_val* b); +std::vector mdb_val_to_vector(const MDB_val& dbVal); +void copy_to_vector(const MDB_val& dbVal, std::vector& target); + +template bool call_lmdb_func(int (*f)(TArgs...), TArgs... args) +{ + int error = f(args...); + return error == 0; +} + +template void call_lmdb_func(const std::string& errorString, int (*f)(TArgs...), TArgs... args) +{ + int error = f(args...); + if (error != 0) { + throw_error(errorString, error); + } +} + +template void call_lmdb_func(void (*f)(TArgs...), TArgs... args) +{ + f(args...); +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.cpp new file mode 100644 index 00000000000..2ac14c70fd0 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.cpp @@ -0,0 +1,36 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" + +namespace bb::crypto::merkle_tree { +LMDBDatabase::LMDBDatabase(const LMDBEnvironment& env, + const LMDBDatabaseCreationTransaction& transaction, + const std::string& name, + bool integerKeys, + bool reverseKeys, + MDB_cmp_func* cmp) + : _environment(env) +{ + unsigned int flags = MDB_CREATE; + if (integerKeys) { + flags |= MDB_INTEGERKEY; + } + if (reverseKeys) { + flags |= MDB_REVERSEKEY; + } + call_lmdb_func("mdb_dbi_open", mdb_dbi_open, transaction.underlying(), name.c_str(), flags, &_dbi); + if (cmp != nullptr) { + call_lmdb_func("mdb_set_compare", mdb_set_compare, transaction.underlying(), _dbi, cmp); + } + transaction.commit(); +} + +LMDBDatabase::~LMDBDatabase() +{ + call_lmdb_func(mdb_dbi_close, _environment.underlying(), _dbi); +} + +const MDB_dbi& LMDBDatabase::underlying() const +{ + return _dbi; +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp new file mode 100644 index 00000000000..c656cf023bb --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp" + +namespace bb::crypto::merkle_tree { +/** + * RAII wrapper atound the opening and closing of an LMDB database + * Contains a reference to its LMDB environment + */ +class LMDBDatabase { + public: + LMDBDatabase(const LMDBEnvironment& env, + const LMDBDatabaseCreationTransaction& transaction, + const std::string& name, + bool integerKeys = false, + bool reverseKeys = false, + MDB_cmp_func* cmp = nullptr); + + LMDBDatabase(const LMDBDatabase& other) = delete; + LMDBDatabase(LMDBDatabase&& other) = delete; + LMDBDatabase& operator=(const LMDBDatabase& other) = delete; + LMDBDatabase& operator=(LMDBDatabase&& other) = delete; + + ~LMDBDatabase(); + + const MDB_dbi& underlying() const; + + private: + MDB_dbi _dbi; + const LMDBEnvironment& _environment; +}; +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.cpp new file mode 100644 index 00000000000..532b3818684 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.cpp @@ -0,0 +1,12 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" + +namespace bb::crypto::merkle_tree { +LMDBDatabaseCreationTransaction::LMDBDatabaseCreationTransaction(LMDBEnvironment& env) + : LMDBTransaction(env) +{} +void LMDBDatabaseCreationTransaction::commit() const +{ + call_lmdb_func("mdb_txn_commit", mdb_txn_commit, _transaction); +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp new file mode 100644 index 00000000000..af5cf528ac2 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_db_transaction.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp" + +namespace bb::crypto::merkle_tree { + +/* + * RAII wrapper to construct a transaction for the purpose of creating/opening a database + */ +class LMDBDatabaseCreationTransaction : public LMDBTransaction { + public: + using Ptr = std::unique_ptr; + + LMDBDatabaseCreationTransaction(LMDBEnvironment& env); + LMDBDatabaseCreationTransaction(const LMDBDatabaseCreationTransaction& other) = delete; + LMDBDatabaseCreationTransaction(LMDBDatabaseCreationTransaction&& other) = delete; + LMDBDatabaseCreationTransaction& operator=(const LMDBDatabaseCreationTransaction& other) = delete; + LMDBDatabaseCreationTransaction& operator=(LMDBDatabaseCreationTransaction&& other) = delete; + + ~LMDBDatabaseCreationTransaction() override = default; + void commit() const; +}; + +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.cpp new file mode 100644 index 00000000000..5350919c8f5 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.cpp @@ -0,0 +1,59 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include +#include + +namespace bb::crypto::merkle_tree { +LMDBEnvironment::LMDBEnvironment(const std::string& directory, + uint64_t mapSizeKB, + uint32_t maxNumDBs, + uint32_t maxNumReaders) + : _maxReaders(maxNumReaders) + , _numReaders(0) +{ + call_lmdb_func("mdb_env_create", mdb_env_create, &_mdbEnv); + uint64_t kb = 1024; + uint64_t totalMapSize = kb * mapSizeKB; + uint32_t flags = MDB_NOTLS; + try { + call_lmdb_func("mdb_env_set_mapsize", mdb_env_set_mapsize, _mdbEnv, totalMapSize); + call_lmdb_func("mdb_env_set_maxdbs", mdb_env_set_maxdbs, _mdbEnv, static_cast(maxNumDBs)); + call_lmdb_func("mdb_env_set_maxreaders", mdb_env_set_maxreaders, _mdbEnv, maxNumReaders); + call_lmdb_func("mdb_env_open", + mdb_env_open, + _mdbEnv, + directory.c_str(), + flags, + static_cast(S_IRWXU | S_IRWXG | S_IRWXO)); + } catch (std::runtime_error& error) { + call_lmdb_func(mdb_env_close, _mdbEnv); + throw error; + } +} + +void LMDBEnvironment::wait_for_reader() +{ + std::unique_lock lock(_readersLock); + if (_numReaders >= _maxReaders) { + _readersCondition.wait(lock, [&] { return _numReaders < _maxReaders; }); + } + ++_numReaders; +} + +void LMDBEnvironment::release_reader() +{ + std::unique_lock lock(_readersLock); + --_numReaders; + _readersCondition.notify_one(); +} + +LMDBEnvironment::~LMDBEnvironment() +{ + call_lmdb_func(mdb_env_close, _mdbEnv); +} + +MDB_env* LMDBEnvironment::underlying() const +{ + return _mdbEnv; +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp new file mode 100644 index 00000000000..32e4ba13270 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +namespace bb::crypto::merkle_tree { +/* + * RAII wrapper around an LMDB environment. + * Opens/creates the environemnt and manages read access to the enviroment. + * The environment has an upper limit on the number of concurrent read transactions + * and this is managed through the use of mutex/condition variables + */ +class LMDBEnvironment { + public: + /** + * @brief Opens/creates the LMDB environment + * @param directory The directory in which the environment is to be created + * @param mapSizeKb The maximum size of the database, can be increased from a previously used value + * @param maxNumDbs The maximum number of databases that can be created withn this environment + * @param maxNumReaders The maximum number of concurrent read transactions permitted. + */ + LMDBEnvironment(const std::string& directory, uint64_t mapSizeKb, uint32_t maxNumDBs, uint32_t maxNumReaders); + LMDBEnvironment(const LMDBEnvironment& other) = delete; + LMDBEnvironment(LMDBEnvironment&& other) = delete; + LMDBEnvironment& operator=(const LMDBEnvironment& other) = delete; + LMDBEnvironment& operator=(LMDBEnvironment&& other) = delete; + + ~LMDBEnvironment(); + + MDB_env* underlying() const; + + void wait_for_reader(); + + void release_reader(); + + private: + MDB_env* _mdbEnv; + uint32_t _maxReaders; + uint32_t _numReaders; + std::mutex _readersLock; + std::condition_variable _readersCondition; +}; +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.cpp new file mode 100644 index 00000000000..03c3894020d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.cpp @@ -0,0 +1,41 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include + +namespace bb::crypto::merkle_tree { +LMDBReadTransaction::LMDBReadTransaction(LMDBEnvironment& env, const LMDBDatabase& database) + : LMDBTransaction(env, true) + , _database(database) +{} + +LMDBReadTransaction::~LMDBReadTransaction() +{ + abort(); +} + +void LMDBReadTransaction::abort() +{ + LMDBTransaction::abort(); + _environment.release_reader(); +} + +bool LMDBReadTransaction::get_value(std::vector& key, std::vector& data) const +{ + MDB_val dbKey; + dbKey.mv_size = key.size(); + dbKey.mv_data = (void*)key.data(); + + MDB_val dbVal; + if (!call_lmdb_func(mdb_get, underlying(), _database.underlying(), &dbKey, &dbVal)) { + return false; + } + copy_to_vector(dbVal, data); + return true; +} + +bool LMDBReadTransaction::get_node(uint32_t level, index_t index, std::vector& data) const +{ + NodeKeyType key = get_key_for_node(level, index); + return get_value(key, data); +} +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp new file mode 100644 index 00000000000..be0080f6cb0 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp @@ -0,0 +1,117 @@ +#pragma once +#include "barretenberg/common/serialize.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include +#include +#include + +namespace bb::crypto::merkle_tree { + +/** + * RAII wrapper around a read transaction. + * Contains various methods for retrieving values by their keys. + * Aborts the transaction upon object destruction. + */ +class LMDBReadTransaction : public LMDBTransaction { + public: + using Ptr = std::unique_ptr; + + LMDBReadTransaction(LMDBEnvironment& env, const LMDBDatabase& database); + LMDBReadTransaction(const LMDBReadTransaction& other) = delete; + LMDBReadTransaction(LMDBReadTransaction&& other) = delete; + LMDBReadTransaction& operator=(const LMDBReadTransaction& other) = delete; + LMDBReadTransaction& operator=(LMDBReadTransaction&& other) = delete; + + ~LMDBReadTransaction() override; + + template bool get_value_or_previous(T& key, std::vector& data) const; + + bool get_node(uint32_t level, index_t index, std::vector& data) const; + + template bool get_value(T& key, std::vector& data) const; + + bool get_value(std::vector& key, std::vector& data) const; + + void abort() override; + + protected: + const LMDBDatabase& _database; +}; + +template bool LMDBReadTransaction::get_value(T& key, std::vector& data) const +{ + std::vector keyBuffer = serialise_key(key); + return get_value(keyBuffer, data); +} + +template bool LMDBReadTransaction::get_value_or_previous(T& key, std::vector& data) const +{ + std::vector keyBuffer = serialise_key(key); + uint32_t keySize = static_cast(keyBuffer.size()); + MDB_cursor* cursor = nullptr; + call_lmdb_func("mdb_cursor_open", mdb_cursor_open, underlying(), _database.underlying(), &cursor); + + MDB_val dbKey; + dbKey.mv_size = keySize; + dbKey.mv_data = (void*)keyBuffer.data(); + + MDB_val dbVal; + + bool success = false; + + // Look for the key >= to that provided + int code = mdb_cursor_get(cursor, &dbKey, &dbVal, MDB_SET_RANGE); + if (code == 0) { + // we found the key, now determine if it is the exact key + std::vector temp = mdb_val_to_vector(dbKey); + if (keyBuffer == temp) { + // we have the exact key + copy_to_vector(dbVal, data); + success = true; + } else { + // We have a key of the same size but larger value OR a larger size + // either way we now need to find the previous key + code = mdb_cursor_get(cursor, &dbKey, &dbVal, MDB_PREV); + if (code == 0) { + // We have found a previous key. It could be of the same size but smaller value, or smaller size which + // is equal to not found + if (dbKey.mv_size != keySize) { + // There is no previous key, do nothing + } else { + copy_to_vector(dbVal, data); + deserialise_key(dbKey.mv_data, key); + success = true; + } + } else if (code == MDB_NOTFOUND) { + // There is no previous key, do nothing + } else { + throw_error("get_value_or_previous::mdb_cursor_get", code); + } + } + } else if (code == MDB_NOTFOUND) { + // The key was not found, use the last key in the db + code = mdb_cursor_get(cursor, &dbKey, &dbVal, MDB_PREV); + if (code == 0) { + // We found the last key, but we need to ensure it is the same size + if (dbKey.mv_size != keySize) { + // The key is not the same size, same as not found, do nothing + } else { + copy_to_vector(dbVal, data); + deserialise_key(dbKey.mv_data, key); + success = true; + } + } else if (code == MDB_NOTFOUND) { + // DB is empty? + } else { + throw_error("get_value_or_previous::mdb_cursor_get", code); + } + } else { + throw_error("get_value_or_previous::mdb_cursor_get", code); + } + call_lmdb_func(mdb_cursor_close, cursor); + return success; +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.cpp new file mode 100644 index 00000000000..b209599e4bd --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.cpp @@ -0,0 +1,28 @@ +#include "lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/numeric/uint128/uint128.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include +#include +#include +#include +#include + +namespace bb::crypto::merkle_tree { +LMDBStore::LMDBStore( + LMDBEnvironment& environment, std::string name, bool integerKeys, bool reverseKeys, MDB_cmp_func* cmp) + : _environment(environment) + , _name(std::move(name)) + , _database(_environment, LMDBDatabaseCreationTransaction(_environment), _name, integerKeys, reverseKeys, cmp) +{} + +LMDBWriteTransaction::Ptr LMDBStore::create_write_transaction() const +{ + return std::make_unique(_environment, _database); +} +LMDBReadTransaction::Ptr LMDBStore::create_read_transaction() +{ + _environment.wait_for_reader(); + return std::make_unique(_environment, _database); +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp new file mode 100644 index 00000000000..52dc5b67f04 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_read_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp" + +namespace bb::crypto::merkle_tree { + +/** + * Creates an named LMDB 'Store' abstraction on top of an environment. + * Provides methods for creating read and write transactions against the store. + */ + +class LMDBStore { + + public: + using ReadTransaction = LMDBReadTransaction; + using WriteTransaction = LMDBWriteTransaction; + LMDBStore(LMDBEnvironment& environment, + std::string name, + bool integerKeys = false, + bool reverseKeys = false, + MDB_cmp_func* cmp = nullptr); + LMDBStore(const LMDBStore& other) = delete; + LMDBStore(LMDBStore&& other) = delete; + LMDBStore& operator=(const LMDBStore& other) = delete; + LMDBStore& operator=(LMDBStore&& other) = delete; + ~LMDBStore() = default; + + LMDBWriteTransaction::Ptr create_write_transaction() const; + LMDBReadTransaction::Ptr create_read_transaction(); + + private: + LMDBEnvironment& _environment; + const std::string _name; + LMDBDatabase _database; +}; +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.test.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.test.cpp new file mode 100644 index 00000000000..ee81fec390d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.test.cpp @@ -0,0 +1,536 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include "barretenberg/common/serialize.hpp" +#include "barretenberg/common/streams.hpp" +#include "barretenberg/common/test.hpp" +#include "barretenberg/crypto/merkle_tree/fixtures.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/numeric/random/engine.hpp" +#include "barretenberg/numeric/uint128/uint128.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include "barretenberg/stdlib/primitives/field/field.hpp" +#include "lmdb_store.hpp" + +using namespace bb::stdlib; +using namespace bb::crypto::merkle_tree; + +using Builder = bb::UltraCircuitBuilder; + +using field_ct = field_t; +using witness_ct = witness_t; + +const int SAMPLE_DATA_SIZE = 1024; + +class LMDBStoreTest : public testing::Test { + protected: + void SetUp() override + { + // setup with 1MB max db size, 1 max database and 2 maximum concurrent readers + _directory = random_temp_directory(); + std::filesystem::create_directories(_directory); + _environment = std::make_unique(_directory, 1024, 2, 2); + } + + void TearDown() override { std::filesystem::remove_all(_directory); } + + static std::string _directory; + + std::unique_ptr _environment; +}; + +std::string LMDBStoreTest::_directory; + +TEST_F(LMDBStoreTest, can_write_to_and_read_from_store) +{ + { + LMDBStore store(*_environment, "DB1"); + { + std::vector buf; + write(buf, VALUES[0]); + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + transaction->put_node(0, 0, buf); + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector buf2; + bool success = transaction->get_node(0, 0, buf2); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf2, 0); + EXPECT_EQ(value, VALUES[0]); + } + } + + { + LMDBStore store(*_environment, "DB2"); + { + std::vector key; + write(key, VALUES[0]); + std::vector value; + write(value, VALUES[1]); + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + transaction->put_value(key, value); + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector key; + write(key, VALUES[0]); + std::vector value; + bool success = transaction->get_value(key, value); + EXPECT_EQ(success, true); + bb::fr v = from_buffer(value, 0); + EXPECT_EQ(v, VALUES[1]); + } + } +} + +TEST_F(LMDBStoreTest, reading_an_empty_key_reports_correctly) +{ + { + LMDBStore store(*_environment, "DB1"); + + { + std::vector buf; + write(buf, VALUES[0]); + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + transaction->put_node(0, 0, buf); + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector buf2; + bool success = transaction->get_node(0, 1, buf2); + EXPECT_EQ(success, false); + } + } + + { + LMDBStore store(*_environment, "DB2"); + + { + std::vector key; + write(key, VALUES[0]); + std::vector value; + write(value, VALUES[1]); + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + transaction->put_value(key, value); + transaction->commit(); + } + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector key2; + write(key2, VALUES[5]); + std::vector value; + bool success = transaction->get_value(key2, value); + EXPECT_EQ(success, false); + } + } +} + +TEST_F(LMDBStoreTest, can_write_and_read_multiple) +{ + + { + LMDBStore store(*_environment, "DB1"); + + { + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf; + write(buf, VALUES[i]); + transaction->put_node(10, i, buf); + } + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf2; + bool success = transaction->get_node(10, i, buf2); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf2, 0); + EXPECT_EQ(value, VALUES[i]); + } + } + } + + { + LMDBStore store(*_environment, "DB2"); + uint32_t num_reads = 128; + + { + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + for (size_t i = 0; i < num_reads; i++) { + std::vector key; + write(key, VALUES[i]); + std::vector buf; + write(buf, VALUES[i + 128]); + transaction->put_value(key, buf); + } + transaction->commit(); + } + + { + for (size_t i = 0; i < num_reads; i++) { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector key; + write(key, VALUES[i]); + std::vector buf2; + bool success = transaction->get_value(key, buf2); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf2, 0); + EXPECT_EQ(value, VALUES[i + 128]); + } + } + } +} + +TEST_F(LMDBStoreTest, throws_if_write_transaction_is_reused) +{ + { + LMDBStore store(*_environment, "DB1"); + { + std::vector buf; + write(buf, VALUES[0]); + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + transaction->put_node(0, 0, buf); + transaction->commit(); + EXPECT_THROW(transaction->put_node(0, 1, buf), std::runtime_error); + } + } +} + +TEST_F(LMDBStoreTest, can_retrieve_the_value_at_the_previous_key) +{ + + // This test is performed using integer keys of uint128_t + // We also add keys of different sizes but with larger and smaller values + // This ensures we don't erroneously return keys of different sizes + LMDBStore store(*_environment, "note hash tree", false, false, integer_key_cmp); + + std::vector values{ 1, 2, 3, 4, 5 }; + + auto& random_engine = bb::numeric::get_randomness(); + uint32_t num_keys = static_cast(values.size()); + // ensure first key is at least 100 + uint128_t key = random_engine.get_random_uint32() + 100; + std::vector keys; + for (uint32_t i = 0; i < num_keys; i++) { + keys.push_back(key); + // ensure space of at least 50 + key += random_engine.get_random_uint32(); + key += 50; + } + + { + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + for (size_t i = 0; i < num_keys; i++) { + std::vector value; + write(value, values[i]); + transaction->put_value(keys[i], value); + } + + // Now put keys in the db that are smaller and larger in length and smaller and larger in value + // First key is at least 100 + uint64_t lower64 = static_cast(keys[0]) - 50; + uint64_t higher64 = static_cast(keys[4]) + 50; + uint256_t lower256 = uint256_t::from_uint128(keys[0]) - 50; + uint256_t higher256 = uint256_t::from_uint128(keys[4]) + 50; + uint32_t value_to_write = 6; + std::vector value; + write(value, value_to_write); + transaction->put_value(lower64, value); + transaction->put_value(higher64, value); + transaction->put_value(lower256, value); + transaction->put_value(higher256, value); + transaction->commit(); + } + + { + // Values are at keys keys[0] -> keys[4] + + // First look for the value at each key, should return the exact keys + for (uint32_t i = 0; i < num_keys; i++) { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector data; + uint128_t key_copy = keys[i]; + bool success = transaction->get_value_or_previous(key_copy, data); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(data, 0); + EXPECT_EQ(value, values[i]); + EXPECT_EQ(key_copy, keys[i]); + } + + // Now look for the value at key <= key[1] + 5 (does not exist), should be at keys[1] + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector data; + uint128_t key = keys[1] + 5; + bool success = transaction->get_value_or_previous(key, data); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(data, 0); + EXPECT_EQ(value, values[1]); + EXPECT_EQ(key, keys[1]); + } + + // Now look for the value at key <= keys[4] + 5 (beyond the range of the current set of keys), should be at + // keys[4] + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector data; + uint128_t key = keys[4] + 5; + bool success = transaction->get_value_or_previous(key, data); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(data, 0); + EXPECT_EQ(value, values[4]); + EXPECT_EQ(key, keys[4]); + } + + // Now look for the value at key <= keys[0] - 5 (does not exist, less than first key), should not exist + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector data; + uint128_t key = keys[0] - 5; + bool success = transaction->get_value_or_previous(key, data); + EXPECT_EQ(success, false); + } + } +} + +TEST_F(LMDBStoreTest, can_not_retrieve_previous_key_from_empty_db) +{ + LMDBStore store(*_environment, "note hash tree", false, false); + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + uint128_t key = 20; + std::vector data; + bool success = transaction->get_value(key, data); + EXPECT_EQ(success, false); +} + +TEST_F(LMDBStoreTest, can_write_and_read_at_random_keys) +{ + LMDBStore store(*_environment, "note hash tree"); + + std::vector keys; + + { + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf; + write(buf, VALUES[i]); + size_t key = static_cast(rand() % 10000000); + keys.push_back(key); + transaction->put_node(0, key, buf); + } + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf2; + bool success = transaction->get_node(0, keys[i], buf2); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf2, 0); + EXPECT_EQ(value, VALUES[i]); + } + } +} + +TEST_F(LMDBStoreTest, can_recreate_the_store_and_use_again) +{ + std::vector keys; + { + LMDBStore store(*_environment, "note hash tree"); + + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf; + write(buf, VALUES[i]); + size_t key = static_cast(rand() % 10000000); + keys.push_back(key); + transaction->put_node(0, key, buf); + } + transaction->commit(); + } + + { + LMDBStore store(*_environment, "note hash tree"); + + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + for (size_t i = 0; i < SAMPLE_DATA_SIZE; i++) { + std::vector buf2; + bool success = transaction->get_node(0, keys[i], buf2); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf2, 0); + EXPECT_EQ(value, VALUES[i]); + } + } +} + +void read_loop(LMDBStore& store, size_t key, std::atomic& flag, bb::fr starting_value) +{ + bool seen = false; + while (true) { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + std::vector buf; + bool success = transaction->get_node(0, key, buf); + EXPECT_EQ(success, true); + bb::fr value = from_buffer(buf, 0); + if (value == starting_value && !seen) { + // acknowledge that we have seen the old value + flag--; + seen = true; + } + if (value == starting_value + bb::fr(1)) { + // exit now that we have seen the new value + break; + } + } +} + +TEST_F(LMDBStoreTest, can_read_from_multiple_threads) +{ + LMDBStore store(*_environment, "note hash tree"); + const int num_threads = 50; + + size_t key = static_cast(rand() % 1000000); + + { + // we write VALUES[0] to a slot + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + std::vector buf; + write(buf, VALUES[0]); + transaction->put_node(0, key, buf); + transaction->commit(); + } + + { + // we setup multiple threads to read the slot and check they shutdown when the value changes + std::vector threads; + std::atomic flag = num_threads; + for (size_t i = 0; i < num_threads; i++) { + threads.emplace_back(read_loop, std::ref(store), key, std::ref(flag), VALUES[0]); + } + // wait until all threads have seen the old value + while (flag != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + { + // we write VALUES[0] + 1 to the slot + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + std::vector buf; + write(buf, VALUES[0] + 1); + transaction->put_node(0, key, buf); + transaction->commit(); + } + // now wait for all threads to exit having seen the new value + for (size_t i = 0; i < num_threads; i++) { + threads[i].join(); + } + } +} + +TEST_F(LMDBStoreTest, can_handle_different_key_spaces) +{ + LMDBStore store(*_environment, "DB1", false, false, integer_key_cmp); + + // create a set of keys + std::vector keys{ 10, 20, 30, 40, 50 }; + + // create values for each key space + std::vector> values{ + { 10, 20, 30, 40, 50 }, + { 100, 200, 300, 400, 500 }, + { 1000, 2000, 3000, 4000, 5000 }, + { 10000, 20000, 30000, 40000, 50000 }, + }; + + auto write_values = [&](LMDBWriteTransaction& transaction, uint32_t values_index) { + for (uint32_t j = 0; j < keys.size(); j++) { + std::vector data; + write(data, values[values_index][j]); + auto key = static_cast(keys[j]); + transaction.put_value(key, data); + } + }; + + auto check_values = [&](LMDBReadTransaction& transaction, uint32_t values_index) { + for (uint32_t j = 0; j < keys.size(); j++) { + std::vector data; + auto key = static_cast(keys[j]); + transaction.get_value(key, data); + auto retrieved_value = from_buffer(data); + ASSERT_EQ(retrieved_value, values[values_index][j]); + } + }; + + { + LMDBWriteTransaction::Ptr transaction = store.create_write_transaction(); + + write_values.template operator()(*transaction, 0); + write_values.template operator()(*transaction, 1); + write_values.template operator()(*transaction, 2); + write_values.template operator()(*transaction, 3); + transaction->commit(); + } + + { + LMDBReadTransaction::Ptr transaction = store.create_read_transaction(); + + // we should be able to read values from different key spaces + check_values.template operator()(*transaction, 0); + check_values.template operator()(*transaction, 1); + check_values.template operator()(*transaction, 2); + check_values.template operator()(*transaction, 3); + } +} + +template void TestSerialisation(const T& key, uint32_t expectedSize) +{ + std::vector buf = serialise_key(key); + // Should serialise to expected size + EXPECT_EQ(expectedSize, buf.size()); + T newValue; + deserialise_key(buf.data(), newValue); + // Should be different objects + EXPECT_NE(&newValue, &key); + // Should have the same value + EXPECT_EQ(newValue, key); +} + +TEST_F(LMDBStoreTest, produces_correct_key_sizes) +{ + auto& random_engine = bb::numeric::get_randomness(); + { + uint8_t value = random_engine.get_random_uint8(); + TestSerialisation(value, 1); + } + { + uint64_t value = random_engine.get_random_uint64(); + TestSerialisation(value, 8); + } + { + uint128_t value = random_engine.get_random_uint128(); + TestSerialisation(value, 16); + } + { + uint256_t value = random_engine.get_random_uint256(); + TestSerialisation(value, 32); + } +} \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.cpp new file mode 100644 index 00000000000..5393ffde3ff --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.cpp @@ -0,0 +1,29 @@ +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" + +namespace bb::crypto::merkle_tree { +LMDBTransaction::LMDBTransaction(LMDBEnvironment& env, bool readOnly) + : _environment(env) + , state(TransactionState::OPEN) +{ + MDB_txn* p = nullptr; + call_lmdb_func( + "mdb_txn_begin", mdb_txn_begin, _environment.underlying(), p, readOnly ? MDB_RDONLY : 0U, &_transaction); +} + +LMDBTransaction::~LMDBTransaction() = default; + +MDB_txn* LMDBTransaction::underlying() const +{ + return _transaction; +} + +void LMDBTransaction::abort() +{ + if (state != TransactionState::OPEN) { + return; + } + call_lmdb_func(mdb_txn_abort, _transaction); + state = TransactionState::ABORTED; +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp new file mode 100644 index 00000000000..0a160fedd5a --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_environment.hpp" + +namespace bb::crypto::merkle_tree { + +/* + * Abstract base class to represent and LMDB transaction. + * Needs to be sub-classed to be either a read or write transaction. + */ + +enum TransactionState { + OPEN, + COMMITTED, + ABORTED, +}; + +class LMDBTransaction { + public: + LMDBTransaction(LMDBEnvironment& env, bool readOnly = false); + LMDBTransaction(const LMDBTransaction& other) = delete; + LMDBTransaction(LMDBTransaction&& other) = delete; + LMDBTransaction& operator=(const LMDBTransaction& other) = delete; + LMDBTransaction& operator=(LMDBTransaction&& other) = delete; + + virtual ~LMDBTransaction() = 0; + + MDB_txn* underlying() const; + + /* + * Rolls back the transaction. + * Must be called by read transactions to signal the end of the transaction. + * Must be called by write transactions if the changes are not to be committed. + */ + virtual void abort(); + + protected: + LMDBEnvironment& _environment; + MDB_txn* _transaction; + TransactionState state; +}; +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.cpp new file mode 100644 index 00000000000..41a15c0be29 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.cpp @@ -0,0 +1,52 @@ + + +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" + +namespace bb::crypto::merkle_tree { + +LMDBWriteTransaction::LMDBWriteTransaction(LMDBEnvironment& env, const LMDBDatabase& database) + : LMDBTransaction(env) + , _database(database) +{} + +LMDBWriteTransaction::~LMDBWriteTransaction() +{ + try_abort(); +} + +void LMDBWriteTransaction::commit() +{ + if (state == TransactionState::ABORTED) { + throw std::runtime_error("Tried to commit reverted transaction"); + } + call_lmdb_func("mdb_txn_commit", mdb_txn_commit, _transaction); + state = TransactionState::COMMITTED; +} + +void LMDBWriteTransaction::try_abort() +{ + if (state != TransactionState::OPEN) { + return; + } + LMDBTransaction::abort(); +} + +void LMDBWriteTransaction::put_node(uint32_t level, index_t index, std::vector& data) +{ + NodeKeyType key = get_key_for_node(level, index); + put_value(key, data); +} + +void LMDBWriteTransaction::put_value(std::vector& key, std::vector& data) +{ + MDB_val dbKey; + dbKey.mv_size = key.size(); + dbKey.mv_data = (void*)key.data(); + + MDB_val dbVal; + dbVal.mv_size = data.size(); + dbVal.mv_data = (void*)data.data(); + call_lmdb_func("mdb_put", mdb_put, underlying(), _database.underlying(), &dbKey, &dbVal, 0U); +} +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp new file mode 100644 index 00000000000..a6723c78574 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/lmdb_store/lmdb_write_transaction.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "barretenberg/common/serialize.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/callbacks.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_database.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_transaction.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" + +namespace bb::crypto::merkle_tree { + +/** + * RAII wrapper for an LMDB write transaction. + * Provides methods for writing values by their key. + * Must be either committed to persist the changes or aborted to roll them back. + * Will automatically abort the transaction during destruction if changes have not been committed. + */ + +class LMDBWriteTransaction : public LMDBTransaction { + public: + using Ptr = std::unique_ptr; + + LMDBWriteTransaction(LMDBEnvironment& env, const LMDBDatabase& database); + LMDBWriteTransaction(const LMDBWriteTransaction& other) = delete; + LMDBWriteTransaction(LMDBWriteTransaction&& other) = delete; + LMDBWriteTransaction& operator=(const LMDBWriteTransaction& other) = delete; + LMDBWriteTransaction& operator=(LMDBWriteTransaction&& other) = delete; + ~LMDBWriteTransaction() override; + + void put_node(uint32_t level, index_t index, std::vector& data); + + template void put_value(T& key, std::vector& data); + + void put_value(std::vector& key, std::vector& data); + + void commit(); + + void try_abort(); + + protected: + const LMDBDatabase& _database; +}; + +template void LMDBWriteTransaction::put_value(T& key, std::vector& data) +{ + std::vector keyBuffer = serialise_key(key); + put_value(keyBuffer, data); +} +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/memory_tree.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/memory_tree.hpp index e85e613aa9d..6533ef6857b 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/memory_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/memory_tree.hpp @@ -32,6 +32,8 @@ template class MemoryTree { fr root() const { return root_; } + fr get_node(uint32_t level, size_t index) const; + public: size_t depth_; size_t total_size_; @@ -61,6 +63,15 @@ MemoryTree::MemoryTree(size_t depth) root_ = current; } +template fr MemoryTree::get_node(uint32_t level, size_t index) const +{ + auto offset = 0UL; + for (size_t i = depth_; i > level; i--) { + offset += 1UL << i; + } + return hashes_[offset + index]; +} + template fr_hash_path MemoryTree::get_hash_path(size_t index) { fr_hash_path path(depth_); @@ -111,4 +122,4 @@ template fr MemoryTree::update_element(s return root_; } -} // namespace bb::crypto::merkle_tree \ No newline at end of file +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/array_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/array_store.hpp new file mode 100644 index 00000000000..2a6a860f808 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/array_store.hpp @@ -0,0 +1,94 @@ +#pragma once +#include "./tree_meta.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/stdlib/primitives/field/field.hpp" +#include + +namespace bb::crypto::merkle_tree { + +class MockTransaction { + public: + using Ptr = std::unique_ptr; + bool get_node(uint32_t, index_t, std::vector&) const { return false; } + + template void get_value_by_integer(T&, std::vector&){}; + + void get_value(std::vector&, std::vector&){}; + + void put_node(uint32_t, index_t, const std::vector&) {} + + template void put_value_by_integer(T&, std::vector&){}; + + void put_value(std::vector&, std::vector&){}; +}; + +class MockPersistedStore { + public: + using ReadTransaction = MockTransaction; + using WriteTransaction = MockTransaction; + static MockTransaction::Ptr create_write_transaction() { return std::make_unique(); } + static MockTransaction::Ptr create_read_transaction() { return std::make_unique(); } +}; + +/** + * @brief A very basic 2-d array for use as a backing store for merkle trees. + * Can store up to 'indices' nodes per row and 'levels' rows. + */ +template class ArrayStore { + + public: + using ReadTransaction = typename PersistedStore::ReadTransaction; + using WriteTransaction = typename PersistedStore::WriteTransaction; + using ReadTransactionPtr = std::unique_ptr; + ArrayStore(const std::string& name, uint32_t depth, index_t indices = 1024) + : map(std::vector>>>( + depth + 1, + std::vector>>( + indices, std::pair>(false, std::vector())))) + { + meta.depth = depth; + meta.name = name; + meta.size = 0; + } + ~ArrayStore() = default; + + ArrayStore() = delete; + ArrayStore(ArrayStore const& other) = delete; + ArrayStore(ArrayStore const&& other) = delete; + ArrayStore& operator=(ArrayStore const& other) = delete; + ArrayStore& operator=(ArrayStore const&& other) = delete; + + void put_node(uint32_t level, index_t index, const std::vector& data) + { + map[level][index] = std::make_pair(true, data); + } + bool get_node(uint32_t level, index_t index, std::vector& data, ReadTransaction&, bool) const + { + const std::pair>& slot = map[level][index]; + if (slot.first) { + data = slot.second; + } + return slot.first; + } + void put_meta(const index_t& size, const bb::fr& root) + { + meta.root = root; + meta.size = size; + } + + void get_meta(index_t& size, bb::fr& root, ReadTransaction&, bool) const + { + size = meta.size; + root = meta.root; + } + + void commit(){}; + void rollback(){}; + + ReadTransactionPtr create_read_transactiono() { return std::make_unique(); } + + private: + std::vector>>> map; + TreeMeta meta; +}; +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp new file mode 100644 index 00000000000..e1fef1a747e --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_tree_store.hpp @@ -0,0 +1,505 @@ +#pragma once +#include "./tree_meta.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/lmdb_store/lmdb_store.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include "barretenberg/stdlib/primitives/field/field.hpp" +#include "msgpack/assert.hpp" +#include +#include +#include +#include +#include +#include + +namespace bb::crypto::merkle_tree { + +/** + * @brief Serves as a key-value node store for merkle trees. Caches all changes in memory before persisting them during + * a 'commit' operation. + * Manages the persisted store by seperating the key spaces as follows: + * 1 byte key of 0: Tree meta data + * 8 byte integers: The index of each leaf to the value of that leaf + * 16 byte integers: Nodes in the tree, key value = ((2 ^ level) + index - 1) + * 32 bytes integers: The value of the leaf (32 bytes) to the set of indices where the leaf exists in the tree. + */ +template class CachedTreeStore { + public: + using PersistedStoreType = PersistedStore; + using LeafType = LeafValueType; + using IndexedLeafValueType = IndexedLeaf; + using ReadTransaction = typename PersistedStore::ReadTransaction; + using WriteTransaction = typename PersistedStore::WriteTransaction; + using ReadTransactionPtr = std::unique_ptr; + using WriteTransactionPtr = std::unique_ptr; + + CachedTreeStore(std::string name, uint32_t levels, PersistedStore& dataStore) + : name(std::move(name)) + , depth(levels) + , nodes(std::vector>>( + depth + 1, std::unordered_map>())) + , dataStore(dataStore) + { + initialise(); + } + ~CachedTreeStore() = default; + + CachedTreeStore() = delete; + CachedTreeStore(CachedTreeStore const& other) = delete; + CachedTreeStore(CachedTreeStore const&& other) = delete; + CachedTreeStore& operator=(CachedTreeStore const& other) = delete; + CachedTreeStore& operator=(CachedTreeStore const&& other) = delete; + + /** + * @brief Returns the index of the leaf with a value immediately lower than the value provided + */ + std::pair find_low_value(const fr& new_leaf_key, bool includeUncommitted, ReadTransaction& tx) const; + + /** + * @brief Returns the leaf at the provided index, if one exists + */ + std::optional get_leaf(const index_t& index, + ReadTransaction& tx, + bool includeUncommitted) const; + + /** + * @brief Adds the leaf at the given index, updates the leaf index if requested + */ + void set_at_index(const index_t& index, const IndexedLeafValueType& leaf, bool add_to_index); + + /** + * @brief Updates the leaf index + */ + void update_index(const index_t& index, const fr& leaf); + + /** + * @brief Writes the provided data at the given node coordinates. Only writes to uncommitted data. + */ + void put_node(uint32_t level, index_t index, const std::vector& data); + + /** + * @brief Returns the data at the given node coordinates if available. Reads from uncommitted state if requested. + */ + bool get_node(uint32_t level, + index_t index, + std::vector& data, + ReadTransaction& transaction, + bool includeUncommitted) const; + + /** + * @brief Writes the provided meta data to uncommitted state + */ + void put_meta(const index_t& size, const bb::fr& root); + + /** + * @brief Reads the tree meta data, including uncommitted data if requested + */ + void get_meta(index_t& size, bb::fr& root, ReadTransaction& tx, bool includeUncommitted) const; + + /** + * @brief Reads the extended tree meta data, including uncommitted data if requested + */ + void get_full_meta(index_t& size, + bb::fr& root, + std::string& name, + uint32_t& depth, + ReadTransaction& tx, + bool includeUncommitted) const; + + /** + * @brief Finds the index of the given leaf value in the tree if available. Includes uncommitted data if requested. + */ + std::optional find_leaf_index(const LeafValueType& leaf, + ReadTransaction& tx, + bool includeUncommitted) const; + + /** + * @brief Finds the index of the given leaf value in the tree if available. Includes uncommitted data if requested. + */ + std::optional find_leaf_index_from(const LeafValueType& leaf, + index_t start_index, + ReadTransaction& tx, + bool includeUncommitted) const; + + /** + * @brief Commits the uncommitted data to the underlying store + */ + void commit(); + + /** + * @brief Rolls back the uncommitted state + */ + void rollback(); + + /** + * @brief Returns the name of the tree + */ + std::string get_name() const { return name; } + + /** + * @brief Returns a read transaction against the underlying store. + */ + ReadTransactionPtr create_read_transaction() const { return dataStore.create_read_transaction(); } + + private: + struct Indices { + std::vector indices; + + MSGPACK_FIELDS(indices); + }; + + std::string name; + uint32_t depth; + std::vector>> nodes; + std::map indices_; + std::unordered_map leaves_; + PersistedStore& dataStore; + TreeMeta meta; + + void initialise(); + + bool read_persisted_meta(TreeMeta& m, ReadTransaction& tx) const; + + void persist_meta(TreeMeta& m, WriteTransaction& tx); + + WriteTransactionPtr create_write_transaction() const { return dataStore.create_write_transaction(); } +}; + +template +std::pair CachedTreeStore::find_low_value(const fr& new_leaf_key, + bool includeUncommitted, + ReadTransaction& tx) const +{ + uint256_t new_value_as_number = uint256_t(new_leaf_key); + std::vector data; + FrKeyType key(new_leaf_key); + tx.get_value_or_previous(key, data); + Indices committed; + msgpack::unpack((const char*)data.data(), data.size()).get().convert(committed); + auto db_index = committed.indices[0]; + uint256_t retrieved_value = key; + if (!includeUncommitted || retrieved_value == new_value_as_number || indices_.empty()) { + return std::make_pair(new_value_as_number == retrieved_value, db_index); + } + + // At this stage, we have been asked to include uncommitted and the value was not exactly found in the db + auto it = indices_.lower_bound(new_value_as_number); + if (it == indices_.end()) { + // there is no element >= the requested value. + // decrement the iterator to get the value preceeding the requested value + --it; + // we need to return the larger of the db value or the cached value + + return std::make_pair(false, it->first > retrieved_value ? it->second.indices[0] : db_index); + } + + if (it->first == uint256_t(new_value_as_number)) { + // the value is already present and the iterator points to it + return std::make_pair(true, it->second.indices[0]); + } + // the iterator points to the element immediately larger than the requested value + // We need to return the highest value from + // 1. The next lowest cached value, if there is one + // 2. The value retrieved from the db + if (it == indices_.begin()) { + // No cached lower value, return the db index + return std::make_pair(false, db_index); + } + --it; + // it now points to the value less than that requested + return std::make_pair(false, it->first > retrieved_value ? it->second.indices[0] : db_index); +} + +template +std::optional::IndexedLeafValueType> CachedTreeStore< + PersistedStore, + LeafValueType>::get_leaf(const index_t& index, ReadTransaction& tx, bool includeUncommitted) const +{ + if (includeUncommitted) { + typename std::unordered_map::const_iterator it = leaves_.find(index); + if (it != leaves_.end()) { + return it->second; + } + } + LeafIndexKeyType key = index; + std::vector data; + bool success = tx.get_value(key, data); + if (success) { + IndexedLeafValueType return_value; + msgpack::unpack((const char*)data.data(), data.size()).get().convert(return_value); + return return_value; + } + return std::nullopt; +} + +template +void CachedTreeStore::set_at_index(const index_t& index, + const IndexedLeafValueType& leaf, + bool add_to_index) +{ + leaves_[index] = leaf; + if (add_to_index) { + auto it = indices_.find(uint256_t(leaf.value.get_key())); + if (it == indices_.end()) { + Indices indices; + indices.indices.push_back(index); + indices_[uint256_t(leaf.value.get_key())] = indices; + return; + } + it->second.indices.push_back(index); + } +} + +template +void CachedTreeStore::update_index(const index_t& index, const fr& leaf) +{ + auto it = indices_.find(uint256_t(leaf)); + if (it == indices_.end()) { + Indices indices; + indices.indices.push_back(index); + indices_[uint256_t(leaf)] = indices; + return; + } + it->second.indices.push_back(index); +} + +template +std::optional CachedTreeStore::find_leaf_index(const LeafValueType& leaf, + ReadTransaction& tx, + bool includeUncommitted) const +{ + return find_leaf_index_from(leaf, 0, tx, includeUncommitted); +} + +template +std::optional CachedTreeStore::find_leaf_index_from( + const LeafValueType& leaf, index_t start_index, ReadTransaction& tx, bool includeUncommitted) const +{ + Indices committed; + std::optional result = std::nullopt; + FrKeyType key = leaf; + std::vector value; + bool success = tx.get_value(key, value); + if (success) { + msgpack::unpack((const char*)value.data(), value.size()).get().convert(committed); + if (!committed.indices.empty()) { + for (size_t i = 0; i < committed.indices.size(); ++i) { + index_t ind = committed.indices[i]; + if (ind < start_index) { + continue; + } + if (!result.has_value()) { + result = ind; + continue; + } + result = std::min(ind, result.value()); + } + } + } + if (includeUncommitted) { + auto it = indices_.find(uint256_t(leaf)); + if (it != indices_.end() && !it->second.indices.empty()) { + for (size_t i = 0; i < it->second.indices.size(); ++i) { + index_t ind = it->second.indices[i]; + if (ind < start_index) { + continue; + } + if (!result.has_value()) { + result = ind; + continue; + } + result = std::min(ind, result.value()); + } + } + } + return result; +} + +template +void CachedTreeStore::put_node(uint32_t level, + index_t index, + const std::vector& data) +{ + nodes[level][index] = data; +} + +template +bool CachedTreeStore::get_node(uint32_t level, + index_t index, + std::vector& data, + ReadTransaction& transaction, + bool includeUncommitted) const +{ + if (includeUncommitted) { + const auto& level_map = nodes[level]; + auto it = level_map.find(index); + if (it != level_map.end()) { + data = it->second; + return true; + } + } + return transaction.get_node(level, index, data); +} + +template +void CachedTreeStore::put_meta(const index_t& size, const bb::fr& root) +{ + meta.root = root; + meta.size = size; +} + +template +void CachedTreeStore::get_meta(index_t& size, + bb::fr& root, + ReadTransaction& tx, + bool includeUncommitted) const +{ + if (includeUncommitted) { + size = meta.size; + root = meta.root; + return; + } + TreeMeta m; + read_persisted_meta(m, tx); + size = m.size; + root = m.root; +} + +template +void CachedTreeStore::get_full_meta( + index_t& size, bb::fr& root, std::string& name, uint32_t& depth, ReadTransaction& tx, bool includeUncommitted) const +{ + if (includeUncommitted) { + size = meta.size; + root = meta.root; + name = meta.name; + depth = meta.depth; + return; + } + TreeMeta m; + read_persisted_meta(m, tx); + size = m.size; + root = m.root; + depth = m.depth; + name = m.name; +} + +template void CachedTreeStore::commit() +{ + { + { + ReadTransactionPtr tx = create_read_transaction(); + for (auto& idx : indices_) { + std::vector value; + FrKeyType key = idx.first; + bool success = tx->get_value(key, value); + if (success) { + Indices indices; + msgpack::unpack((const char*)value.data(), value.size()).get().convert(indices); + idx.second.indices.insert( + idx.second.indices.begin(), indices.indices.begin(), indices.indices.end()); + } + } + } + WriteTransactionPtr tx = create_write_transaction(); + try { + for (uint32_t i = 1; i < nodes.size(); i++) { + auto& level = nodes[i]; + for (auto& item : level) { + index_t index = item.first; + std::vector& data = item.second; + tx->put_node(i, index, data); + } + } + for (auto& idx : indices_) { + msgpack::sbuffer buffer; + msgpack::pack(buffer, idx.second); + std::vector encoded(buffer.data(), buffer.data() + buffer.size()); + FrKeyType key = idx.first; + tx->put_value(key, encoded); + } + for (const auto& leaf : leaves_) { + msgpack::sbuffer buffer; + msgpack::pack(buffer, leaf.second); + std::vector value(buffer.data(), buffer.data() + buffer.size()); + LeafIndexKeyType key = leaf.first; + tx->put_value(key, value); + } + persist_meta(meta, *tx); + tx->commit(); + } catch (std::exception& e) { + tx->try_abort(); + throw; + } + } + rollback(); +} + +template +void CachedTreeStore::rollback() +{ + // Extract the committed meta data and destroy the cache + { + ReadTransactionPtr tx = create_read_transaction(); + read_persisted_meta(meta, *tx); + } + nodes = std::vector>>( + depth + 1, std::unordered_map>()); + indices_ = std::map(); + leaves_ = std::unordered_map(); +} + +template +bool CachedTreeStore::read_persisted_meta(TreeMeta& m, ReadTransaction& tx) const +{ + std::vector data; + bool success = tx.get_node(0, 0, data); + if (success) { + msgpack::unpack((const char*)data.data(), data.size()).get().convert(m); + } + return success; +} + +template +void CachedTreeStore::persist_meta(TreeMeta& m, WriteTransaction& tx) +{ + msgpack::sbuffer buffer; + msgpack::pack(buffer, m); + std::vector encoded(buffer.data(), buffer.data() + buffer.size()); + tx.put_node(0, 0, encoded); +} + +template +void CachedTreeStore::initialise() +{ + // Read the persisted meta data, if the name or depth of the tree is not consistent with what was provided during + // construction then we throw + std::vector data; + { + ReadTransactionPtr tx = create_read_transaction(); + bool success = read_persisted_meta(meta, *tx); + if (success) { + if (name == meta.name && depth == meta.depth) { + return; + } + throw std::runtime_error("Invalid tree meta data"); + } + } + + // No meta data available. Write the initial state down + meta.name = name; + meta.size = 0; + meta.depth = depth; + WriteTransactionPtr tx = create_write_transaction(); + try { + persist_meta(meta, *tx); + tx->commit(); + } catch (std::exception& e) { + tx->try_abort(); + throw e; + } +} + +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/tree_meta.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/tree_meta.hpp new file mode 100644 index 00000000000..94132dd5f57 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/tree_meta.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include +#include + +namespace bb::crypto::merkle_tree { + +struct TreeMeta { + std::string name; + uint32_t depth; + index_t size; + bb::fr root; + + MSGPACK_FIELDS(name, depth, size, root) +}; + +struct LeavesMeta { + index_t size; + + MSGPACK_FIELDS(size) +}; + +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_leaf.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_leaf.hpp index 1ac402ac310..e91a0cf744d 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_leaf.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_leaf.hpp @@ -1,20 +1,19 @@ #pragma once +#include "barretenberg/crypto/merkle_tree/types.hpp" #include "barretenberg/crypto/pedersen_commitment/pedersen.hpp" #include "barretenberg/serialize/msgpack.hpp" #include "barretenberg/stdlib/hash/poseidon2/poseidon2.hpp" namespace bb::crypto::merkle_tree { -typedef uint256_t index_t; - -struct nullifier_leaf { +struct indexed_nullifier_leaf { fr value; index_t nextIndex; fr nextValue; // For serialization, update with any new fields MSGPACK_FIELDS(value, nextIndex, nextValue); - bool operator==(nullifier_leaf const&) const = default; + bool operator==(indexed_nullifier_leaf const&) const = default; std::ostream& operator<<(std::ostream& os) { @@ -22,9 +21,12 @@ struct nullifier_leaf { return os; } - std::vector get_hash_inputs() const { return std::vector{ value, nextIndex, nextValue }; } + std::vector get_hash_inputs() const { return std::vector{ value, nextValue, nextIndex }; } - static nullifier_leaf zero() { return nullifier_leaf{ .value = 0, .nextIndex = 0, .nextValue = 0 }; } + static indexed_nullifier_leaf zero() + { + return indexed_nullifier_leaf{ .value = 0, .nextIndex = 0, .nextValue = 0 }; + } }; /** @@ -35,7 +37,7 @@ template class WrappedNullifierLeaf { public: // Initialize with a nullifier leaf - WrappedNullifierLeaf(nullifier_leaf value) + WrappedNullifierLeaf(indexed_nullifier_leaf value) : data(value) {} // Initialize an empty leaf @@ -58,14 +60,14 @@ template class WrappedNullifierLeaf { * * @return nullifier_leaf */ - nullifier_leaf unwrap() const { return data.value(); } + indexed_nullifier_leaf unwrap() const { return data.value(); } /** * @brief Set the wrapped nullifier_leaf object value * * @param value */ - void set(nullifier_leaf value) { data.emplace(value); } + void set(indexed_nullifier_leaf value) { data.emplace(value); } /** * @brief Return the hash of the wrapped object, other return the zero hash of 0 @@ -86,7 +88,7 @@ template class WrappedNullifierLeaf { private: // Underlying data - std::optional data; + std::optional data; }; template diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_memory_tree.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_memory_tree.hpp index 5b9fdb94ee2..ce662250045 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_memory_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_memory_tree.hpp @@ -1,6 +1,7 @@ #pragma once #include "../hash.hpp" #include "../memory_tree.hpp" +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" #include "nullifier_leaf.hpp" namespace bb::crypto::merkle_tree { @@ -66,18 +67,20 @@ namespace bb::crypto::merkle_tree { template class NullifierMemoryTree : public MemoryTree { public: - NullifierMemoryTree(size_t depth, size_t initial_size = 1); + NullifierMemoryTree(size_t depth, size_t initial_size = 2); using MemoryTree::get_hash_path; + using MemoryTree::get_sibling_path; using MemoryTree::root; using MemoryTree::update_element; - fr_hash_path update_element(fr const& value); + fr_sibling_path update_element(fr const& value); const std::vector& get_hashes() { return hashes_; } const WrappedNullifierLeaf get_leaf(size_t index) { - return (index < leaves_.size()) ? leaves_[index] : WrappedNullifierLeaf(nullifier_leaf::zero()); + return (index < leaves_.size()) ? leaves_[index] + : WrappedNullifierLeaf(indexed_nullifier_leaf::zero()); } const std::vector>& get_leaves() { return leaves_; } @@ -99,7 +102,8 @@ NullifierMemoryTree::NullifierMemoryTree(size_t depth, size_t ini hashes_.resize(total_size_ * 2 - 2); // Build the entire tree and fill with 0 hashes. - auto current = WrappedNullifierLeaf(nullifier_leaf::zero()).hash(); + // auto current = WrappedNullifierLeaf(nullifier_leaf::zero()).hash(); + auto current = fr::zero(); size_t layer_size = total_size_; for (size_t offset = 0; offset < hashes_.size(); offset += layer_size, layer_size /= 2) { for (size_t i = 0; i < layer_size; ++i) { @@ -110,28 +114,28 @@ NullifierMemoryTree::NullifierMemoryTree(size_t depth, size_t ini // Insert the initial leaves for (size_t i = 0; i < initial_size; i++) { - auto initial_leaf = - WrappedNullifierLeaf(nullifier_leaf{ .value = i, .nextIndex = i + 1, .nextValue = i + 1 }); + auto initial_leaf = WrappedNullifierLeaf( + indexed_nullifier_leaf{ .value = i, .nextIndex = i + 1, .nextValue = i + 1 }); leaves_.push_back(initial_leaf); } leaves_[initial_size - 1] = WrappedNullifierLeaf( - nullifier_leaf{ .value = leaves_[initial_size - 1].unwrap().value, .nextIndex = 0, .nextValue = 0 }); + indexed_nullifier_leaf{ .value = leaves_[initial_size - 1].unwrap().value, .nextIndex = 0, .nextValue = 0 }); for (size_t i = 0; i < initial_size; ++i) { update_element(i, leaves_[i].hash()); } } -template fr_hash_path NullifierMemoryTree::update_element(fr const& value) +template fr_sibling_path NullifierMemoryTree::update_element(fr const& value) { // Find the leaf with the value closest and less than `value` // If value is 0 we simply append 0 a null NullifierLeaf to the tree - fr_hash_path hash_path; + fr_sibling_path hash_path; if (value == 0) { auto zero_leaf = WrappedNullifierLeaf::zero(); - hash_path = get_hash_path(leaves_.size() - 1); + hash_path = get_sibling_path(leaves_.size() - 1); leaves_.push_back(zero_leaf); update_element(leaves_.size() - 1, zero_leaf.hash()); return hash_path; @@ -141,10 +145,10 @@ template fr_hash_path NullifierMemoryTree fr_hash_path NullifierMemoryTree hashes, std::string const& m } } -bool check_hash_path(const fr& root, const fr_hash_path& path, const nullifier_leaf& leaf_value, const size_t idx) +bool check_hash_path(const fr& root, + const fr_hash_path& path, + const indexed_nullifier_leaf& leaf_value, + const size_t idx) { auto current = WrappedLeaf(leaf_value).hash(); size_t depth_ = path.size(); @@ -50,9 +53,9 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) * nextIdx 0 0 0 0 0 0 0 0 * nextVal 0 0 0 0 0 0 0 0 */ - nullifier_leaf zero_leaf = { 0, 0, 0 }; - EXPECT_EQ(tree.get_leaves().size(), 1); - EXPECT_EQ(tree.get_leaves()[0].unwrap(), zero_leaf); + indexed_nullifier_leaf first_leaf = { 0, 1, 1 }; + EXPECT_EQ(tree.get_leaves().size(), 2); + EXPECT_EQ(tree.get_leaves()[0].unwrap(), first_leaf); /** * Add new value 30: @@ -64,9 +67,10 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) * nextVal 30 0 0 0 0 0 0 0 */ tree.update_element(30); - EXPECT_EQ(tree.get_leaves().size(), 2); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 3); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 2, 30 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); /** * Add new value 10: @@ -78,10 +82,11 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) * nextVal 10 0 30 0 0 0 0 0 */ tree.update_element(10); - EXPECT_EQ(tree.get_leaves().size(), 3); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 4); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 2, 30 }).hash()); /** * Add new value 20: @@ -93,19 +98,21 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) * nextVal 10 0 20 30 0 0 0 0 */ tree.update_element(20); - EXPECT_EQ(tree.get_leaves().size(), 4); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 5); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); // Adding the same value must not affect anything tree.update_element(20); - EXPECT_EQ(tree.get_leaves().size(), 4); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 5); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); /** * Add new value 50: @@ -117,12 +124,13 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) * nextVal 10 50 20 30 0 0 0 0 */ tree.update_element(50); - EXPECT_EQ(tree.get_leaves().size(), 5); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 4, 50 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 50, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 6); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 5, 50 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); + EXPECT_EQ(tree.get_leaves()[5].hash(), WrappedLeaf({ 50, 0, 0 }).hash()); // Manually compute the node values auto e000 = tree.get_leaves()[0].hash(); @@ -130,9 +138,9 @@ TEST(crypto_nullifier_tree, test_nullifier_memory) auto e010 = tree.get_leaves()[2].hash(); auto e011 = tree.get_leaves()[3].hash(); auto e100 = tree.get_leaves()[4].hash(); - auto e101 = WrappedLeaf(zero_leaf).hash(); - auto e110 = WrappedLeaf(zero_leaf).hash(); - auto e111 = WrappedLeaf(zero_leaf).hash(); + auto e101 = tree.get_leaves()[5].hash(); + auto e110 = fr::zero(); + auto e111 = fr::zero(); auto e00 = HashPolicy::hash_pair(e000, e001); auto e01 = HashPolicy::hash_pair(e010, e011); @@ -179,9 +187,9 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextIdx 0 0 0 0 0 0 0 0 * nextVal 0 0 0 0 0 0 0 0 */ - WrappedLeaf zero_leaf = WrappedLeaf({ 0, 0, 0 }); - EXPECT_EQ(tree.get_leaves().size(), 1); - EXPECT_EQ(tree.get_leaves()[0], zero_leaf); + WrappedLeaf first_leaf = WrappedLeaf({ 0, 1, 1 }); + EXPECT_EQ(tree.get_leaves().size(), 2); + EXPECT_EQ(tree.get_leaves()[0], first_leaf); /** * Add new value 30: @@ -193,9 +201,10 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 30 0 0 0 0 0 0 0 */ tree.update_element(30); - EXPECT_EQ(tree.get_leaves().size(), 2); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 3); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 2, 30 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); /** * Add new value 10: @@ -207,10 +216,11 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 10 0 30 0 0 0 0 0 */ tree.update_element(10); - EXPECT_EQ(tree.get_leaves().size(), 3); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 4); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 2, 30 }).hash()); /** * Add new value 20: @@ -222,19 +232,21 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 10 0 20 30 0 0 0 0 */ tree.update_element(20); - EXPECT_EQ(tree.get_leaves().size(), 4); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 5); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); // Adding the same value must not affect anything tree.update_element(20); - EXPECT_EQ(tree.get_leaves().size(), 4); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); + EXPECT_EQ(tree.get_leaves().size(), 5); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); /** * Add new value 0: @@ -246,12 +258,13 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 10 0 20 30 0 0 0 0 */ tree.update_element(0); - EXPECT_EQ(tree.get_leaves().size(), 5); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf::zero().hash()); + EXPECT_EQ(tree.get_leaves().size(), 6); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); + EXPECT_EQ(tree.get_leaves()[5].hash(), WrappedLeaf::zero().hash()); /* * Add new value 0: @@ -263,13 +276,14 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 10 0 20 30 0 0 0 0 */ tree.update_element(0); - EXPECT_EQ(tree.get_leaves().size(), 6); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 20, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf::zero().hash()); + EXPECT_EQ(tree.get_leaves().size(), 7); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 0, 0 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); EXPECT_EQ(tree.get_leaves()[5].hash(), WrappedLeaf::zero().hash()); + EXPECT_EQ(tree.get_leaves()[6].hash(), WrappedLeaf::zero().hash()); /** * Add new value 50: @@ -281,15 +295,16 @@ TEST(crypto_nullifier_tree, test_nullifier_memory_appending_zero) * nextVal 10 50 20 30 0 0 0 0 */ tree.update_element(50); - EXPECT_EQ(tree.get_leaves().size(), 7); - EXPECT_EQ(tree.get_leaf(0).hash(), WrappedLeaf({ 0, 2, 10 }).hash()); - EXPECT_EQ(tree.get_leaf(1).hash(), WrappedLeaf({ 30, 6, 50 }).hash()); - EXPECT_EQ(tree.get_leaf(2).hash(), WrappedLeaf({ 10, 3, 20 }).hash()); - EXPECT_EQ(tree.get_leaf(3).hash(), WrappedLeaf({ 20, 1, 30 }).hash()); - EXPECT_EQ(tree.get_leaf(4).hash(), WrappedLeaf::zero().hash()); + EXPECT_EQ(tree.get_leaves().size(), 8); + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf({ 0, 1, 1 }).hash()); + EXPECT_EQ(tree.get_leaves()[1].hash(), WrappedLeaf({ 1, 3, 10 }).hash()); + EXPECT_EQ(tree.get_leaves()[2].hash(), WrappedLeaf({ 30, 7, 50 }).hash()); + EXPECT_EQ(tree.get_leaves()[3].hash(), WrappedLeaf({ 10, 4, 20 }).hash()); + EXPECT_EQ(tree.get_leaves()[4].hash(), WrappedLeaf({ 20, 2, 30 }).hash()); EXPECT_EQ(tree.get_leaf(5).hash(), WrappedLeaf::zero().hash()); - EXPECT_EQ(tree.get_leaf(6).hash(), WrappedLeaf({ 50, 0, 0 }).hash()); - EXPECT_EQ(tree.get_leaf(7).hash(), zero_leaf.hash()); + EXPECT_EQ(tree.get_leaf(6).hash(), WrappedLeaf::zero().hash()); + EXPECT_EQ(tree.get_leaf(7).hash(), WrappedLeaf({ 50, 0, 0 }).hash()); + // EXPECT_EQ(tree.get_leaf(7).hash(), first_leaf.hash()); // Manually compute the node values auto e000 = tree.get_leaf(0).hash(); @@ -344,9 +359,8 @@ TEST(crypto_nullifier_tree, test_nullifier_tree) constexpr size_t depth = 8; NullifierMemoryTree tree(depth); - nullifier_leaf zero_leaf = { 0, 0, 0 }; - EXPECT_EQ(tree.get_leaves().size(), 1); - EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf(zero_leaf).hash()); + EXPECT_EQ(tree.get_leaves().size(), 2); // prefill is 2 + EXPECT_EQ(tree.get_leaves()[0].hash(), WrappedLeaf(indexed_nullifier_leaf{ 0, 1, 1 }).hash()); // Add 20 random values to the tree for (size_t i = 0; i < 20; i++) { @@ -379,4 +393,4 @@ TEST(crypto_nullifier_tree, test_nullifier_tree) // Merkle proof at `index` proves non-membership of `new_member` auto hash_path = tree.get_hash_path(index); EXPECT_TRUE(check_hash_path(tree.root(), hash_path, leaves[index].unwrap(), index)); -} \ No newline at end of file +} diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.cpp index 95ee0ea72ab..a6e43cfcc58 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.cpp @@ -20,8 +20,7 @@ NullifierTree::NullifierTree(Store& store, size_t depth, s zero_hashes_.resize(depth); // Create the zero hashes for the tree - auto current = - WrappedNullifierLeaf(nullifier_leaf{ .value = 0, .nextIndex = 0, .nextValue = 0 }).hash(); + auto current = fr::zero(); for (size_t i = 0; i < depth; ++i) { zero_hashes_[i] = current; current = HashingPolicy::hash_pair(current, current); @@ -29,13 +28,13 @@ NullifierTree::NullifierTree(Store& store, size_t depth, s // Insert the initial leaves for (size_t i = 0; i < initial_size; i++) { - auto initial_leaf = - WrappedNullifierLeaf(nullifier_leaf{ .value = i, .nextIndex = i + 1, .nextValue = i + 1 }); + auto initial_leaf = WrappedNullifierLeaf( + indexed_nullifier_leaf{ .value = i, .nextIndex = i + 1, .nextValue = i + 1 }); leaves.push_back(initial_leaf); } leaves[initial_size - 1] = WrappedNullifierLeaf( - nullifier_leaf{ .value = leaves[initial_size - 1].unwrap().value, .nextIndex = 0, .nextValue = 0 }); + indexed_nullifier_leaf{ .value = leaves[initial_size - 1].unwrap().value, .nextIndex = 0, .nextValue = 0 }); for (size_t i = 0; i < initial_size; ++i) { update_element(i, leaves[i].hash()); @@ -57,7 +56,7 @@ fr NullifierTree::update_element(fr const& value) bool is_already_present; std::tie(current, is_already_present) = find_closest_leaf(leaves, value); - nullifier_leaf current_leaf = leaves[current].unwrap(); + indexed_nullifier_leaf current_leaf = leaves[current].unwrap(); WrappedNullifierLeaf new_leaf = WrappedNullifierLeaf( { .value = value, .nextIndex = current_leaf.nextIndex, .nextValue = current_leaf.nextValue }); if (!is_already_present) { @@ -86,4 +85,4 @@ fr NullifierTree::update_element(fr const& value) template class NullifierTree; -} // namespace bb::crypto::merkle_tree \ No newline at end of file +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.test.cpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.test.cpp index 41ddc1670b2..63622db5643 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.test.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/nullifier_tree/nullifier_tree.test.cpp @@ -39,26 +39,27 @@ inline void print_tree(const size_t depth, std::vector hashes, std::string c TEST(stdlib_nullifier_tree, test_kv_memory_vs_memory_consistency) { constexpr size_t depth = 2; - NullifierMemoryTree memdb(depth); + constexpr size_t prefill = 2; + NullifierMemoryTree memdb(depth, prefill); MemoryStore store; - NullifierTree db(store, depth); + NullifierTree db(store, depth, prefill); EXPECT_EQ(db.root(), memdb.root()); - std::vector indicies(1 << depth); + std::vector indicies((1 << depth) - prefill); std::iota(indicies.begin(), indicies.end(), 0); std::random_device rd; std::mt19937 g(rd()); std::shuffle(indicies.begin(), indicies.end(), g); - for (size_t i = 0; i < indicies.size() - 1; ++i) { + for (size_t i = 0; i < indicies.size(); ++i) { size_t idx = indicies[i]; memdb.update_element(VALUES[idx]); db.update_element(VALUES[idx]); } - for (size_t i = 0; i < indicies.size() - 1; ++i) { + for (size_t i = 0; i < indicies.size(); ++i) { size_t idx = indicies[i]; EXPECT_EQ(db.get_hash_path(idx), memdb.get_hash_path(idx)); } @@ -93,10 +94,10 @@ TEST(stdlib_nullifier_tree, test_size) TEST(stdlib_nullifier_tree, test_get_hash_path) { - NullifierMemoryTree memdb(10); + NullifierMemoryTree memdb(10, 2); MemoryStore store; - auto db = NullifierTree(store, 10); + auto db = NullifierTree(store, 10, 2); EXPECT_EQ(memdb.get_hash_path(512), db.get_hash_path(512)); @@ -140,4 +141,4 @@ TEST(stdlib_nullifier_tree, test_get_hash_path_layers) EXPECT_EQ(before[1], after[1]); EXPECT_NE(before[2], after[2]); } -} \ No newline at end of file +} diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/response.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/response.hpp new file mode 100644 index 00000000000..38e7f8c6e4d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/response.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/types.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include +#include +#include +#include +#include + +namespace bb::crypto::merkle_tree { +struct TreeMetaResponse { + std::string name; + uint32_t depth; + index_t size; + fr root; +}; + +struct AddDataResponse { + index_t size; + fr root; +}; + +struct GetSiblingPathResponse { + fr_sibling_path path; +}; + +template struct LowLeafWitnessData { + IndexedLeaf leaf; + index_t index; + fr_sibling_path path; + + MSGPACK_FIELDS(leaf, index, path); +}; + +template struct AddIndexedDataResponse { + AddDataResponse add_data_result; + fr_sibling_path subtree_path; + std::shared_ptr>> sorted_leaves; + std::shared_ptr>> low_leaf_witness_data; +}; + +struct FindLeafIndexResponse { + index_t leaf_index; +}; + +struct GetLeafResponse { + std::optional leaf; +}; + +template struct GetIndexedLeafResponse { + std::optional> indexed_leaf; +}; + +template struct TypedResponse { + ResponseType inner; + bool success{ true }; + std::string message; +}; + +struct Response { + bool success; + std::string message; +}; + +template +void execute_and_report(const std::function&)>& f, + const std::function&)>& on_completion) +{ + TypedResponse response; + try { + f(response); + } catch (std::exception& e) { + response.success = false; + response.message = e.what(); + } + try { + on_completion(response); + } catch (std::exception&) { + } +} + +inline void execute_and_report(const std::function& f, + const std::function& on_completion) +{ + Response response{ true, "" }; + try { + f(); + } catch (std::exception& e) { + response.success = false; + response.message = e.what(); + } + try { + on_completion(response); + } catch (std::exception&) { + } +} +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/signal.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/signal.hpp new file mode 100644 index 00000000000..b9ff1e7a201 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/signal.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include + +namespace bb::crypto::merkle_tree { +/** + * @brief Used in parallel insertions in the the IndexedTree. Workers signal to other following workes as they move up + * the level of the tree. + * + */ +class Signal { + public: + Signal(uint32_t initial_level = 1) + : signal_(initial_level){}; + ~Signal() = default; + Signal(const Signal& other) + : signal_(other.signal_.load()) + {} + Signal(const Signal&& other) = delete; + Signal& operator=(const Signal& other) + { + if (this != &other) { + signal_.store(other.signal_.load()); + } + return *this; + } + Signal& operator=(const Signal&& other) = delete; + + /** + * @brief Causes the thread to wait until the required level has been signalled + * @param level The required level + * + */ + void wait_for_level(uint32_t level = 0) + { + uint32_t current_level = signal_.load(); + while (current_level > level) { + signal_.wait(current_level); + current_level = signal_.load(); + } + } + + /** + * @brief Signals that the given level has been passed + * @param level The level to be signalled + * + */ + void signal_level(uint32_t level = 0) + { + signal_.store(level); + signal_.notify_all(); + } + + void signal_decrement(uint32_t delta = 1) + { + signal_.fetch_sub(delta); + signal_.notify_all(); + } + + private: + std::atomic signal_; +}; +} // namespace bb::crypto::merkle_tree diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/types.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/types.hpp new file mode 100644 index 00000000000..9cf32fafff0 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/types.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +namespace bb::crypto::merkle_tree { +using index_t = uint64_t; +} // namespace bb::crypto::merkle_tree \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/messaging/header.hpp b/barretenberg/cpp/src/barretenberg/messaging/header.hpp new file mode 100644 index 00000000000..62bb3d9c5a1 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/messaging/header.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "barretenberg/serialize/msgpack.hpp" +#include +#include + +namespace bb::messaging { + +enum SystemMsgTypes { TERMINATE = 0, PING = 1, PONG = 2 }; + +const uint32_t FIRST_APP_MSG_TYPE = 100; + +// #pragma pack(push, 1) +struct MsgHeader { + uint32_t messageId; // Unique Id for the message + uint32_t requestId; // Id of the message this is responding too (may not be used) + + MSGPACK_FIELDS(messageId, requestId); + + MsgHeader() = default; + + MsgHeader(uint32_t reqId) + : requestId(reqId) + {} + + MsgHeader(uint32_t msgId, uint32_t reqId) + : messageId(msgId) + , requestId(reqId) + {} +}; + +struct HeaderOnlyMessage { + uint32_t msgType; + MsgHeader header; + + HeaderOnlyMessage(uint32_t type, MsgHeader& hdr) + : msgType(type) + , header(hdr) + {} + + HeaderOnlyMessage() = default; + + MSGPACK_FIELDS(msgType, header); +}; + +template struct TypedMessage { + uint32_t msgType; + MsgHeader header; + T value; + + TypedMessage(uint32_t type, MsgHeader& hdr, const T& val) + : msgType(type) + , header(hdr) + , value(val) + {} + + TypedMessage() = default; + + MSGPACK_FIELDS(msgType, header, value); +}; + +// #pragma pack(pop) +} // namespace bb::messaging diff --git a/barretenberg/cpp/src/barretenberg/messaging/stream_parser.hpp b/barretenberg/cpp/src/barretenberg/messaging/stream_parser.hpp new file mode 100644 index 00000000000..9f5006dced1 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/messaging/stream_parser.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "barretenberg/messaging/header.hpp" +#include "barretenberg/serialize/cbind.hpp" +#include "msgpack/v3/object_fwd_decl.hpp" +#include +#include +#include +#include +#include + +namespace bb::messaging { +template class StreamDispatcher { + private: + OutputStream& outputStream; + std::unordered_map> messageHandlers; + void sendPong(uint32_t pingId); + bool handleSystemMessage(msgpack::object& obj); + + public: + StreamDispatcher(OutputStream& out) + : outputStream(out) + {} + bool onNewData(msgpack::object& obj); + void registerTarget(uint32_t msgType, std::function& handler); +}; + +template bool StreamDispatcher::onNewData(msgpack::object& obj) +{ + bb::messaging::HeaderOnlyMessage header; + obj.convert(header); + + if (header.msgType < FIRST_APP_MSG_TYPE) { + return handleSystemMessage(obj); + } + auto iter = messageHandlers.find(header.msgType); + if (iter == messageHandlers.end()) { + std::cerr << "No registered handler for message of type " << header.msgType << std::endl; + return true; + } + return (iter->second)(obj); +} + +template +void StreamDispatcher::registerTarget(uint32_t msgType, std::function& handler) +{ + messageHandlers.insert({ msgType, handler }); +} + +template bool StreamDispatcher::handleSystemMessage(msgpack::object& obj) +{ + bb::messaging::HeaderOnlyMessage header; + obj.convert(header); + if (header.msgType == SystemMsgTypes::TERMINATE) { + return false; + } + if (header.msgType == SystemMsgTypes::PING) { + sendPong(header.header.messageId); + } + return true; +} + +template void StreamDispatcher::sendPong(uint32_t pingId) +{ + MsgHeader header(pingId); + HeaderOnlyMessage packedHeader(SystemMsgTypes::PONG, header); + outputStream.send(packedHeader); +} +} // namespace bb::messaging