diff --git a/Makefile b/Makefile index 584f0284ab7..c9ae79621ff 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ bench-labs: labs-bench bench: bench-foundation bench-labs # Release. Everything plus copy bb cross compiles to ts projects. -release-foundation: fast-foundation bb-cpp-release-dir bb-ts-cross-copy bb-avm-sim-cross-copy bb-bin-cross-copy ipc-runtime-cross +release-foundation: fast-foundation bb-cpp-release-dir bb-ts-cross-copy bb-avm-sim-cross-copy bb-wsdb-ref-cross-copy bb-bin-cross-copy ipc-runtime-cross release-labs: fast-labs @@ -143,7 +143,7 @@ avm-transpiler-cross: avm-transpiler-cross-amd64-macos avm-transpiler-cross-arm6 #============================================================================== # Barretenberg - Aggregate target for all barretenberg sub-projects. -barretenberg: bb-cpp bb-ts bb-avm-sim bb-cdb bb-bin bb-rs bb-acir bb-docs bb-sol bb-bbup bb-crs +barretenberg: bb-cpp bb-ts bb-avm-sim bb-cdb bb-wsdb-ref bb-bin bb-rs bb-acir bb-docs bb-sol bb-bbup bb-crs # BB C++ - Main aggregate target. bb-cpp: bb-cpp-native bb-cpp-wasm bb-cpp-wasm-threads @@ -290,6 +290,15 @@ bb-avm-sim-cross-copy: bb-avm-sim bb-cdb bb-ts-cross-copy bb-cpp-cross bb-cdb: ipc-codegen ipc-runtime bb-avm-sim $(call build,$@,barretenberg/ts,build_cdb) +# @aztec-foundation/wsdb-ref: the in-memory reference world state, run in-process through its +# wasm module or its Node-API addon. Ordered after bb-cdb because every barretenberg/ts target +# regenerates the same workspaces and installs into the same node_modules. +bb-wsdb-ref: ipc-codegen ipc-runtime bb-cpp-native bb-cpp-wasm bb-cdb + $(call build,$@,barretenberg/ts,build_wsdb_ref) + +bb-wsdb-ref-cross-copy: bb-wsdb-ref bb-avm-sim-cross-copy bb-cpp-cross + $(call build,$@,barretenberg/ts,cross_copy_wsdb_ref) + # bb and bb-avm as npm packages (meta + one package per platform). bb-bin: bb-cpp-native $(call build,$@,barretenberg/ts,build_bb_bin) @@ -347,6 +356,9 @@ bb-ts-tests: bb-ts bb-avm-sim-tests: bb-avm-sim bb-cdb $(call test,$@,barretenberg/ts,bb_avm_sim) +bb-wsdb-ref-tests: bb-wsdb-ref + $(call test,$@,barretenberg/ts,wsdb_ref) + bb-sol-tests: bb-sol $(call test,$@,barretenberg/sol) @@ -359,7 +371,7 @@ bb-bbup-tests: bb-bbup bb-rs-tests: bb-rs $(call test,$@,barretenberg/rust) -bb-tests: bb-cpp-native-tests bb-cpp-wasm-tests bb-acir-tests bb-ts-tests bb-avm-sim-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests +bb-tests: bb-cpp-native-tests bb-cpp-wasm-tests bb-acir-tests bb-ts-tests bb-avm-sim-tests bb-wsdb-ref-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests bb-full-tests: bb-cpp-wasm-threads-tests bb-cpp-asan-tests bb-cpp-smt-tests @@ -415,10 +427,11 @@ ipc-runtime-cross: ipc-runtime ipc-runtime-cross-arm64-linux ipc-runtime-cross-a lmdblib: $(call build,$@,native-packages/lmdblib) -kvdb: lmdblib +kvdb: $(call build,$@,native-packages/kvdb) -wsdb: lmdblib +# wsdb builds barretenberg's wsdb-ref sources itself (see native-packages/wsdb/README.md), so it needs no bb build. +wsdb: $(call build,$@,native-packages/wsdb) # Native-package tests. lmdblib and wsdb are self-contained gtest binaries. kvdb has no diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 5dfec5c889a..ef0cfde18ac 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -770,7 +770,8 @@ "barretenberg.wasm.gz", "barretenberg-debug.wasm", "bb-avm-sim.wasm", - "avm_ffi_tests" + "avm_ffi_tests", + "wsdb-ref.wasm" ] }, { @@ -803,25 +804,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/cmake/module.cmake b/barretenberg/cpp/cmake/module.cmake index c558af81b00..0b4ac6fd017 100644 --- a/barretenberg/cpp/cmake/module.cmake +++ b/barretenberg/cpp/cmake/module.cmake @@ -100,10 +100,6 @@ function(barretenberg_module_with_sources MODULE_NAME) endif() endif() - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${MODULE_NAME} msgpack-c) - add_dependencies(${MODULE_NAME}_objects msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${MODULE_NAME} lmdb_repo) @@ -193,10 +189,6 @@ function(barretenberg_module_with_sources MODULE_NAME) ${TBB_IMPORTED_TARGETS} ) - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${MODULE_NAME}_test_objects msgpack-c) - add_dependencies(${MODULE_NAME}_tests msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${MODULE_NAME}_test_objects lmdb_repo) @@ -307,10 +299,6 @@ function(barretenberg_module_with_sources MODULE_NAME) ) endif() - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${BENCHMARK_NAME}_bench_objects msgpack-c) - add_dependencies(${BENCHMARK_NAME}_bench msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${BENCHMARK_NAME}_bench_objects lmdb_repo) diff --git a/barretenberg/cpp/cmake/msgpack.cmake b/barretenberg/cpp/cmake/msgpack.cmake index db8f05ac6d3..38f217b51b7 100644 --- a/barretenberg/cpp/cmake/msgpack.cmake +++ b/barretenberg/cpp/cmake/msgpack.cmake @@ -1,18 +1,13 @@ -include(ExternalProject) +include(FetchContent) -# External project: Download msgpack-c from GitHub -set(MSGPACK_PREFIX "${CMAKE_BINARY_DIR}/_deps/msgpack-c") -set(MSGPACK_INCLUDE "${MSGPACK_PREFIX}/src/msgpack-c/include") - -ExternalProject_Add( - msgpack-c - PREFIX ${MSGPACK_PREFIX} - # We need to go through some hoops to do a shallow clone of a fixed commit (as opposed to a tag). - DOWNLOAD_COMMAND - sh -c "mkdir -p ${MSGPACK_PREFIX}/src/msgpack-c && cd ${MSGPACK_PREFIX}/src/msgpack-c && git init --quiet && (git remote add origin https://github.com/AztecProtocol/msgpack-c.git 2>/dev/null || true) && git fetch --depth 1 origin --quiet c0334576ed657fb3b3c49e8e61402989fb84146d && git reset --quiet --hard FETCH_HEAD" - SOURCE_DIR ${MSGPACK_PREFIX}/src/msgpack-c - CONFIGURE_COMMAND "" # No configure step - BUILD_COMMAND "" # No build step - INSTALL_COMMAND "" # No install step - UPDATE_COMMAND "" # No update step +# msgpack-c is used header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so +# FetchContent_MakeAvailable only fetches it and never configures msgpack-c's own targets. +FetchContent_Declare( + msgpack + GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git + GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include ) +FetchContent_MakeAvailable(msgpack) +set(MSGPACK_INCLUDE "${msgpack_SOURCE_DIR}/include") diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 24f10bdc9f3..a0f7fdb9eb5 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -54,7 +54,8 @@ endif() # The build tree's src/ holds generated code (e.g. barretenberg/bbapi/generated/), included by the same # paths as sources. -include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${MSGPACK_INCLUDE} ${TRACY_INCLUDE} ${LMDB_INCLUDE} ${LIBDEFLATE_INCLUDE} ${HTTPLIB_INCLUDE} ${BACKWARD_INCLUDE} ${NLOHMANN_JSON_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/../../../ipc-runtime/cpp) +include_directories(SYSTEM ${MSGPACK_INCLUDE}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${TRACY_INCLUDE} ${LMDB_INCLUDE} ${LIBDEFLATE_INCLUDE} ${HTTPLIB_INCLUDE} ${BACKWARD_INCLUDE} ${NLOHMANN_JSON_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/../../../ipc-runtime/cpp) # Add avm-transpiler include path when library is provided if(AVM_TRANSPILER_LIB) @@ -121,6 +122,7 @@ add_subdirectory(barretenberg/vm2_stub) # AVM simulator (aztec, world_state_reference, yes yes - FUZZING_AVM # vm2; vm2 defines vm2_sim whatever AVM says) # bb-avm-sim (avm) yes yes - - +# wsdb-ref (wsdb_ref) yes yes - - # wsdb and cdb IPC clients (vm2_wsdb, cdb) yes - - - # ipc-runtime yes yes - - # world_state_reference and api in the archives yes - - - @@ -128,18 +130,21 @@ add_subdirectory(barretenberg/vm2_stub) # The AVM fuzzer (FUZZING_AVM) builds on top of either a fuzzing or a native build. set(BB_WITH_AVM_SIM OFF) set(BB_WITH_AVM_SERVICE OFF) +set(BB_WITH_WSDB_REF OFF) set(BB_WITH_IPC_CLIENTS OFF) set(BB_WITH_IPC_RUNTIME OFF) set(BB_ARCHIVE_WORLD_STATE_REFERENCE OFF) if(WASM) set(BB_WITH_AVM_SIM ON) set(BB_WITH_AVM_SERVICE ON) + set(BB_WITH_WSDB_REF ON) set(BB_WITH_IPC_RUNTIME ON) elseif(FUZZING) set(BB_WITH_AVM_SIM ${FUZZING_AVM}) elseif(NOT BB_LITE) set(BB_WITH_AVM_SIM ON) set(BB_WITH_AVM_SERVICE ON) + set(BB_WITH_WSDB_REF ON) set(BB_WITH_IPC_CLIENTS ON) set(BB_WITH_IPC_RUNTIME ON) set(BB_ARCHIVE_WORLD_STATE_REFERENCE ON) @@ -151,6 +156,9 @@ if(BB_WITH_AVM_SIM) # NOTE: Do not conditionally base this on the AVM flag as it defines a necessary vm2_sim library. add_subdirectory(barretenberg/vm2) endif() +if(BB_WITH_WSDB_REF) + add_subdirectory(barretenberg/wsdb_ref) +endif() if(BB_WITH_IPC_CLIENTS) add_subdirectory(barretenberg/vm2_wsdb) add_subdirectory(barretenberg/cdb) @@ -224,14 +232,12 @@ set(BARRETENBERG_TARGET_OBJECTS # The core object list: what FFI consumers of bb-external need (bb_ipc_ffi_entry()). set(BB_EXTERNAL_TARGET_OBJECTS ${BARRETENBERG_TARGET_OBJECTS}) -# world_state_reference is the in-memory reference merkle DB and its bb_wsref_* C ABI, which external -# world-state implementations link for their conformance tests. It is vm2-free; vm2_stub satisfies its +# world_state_reference is the in-memory reference world state. It is vm2-free; vm2_stub satisfies its # Stats references. if(BB_ARCHIVE_WORLD_STATE_REFERENCE) list(APPEND BARRETENBERG_TARGET_OBJECTS $ $) - list(APPEND BB_EXTERNAL_TARGET_OBJECTS $) endif() add_library( diff --git a/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt index 0ad01500fa3..8f6ebdc3927 100644 --- a/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt @@ -17,7 +17,6 @@ target_link_libraries( vm2_stub libdeflate::libdeflate_static ) -add_dependencies(acir_components_check_lib msgpack-c) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(acir_components_check_lib lmdb_repo) endif() diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp index dc0096d070a..c8541f7dc42 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp @@ -4,7 +4,6 @@ #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 { diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp index f605b36ce70..c9360621eff 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp @@ -9,7 +9,7 @@ * * Attack vector: An attacker crafts raw ACIR bytecode (bypassing the Noir compiler) * containing a BlackBoxFuncCall opcode where a fixed-size array field is encoded as - * msgpack NIL (0xc0). Without the fix, the AztecProtocol/msgpack-c fork silently + * msgpack NIL (0xc0). Without the fix, msgpack-c's shared_ptr adaptor silently * converts NIL to a null shared_ptr, which is then dereferenced unconditionally. * With the fix, deserialization rejects NIL for required fields and throws. */ diff --git a/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt index 571e621b445..6902222f4ab 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt @@ -5,5 +5,4 @@ file(GLOB_RECURSE SOURCE_FILES *.cpp) list(FILTER SOURCE_FILES EXCLUDE REGEX ".*\\.(fuzzer|test|bench)\\.cpp$") add_library(vm2_stub OBJECT ${SOURCE_FILES}) -add_dependencies(vm2_stub msgpack-c) target_link_libraries(vm2_stub PUBLIC barretenberg_headers) diff --git a/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt index 0a3c1b70769..950c532eb95 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt @@ -1,13 +1,10 @@ # VM2 binding to the wsdb service: the generated IPC client plus the AVM adapter # (WsdbIpcMerkleDB) that drives native AVM simulation against aztec-wsdb. # -# The wsdb wire contract (wsdb_schema.jsonc) and the server live in the -# top-level native-packages/wsdb package, which owns the service. bb only -# needs the generated CLIENT, so it codegens from that external schema path. -# This is a deliberate (and currently the only) build-order inversion: -# bb reads native-packages/wsdb/wsdb_schema.jsonc at configure/build time. +# The wsdb wire contract (wsdb_schema.jsonc) lives with wsdb-ref, its reference implementation, in +# ../wsdb_ref. The AVM only needs the generated client. if(TARGET vm2_sim) - set(WSDB_SCHEMA ${CMAKE_SOURCE_DIR}/../../native-packages/wsdb/wsdb_schema.jsonc) + set(WSDB_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/../wsdb_ref/wsdb_schema.jsonc) # Generated code is per build tree (each preset generates its own, so presets building at once never # share a file). Remove the copy older checkouts generated into the source tree: it would shadow this one. file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/generated) @@ -37,7 +34,7 @@ if(TARGET vm2_sim) --cpp-namespace bb::wsdb --cpp-include-dir barretenberg/vm2_wsdb/generated DEPENDS ${WSDB_SCHEMA} ${IPC_CODEGEN_SRC} - COMMENT "Generating WSDB IPC client from native-packages/wsdb/wsdb_schema.jsonc" + COMMENT "Generating WSDB IPC client from wsdb_ref/wsdb_schema.jsonc" VERBATIM ) add_custom_target(wsdb_ipc_generated DEPENDS ${WSDB_GEN_OUTPUTS}) diff --git a/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp b/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp index cd4395a9e7c..41d09cbbc25 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp @@ -3,7 +3,7 @@ * @file wsdb_wire.hpp * @brief The boundary between bb's merkle-tree domain types and the generated wsdb wire types. * - * The generated `wire::` records (native-packages/wsdb/wsdb_schema.jsonc) are the serialisation + * The generated `wire::` records (wsdb_ref/wsdb_schema.jsonc) are the serialisation * types; bb's own merkle types are what the AVM computes with. `Wire` names a domain type's * wire counterpart and converts both ways: * diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp new file mode 100644 index 00000000000..9fd0606961d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp @@ -0,0 +1,56 @@ +// GENERATED FILE - DO NOT EDIT. +// Regenerate with noir-projects/fnd/scripts/regenerate_genesis_constants.sh. +#pragma once + +#include "barretenberg/ecc/curves/bn254/fr.hpp" + +#include + +namespace bb::world_state { + +/** + * @brief The protocol contracts' registration nullifiers, seeded into the nullifier tree of a production genesis. + * + * Derived from the protocol contract artifacts: per protocol contract, the class registration nullifier siloed by + * ContractClassRegistry and the instance publication nullifier siloed by ContractInstanceRegistry. Sorted ascending + * because the indexed nullifier tree requires unique, strictly increasing prefilled leaves. These determine the + * three roots below, and GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT in + * constants.nr, so all of them are rewritten together and never on their own. + */ +inline std::vector genesis_protocol_nullifiers() +{ + return { + bb::fr("0x04287df03953b0d68b0e83e08a89067afc602626defd6002277cd6df34696818"), + bb::fr("0x0d99507b7ecac720c73bf197a0e7366a5ed80c1c1b0afe8ff8c6ecc7b5a7aefe"), + bb::fr("0x0eb50b367fb754d3a7d1238bfc105cc9b391a02e187be69038876ae9a502e877"), + bb::fr("0x16f917edd8e333013e5bb0633f7f484725bd19403854d6112992dccef4961e32"), + bb::fr("0x1cea539e01abaa5db980e7ff52ef0d2a7772310306ac625783ae435756ee326d"), + bb::fr("0x268b77cfa59397698ab19812d17573156d9a964fae96ab2d80929882f4a93837"), + }; +} + +/** + * @brief The genesis roots the seed vector above produces. + * + * Measured from the protocol contract artifacts together with the seeds, so that this package can check its tree + * implementation against them without reaching for the protocol constants: the GENESIS_* macros in + * common/aztec_constants.hpp come from the release named in foundation.pin, which lags this tree whenever the genesis + * moves. constants.nr remains the protocol's source of truth, and is held to this same measurement by the script that + * writes this file. + */ +inline bb::fr genesis_nullifier_tree_root() +{ + return bb::fr("0x238a0a0523fa750b77e243f8d6f485f03e90f1319fe76f5876f4af3d88b3fa89"); +} + +inline bb::fr genesis_block_header_hash() +{ + return bb::fr("0x0a72ed6f85f785d34e0a09a57c971b1623bb579531f3b85325e1d60f8209b3b8"); +} + +inline bb::fr genesis_archive_root() +{ + return bb::fr("0x1fdfe7b76afd4dc9b9b6bd09f5fd6fb354f22cf7055ef1f11490b55a4e197291"); +} + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp index 88c7af336ec..ef90f5736ff 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp @@ -1,16 +1,19 @@ #include "barretenberg/world_state_reference/memory_merkle_db.hpp" -#include #include #include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/common/throw_or_abort.hpp" namespace bb::world_state { -MemoryMerkleDB::MemoryMerkleDB(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) +MemoryMerkleDB::MemoryMerkleDB(size_t nullifier_tree_prefill, + size_t public_data_tree_prefill, + std::span prefilled_nullifiers, + std::span prefilled_public_data) : state_{ - .nullifier_tree = NullifierTree(NULLIFIER_TREE_HEIGHT, nullifier_tree_prefill), - .public_data_tree = PublicDataTree(PUBLIC_DATA_TREE_HEIGHT, public_data_tree_prefill), + .nullifier_tree = NullifierTree(NULLIFIER_TREE_HEIGHT, nullifier_tree_prefill, prefilled_nullifiers), + .public_data_tree = PublicDataTree(PUBLIC_DATA_TREE_HEIGHT, public_data_tree_prefill, prefilled_public_data), .note_hash_tree = NoteHashTree(NOTE_HASH_TREE_HEIGHT), .l1_to_l2_message_tree = L1ToL2MessageTree(L1_TO_L2_MSG_TREE_HEIGHT), } @@ -38,8 +41,7 @@ SiblingPath MemoryMerkleDB::get_sibling_path(MerkleTreeId tree_id, index_t leaf_ case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: return state_.l1_to_l2_message_tree.get_sibling_path(leaf_index); default: - throw std::runtime_error("get_sibling_path: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_sibling_path: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -51,8 +53,7 @@ GetLowIndexedLeafResponse MemoryMerkleDB::get_low_indexed_leaf(MerkleTreeId tree case MerkleTreeId::PUBLIC_DATA_TREE: return state_.public_data_tree.get_low_indexed_leaf(value); default: - throw std::runtime_error("get_low_indexed_leaf: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_low_indexed_leaf: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -68,8 +69,7 @@ FF MemoryMerkleDB::get_leaf_value(MerkleTreeId tree_id, index_t leaf_index) cons case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: return state_.l1_to_l2_message_tree.get_leaf_value(leaf_index); default: - throw std::runtime_error("get_leaf_value: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_leaf_value: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -105,7 +105,7 @@ void MemoryMerkleDB::append_leaves(MerkleTreeId tree_id, std::span lea state_.l1_to_l2_message_tree.append_leaves(leaves); break; default: - throw std::runtime_error("append_leaves is only supported for NOTE_HASH_TREE and L1_TO_L2_MESSAGE_TREE"); + throw_or_abort("append_leaves is only supported for NOTE_HASH_TREE and L1_TO_L2_MESSAGE_TREE"); } } @@ -119,7 +119,7 @@ void MemoryMerkleDB::pad_tree(MerkleTreeId tree_id, size_t num_leaves) state_.note_hash_tree.pad_leaves(num_leaves); break; default: - throw std::runtime_error("Padding not supported for tree " + std::to_string(static_cast(tree_id))); + throw_or_abort("Padding not supported for tree " + std::to_string(static_cast(tree_id))); } } diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp index a1f9c392871..579c33fe5f6 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp @@ -2,11 +2,14 @@ #include #include +#include #include #include +#include #include #include "barretenberg/aztec/aztec_hash_policy.hpp" +#include "barretenberg/common/throw_or_abort.hpp" #include "barretenberg/crypto/merkle_tree/indexed_leaf.hpp" #include "barretenberg/crypto/merkle_tree/response.hpp" #include "barretenberg/numeric/uint256/uint256.hpp" @@ -86,26 +89,59 @@ template class MemoryAppendOnlyTree { * @brief A full-height, sparse, indexed Merkle tree over a SparseMemoryTree. * * Used for the nullifier and public-data trees. The genesis state matches the WorldState's - * ContentAddressedIndexedTree: `initial_size` prefill leaves, all LeafType::padding(i) for i in - * [0, initial_size), linked as an ascending chain. Insert / low-leaf logic mirrors - * IndexedMemoryTree, but the backing is sparse so it works at full tree height. + * ContentAddressedIndexedTree: `initial_size` prefill leaves, LeafType::padding(i) for the first + * initial_size - prefilled.size() of them followed by `prefilled`, linked as an ascending chain. + * Insert / low-leaf logic mirrors IndexedMemoryTree, but the backing is sparse so it works at full + * tree height. */ template class MemoryIndexedTree { public: using Leaf = crypto::merkle_tree::IndexedLeaf; - MemoryIndexedTree(size_t depth, size_t initial_size) + /** + * @brief Why `prefilled` cannot seed a tree of `initial_size` leaves, or nullopt when it can. + * @details The prefill must fit, and its keys must be strictly increasing and above the + * padding keys, since the genesis leaves form one ascending chain. + */ + static std::optional check_prefill(size_t initial_size, std::span prefilled) + { + if (initial_size < 2) { + return "Indexed trees must have initial size > 1"; + } + if (prefilled.size() > initial_size) { + return "Number of prefilled values can't be more than initial size"; + } + size_t num_padding = initial_size - prefilled.size(); + uint256_t previous = + num_padding > 0 ? static_cast(LeafType::padding(num_padding - 1).get_key()) : uint256_t(0); + for (size_t i = 0; i < prefilled.size(); ++i) { + uint256_t key = static_cast(prefilled[i].get_key()); + if ((i > 0 || num_padding > 0) && key <= previous) { + return i == 0 ? "Prefilled values must not be the same as the default values" + : "Prefilled values must be unique and sorted"; + } + previous = key; + } + return std::nullopt; + } + + MemoryIndexedTree(size_t depth, size_t initial_size, std::span prefilled = {}) : tree_(depth) { - BB_ASSERT_GT(initial_size, static_cast(1), "Indexed trees must have initial size > 1"); + auto problem = check_prefill(initial_size, prefilled); + BB_ASSERT(!problem.has_value(), problem.value_or("")); + size_t num_padding = initial_size - prefilled.size(); leaves_.reserve(initial_size); - for (size_t i = 0; i < initial_size; ++i) { + for (size_t i = 0; i < num_padding; ++i) { leaves_.push_back(Leaf(LeafType::padding(i), /*nextIndex=*/0, /*nextKey=*/0)); } + for (const auto& value : prefilled) { + leaves_.push_back(Leaf(value, /*nextIndex=*/0, /*nextKey=*/0)); + } for (size_t i = 0; i < initial_size; ++i) { size_t next_index = i == (initial_size - 1) ? 0 : i + 1; leaves_[i].nextIndex = next_index; - leaves_[i].nextKey = leaves_[next_index].leaf.get_key(); + leaves_[i].nextKey = leaves_[static_cast(next_index)].leaf.get_key(); tree_.update_element(i, HashingPolicy::hash(leaves_[i].get_hash_inputs())); } } @@ -171,7 +207,7 @@ template class MemoryIndexedTree { tree_.update_element(find_low_leaf_result.index, HashingPolicy::hash(low_leaf.get_hash_inputs())); result.insertion_witness_data.emplace_back(Leaf::empty(), 0, SiblingPath{}); } else { - throw std::runtime_error("Leaf is not updateable"); + throw_or_abort("Leaf is not updateable"); } return result; @@ -233,7 +269,9 @@ class MemoryMerkleDB { static constexpr size_t DEFAULT_PUBLIC_DATA_TREE_PREFILL = 128; MemoryMerkleDB(size_t nullifier_tree_prefill = DEFAULT_NULLIFIER_TREE_PREFILL, - size_t public_data_tree_prefill = DEFAULT_PUBLIC_DATA_TREE_PREFILL); + size_t public_data_tree_prefill = DEFAULT_PUBLIC_DATA_TREE_PREFILL, + std::span prefilled_nullifiers = {}, + std::span prefilled_public_data = {}); TreeRoots get_tree_roots() const; diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp deleted file mode 100644 index 20ae3af87ef..00000000000 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include -#include -#include - -#include "barretenberg/common/throw_or_abort.hpp" -#include "barretenberg/world_state_reference/memory_merkle_db.hpp" - -// Flat C ABI over the in-memory reference world state, for out-of-tree world-state -// implementations that prove conformance against it (e.g. the standalone wsdb service) -// without depending on barretenberg's C++ headers, field type, or build flags. Field -// elements cross as 32 canonical bytes (barretenberg's field serialization); tree ids -// are the bb::world_state::MerkleTreeId values (0 = nullifier, 1 = note hash, -// 2 = public data, 3 = L1-to-L2 message). Passing a tree id a method does not support -// throws, as in the underlying class; conformance drivers must pass valid ids. -namespace { -using bb::world_state::MemoryMerkleDB; -using bb::world_state::MerkleTreeId; -using FF = bb::fr; - -MemoryMerkleDB& db(void* handle) -{ - return *static_cast(handle); -} - -MerkleTreeId tree(uint8_t tree_id) -{ - return static_cast(tree_id); -} -} // namespace - -extern "C" { - -void* bb_wsref_create(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) -{ - return new MemoryMerkleDB(nullifier_tree_prefill, public_data_tree_prefill); -} - -void bb_wsref_destroy(void* handle) -{ - delete static_cast(handle); -} - -// Snapshot of one tree: root as 32 bytes plus the next free leaf index. -void bb_wsref_get_snapshot(void* handle, uint8_t tree_id, uint8_t* out_root, uint64_t* out_next_index) -{ - const auto roots = db(handle).get_tree_roots(); - const bb::world_state::TreeSnapshot* snap = nullptr; - switch (tree(tree_id)) { - case MerkleTreeId::NULLIFIER_TREE: - snap = &roots.nullifier_tree; - break; - case MerkleTreeId::NOTE_HASH_TREE: - snap = &roots.note_hash_tree; - break; - case MerkleTreeId::PUBLIC_DATA_TREE: - snap = &roots.public_data_tree; - break; - case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - snap = &roots.l1_to_l2_message_tree; - break; - default: - throw_or_abort("bb_wsref_get_snapshot: unsupported tree id"); - } - FF::serialize_to_buffer(snap->root, out_root); - *out_next_index = snap->next_available_leaf_index; -} - -// Writes the sibling path (leaf to root) as consecutive 32-byte nodes into `out`, which -// must hold tree-height nodes. Returns the node count. -size_t bb_wsref_get_sibling_path(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out) -{ - const auto path = db(handle).get_sibling_path(tree(tree_id), leaf_index); - for (size_t i = 0; i < path.size(); ++i) { - FF::serialize_to_buffer(path[i], out + (i * 32)); - } - return path.size(); -} - -void bb_wsref_get_low_indexed_leaf( - void* handle, uint8_t tree_id, const uint8_t* key, uint8_t* out_is_present, uint64_t* out_index) -{ - const auto response = db(handle).get_low_indexed_leaf(tree(tree_id), FF::serialize_from_buffer(key)); - *out_is_present = response.is_already_present ? 1 : 0; - *out_index = response.index; -} - -void bb_wsref_get_leaf_value(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out) -{ - FF::serialize_to_buffer(db(handle).get_leaf_value(tree(tree_id), leaf_index), out); -} - -void bb_wsref_get_nullifier_preimage( - void* handle, uint64_t leaf_index, uint8_t* out_nullifier, uint64_t* out_next_index, uint8_t* out_next_key) -{ - const auto preimage = db(handle).get_leaf_preimage_nullifier_tree(leaf_index); - FF::serialize_to_buffer(preimage.leaf.nullifier, out_nullifier); - *out_next_index = preimage.nextIndex; - FF::serialize_to_buffer(preimage.nextKey, out_next_key); -} - -void bb_wsref_get_public_data_preimage(void* handle, - uint64_t leaf_index, - uint8_t* out_slot, - uint8_t* out_value, - uint64_t* out_next_index, - uint8_t* out_next_key) -{ - const auto preimage = db(handle).get_leaf_preimage_public_data_tree(leaf_index); - FF::serialize_to_buffer(preimage.leaf.slot, out_slot); - FF::serialize_to_buffer(preimage.leaf.value, out_value); - *out_next_index = preimage.nextIndex; - FF::serialize_to_buffer(preimage.nextKey, out_next_key); -} - -void bb_wsref_insert_nullifier(void* handle, const uint8_t* nullifier) -{ - db(handle).insert_indexed_leaves_nullifier_tree( - bb::crypto::merkle_tree::NullifierLeafValue(FF::serialize_from_buffer(nullifier))); -} - -void bb_wsref_insert_public_data(void* handle, const uint8_t* slot, const uint8_t* value) -{ - db(handle).insert_indexed_leaves_public_data_tree(bb::crypto::merkle_tree::PublicDataLeafValue( - FF::serialize_from_buffer(slot), FF::serialize_from_buffer(value))); -} - -// `leaves` is count*32 canonical bytes. -void bb_wsref_append_leaves(void* handle, uint8_t tree_id, const uint8_t* leaves, size_t count) -{ - std::vector values; - values.reserve(count); - for (size_t i = 0; i < count; ++i) { - values.push_back(FF::serialize_from_buffer(leaves + (i * 32))); - } - db(handle).append_leaves(tree(tree_id), values); -} - -void bb_wsref_pad_tree(void* handle, uint8_t tree_id, size_t num_leaves) -{ - db(handle).pad_tree(tree(tree_id), num_leaves); -} - -void bb_wsref_create_checkpoint(void* handle) -{ - db(handle).create_checkpoint(); -} - -void bb_wsref_commit_checkpoint(void* handle) -{ - db(handle).commit_checkpoint(); -} - -void bb_wsref_revert_checkpoint(void* handle) -{ - db(handle).revert_checkpoint(); -} - -uint32_t bb_wsref_get_checkpoint_id(void* handle) -{ - return db(handle).get_checkpoint_id(); -} - -} // extern "C" diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp index f442f0fffac..ae5ef034cbd 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp @@ -1,6 +1,6 @@ #include "barretenberg/world_state_reference/merkle_tree_id.hpp" -#include +#include "barretenberg/common/throw_or_abort.hpp" #include namespace bb::world_state { @@ -19,7 +19,7 @@ std::string getMerkleTreeName(MerkleTreeId id) case MerkleTreeId::ARCHIVE: return "ArchiveTree"; default: - throw std::invalid_argument("Unknown MerkleTreeId"); + throw_or_abort("Unknown MerkleTreeId"); } } diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp new file mode 100644 index 00000000000..6846f030547 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include + +#include "barretenberg/numeric/uint256/uint256.hpp" + +namespace bb::world_state { + +/** + * @brief An ordered map from a leaf key to its leaf index whose versions share structure. + * + * A treap with path copying: an insert copies O(log n) nodes and shares the rest, so copying the + * index is copying one pointer, like PersistentMerkleTree. Priorities are a hash of the key, so the + * shape depends only on the set of keys and every run builds the same tree. + * + * A key keeps the first index it was inserted with. + */ +class PersistentKeyIndex { + public: + using Key = numeric::uint256_t; + + std::optional find(const Key& key) const + { + const Node* node = root_.get(); + while (node != nullptr) { + if (key == node->key) { + return node->index; + } + node = (key < node->key ? node->left : node->right).get(); + } + return std::nullopt; + } + + /** The entry with the largest key strictly below `key`. */ + std::optional> predecessor(const Key& key) const + { + std::optional> best; + const Node* node = root_.get(); + while (node != nullptr) { + if (node->key < key) { + best = std::make_pair(node->key, node->index); + node = node->right.get(); + } else { + node = node->left.get(); + } + } + return best; + } + + /** A copy of this index holding `key` at `index`, unless `key` is already present. */ + PersistentKeyIndex with(const Key& key, uint64_t index) const + { + if (find(key).has_value()) { + return *this; + } + PersistentKeyIndex out; + out.root_ = insert(root_, std::make_shared(Node{ key, index, priority(key), nullptr, nullptr })); + return out; + } + + private: + struct Node { + Key key; + uint64_t index; + uint64_t priority; + std::shared_ptr left; + std::shared_ptr right; + }; + using NodePtr = std::shared_ptr; + + static uint64_t priority(const Key& key) + { + // splitmix64 over the key's limbs. + uint64_t x = key.data[0] ^ (key.data[1] * 0x9e3779b97f4a7c15ULL) ^ (key.data[2] * 0xbf58476d1ce4e5b9ULL) ^ + (key.data[3] * 0x94d049bb133111ebULL); + x += 0x9e3779b97f4a7c15ULL; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; + x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; + return x ^ (x >> 31); + } + + static NodePtr with_children(const NodePtr& node, NodePtr left, NodePtr right) + { + return std::make_shared( + Node{ node->key, node->index, node->priority, std::move(left), std::move(right) }); + } + + /** Splits `node` into the keys below `key` and the keys above it (`key` itself is absent). */ + static std::pair split(const NodePtr& node, const Key& key) + { + if (!node) { + return { nullptr, nullptr }; + } + if (node->key < key) { + auto [below, above] = split(node->right, key); + return { with_children(node, node->left, std::move(below)), std::move(above) }; + } + auto [below, above] = split(node->left, key); + return { std::move(below), with_children(node, std::move(above), node->right) }; + } + + static NodePtr insert(const NodePtr& node, const NodePtr& fresh) + { + if (!node) { + return fresh; + } + if (fresh->priority > node->priority) { + auto [below, above] = split(node, fresh->key); + return with_children(fresh, std::move(below), std::move(above)); + } + if (fresh->key < node->key) { + return with_children(node, insert(node->left, fresh), node->right); + } + return with_children(node, node->left, insert(node->right, fresh)); + } + + NodePtr root_; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp new file mode 100644 index 00000000000..a40c810df7d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "barretenberg/common/assert.hpp" +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" + +namespace bb::world_state { + +/** + * @brief A full-height Merkle tree whose versions share structure. + * + * Every node is immutable. A write copies the path from the root to the leaves it touches and shares + * every other subtree with the version it was made from, so copying a tree is copying one pointer. + * That is what makes a fork, a checkpoint or a per-block snapshot free. An empty subtree is a null + * pointer and hashes to the zero-subtree root for its level. + * + * Each leaf may carry a payload (an indexed tree's leaf preimage). Node hashing is + * HashingPolicy::hash_pair; the caller computes each leaf's hash. + * + * Levels are numbered from the leaves: level 0 holds leaves, level `depth` holds the root. + */ +template class PersistentMerkleTree { + public: + using FF = bb::fr; + + struct Leaf { + FF hash; + std::shared_ptr payload; + }; + + explicit PersistentMerkleTree(size_t depth) + : zero_hashes_(make_zero_hashes(depth)) + {} + + size_t depth() const { return zero_hashes_->size() - 1; } + + FF root() const { return hash_of(root_, depth()); } + + /** The hash at (level, index); untouched nodes are zero-subtree roots. */ + FF node(size_t level, uint64_t index) const + { + const Node* current = root_.get(); + for (size_t at = depth(); at > level && current != nullptr; --at) { + current = current->children[bit(index, at - 1 - level)].get(); + } + return hash_of(current, level); + } + + FF leaf_hash(uint64_t index) const { return node(0, index); } + + /** The payload stored at `index`, or null when none was set. */ + const Payload* payload(uint64_t index) const + { + const Node* current = root_.get(); + for (size_t at = depth(); at > 0 && current != nullptr; --at) { + current = current->children[bit(index, at - 1)].get(); + } + return current != nullptr ? current->payload.get() : nullptr; + } + + crypto::merkle_tree::fr_sibling_path sibling_path(uint64_t index) const { return subtree_sibling_path(index, 0); } + + /** The siblings from `level` up to the root of the subtree holding `index` at `level`. */ + crypto::merkle_tree::fr_sibling_path subtree_sibling_path(uint64_t index, size_t level) const + { + crypto::merkle_tree::fr_sibling_path path(depth() - level); + std::vector siblings(depth() + 1, nullptr); + const Node* current = root_.get(); + for (size_t at = depth(); at > level; --at) { + size_t direction = bit(index, at - 1 - level); + siblings[at - 1] = current != nullptr ? current->children[direction ^ 1].get() : nullptr; + current = current != nullptr ? current->children[direction].get() : nullptr; + } + for (size_t at = level; at < depth(); ++at) { + path[at - level] = hash_of(siblings[at], at); + } + return path; + } + + /** A copy of this tree with the leaf at `index` set. */ + PersistentMerkleTree with_leaf(uint64_t index, Leaf leaf) const + { + PersistentMerkleTree out = *this; + out.root_ = set(root_, depth(), 0, index, std::span(&leaf, 1)); + return out; + } + + /** A copy of this tree with `leaves` written at consecutive indices from `start`, hashing each node once. */ + PersistentMerkleTree with_leaves(uint64_t start, std::span leaves) const + { + if (leaves.empty()) { + return *this; + } + PersistentMerkleTree out = *this; + out.root_ = set(root_, depth(), 0, start, leaves); + return out; + } + + private: + struct Node { + FF hash; + std::shared_ptr children[2]; + std::shared_ptr payload; + }; + using NodePtr = std::shared_ptr; + + static std::shared_ptr> make_zero_hashes(size_t depth) + { + BB_ASSERT_GTE(depth, static_cast(1)); + auto zero_hashes = std::make_shared>(depth + 1); + (*zero_hashes)[0] = FF::zero(); + for (size_t level = 1; level <= depth; ++level) { + (*zero_hashes)[level] = HashingPolicy::hash_pair((*zero_hashes)[level - 1], (*zero_hashes)[level - 1]); + } + return zero_hashes; + } + + static size_t bit(uint64_t index, size_t position) { return static_cast((index >> position) & 1ULL); } + + FF hash_of(const Node* node, size_t level) const { return node != nullptr ? node->hash : (*zero_hashes_)[level]; } + FF hash_of(const NodePtr& node, size_t level) const { return hash_of(node.get(), level); } + + /** + * Writes `leaves` (from leaf index `start`) into the subtree at `level` whose first leaf is `first`, + * returning the new subtree. A subtree left with nothing but zero leaves and no payloads is null. + */ + NodePtr set(const NodePtr& node, size_t level, uint64_t first, uint64_t start, std::span leaves) const + { + if (level == 0) { + const Leaf& leaf = leaves[static_cast(first - start)]; + if (leaf.hash.is_zero() && !leaf.payload) { + return nullptr; + } + return std::make_shared(Node{ .hash = leaf.hash, .children = {}, .payload = leaf.payload }); + } + uint64_t half = uint64_t(1) << (level - 1); + uint64_t end = start + leaves.size(); + NodePtr children[2] = { node ? node->children[0] : nullptr, node ? node->children[1] : nullptr }; + for (size_t side = 0; side < 2; ++side) { + uint64_t child_first = first + side * half; + uint64_t child_end = child_first + half; + if (child_first < end && start < child_end) { + children[side] = set(children[side], level - 1, child_first, start, leaves); + } + } + if (!children[0] && !children[1]) { + return nullptr; + } + FF hash = HashingPolicy::hash_pair(hash_of(children[0], level - 1), hash_of(children[1], level - 1)); + return std::make_shared( + Node{ .hash = hash, .children = { std::move(children[0]), std::move(children[1]) }, .payload = nullptr }); + } + + std::shared_ptr> zero_hashes_; + NodePtr root_; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp new file mode 100644 index 00000000000..3ab1eb2f751 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp @@ -0,0 +1,986 @@ +#include "barretenberg/world_state_reference/reference_world_state.hpp" + +#include +#include +#include +#include + +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/crypto/merkle_tree/hash.hpp" +#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" + +namespace bb::world_state { + +namespace { + +// The trees a state reference covers, in the order the reference reports them. A node world state +// builds its reference from an unordered map, so clients match entries by tree id, not position. +constexpr MerkleTreeId STATE_TREES[] = { + MerkleTreeId::NULLIFIER_TREE, + MerkleTreeId::NOTE_HASH_TREE, + MerkleTreeId::PUBLIC_DATA_TREE, + MerkleTreeId::L1_TO_L2_MESSAGE_TREE, +}; + +constexpr MerkleTreeId ALL_TREES[] = { + MerkleTreeId::NULLIFIER_TREE, MerkleTreeId::NOTE_HASH_TREE, MerkleTreeId::PUBLIC_DATA_TREE, + MerkleTreeId::L1_TO_L2_MESSAGE_TREE, MerkleTreeId::ARCHIVE, +}; + +bool is_append_only(MerkleTreeId tree_id) +{ + return tree_id == MerkleTreeId::NOTE_HASH_TREE || tree_id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE || + tree_id == MerkleTreeId::ARCHIVE; +} + +template [[noreturn]] void fail(const Parts&... parts) +{ + std::ostringstream message; + (message << ... << parts); + throw std::runtime_error(message.str()); +} + +/** An indexed-tree leaf as the tree stores it: an empty leaf hashes to zero and carries no preimage. */ +template typename Tree::Leaf leaf_of(const Payload& indexed_leaf) +{ + if (indexed_leaf.is_empty()) { + return { .hash = FF::zero(), .payload = nullptr }; + } + return { .hash = Tree::Hashing::hash(indexed_leaf.get_hash_inputs()), + .payload = std::make_shared(indexed_leaf) }; +} + +} // namespace + +GenesisConfig GenesisConfig::canonical() +{ + GenesisConfig genesis; + for (const auto& nullifier : genesis_protocol_nullifiers()) { + genesis.prefilled_nullifiers.emplace_back(nullifier); + } + return genesis; +} + +ReferenceWorldState::ReferenceWorldState(const GenesisConfig& genesis) +{ + reset(genesis); +} + +std::optional ReferenceWorldState::check_genesis(const GenesisConfig& genesis) +{ + for (auto tree_id : ALL_TREES) { + uint32_t height = genesis.tree_heights[static_cast(tree_id)]; + if (height == 0 || height > 63) { + return "tree " + std::to_string(static_cast(tree_id)) + ": height " + std::to_string(height) + + " is not in 1..63"; + } + } + auto check = [&](const auto& prefilled, uint64_t initial_size, MerkleTreeId tree_id) -> std::optional { + if (initial_size < 2) { + return "Indexed trees must have initial size > 1"; + } + if (initial_size > (uint64_t(1) << genesis.tree_heights[static_cast(tree_id)])) { + return "Initial size is more than the tree holds"; + } + if (prefilled.size() > initial_size) { + return "Number of prefilled values can't be more than initial size"; + } + size_t num_padding = static_cast(initial_size - prefilled.size()); + using LeafType = std::remove_cvref_t; + uint256_t previous = num_padding > 0 ? uint256_t(LeafType::padding(num_padding - 1).get_key()) : uint256_t(0); + for (size_t i = 0; i < prefilled.size(); ++i) { + uint256_t key = uint256_t(prefilled[i].get_key()); + if ((i > 0 || num_padding > 0) && key <= previous) { + return i == 0 ? "Prefilled values must not be the same as the default values" + : "Prefilled values must be unique and sorted"; + } + previous = key; + } + return std::nullopt; + }; + if (auto problem = + check(genesis.prefilled_nullifiers, genesis.nullifier_tree_initial_size, MerkleTreeId::NULLIFIER_TREE)) { + return "prefilled nullifiers: " + *problem; + } + if (auto problem = check( + genesis.prefilled_public_data, genesis.public_data_tree_initial_size, MerkleTreeId::PUBLIC_DATA_TREE)) { + return "prefilled public data: " + *problem; + } + return std::nullopt; +} + +void ReferenceWorldState::reset(const GenesisConfig& genesis) +{ + if (auto problem = check_genesis(genesis)) { + fail(*problem); + } + tree_heights_ = genesis.tree_heights; + blocks_.clear(); + forks_.clear(); + blocks_.push_back(build_genesis(genesis)); + forks_.emplace(0, Fork{ .block_number = 0, .base = blocks_[0], .current = blocks_[0], .checkpoints = {} }); + next_fork_id_ = 1; + finalized_block_number_ = 0; + oldest_historical_block_ = 0; +} + +uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) const +{ + auto index = static_cast(tree_id); + if (index >= tree_heights_.size()) { + fail("Unknown tree id ", static_cast(tree_id)); + } + return tree_heights_[index]; +} + +ReferenceWorldState::Trees ReferenceWorldState::build_genesis(const GenesisConfig& genesis) +{ + // The indexed trees start as on a node: LeafType::padding(i) for the leaves the prefill leaves + // free, then the prefill, linked as one ascending chain. + auto indexed = [](auto tree, const auto& prefilled, uint64_t initial_size) { + using Tree = decltype(tree); + using LeafType = std::remove_cvref_t; + std::vector> leaves; + size_t num_padding = static_cast(initial_size - prefilled.size()); + for (size_t i = 0; i < num_padding; ++i) { + leaves.push_back(IndexedLeaf(LeafType::padding(i), 0, 0)); + } + for (const auto& value : prefilled) { + leaves.push_back(IndexedLeaf(value, 0, 0)); + } + std::vector hashed; + for (size_t i = 0; i < leaves.size(); ++i) { + size_t next = i + 1 == leaves.size() ? 0 : i + 1; + leaves[i].nextIndex = next; + leaves[i].nextKey = leaves[next].leaf.get_key(); + hashed.push_back(leaf_of(leaves[i])); + tree.indices = tree.indices.with(uint256_t(leaves[i].leaf.get_key()), i); + } + tree.tree = tree.tree.with_leaves(0, hashed); + tree.size = leaves.size(); + return tree; + }; + auto height = [&](MerkleTreeId tree_id) { return genesis.tree_heights[static_cast(tree_id)]; }; + auto append_only = [&](MerkleTreeId tree_id) { + return AppendOnlyTree{ .tree = decltype(AppendOnlyTree::tree)(height(tree_id)), .indices = {}, .size = 0 }; + }; + + Trees trees{ + .nullifier = + indexed(NullifierTree{ .tree = decltype(NullifierTree::tree)(height(MerkleTreeId::NULLIFIER_TREE)) }, + genesis.prefilled_nullifiers, + genesis.nullifier_tree_initial_size), + .note_hash = append_only(MerkleTreeId::NOTE_HASH_TREE), + .public_data = + indexed(PublicDataTree{ .tree = decltype(PublicDataTree::tree)(height(MerkleTreeId::PUBLIC_DATA_TREE)) }, + genesis.prefilled_public_data, + genesis.public_data_tree_initial_size), + .l1_to_l2 = append_only(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archive = append_only(MerkleTreeId::ARCHIVE), + }; + FF header_hash = compute_genesis_block_header_hash( + state_reference(trees), genesis.genesis_timestamp, genesis.header_generator_point); + append(trees.archive, std::span(&header_hash, 1)); + return trees; +} + +// --------------------------------------------------------------------------- +// Revisions +// --------------------------------------------------------------------------- + +const ReferenceWorldState::Fork& ReferenceWorldState::fork(uint64_t fork_id) const +{ + auto it = forks_.find(fork_id); + if (it == forks_.end()) { + fail("Fork not found"); + } + return it->second; +} + +ReferenceWorldState::Fork& ReferenceWorldState::fork(uint64_t fork_id) +{ + return const_cast(std::as_const(*this).fork(fork_id)); +} + +const ReferenceWorldState::Trees& ReferenceWorldState::committed_block(block_number_t block_number) const +{ + if (block_number > latest_block_number()) { + fail("Unable to get state for block ", block_number, ", latest block is ", latest_block_number()); + } + if (block_number != 0 && block_number < oldest_historical_block_) { + fail("Unable to get state for block ", block_number, ", oldest historical block is ", oldest_historical_block_); + } + return blocks_[block_number]; +} + +const ReferenceWorldState::Trees& ReferenceWorldState::view(const WorldStateRevision& revision) const +{ + const Fork& f = fork(revision.forkId); + if (revision.is_historical()) { + return committed_block(revision.blockNumber); + } + return revision.includeUncommitted ? f.current : f.base; +} + +TreeState ReferenceWorldState::tree_state(const Trees& trees, MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return { tree_id, trees.nullifier.tree.root(), trees.nullifier.size }; + case MerkleTreeId::NOTE_HASH_TREE: + return { tree_id, trees.note_hash.tree.root(), trees.note_hash.size }; + case MerkleTreeId::PUBLIC_DATA_TREE: + return { tree_id, trees.public_data.tree.root(), trees.public_data.size }; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return { tree_id, trees.l1_to_l2.tree.root(), trees.l1_to_l2.size }; + case MerkleTreeId::ARCHIVE: + return { tree_id, trees.archive.tree.root(), trees.archive.size }; + } + fail("Unknown tree id ", static_cast(tree_id)); +} + +uint64_t ReferenceWorldState::tree_size(const Trees& trees, MerkleTreeId tree_id) +{ + return tree_state(trees, tree_id).size; +} + +std::vector ReferenceWorldState::state_reference(const Trees& trees) +{ + std::vector state; + for (auto tree_id : STATE_TREES) { + state.push_back(tree_state(trees, tree_id)); + } + return state; +} + +bool ReferenceWorldState::state_matches(const Trees& trees, std::span state) +{ + // The node compares maps keyed by tree id: the same four trees, each with the same root and size. + const auto current = state_reference(trees); + if (state.size() != current.size()) { + return false; + } + return std::all_of(current.begin(), current.end(), [&](const TreeState& expected) { + return std::any_of(state.begin(), state.end(), [&](const TreeState& given) { return given == expected; }); + }); +} + +// --------------------------------------------------------------------------- +// Reads +// --------------------------------------------------------------------------- + +std::vector ReferenceWorldState::get_state_reference(const WorldStateRevision& revision) const +{ + return state_reference(view(revision)); +} + +std::vector ReferenceWorldState::get_initial_state_reference() const +{ + return state_reference(blocks_[0]); +} + +TreeState ReferenceWorldState::get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const +{ + return tree_state(view(revision), tree_id); +} + +std::optional ReferenceWorldState::get_leaf_value(const WorldStateRevision& revision, + MerkleTreeId tree_id, + uint64_t index) const +{ + if (!is_append_only(tree_id)) { + fail("Unsupported tree type for get_leaf_value"); + } + // As on a node: any leaf inside the tree's capacity reads (zero if unwritten), except that a historical + // read past that block's size, or of a block it does not have, reads as no leaf. + const Fork& f = fork(revision.forkId); + const Trees* trees = &(revision.includeUncommitted ? f.current : f.base); + if (revision.is_historical()) { + if (revision.blockNumber > latest_block_number() || + (revision.blockNumber != 0 && revision.blockNumber < oldest_historical_block_)) { + return std::nullopt; + } + trees = &blocks_[revision.blockNumber]; + } + const AppendOnlyTree& tree = tree_id == MerkleTreeId::NOTE_HASH_TREE ? trees->note_hash + : tree_id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE ? trees->l1_to_l2 + : trees->archive; + if (index >= (uint64_t(1) << tree.tree.depth()) || (revision.is_historical() && index >= tree.size)) { + return std::nullopt; + } + return tree.tree.leaf_hash(index); +} + +std::optional> ReferenceWorldState::get_nullifier_preimage( + const WorldStateRevision& revision, uint64_t index) const +{ + // As on a node: a slot a batch left empty reads as the empty leaf; past the tree's size there is none. + const auto& tree = view(revision).nullifier; + if (index >= tree.size) { + return std::nullopt; + } + const auto* leaf = tree.tree.payload(index); + return leaf != nullptr ? *leaf : IndexedLeaf::empty(); +} + +std::optional> ReferenceWorldState::get_public_data_preimage( + const WorldStateRevision& revision, uint64_t index) const +{ + // As on a node: a slot a batch left empty reads as the empty leaf; past the tree's size there is none. + const auto& tree = view(revision).public_data; + if (index >= tree.size) { + return std::nullopt; + } + const auto* leaf = tree.tree.payload(index); + return leaf != nullptr ? *leaf : IndexedLeaf::empty(); +} + +SiblingPath ReferenceWorldState::get_sibling_path(const WorldStateRevision& revision, + MerkleTreeId tree_id, + uint64_t index) const +{ + const Trees& trees = view(revision); + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return trees.nullifier.tree.sibling_path(index); + case MerkleTreeId::NOTE_HASH_TREE: + return trees.note_hash.tree.sibling_path(index); + case MerkleTreeId::PUBLIC_DATA_TREE: + return trees.public_data.tree.sibling_path(index); + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return trees.l1_to_l2.tree.sibling_path(index); + case MerkleTreeId::ARCHIVE: + return trees.archive.tree.sibling_path(index); + } + fail("Unknown tree id ", static_cast(tree_id)); +} + +std::vector> ReferenceWorldState::get_block_numbers_for_leaf_indices( + const WorldStateRevision& revision, MerkleTreeId tree_id, std::span indices) const +{ + // Only committed blocks number leaves: up to the revision's block, or the block the fork started from. + const Fork& f = fork(revision.forkId); + block_number_t last = revision.is_historical() ? revision.blockNumber : f.block_number; + committed_block(last); + std::vector> out; + out.reserve(indices.size()); + for (uint64_t index : indices) { + if (index >= tree_size(blocks_[last], tree_id)) { + out.emplace_back(std::nullopt); + continue; + } + // The first block whose tree holds the index. + block_number_t low = 0; + block_number_t high = last; + while (low < high) { + block_number_t mid = low + (high - low) / 2; + if (tree_size(blocks_[mid], tree_id) > index) { + high = mid; + } else { + low = mid + 1; + } + } + out.emplace_back(low); + } + return out; +} + +const PersistentKeyIndex& ReferenceWorldState::key_index(const Trees& trees, MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return trees.nullifier.indices; + case MerkleTreeId::PUBLIC_DATA_TREE: + return trees.public_data.indices; + case MerkleTreeId::NOTE_HASH_TREE: + return trees.note_hash.indices; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return trees.l1_to_l2.indices; + case MerkleTreeId::ARCHIVE: + return trees.archive.indices; + } + fail("Unknown tree id ", static_cast(tree_id)); +} + +std::optional ReferenceWorldState::find_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key, + uint64_t start) const +{ + const uint256_t k = uint256_t(key); + const Trees& trees = view(revision); + std::optional index; + if (revision.includeUncommitted && revision.is_historical()) { + // As on a node: a key the fork wrote since its base is found first, whatever block the read names. + const Fork& f = fork(revision.forkId); + auto written = key_index(f.current, tree_id).find(k); + if (written.has_value() && !key_index(f.base, tree_id).find(k).has_value()) { + index = written; + } + } + if (!index.has_value()) { + index = key_index(trees, tree_id).find(k); + } + if (!index.has_value() || *index < start) { + return std::nullopt; + } + return index; +} + +GetLowIndexedLeafResponse ReferenceWorldState::find_low_leaf(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key) const +{ + if (tree_id != MerkleTreeId::NULLIFIER_TREE && tree_id != MerkleTreeId::PUBLIC_DATA_TREE) { + fail("Unsupported tree type for find_low_leaf"); + } + if (auto index = find_leaf_index(revision, tree_id, key, 0)) { + return GetLowIndexedLeafResponse(true, *index); + } + const uint256_t k = uint256_t(key); + auto low = key_index(view(revision), tree_id).predecessor(k); + if (revision.includeUncommitted && revision.is_historical()) { + // As on a node: keys the fork wrote since its base count too, whatever block the read names. + const Fork& f = fork(revision.forkId); + const auto& current = key_index(f.current, tree_id); + const auto& base = key_index(f.base, tree_id); + for (auto written = current.predecessor(k); written.has_value() && (!low || written->first > low->first); + written = current.predecessor(written->first)) { + if (!base.find(written->first).has_value()) { + low = written; + break; + } + } + } + return GetLowIndexedLeafResponse(false, low.has_value() ? low->second : 0); +} + +// --------------------------------------------------------------------------- +// Tree writes +// --------------------------------------------------------------------------- + +void ReferenceWorldState::append(AppendOnlyTree& tree, std::span leaves) +{ + uint64_t max = uint64_t(1) << tree.tree.depth(); + if (tree.size + leaves.size() > max) { + fail("Unable to append leaves to tree, new size: ", tree.size + leaves.size(), " max size: ", max); + } + std::vector hashed; + hashed.reserve(leaves.size()); + for (size_t i = 0; i < leaves.size(); ++i) { + hashed.push_back({ .hash = leaves[i], .payload = nullptr }); + // A node does not index zero leaves. + if (!leaves[i].is_zero()) { + tree.indices = tree.indices.with(uint256_t(leaves[i]), tree.size + i); + } + } + tree.tree = tree.tree.with_leaves(tree.size, hashed); + tree.size += leaves.size(); +} + +template +BatchInsertionResult ReferenceWorldState::batch_insert(Tree& target, + std::span leaves, + uint32_t subtree_depth, + bool capture_witness) +{ + // Mirrors ContentAddressedIndexedTree::add_or_update_values: sort the batch by descending key, update each + // value's low leaf in that order, then append one slot per value at the batch's own position, empty for a + // zero value or a value that updated its low leaf in place. Works on a copy, so a failure changes nothing. + using Leaf = IndexedLeaf; + Tree tree = target; + const uint32_t depth = static_cast(tree.tree.depth()); + const uint64_t start = tree.size; + if (start + leaves.size() > (uint64_t(1) << depth)) { + fail("Unable to insert values into tree ", + LeafValueType::name(), + " new size: ", + start + leaves.size(), + " max size: ", + uint64_t(1) << depth); + } + + BatchInsertionResult result; + result.sorted_leaves.reserve(leaves.size()); + for (size_t i = 0; i < leaves.size(); ++i) { + result.sorted_leaves.emplace_back(leaves[i], i); + } + std::sort(result.sorted_leaves.begin(), result.sorted_leaves.end(), [](const auto& a, const auto& b) { + uint256_t a_key = uint256_t(a.first.get_key()); + uint256_t b_key = uint256_t(b.first.get_key()); + return a_key == b_key ? a.second < b.second : a_key > b_key; + }); + + std::vector appended(leaves.size(), Leaf::empty()); + std::vector> witnesses; + std::set unique; + for (const auto& [value, position] : result.sorted_leaves) { + if (value.is_empty()) { + continue; + } + uint256_t key = uint256_t(value.get_key()); + if (!unique.insert(key).second) { + fail("Duplicate key not allowed in same batch, key value: ", value.get_key(), ", tree: ", Leaf::name()); + } + uint64_t low_index = 0; + bool present = false; + if (auto exact = tree.indices.find(key)) { + present = true; + low_index = *exact; + } else if (auto low = tree.indices.predecessor(key)) { + low_index = low->second; + } + const Leaf* stored = tree.tree.payload(low_index); + if (stored == nullptr) { + fail("Unable to insert values into tree ", Leaf::name(), ", failed to find low leaf at index ", low_index); + } + Leaf low_leaf = *stored; + if (capture_witness) { + witnesses.emplace_back(low_leaf, low_index, tree.tree.sibling_path(low_index)); + } + Leaf updated = low_leaf; + if (!present) { + uint64_t new_index = start + position; + appended[static_cast(position)] = Leaf(value, low_leaf.nextIndex, low_leaf.nextKey); + updated.nextIndex = new_index; + updated.nextKey = value.get_key(); + tree.indices = tree.indices.with(key, new_index); + } else if (Leaf::is_updateable()) { + updated = Leaf(value, low_leaf.nextIndex, low_leaf.nextKey); + } else { + fail("Unable to insert values into tree ", + Leaf::name(), + " leaf type ", + Leaf::name(), + " is not updateable and ", + value.get_key(), + " is already present"); + } + tree.tree = tree.tree.with_leaf(low_index, leaf_of(updated)); + } + + if (capture_witness) { + // One witness slot per value: the low-leaf updates in the order they were applied, then empty ones. + witnesses.resize(leaves.size(), + LeafUpdateWitnessData(Leaf::empty(), 0, SiblingPath(depth, FF::zero()))); + result.low_leaf_witness_data = std::move(witnesses); + result.subtree_path = subtree_depth >= depth + ? SiblingPath{} + : tree.tree.subtree_sibling_path(start >> subtree_depth, subtree_depth); + } + + std::vector hashed; + hashed.reserve(appended.size()); + for (const auto& leaf : appended) { + hashed.push_back(leaf_of(leaf)); + } + tree.tree = tree.tree.with_leaves(start, hashed); + tree.size = start + leaves.size(); + target = std::move(tree); + if (!capture_witness) { + result.sorted_leaves.clear(); + } + return result; +} + +template +SequentialInsertionResult ReferenceWorldState::sequential_insert(Tree& target, + std::span leaves) +{ + // Mirrors ContentAddressedIndexedTree::add_or_update_values_sequentially: each value in order updates its low + // leaf, then (if new) is written at the next index. Zero values are skipped. Works on a copy. + using Leaf = IndexedLeaf; + Tree tree = target; + const uint32_t depth = static_cast(tree.tree.depth()); + SequentialInsertionResult result; + for (const auto& value : leaves) { + if (value.is_empty()) { + continue; + } + uint256_t key = uint256_t(value.get_key()); + uint64_t low_index = 0; + bool present = false; + if (auto exact = tree.indices.find(key)) { + present = true; + low_index = *exact; + } else if (auto low = tree.indices.predecessor(key)) { + low_index = low->second; + } + const Leaf* stored = tree.tree.payload(low_index); + if (stored == nullptr) { + fail("Unable to insert values into tree ", Leaf::name(), ", failed to find low leaf at index ", low_index); + } + Leaf low_leaf = *stored; + result.low_leaf_witness_data.emplace_back(low_leaf, low_index, tree.tree.sibling_path(low_index)); + if (!present) { + uint64_t new_index = tree.size; + Leaf fresh(value, low_leaf.nextIndex, low_leaf.nextKey); + Leaf updated = low_leaf; + updated.nextIndex = new_index; + updated.nextKey = value.get_key(); + tree.tree = tree.tree.with_leaf(low_index, leaf_of(updated)); + result.insertion_witness_data.emplace_back(Leaf::empty(), new_index, tree.tree.sibling_path(new_index)); + tree.tree = tree.tree.with_leaf(new_index, leaf_of(fresh)); + tree.indices = tree.indices.with(key, new_index); + tree.size++; + } else if (Leaf::is_updateable()) { + tree.tree = + tree.tree.with_leaf(low_index, leaf_of(Leaf(value, low_leaf.nextIndex, low_leaf.nextKey))); + result.insertion_witness_data.emplace_back(Leaf::empty(), 0, SiblingPath(depth, FF::zero())); + } else { + fail("Unable to insert values into tree ", + Leaf::name(), + " leaf type ", + Leaf::name(), + " is not updateable and ", + value.get_key(), + " is already present"); + } + } + if (tree.size > (uint64_t(1) << depth)) { + fail("Unable to insert values into tree ", Leaf::name(), " new size: ", tree.size); + } + target = std::move(tree); + return result; +} + +void ReferenceWorldState::append_leaves(uint64_t fork_id, MerkleTreeId tree_id, std::span leaves) +{ + Trees& trees = fork(fork_id).current; + switch (tree_id) { + case MerkleTreeId::NOTE_HASH_TREE: + return append(trees.note_hash, leaves); + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return append(trees.l1_to_l2, leaves); + case MerkleTreeId::ARCHIVE: + return append(trees.archive, leaves); + default: + fail("Unsupported tree type for append_leaves"); + } +} + +BatchInsertionResult ReferenceWorldState::batch_insert_nullifiers( + uint64_t fork_id, std::span leaves, uint32_t subtree_depth) +{ + return batch_insert(fork(fork_id).current.nullifier, leaves, subtree_depth, true); +} + +BatchInsertionResult ReferenceWorldState::batch_insert_public_data( + uint64_t fork_id, std::span leaves, uint32_t subtree_depth) +{ + return batch_insert(fork(fork_id).current.public_data, leaves, subtree_depth, true); +} + +SequentialInsertionResult ReferenceWorldState::sequential_insert_nullifiers( + uint64_t fork_id, std::span leaves) +{ + return sequential_insert(fork(fork_id).current.nullifier, leaves); +} + +SequentialInsertionResult ReferenceWorldState::sequential_insert_public_data( + uint64_t fork_id, std::span leaves) +{ + return sequential_insert(fork(fork_id).current.public_data, leaves); +} + +void ReferenceWorldState::update_archive(uint64_t fork_id, + std::span block_state_ref, + const FF& block_header_hash) +{ + Trees& trees = fork(fork_id).current; + if (!state_matches(trees, block_state_ref)) { + fail("Can't update archive tree: Block state does not match world state"); + } + append(trees.archive, std::span(&block_header_hash, 1)); +} + +// --------------------------------------------------------------------------- +// Checkpoints +// --------------------------------------------------------------------------- + +void ReferenceWorldState::create_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + f.checkpoints.push_back(f.current); +} + +void ReferenceWorldState::commit_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Cannot commit without a checkpoint"); + } + f.checkpoints.pop_back(); +} + +void ReferenceWorldState::revert_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Cannot revert without a checkpoint"); + } + f.current = std::move(f.checkpoints.back()); + f.checkpoints.pop_back(); +} + +void ReferenceWorldState::commit_all_checkpoints(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Invalid depth for commit_to_depth"); + } + f.checkpoints.clear(); +} + +void ReferenceWorldState::revert_all_checkpoints(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Invalid depth for revert_to_depth"); + } + f.current = std::move(f.checkpoints.front()); + f.checkpoints.clear(); +} + +// --------------------------------------------------------------------------- +// Forks +// --------------------------------------------------------------------------- + +uint64_t ReferenceWorldState::create_fork(std::optional block_number) +{ + block_number_t block = block_number.value_or(latest_block_number()); + const Trees& trees = committed_block(block); + uint64_t fork_id = next_fork_id_++; + forks_.emplace(fork_id, Fork{ .block_number = block, .base = trees, .current = trees, .checkpoints = {} }); + return fork_id; +} + +void ReferenceWorldState::delete_fork(uint64_t fork_id) +{ + if (fork_id == 0) { + fail("Unable to delete canonical fork"); + } + fork(fork_id); + forks_.erase(fork_id); +} + +// --------------------------------------------------------------------------- +// The canonical chain +// --------------------------------------------------------------------------- + +void ReferenceWorldState::commit() +{ + Fork& canonical = fork(0); + blocks_.push_back(canonical.current); + // As on a node, the oldest historical block is 1 from the first commit on, unwinding included; block 0 + // stays readable regardless. + oldest_historical_block_ = std::max(oldest_historical_block_, 1); + canonical.block_number = latest_block_number(); + canonical.base = canonical.current; + canonical.checkpoints.clear(); +} + +void ReferenceWorldState::rollback() +{ + Fork& canonical = fork(0); + canonical.current = canonical.base; + canonical.checkpoints.clear(); +} + +void ReferenceWorldState::sync_block(std::span block_state_ref, + const FF& block_header_hash, + std::span padded_note_hashes, + std::span padded_l1_to_l2_messages, + std::span padded_nullifiers, + std::span public_data_writes, + const std::optional& expected_archive_root, + const std::optional& expected_previous_archive_root) +{ + rollback(); + Fork& canonical = fork(0); + if (expected_previous_archive_root.has_value()) { + FF actual = canonical.base.archive.tree.root(); + if (actual != *expected_previous_archive_root) { + fail("Can't sync block: local archive root ", + actual, + " does not match the block's previous archive root ", + *expected_previous_archive_root, + "; world state has diverged from the canonical chain and must be resynced"); + } + } + + Trees trees = canonical.base; + try { + batch_insert(trees.nullifier, padded_nullifiers, 0, false); + append(trees.note_hash, padded_note_hashes); + append(trees.l1_to_l2, padded_l1_to_l2_messages); + append(trees.archive, std::span(&block_header_hash, 1)); + sequential_insert(trees.public_data, public_data_writes); + } catch (const std::exception& e) { + fail("Failed to sync block: ", e.what()); + } + + auto tip = trees.archive.indices.find(uint256_t(block_header_hash)); + if (!tip.has_value() || *tip + 1 != trees.archive.size) { + fail("Can't synch block: block header hash is not the tip of the archive tree"); + } + if (!state_matches(trees, block_state_ref)) { + fail("Can't synch block: block state does not match world state"); + } + if (expected_archive_root.has_value() && trees.archive.tree.root() != *expected_archive_root) { + fail("Can't sync block: resulting archive root ", + trees.archive.tree.root(), + " does not match the block's archive root ", + *expected_archive_root, + "; world state has diverged from the canonical chain and must be resynced"); + } + canonical.current = std::move(trees); + commit(); +} + +void ReferenceWorldState::finalize_blocks(block_number_t to_block_number) +{ + if (to_block_number == 0) { + fail("Unable to finalize block 0"); + } + if (to_block_number <= finalized_block_number_) { + return; + } + if (to_block_number > latest_block_number()) { + fail("Unable to finalize block ", + to_block_number, + " currently unfinalized block height ", + latest_block_number()); + } + finalized_block_number_ = to_block_number; +} + +void ReferenceWorldState::drop_forks_of(block_number_t block_number) +{ + for (auto it = forks_.begin(); it != forks_.end();) { + it = it->first != 0 && it->second.block_number == block_number ? forks_.erase(it) : std::next(it); + } +} + +void ReferenceWorldState::unwind_blocks(block_number_t to_block_number) +{ + // As on a node: one block at a time from the tip, so a request that reaches a finalized block fails there, + // keeping the blocks it already unwound. + rollback(); + block_number_t latest = latest_block_number(); + if (to_block_number >= latest) { + fail("Unable to unwind blocks to block number ", to_block_number, ", current pending block ", latest); + } + Fork& canonical = fork(0); + for (block_number_t block = latest; block > to_block_number; --block) { + if (block <= finalized_block_number_) { + fail("Unable to unwind block: ", block, " finalizedBlockHeight: ", finalized_block_number_); + } + blocks_.pop_back(); + drop_forks_of(block); + canonical.block_number = latest_block_number(); + canonical.base = blocks_.back(); + canonical.current = blocks_.back(); + } +} + +void ReferenceWorldState::remove_historical_blocks(block_number_t to_block_number) +{ + // As on a node: one block at a time from the oldest, never a block that is not yet finalized. The genesis + // block is never removed. States stay in memory (they share almost all of their nodes with their + // successors); only reads of removed blocks stop. + block_number_t oldest = status().oldest_historical_block; + if (to_block_number <= oldest) { + fail("Unable to remove historical blocks to block number ", + to_block_number, + ", blocks not found. Current oldest block: ", + oldest); + } + for (block_number_t block = oldest; block < to_block_number; ++block) { + if (block < 1) { + fail("Unable to remove historical block: ", block); + } + if (block >= finalized_block_number_) { + fail("Unable to remove historical block: ", block, " finalizedBlockHeight: ", finalized_block_number_); + } + oldest_historical_block_ = block + 1; + drop_forks_of(block); + } +} + +StatusSummary ReferenceWorldState::status() const +{ + return StatusSummary{ .unfinalized_block_number = latest_block_number(), + .finalized_block_number = finalized_block_number_, + .oldest_historical_block = oldest_historical_block_, + .trees_are_synched = true }; +} + +std::array ReferenceWorldState::tree_meta() const +{ + const Fork& canonical = fork(0); + const StatusSummary summary = status(); + std::array meta{}; + for (auto tree_id : ALL_TREES) { + const TreeState current = tree_state(canonical.current, tree_id); + const TreeState initial = tree_state(blocks_[0], tree_id); + meta[static_cast(tree_id)] = TreeMetaInfo{ + .tree_id = tree_id, + .depth = tree_height(tree_id), + .root = current.root, + .size = current.size, + .committed_size = tree_size(canonical.base, tree_id), + .initial_root = initial.root, + .initial_size = initial.size, + .oldest_historic_block = summary.oldest_historical_block, + .unfinalized_block_height = summary.unfinalized_block_number, + .finalized_block_height = summary.finalized_block_number, + }; + } + return meta; +} + +FF ReferenceWorldState::compute_genesis_block_header_hash(std::span state, + uint64_t genesis_timestamp, + const FF& header_generator_point) +{ + auto tree = [&](MerkleTreeId tree_id) { + for (const auto& entry : state) { + if (entry.tree_id == tree_id) { + return entry; + } + } + fail("compute_genesis_block_header_hash: missing tree ", static_cast(tree_id)); + }; + // BlockHeader::serialize (block_header.nr) with every field zero except the state: the last + // archive is empty, and the sponge blob hash, tx effects root, global variables (other than the + // timestamp), total fees and mana used are all zero at genesis. + return crypto::merkle_tree::Poseidon2HashPolicy::hash({ + header_generator_point, + FF(0), // last archive root + FF(0), // last archive next_available_leaf_index + tree(MerkleTreeId::L1_TO_L2_MESSAGE_TREE).root, + FF(tree(MerkleTreeId::L1_TO_L2_MESSAGE_TREE).size), + tree(MerkleTreeId::NOTE_HASH_TREE).root, + FF(tree(MerkleTreeId::NOTE_HASH_TREE).size), + tree(MerkleTreeId::NULLIFIER_TREE).root, + FF(tree(MerkleTreeId::NULLIFIER_TREE).size), + tree(MerkleTreeId::PUBLIC_DATA_TREE).root, + FF(tree(MerkleTreeId::PUBLIC_DATA_TREE).size), + FF(0), // sponge_blob_hash + FF(0), // tx_effects_tree_root + FF(0), // chain_id + FF(0), // version + FF(0), // block_number + FF(0), // slot_number + FF(genesis_timestamp), + FF(0), // coinbase + FF(0), // fee_recipient + FF(0), // gas_fees.fee_per_da_gas + FF(0), // gas_fees.fee_per_l2_gas + FF(0), // total_fees + FF(0), // total_mana_used + }); +} + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp new file mode 100644 index 00000000000..552699d3ee7 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp @@ -0,0 +1,290 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/aztec/aztec_hash_policy.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" +#include "barretenberg/world_state_reference/merkle_tree_id.hpp" +#include "barretenberg/world_state_reference/persistent_key_index.hpp" +#include "barretenberg/world_state_reference/persistent_merkle_tree.hpp" + +namespace bb::world_state { + +using FF = bb::fr; +using crypto::merkle_tree::GetLowIndexedLeafResponse; +using crypto::merkle_tree::IndexedLeaf; +using crypto::merkle_tree::LeafUpdateWitnessData; +using crypto::merkle_tree::NullifierLeafValue; +using crypto::merkle_tree::PublicDataLeafValue; +using crypto::merkle_tree::SequentialInsertionResult; +using SiblingPath = crypto::merkle_tree::fr_sibling_path; +using numeric::uint256_t; + +/** + * @brief The genesis a world state starts from, as a node world state is configured with it. + * @details canonical() is the protocol's: the protocol's tree heights and initial sizes, the protocol contracts' + * registration nullifiers, no public data, timestamp 0. + */ +struct GenesisConfig { + /** Each tree's height, by MerkleTreeId. */ + std::array tree_heights = { + NULLIFIER_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT, PUBLIC_DATA_TREE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, ARCHIVE_HEIGHT + }; + /** Leaves each indexed tree starts with, its prefill included. */ + uint64_t nullifier_tree_initial_size = 128; + uint64_t public_data_tree_initial_size = 128; + std::vector prefilled_nullifiers; + std::vector prefilled_public_data; + uint64_t genesis_timestamp = 0; + /** The first field of the genesis block header's hash preimage (its domain separator). */ + FF header_generator_point = FF(DOM_SEP__BLOCK_HEADER_HASH); + + static GenesisConfig canonical(); +}; + +/** @brief One tree's root and size, as a state reference reports it. */ +struct TreeState { + MerkleTreeId tree_id; + FF root; + uint64_t size; + bool operator==(const TreeState& other) const = default; +}; + +/** @brief What a node world state reports about one tree of the canonical chain. */ +struct TreeMetaInfo { + MerkleTreeId tree_id; + uint32_t depth; + FF root; + uint64_t size; + uint64_t committed_size; + FF initial_root; + uint64_t initial_size; + block_number_t oldest_historic_block; + block_number_t unfinalized_block_height; + block_number_t finalized_block_height; +}; + +struct StatusSummary { + block_number_t unfinalized_block_number; + block_number_t finalized_block_number; + block_number_t oldest_historical_block; + bool trees_are_synched; +}; + +template struct BatchInsertionResult { + std::vector> low_leaf_witness_data; + /** The batch sorted by descending key, each with its position in the batch. */ + std::vector> sorted_leaves; + SiblingPath subtree_path; +}; + +/** + * @brief The in-memory reference world state: the node world state's contract (wsdb_schema.jsonc) + * over trees whose versions share structure. + * + * Holds the canonical chain (one state per committed block, from genesis), its uncommitted state, and + * any number of forks. A fork starts from the state at a committed block and is written to on its own, + * with a stack of checkpoints. Every one of these is a copy of the persistent trees, which costs one + * pointer per tree, so keeping every block, forking and checkpointing are all free. + * + * Reads take a revision, resolved as a node world state resolves it: + * - a block number other than WorldStateRevision::LATEST reads the committed state at that block; + * - otherwise includeUncommitted reads the fork's current state, and a committed read sees the state + * the fork started from (for the canonical fork, the latest committed block). + * + * Roots, sibling paths, insertion witnesses and indices follow the node world state (the + * content-addressed trees in native-packages/wsdb) for the same requests. Every failure is a + * std::runtime_error whose message a caller can return as the request's error. + */ +class ReferenceWorldState { + public: + /** Leaves each indexed tree of the canonical genesis starts with, prefill included, as on a node. */ + static constexpr size_t INDEXED_TREE_INITIAL_SIZE = 128; + + explicit ReferenceWorldState(const GenesisConfig& genesis = GenesisConfig::canonical()); + + /** Why `genesis` cannot seed a world state, or nullopt when it can. */ + static std::optional check_genesis(const GenesisConfig& genesis); + + /** Restarts from `genesis`: every block, fork and uncommitted write is dropped. */ + void reset(const GenesisConfig& genesis); + + /** The height this world state's genesis gives `tree_id`. */ + uint32_t tree_height(MerkleTreeId tree_id) const; + + // Reads. + std::vector get_state_reference(const WorldStateRevision& revision) const; + std::vector get_initial_state_reference() const; + TreeState get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const; + /** + * @brief Leaf of an append-only tree (note hash, L1-to-L2 message, archive), read as a node reads it: zero if + * unwritten, nullopt past the tree's capacity, and for a historical read nullopt past that block's size or + * for a block the chain does not have. + */ + std::optional get_leaf_value(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; + /** Preimage of an indexed-tree leaf: the empty leaf for a slot a batch left empty, nullopt past the tree's size. */ + std::optional> get_nullifier_preimage(const WorldStateRevision& revision, + uint64_t index) const; + std::optional> get_public_data_preimage(const WorldStateRevision& revision, + uint64_t index) const; + SiblingPath get_sibling_path(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; + /** The block that added each leaf index; nullopt for an index the revision's committed tree does not hold. */ + std::vector> get_block_numbers_for_leaf_indices( + const WorldStateRevision& revision, MerkleTreeId tree_id, std::span indices) const; + /** + * @brief The index holding `key` (an append-only leaf, a nullifier or a public-data slot), if it is at or + * after `start`. Each key has one index, the first it was written at; zero leaves are not indexed. + */ + std::optional find_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key, + uint64_t start) const; + GetLowIndexedLeafResponse find_low_leaf(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key) const; + + // Writes to a fork's current state (fork 0 is the canonical chain's uncommitted state). + void append_leaves(uint64_t fork_id, MerkleTreeId tree_id, std::span leaves); + BatchInsertionResult batch_insert_nullifiers(uint64_t fork_id, + std::span leaves, + uint32_t subtree_depth); + BatchInsertionResult batch_insert_public_data(uint64_t fork_id, + std::span leaves, + uint32_t subtree_depth); + SequentialInsertionResult sequential_insert_nullifiers( + uint64_t fork_id, std::span leaves); + SequentialInsertionResult sequential_insert_public_data( + uint64_t fork_id, std::span leaves); + /** Appends `block_header_hash` to the archive, if `block_state_ref` is the fork's current state. */ + void update_archive(uint64_t fork_id, std::span block_state_ref, const FF& block_header_hash); + + void create_checkpoint(uint64_t fork_id); + void commit_checkpoint(uint64_t fork_id); + void revert_checkpoint(uint64_t fork_id); + void commit_all_checkpoints(uint64_t fork_id); + void revert_all_checkpoints(uint64_t fork_id); + + /** A fork from the committed state at `block_number`, or at the latest block. */ + uint64_t create_fork(std::optional block_number); + void delete_fork(uint64_t fork_id); + + // The canonical chain. + /** Commits the uncommitted state as the next block. */ + void commit(); + void rollback(); + /** + * @brief Applies a block's effects to the canonical chain and commits it, as a node syncing a block does. + * @details Rolls back first. Fails, committing nothing, unless the result is `block_state_ref` with + * `block_header_hash` at the archive's tip, and the archive roots before and after match the expected + * ones when given. + */ + void sync_block(std::span block_state_ref, + const FF& block_header_hash, + std::span padded_note_hashes, + std::span padded_l1_to_l2_messages, + std::span padded_nullifiers, + std::span public_data_writes, + const std::optional& expected_archive_root, + const std::optional& expected_previous_archive_root); + void finalize_blocks(block_number_t to_block_number); + void unwind_blocks(block_number_t to_block_number); + void remove_historical_blocks(block_number_t to_block_number); + + StatusSummary status() const; + /** Per-tree meta for a full status, in MerkleTreeId order. */ + std::array tree_meta() const; + + block_number_t latest_block_number() const { return static_cast(blocks_.size() - 1); } + bool has_fork(uint64_t fork_id) const { return forks_.contains(fork_id); } + + /** + * @brief Hash of the genesis block header over the four state trees' genesis state. + * @details Must match BlockHeader::hash in noir-protocol-circuits' block_header.nr for a header whose + * only non-zero fields are the four trees' state and the timestamp. + */ + static FF compute_genesis_block_header_hash(std::span state, + uint64_t genesis_timestamp, + const FF& header_generator_point); + + private: + struct NoPayload {}; + + /** A note hash, L1-to-L2 message or archive tree: leaves are values. */ + struct AppendOnlyTree { + PersistentMerkleTree tree; + PersistentKeyIndex indices; + uint64_t size = 0; + }; + + /** A nullifier or public-data tree: leaves are hashes of the preimages they carry. */ + template struct IndexedTree { + using Hashing = HashingPolicy; + using Leaf = typename PersistentMerkleTree>::Leaf; + PersistentMerkleTree> tree; + PersistentKeyIndex indices; + uint64_t size = 0; + }; + using NullifierTree = IndexedTree; + using PublicDataTree = IndexedTree; + + /** All five trees. Copying one is copying ten pointers. */ + struct Trees { + NullifierTree nullifier; + AppendOnlyTree note_hash; + PublicDataTree public_data; + AppendOnlyTree l1_to_l2; + AppendOnlyTree archive; + }; + + struct Fork { + /** The committed block the fork started from (for the canonical fork, the latest block). */ + block_number_t block_number; + Trees base; + Trees current; + std::vector checkpoints; + }; + + static Trees build_genesis(const GenesisConfig& genesis); + + const Fork& fork(uint64_t fork_id) const; + Fork& fork(uint64_t fork_id); + const Trees& committed_block(block_number_t block_number) const; + const Trees& view(const WorldStateRevision& revision) const; + /** Drops the forks of `block_number`, as unwinding or removing that block does. */ + void drop_forks_of(block_number_t block_number); + + static TreeState tree_state(const Trees& trees, MerkleTreeId tree_id); + static const PersistentKeyIndex& key_index(const Trees& trees, MerkleTreeId tree_id); + static std::vector state_reference(const Trees& trees); + static bool state_matches(const Trees& trees, std::span state); + static uint64_t tree_size(const Trees& trees, MerkleTreeId tree_id); + + static void append(AppendOnlyTree& tree, std::span leaves); + template + static BatchInsertionResult batch_insert(Tree& tree, + std::span leaves, + uint32_t subtree_depth, + bool capture_witness); + template + static SequentialInsertionResult sequential_insert(Tree& tree, + std::span leaves); + + std::array tree_heights_{}; + std::vector blocks_; + std::map forks_; + uint64_t next_fork_id_ = 1; + block_number_t finalized_block_number_ = 0; + block_number_t oldest_historical_block_ = 0; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp new file mode 100644 index 00000000000..cc6ad40f44b --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp @@ -0,0 +1,395 @@ +#include "barretenberg/world_state_reference/reference_world_state.hpp" + +#include "barretenberg/aztec/aztec_constants.hpp" + +#include + +#include +#include + +using namespace bb; +using namespace bb::world_state; + +namespace { + +constexpr uint64_t CANONICAL = 0; +const WorldStateRevision LATEST_UNCOMMITTED{ .includeUncommitted = true }; +const WorldStateRevision LATEST_COMMITTED{ .includeUncommitted = false }; + +WorldStateRevision at_block(block_number_t block) +{ + return WorldStateRevision{ .blockNumber = block }; +} + +WorldStateRevision fork_current(uint64_t fork_id) +{ + return WorldStateRevision{ .forkId = fork_id, .includeUncommitted = true }; +} + +// Recompute a tree root from a leaf hash and its sibling path. +template FF root_from_path(FF node, uint64_t index, const SiblingPath& path) +{ + for (const auto& sibling : path) { + node = (index & 1) == 0 ? HashPolicy::hash_pair(node, sibling) : HashPolicy::hash_pair(sibling, node); + index >>= 1; + } + return node; +} + +uint64_t size_of(const ReferenceWorldState& ws, const WorldStateRevision& revision, MerkleTreeId tree_id) +{ + return ws.get_tree_info(revision, tree_id).size; +} + +/** Commits a block that appends `note_hash` and nullifies `nullifier`, the way a node syncs one. */ +void sync_block(ReferenceWorldState& ws, FF note_hash, FF nullifier) +{ + uint64_t fork_id = ws.create_fork(std::nullopt); + std::vector notes{ note_hash }; + std::vector nullifiers{ NullifierLeafValue(nullifier) }; + ws.append_leaves(fork_id, MerkleTreeId::NOTE_HASH_TREE, notes); + ws.batch_insert_nullifiers(fork_id, nullifiers, 0); + auto state = ws.get_state_reference(fork_current(fork_id)); + FF header_hash = FF(1000) + FF(ws.latest_block_number()); + ws.delete_fork(fork_id); + ws.sync_block(state, header_hash, notes, {}, nullifiers, {}, std::nullopt, std::nullopt); +} + +} // namespace + +TEST(ReferenceWorldState, CanonicalGenesisMatchesProtocolConstants) +{ + ReferenceWorldState ws; + + auto nullifiers = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE); + EXPECT_EQ(nullifiers.root, FF(GENESIS_NULLIFIER_TREE_ROOT)); + EXPECT_EQ(nullifiers.size, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + + auto archive = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::ARCHIVE); + EXPECT_EQ(archive.root, FF(GENESIS_ARCHIVE_ROOT)); + EXPECT_EQ(archive.size, 1); + EXPECT_EQ(ws.get_leaf_value(LATEST_COMMITTED, MerkleTreeId::ARCHIVE, 0), FF(GENESIS_BLOCK_HEADER_HASH)); + EXPECT_EQ(ws.latest_block_number(), 0); +} + +TEST(ReferenceWorldState, EmptyGenesisMatchesEmptyWorldStateRoots) +{ + // The empty-tree roots hash_of_genesis_block_header in block_header.nr uses for the unseeded trees. + ReferenceWorldState ws(GenesisConfig{}); + auto state = ws.get_initial_state_reference(); + ASSERT_EQ(state.size(), 4); + for (const auto& tree : state) { + switch (tree.tree_id) { + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + EXPECT_EQ(tree.root, FF("0x0fef6d80d31109ddb56d6b3f607cbc9c0af0bff3ea0d43e8f278983c64c11f7a")); + EXPECT_EQ(tree.size, 0); + break; + case MerkleTreeId::NOTE_HASH_TREE: + EXPECT_EQ(tree.root, FF("0x2590f2aab19dd791700b4a43d3f52bb88ef2409a3731da8e848663559202e4c6")); + EXPECT_EQ(tree.size, 0); + break; + case MerkleTreeId::PUBLIC_DATA_TREE: + EXPECT_EQ(tree.root, FF("0x1bef38b621017d3c7416663d0cd81369424560710526a3fbaaec13e356b9d084")); + EXPECT_EQ(tree.size, 128); + break; + case MerkleTreeId::NULLIFIER_TREE: + EXPECT_EQ(tree.size, 128); + EXPECT_NE(tree.root, FF(GENESIS_NULLIFIER_TREE_ROOT)); + break; + default: + ADD_FAILURE() << "unexpected tree in state reference"; + } + } +} + +TEST(ReferenceWorldState, GenesisPrefillValidation) +{ + auto with_nullifiers = [](std::vector keys) { + GenesisConfig genesis; + for (auto key : keys) { + genesis.prefilled_nullifiers.emplace_back(key); + } + return ReferenceWorldState::check_genesis(genesis); + }; + EXPECT_FALSE(with_nullifiers({ FF(1000), FF(2000) }).has_value()); + EXPECT_EQ(with_nullifiers({ FF(2000), FF(1000) }), + "prefilled nullifiers: Prefilled values must be unique and sorted"); + EXPECT_EQ(with_nullifiers({ FF(1000), FF(1000) }), + "prefilled nullifiers: Prefilled values must be unique and sorted"); + // Padding occupies keys 0..127 when one value is prefilled, so a key of 5 collides with it. + EXPECT_EQ(with_nullifiers({ FF(5) }), + "prefilled nullifiers: Prefilled values must not be the same as the default values"); + std::vector too_many; + for (size_t i = 0; i < ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 1; ++i) { + too_many.emplace_back(1000 + i); + } + EXPECT_EQ(with_nullifiers(too_many), + "prefilled nullifiers: Number of prefilled values can't be more than initial size"); + + GenesisConfig public_data; + public_data.prefilled_public_data = { PublicDataLeafValue(FF(2000), FF(1)), PublicDataLeafValue(FF(1000), FF(2)) }; + EXPECT_EQ(ReferenceWorldState::check_genesis(public_data), + "prefilled public data: Prefilled values must be unique and sorted"); + EXPECT_THROW(ReferenceWorldState{ public_data }, std::runtime_error); +} + +TEST(ReferenceWorldState, CommittedReadsSeeTheLastBlockUncommittedSeeCurrent) +{ + ReferenceWorldState ws; + const std::vector note_hashes{ FF(11), FF(22), FF(33) }; + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, note_hashes); + + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); + EXPECT_EQ(size_of(ws, at_block(0), MerkleTreeId::NOTE_HASH_TREE), 0); + + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 1), FF(22)); + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 3), FF(0)); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), 2); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(11), 1), std::nullopt); + EXPECT_EQ(ws.find_leaf_index(LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), std::nullopt); + + auto path = ws.get_sibling_path(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 1); + EXPECT_EQ(path.size(), NOTE_HASH_TREE_HEIGHT); + EXPECT_EQ(root_from_path(FF(22), 1, path), + ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).root); + + ws.commit(); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(size_of(ws, LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, at_block(0), MerkleTreeId::NOTE_HASH_TREE), 0); + EXPECT_EQ(size_of(ws, at_block(1), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_THROW(ws.get_tree_info(at_block(2), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); +} + +TEST(ReferenceWorldState, ForksAreIsolatedAndCommittedReadsSeeTheirBlock) +{ + ReferenceWorldState ws; + sync_block(ws, FF(1), FF(0x1001)); + sync_block(ws, FF(2), FF(0x1002)); + ASSERT_EQ(ws.latest_block_number(), 2); + + uint64_t at_latest = ws.create_fork(std::nullopt); + uint64_t at_one = ws.create_fork(1); + ws.append_leaves(at_latest, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); + + EXPECT_EQ(size_of(ws, fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, WorldStateRevision{ .forkId = at_latest }, MerkleTreeId::NOTE_HASH_TREE), 2); + EXPECT_EQ(size_of(ws, fork_current(at_one), MerkleTreeId::NOTE_HASH_TREE), 1); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 2); + EXPECT_EQ(ws.find_leaf_index(fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE, FF(3), 0), 2); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(3), 0), std::nullopt); + + // Leaves are numbered by the committed block that added them; a fork's uncommitted ones have none. + std::vector indices{ 0, 1, 2 }; + auto blocks = ws.get_block_numbers_for_leaf_indices(fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE, indices); + EXPECT_EQ(blocks, (std::vector>{ 1, 2, std::nullopt })); + blocks = ws.get_block_numbers_for_leaf_indices(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE, indices); + EXPECT_EQ(blocks, (std::vector>{ 0, 0, 0 })); + + EXPECT_THROW(ws.delete_fork(CANONICAL), std::runtime_error); + ws.delete_fork(at_one); + EXPECT_THROW(ws.get_tree_info(fork_current(at_one), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); + EXPECT_THROW(ws.create_fork(3), std::runtime_error); +} + +TEST(ReferenceWorldState, SyncBlockChecksTheStateAndCommitsNothingOnFailure) +{ + ReferenceWorldState ws; + std::vector notes{ FF(5) }; + auto initial = ws.get_state_reference(LATEST_COMMITTED); + // The block claims the state before its own effects, so the result does not match it. + EXPECT_THROW(ws.sync_block(initial, FF(77), notes, {}, {}, {}, std::nullopt, std::nullopt), std::runtime_error); + EXPECT_EQ(ws.latest_block_number(), 0); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); + + FF genesis_archive = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::ARCHIVE).root; + EXPECT_THROW(ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, FF(123)), std::runtime_error); + ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, genesis_archive); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(ws.get_leaf_value(LATEST_COMMITTED, MerkleTreeId::ARCHIVE, 1), FF(77)); + // A header hash already in the archive is not its tip. + EXPECT_THROW(ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, std::nullopt), std::runtime_error); +} + +TEST(ReferenceWorldState, UnwindDropsBlocksAndTheirForks) +{ + ReferenceWorldState ws; + sync_block(ws, FF(1), FF(0x1001)); + sync_block(ws, FF(2), FF(0x1002)); + sync_block(ws, FF(3), FF(0x1003)); + uint64_t at_three = ws.create_fork(std::nullopt); + uint64_t at_one = ws.create_fork(1); + FF root_at_one = ws.get_tree_info(at_block(1), MerkleTreeId::NULLIFIER_TREE).root; + + EXPECT_THROW(ws.unwind_blocks(3), std::runtime_error); + ws.unwind_blocks(1); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE).root, root_at_one); + EXPECT_THROW(ws.get_tree_info(fork_current(at_three), MerkleTreeId::NULLIFIER_TREE), std::runtime_error); + EXPECT_EQ(ws.get_tree_info(fork_current(at_one), MerkleTreeId::NULLIFIER_TREE).root, root_at_one); + + // The chain carries on from block 1, and a finalized block cannot be unwound. + sync_block(ws, FF(4), FF(0x1004)); + EXPECT_EQ(ws.latest_block_number(), 2); + ws.finalize_blocks(2); + EXPECT_EQ(ws.status().finalized_block_number, 2); + sync_block(ws, FF(5), FF(0x1005)); + EXPECT_THROW(ws.unwind_blocks(1), std::runtime_error); + EXPECT_THROW(ws.finalize_blocks(9), std::runtime_error); +} + +TEST(ReferenceWorldState, RemovingHistoryStopsReadsOfOldBlocks) +{ + ReferenceWorldState ws; + EXPECT_EQ(ws.status().oldest_historical_block, 0); + for (int i = 1; i <= 4; ++i) { + sync_block(ws, FF(i), FF(0x1000 + i)); + } + EXPECT_EQ(ws.status().oldest_historical_block, 1); + EXPECT_THROW(ws.remove_historical_blocks(3), std::runtime_error); + ws.finalize_blocks(3); + ws.remove_historical_blocks(3); + EXPECT_EQ(ws.status().oldest_historical_block, 3); + EXPECT_THROW(ws.get_tree_info(at_block(2), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); + EXPECT_EQ(size_of(ws, at_block(3), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_THROW(ws.remove_historical_blocks(3), std::runtime_error); +} + +TEST(ReferenceWorldState, SequentialNullifierWitnessesAreConsistent) +{ + ReferenceWorldState ws; + const FF nullifier(0x1234); + auto low = ws.find_low_leaf(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier); + EXPECT_FALSE(low.is_already_present); + + std::vector leaves{ NullifierLeafValue(nullifier) }; + auto result = ws.sequential_insert_nullifiers(CANONICAL, leaves); + ASSERT_EQ(result.low_leaf_witness_data.size(), 1); + ASSERT_EQ(result.insertion_witness_data.size(), 1); + EXPECT_EQ(result.low_leaf_witness_data[0].index, low.index); + EXPECT_EQ(result.insertion_witness_data[0].index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + + auto index = ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier, 0); + ASSERT_EQ(index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + auto preimage = ws.get_nullifier_preimage(LATEST_UNCOMMITTED, *index); + ASSERT_TRUE(preimage.has_value()); + EXPECT_EQ(preimage->leaf.nullifier, nullifier); + EXPECT_EQ(ws.get_nullifier_preimage(LATEST_UNCOMMITTED, low.index)->nextKey, nullifier); + + // A membership witness read back from the tree reproduces its root. + auto path = ws.get_sibling_path(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, *index); + FF leaf_hash = aztec::NullifierMerkleHashPolicy::hash(preimage->get_hash_inputs()); + EXPECT_EQ(root_from_path(leaf_hash, *index, path), + ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root); + + // Inserting it again fails and changes nothing; the committed view is untouched throughout. + FF root = ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root; + EXPECT_THROW(ws.sequential_insert_nullifiers(CANONICAL, leaves), std::runtime_error); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root, root); + EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE).root, FF(GENESIS_NULLIFIER_TREE_ROOT)); +} + +TEST(ReferenceWorldState, BatchInsertionTakesOneSlotPerValue) +{ + // A batch takes one slot per value at the value's own position, a zero value's left empty, and links + // the values in key order, updating low leaves in descending key order. + ReferenceWorldState batch; + std::vector leaves{ NullifierLeafValue(FF(0x3000)), + NullifierLeafValue(FF(0)), + NullifierLeafValue(FF(0x1000)), + NullifierLeafValue(FF(0x2000)) }; + auto result = batch.batch_insert_nullifiers(CANONICAL, leaves, 2); + EXPECT_EQ(size_of(batch, LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE), + ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 4); + // The zero value's slot is empty: it reads as the empty leaf, as on a node. + EXPECT_EQ(batch.get_nullifier_preimage(LATEST_UNCOMMITTED, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 1), + IndexedLeaf::empty()); + + ASSERT_EQ(result.sorted_leaves.size(), 4); + EXPECT_EQ(result.sorted_leaves[0].second, 0); + EXPECT_EQ(result.sorted_leaves[1].second, 3); + EXPECT_EQ(result.sorted_leaves[2].second, 2); + EXPECT_EQ(result.sorted_leaves[3].second, 1); + ASSERT_EQ(result.low_leaf_witness_data.size(), 4); + EXPECT_EQ(result.low_leaf_witness_data[3].index, 0); + EXPECT_EQ(result.subtree_path.size(), NULLIFIER_TREE_HEIGHT - 2); + + // Every value is linked in key order. + for (size_t i = 0; i < leaves.size(); ++i) { + if (leaves[i].is_empty()) { + continue; + } + auto index = batch.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, leaves[i].nullifier, 0); + EXPECT_EQ(index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + i); + } + auto low_of_2000 = batch.find_low_leaf(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, FF(0x2000)); + EXPECT_TRUE(low_of_2000.is_already_present); + EXPECT_EQ( + batch.get_nullifier_preimage(LATEST_UNCOMMITTED, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 2)->nextKey, + FF(0x2000)); + + std::vector duplicate{ NullifierLeafValue(FF(9)), NullifierLeafValue(FF(9)) }; + EXPECT_THROW(batch.batch_insert_nullifiers(CANONICAL, duplicate, 1), std::runtime_error); +} + +TEST(ReferenceWorldState, PublicDataUpdatesInPlace) +{ + ReferenceWorldState ws; + ws.sequential_insert_public_data(CANONICAL, std::vector{ PublicDataLeafValue(FF(5000), FF(1)) }); + auto size_after_insert = size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE); + auto update = ws.sequential_insert_public_data(CANONICAL, std::vector{ PublicDataLeafValue(FF(5000), FF(2)) }); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE), size_after_insert); + ASSERT_EQ(update.insertion_witness_data.size(), 1); + EXPECT_EQ(update.insertion_witness_data[0].path, SiblingPath(PUBLIC_DATA_TREE_HEIGHT, FF::zero())); + auto index = ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE, FF(5000), 0); + ASSERT_TRUE(index.has_value()); + EXPECT_EQ(ws.get_public_data_preimage(LATEST_UNCOMMITTED, *index)->leaf.value, FF(2)); +} + +TEST(ReferenceWorldState, CheckpointsRevertAndCommit) +{ + ReferenceWorldState ws; + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(1) }); + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(2) }); + ws.revert_checkpoint(CANONICAL); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + ws.commit_checkpoint(CANONICAL); + EXPECT_THROW(ws.commit_checkpoint(CANONICAL), std::runtime_error); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + + ws.create_checkpoint(CANONICAL); + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); + ws.revert_all_checkpoints(CANONICAL); + EXPECT_THROW(ws.revert_checkpoint(CANONICAL), std::runtime_error); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + + ws.rollback(); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); +} + +TEST(ReferenceWorldState, UpdateArchiveRequiresTheCurrentState) +{ + ReferenceWorldState ws; + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(7) }); + auto current = ws.get_state_reference(LATEST_UNCOMMITTED); + EXPECT_THROW(ws.update_archive(CANONICAL, ws.get_initial_state_reference(), FF(99)), std::runtime_error); + // Order is not part of the contract. + std::reverse(current.begin(), current.end()); + ws.update_archive(CANONICAL, current, FF(99)); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE), 2); + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, 1), FF(99)); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, FF(99), 0), 1); +} + +// wasm does not find main() in gtest_main (see ecc_tests' secp256r1.test.cpp). +#ifdef __wasm__ +GTEST_API_ int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt new file mode 100644 index 00000000000..659246711cd --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -0,0 +1,117 @@ +# wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the node +# world state's contract, wsdb_schema.jsonc here. wsdb-ref owns the schema: the AVM's wsdb client in +# vm2_wsdb and native-packages/wsdb's aztec-wsdb are generated from it too. The generated dispatch +# runs the handlers here; the generated FFI entry (wsdb_ref_ipc_ffi_entry) runs the dispatch in-process. +# The FFI symbols carry this implementation's name, not the schema's service (which names the wire +# protocol aztec-wsdb serves too), so the two can be linked into one binary. +# Three artifacts carry it to @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref), one per transport: +# - the wsdb-ref binary (native builds), the dispatch served over IPC, for its process transports +# (and for a downstream world state's conformance test, which runs it beside its own server); +# - wsdb-ref.wasm, a WASI reactor (wasm builds), for the package's wasm transport; +# - wsdb_ref_napi.node, a Node-API addon over the entry (native builds), for its napi transport. +set(WSDB_REF_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_schema.jsonc) +# Generated code is per build tree (each preset generates its own, so presets building at once never +# share a file). Remove the copy older checkouts generated into the source tree: it would shadow this one. +file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/generated) +set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) +set(WSDB_REF_GEN_OUTPUTS + ${WSDB_REF_GEN_DIR}/wsdb_dispatch.hpp + ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp + ${WSDB_REF_GEN_DIR}/wsdb_ffi.hpp + ${WSDB_REF_GEN_DIR}/wsdb_ipc_server.hpp + ${WSDB_REF_GEN_DIR}/wsdb_napi.cpp + ${WSDB_REF_GEN_DIR}/wsdb_types.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/msgpack_adaptor.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/msgpack_include.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/throw.hpp +) +set(IPC_CODEGEN_DIR ${CMAKE_SOURCE_DIR}/../../ipc-codegen) +file(GLOB_RECURSE IPC_CODEGEN_SRC ${IPC_CODEGEN_DIR}/src/*.ts ${IPC_CODEGEN_DIR}/templates/cpp/*.hpp) +add_custom_command( + OUTPUT ${WSDB_REF_GEN_OUTPUTS} + COMMAND node --experimental-strip-types --no-warnings + ${IPC_CODEGEN_DIR}/src/generate.ts + --schema ${WSDB_REF_SCHEMA} + --lang cpp + --out ${WSDB_REF_GEN_DIR} + --server + --ffi + --napi + --ffi-name wsdb_ref + --cpp-namespace bb::wsdb_ref + --cpp-include-dir barretenberg/wsdb_ref/generated + DEPENDS ${WSDB_REF_SCHEMA} ${IPC_CODEGEN_SRC} + COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from wsdb_schema.jsonc" + VERBATIM +) +add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) + +# Explicit sources: the generated Node-API addon must stay out of the module (it needs napi.h), and +# the generated FFI entry is in the build tree, which the module's source glob does not cover. +file(GLOB WSDB_REF_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.test.cpp) +barretenberg_module_with_sources( + wsdb_ref + SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.cpp ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp + HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.hpp + TEST_SOURCE_FILES ${WSDB_REF_TESTS} + DEPENDENCIES world_state_reference +) +add_dependencies(wsdb_ref_objects wsdb_ref_generated) +if(TARGET wsdb_ref_test_objects) + add_dependencies(wsdb_ref_test_objects wsdb_ref_generated) +endif() +set_target_properties(wsdb_ref_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) + +if(WASM) + # A standard WASI reactor exporting the FFI entry, like barretenberg.wasm: wasi-sdk's reactor + # startup provides _initialize, and the module imports nothing but WASI. + add_executable( + wsdb-ref-debug.wasm + $ + ) + target_link_libraries(wsdb-ref-debug.wasm PRIVATE world_state_reference) + target_link_options(wsdb-ref-debug.wasm PRIVATE -mexec-model=reactor -Wl,--export-dynamic) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm + COMMAND ${CMAKE_STRIP} ${CMAKE_BINARY_DIR}/bin/wsdb-ref-debug.wasm -o ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm + DEPENDS wsdb-ref-debug.wasm + COMMENT "Stripping debug info to create wsdb-ref.wasm" + ) + add_custom_target(wsdb-ref.wasm ALL DEPENDS ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm) +else() + # The Node-API addon over the FFI entry. Its headers come from the node-addon-api and + # node-api-headers packages ipc-runtime already pins for its own addon. + set(NAPI_PACKAGE_DIR ${CMAKE_SOURCE_DIR}/../../ipc-runtime/cpp/napi) + execute_process(COMMAND yarn --immutable WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} OUTPUT_QUIET) + execute_process( + COMMAND node -p "require('node-addon-api').include" + WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} + OUTPUT_VARIABLE NODE_ADDON_API_DIR + ) + execute_process( + COMMAND node -p "require('node-api-headers').include_dir" + WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} + OUTPUT_VARIABLE NODE_API_HEADERS_DIR + ) + string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}") + string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR "${NODE_API_HEADERS_DIR}") + + add_executable(wsdb-ref ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) + add_dependencies(wsdb-ref wsdb_ref_generated) + target_link_libraries(wsdb-ref PRIVATE wsdb_ref world_state_reference ipc_runtime) + + add_library(wsdb_ref_napi SHARED ${WSDB_REF_GEN_DIR}/wsdb_napi.cpp) + add_dependencies(wsdb_ref_napi wsdb_ref_generated) + target_compile_definitions(wsdb_ref_napi PRIVATE NAPI_VERSION=9) + target_include_directories(wsdb_ref_napi SYSTEM PRIVATE ${NODE_API_HEADERS_DIR} ${NODE_ADDON_API_DIR}) + target_link_libraries(wsdb_ref_napi PRIVATE wsdb_ref world_state_reference) + set_target_properties(wsdb_ref_napi PROPERTIES + PREFIX "" + SUFFIX ".node" + OUTPUT_NAME wsdb_ref_napi + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + # Node provides the N-API symbols at load time. + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + target_link_options(wsdb_ref_napi PRIVATE -undefined dynamic_lookup) + endif() +endif() diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md b/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md new file mode 100644 index 00000000000..2b46d37b7dc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md @@ -0,0 +1,25 @@ +# wsdb-ref + +The in-memory reference world state (`world_state_reference/`) served as a service over the node world state's +contract, `wsdb_schema.jsonc`. wsdb-ref owns that schema: the AVM's wsdb client (`vm2_wsdb/`) and +`native-packages/wsdb`'s `aztec-wsdb` are generated from it too, so wsdb-ref answers the same requests as the +production world state and serves as its reference. + +The ipc-codegen dispatch runs the handlers in `wsdb_ref_handlers.cpp`. Each of the three build outputs carries +it to one transport of `@aztec-foundation/wsdb-ref` (`barretenberg/ts/wsdb-ref`): + +| Output | Built in | Transport | +|---|---|---| +| `wsdb-ref` binary (`main.cpp`) | native builds | a spawned process over a socket or shared memory | +| `wsdb_ref_napi.node` | native builds | in-process in node, through Node-API | +| `wsdb-ref.wasm` (WASI reactor) | wasm builds | in-process in node or a browser | + +A client sends `Init` first, with the genesis to start from; wsdb-ref has no built-in genesis. + +## How forks stay cheap + +Every tree in the reference world state is persistent: a write copies only the path it changes and shares the +rest with the version it came from. Blocks, forks and checkpoints are therefore copies of ten root pointers. +[`treap_forks.html`](treap_forks.html) is an interactive explainer of the treap behind the key index +(`world_state_reference/persistent_key_index.hpp`) and of how the forks share structure. Download it and open +it in a browser. diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp new file mode 100644 index 00000000000..6c4b52c43bc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp @@ -0,0 +1,34 @@ +/** + * @file main.cpp + * @brief The wsdb-ref binary: wsdb-ref served over IPC, for a client that runs its world state as a process. + * + * `wsdb-ref msgpack run --input ` serves one reference world state on a .sock or .shm path until a + * shutdown signal. Its client sends Init first, with the genesis to start from. + */ +#include "barretenberg/bb/deps/cli11.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" + +#include "barretenberg/wsdb_ref/generated/wsdb_ipc_server.hpp" + +#include + +int main(int argc, char* argv[]) +{ + CLI::App app{ "wsdb-ref: the reference world state served over IPC" }; + app.require_subcommand(1); + CLI::App* msgpack_command = app.add_subcommand("msgpack", "Msgpack API interface."); + CLI::App* run_command = msgpack_command->add_subcommand("run", "Start the wsdb-ref IPC server."); + std::string input_path; + run_command->add_option("-i,--input", input_path, "IPC socket/shm path (.sock or .shm)")->required(); + CLI11_PARSE(app, argc, argv); + + try { + bb::wsdb_ref::WsdbRefContext ctx; + bb::wsdb_ref::serve(input_path, ctx); + } catch (const std::exception& e) { + info("Error: ", e.what()); + return 1; + } + return 0; +} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt new file mode 100644 index 00000000000..39423474722 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.24) +# wsdb-ref (the reference world state behind its FFI entry, wsdb_ref_ipc_ffi_entry) and barretenberg's +# poseidon2 C bindings, as a static library a downstream project builds from source with its own +# toolchain: add it with FetchContent (or add_subdirectory) and link `wsdb_ref`. It compiles only the +# barretenberg sources those need, and none of barretenberg's own CMake. +# +# Its interface is C: wsdb_ref_ipc_ffi_entry/_alloc/_free and bb_poseidon2_hash{,_pair_with_separator}. +# The C++ behind it is linked with the consumer's, so both must use the same msgpack-c: this declares +# it under FetchContent's name `msgpack`, and the first declaration in a build wins. +project(wsdb_ref CXX) + +get_filename_component(bb_src "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +get_filename_component(repo "${bb_src}/../../.." ABSOLUTE) +set(gen_root "${CMAKE_CURRENT_BINARY_DIR}/gen") +set(gen_dir "${gen_root}/barretenberg/wsdb_ref/generated") + +include(FetchContent) +# Header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so this only fetches. +FetchContent_Declare( + msgpack + GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git + GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include +) +FetchContent_MakeAvailable(msgpack) +# Only Tracy's headers: barretenberg's profiling macros expand to nothing without TRACY_ENABLE. +FetchContent_Declare( + tracy + URL https://github.com/wolfpld/tracy/archive/5d542dc09f3d9378d005092a4ad446bd405f819a.tar.gz + SOURCE_SUBDIR public/tracy +) +FetchContent_MakeAvailable(tracy) + +# The protocol constants header, regenerated as barretenberg's own configure does (atomically, so the +# two may run at once). +execute_process(COMMAND "${bb_src}/../scripts/remake-constants.sh" COMMAND_ERROR_IS_FATAL ANY) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${repo}/noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr") + +set(schema "${bb_src}/barretenberg/wsdb_ref/wsdb_schema.jsonc") +file(GLOB_RECURSE ipc_codegen_inputs CONFIGURE_DEPENDS ${repo}/ipc-codegen/src/*.ts + ${repo}/ipc-codegen/templates/cpp/*) +add_custom_command( + OUTPUT ${gen_dir}/wsdb_ffi.cpp ${gen_dir}/wsdb_ffi.hpp ${gen_dir}/wsdb_dispatch.hpp + ${gen_dir}/wsdb_ipc_server.hpp ${gen_dir}/wsdb_types.hpp + COMMAND node --experimental-strip-types --no-warnings ${repo}/ipc-codegen/src/generate.ts + --schema ${schema} --lang cpp --out ${gen_dir} --server --ffi --ffi-name wsdb_ref + --cpp-namespace bb::wsdb_ref --cpp-include-dir barretenberg/wsdb_ref/generated + DEPENDS ${schema} ${ipc_codegen_inputs} + COMMENT "Generating the wsdb-ref dispatch and FFI entry from wsdb_schema.jsonc" + VERBATIM) + +add_library(wsdb_ref STATIC + ${bb_src}/barretenberg/common/assert.cpp + ${bb_src}/barretenberg/common/log.cpp + ${bb_src}/barretenberg/common/logstr.cpp + ${bb_src}/barretenberg/common/throw_or_abort.cpp + ${bb_src}/barretenberg/crypto/poseidon2/poseidon2.cpp + ${bb_src}/barretenberg/crypto/poseidon2/poseidon2_capi.cpp + ${bb_src}/barretenberg/world_state_reference/merkle_tree_id.cpp + ${bb_src}/barretenberg/world_state_reference/reference_world_state.cpp + ${bb_src}/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp + ${gen_dir}/wsdb_ffi.cpp) +target_compile_features(wsdb_ref PRIVATE cxx_std_20) +set_target_properties(wsdb_ref PROPERTIES POSITION_INDEPENDENT_CODE ON) +# All private: a consumer declares the C entry points itself and compiles no barretenberg header. +target_include_directories(wsdb_ref PRIVATE ${bb_src} ${gen_root} ${repo}/ipc-runtime/cpp) +target_include_directories(wsdb_ref SYSTEM PRIVATE ${msgpack_SOURCE_DIR}/include ${tracy_SOURCE_DIR}/public) +# What barretenberg's build defines for these sources. +target_compile_definitions(wsdb_ref PRIVATE MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) +target_compile_options(wsdb_ref PRIVATE -fconstexpr-steps=100000000 -fbracket-depth=1024) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") + target_compile_options(wsdb_ref PRIVATE -march=skylake) +endif() diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html b/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html new file mode 100644 index 00000000000..305ea11766a --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html @@ -0,0 +1,327 @@ + + + + + +Persistent Treap Forks + + + + + +
+
+

Persistent Treap Forks

+

How wsdb-ref's PersistentKeyIndex keeps an ordered key → leaf-index map that can be forked, checkpointed and kept per block for the cost of copying one pointer.

+
+ +
+

A treap is a search tree and a heap at once

+

Each node carries a key and a priority. The tree obeys two orderings at the same time, and for a given set of keys and priorities only one tree satisfies both.

+
+
+

By key: a search tree

Smaller keys go left, larger keys go right, so find and predecessor (the indexed trees' low-leaf lookup) walk one root-to-leaf path.

+

By priority: a heap

A parent's priority is higher than its children's. Random priorities make the shape that of a random insertion order, so the depth stays O(log n) on average.

+

Priority = hash(key)

wsdb-ref uses splitmix64 of the key instead of a random number, so the shape depends only on the set of keys and every run builds the same tree.

+
+ +
+
+
+ + +
+
+
+
+

Versions

+
+
+
+ allocated by this version + the same node object as in the compared version + - - - new node pointing into a shared subtree +
+
+
+
+
+ +
+

Why an insert copies only one path

+

Nodes are immutable (shared_ptr<const Node>). An insert walks down from the root while the existing node's priority is higher, rebuilding each node it passes with one new child pointer. Where the new key's priority wins, it splits that subtree by key and takes the two halves as its children; the split also rebuilds only the nodes on its path. Every subtree off that path is reused as is.

+

So a version is just a root pointer. The old root still describes the old tree, untouched, and the new root shares everything except about log n nodes with it. Select an older version above and insert into it: that is a fork, and both branches keep sharing the nodes they had in common.

+
+ +
+

How the reference world state forks

+

Each of the five trees pairs a PersistentMerkleTree (built on the same path-copying idea) with a PersistentKeyIndex. Both are a root pointer, so the whole state is ten pointers:

+
struct Trees {
+    NullifierTree   nullifier;    // tree root + key index root
+    AppendOnlyTree  note_hash;    // tree root + key index root
+    PublicDataTree  public_data;  // tree root + key index root
+    AppendOnlyTree  l1_to_l2;     // tree root + key index root
+    AppendOnlyTree  archive;      // tree root + key index root
+};  // "Copying one is copying ten pointers."
+

From there each operation is a copy of Trees:

+
    +
  • Committed blocks: keep a Trees per block, so reads at any block are direct.
  • +
  • Fork: copy the Trees of the block it starts from, then write to the copy.
  • +
  • Checkpoint: push a copy of the fork's current Trees; revert pops it back, commit drops it.
  • +
+

No operation copies tree contents. Writes pay O(log n) new nodes each, whichever version they write to, and memory is shared by every version that still points at it.

+
+
+ + + + diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp new file mode 100644 index 00000000000..c1b5afd51d2 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -0,0 +1,426 @@ +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" + +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" + +#include + +#include +#include +#include + +using namespace bb; +using namespace bb::wsdb_ref; + +namespace { + +constexpr uint32_t LATEST = std::numeric_limits::max(); +const wire::WorldStateRevision LATEST_UNCOMMITTED{ .forkId = 0, .blockNumber = LATEST, .includeUncommitted = true }; +const wire::WorldStateRevision LATEST_COMMITTED{ .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false }; + +std::vector encode_request(const char* name, const auto& cmd) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(name)); + pk.pack(cmd); + return { buf.data(), buf.data() + buf.size() }; +} + +/** A decoded response: the payload, or the service's error message. */ +template using Result = std::variant; + +template Result decode_response(const std::vector& frame, const char* expected) +{ + auto unpacked = msgpack::unpack(reinterpret_cast(frame.data()), frame.size()); + auto obj = unpacked.get(); + std::string tag(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (tag == "WsdbErrorResponse") { + wire::WsdbErrorResponse error; + obj.via.array.ptr[1].convert(error); + return error.message; + } + EXPECT_EQ(tag, expected); + Resp resp; + obj.via.array.ptr[1].convert(resp); + return resp; +} + +wire::Fr bytes_of_fr(const fr& value) +{ + wire::Fr w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + +/** Call a service through the generated in-process FFI entry: the path the napi and wasm transports take. */ +template Result call_ffi(const char* name, const auto& cmd, const char* expected) +{ + auto request = encode_request(name, cmd); + uint8_t* output = nullptr; + size_t output_len = 0; + wsdb_ref_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); + std::vector frame(output, output + output_len); + wsdb_ref_ipc_ffi_free(output); + return decode_response(frame, expected); +} + +std::vector protocol_tree_heights() +{ + return { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }; +} + +/** The canonical genesis, built as a C++ caller of Init builds it: the protocol's tree heights and initial sizes and + * the protocol contracts' registration nullifiers. */ +wire::WsdbInit canonical_init() +{ + wire::WsdbInit init{ .treeHeights = protocol_tree_heights(), + .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; + for (const auto& nullifier : world_state::genesis_protocol_nullifiers()) { + init.prefilledNullifiers.push_back(bytes_of_fr(nullifier)); + } + return init; +} + +/** A fresh service instance at the canonical genesis, called through the generated dispatch, independent of the + * FFI's shared state. */ +struct Service { + WsdbRefContext ctx; + AsyncDispatchHandler handler = make_wsdb_handler(ctx); + + Service() + { + auto init = call("WsdbInit", canonical_init(), "WsdbInitResponse"); + EXPECT_TRUE(std::holds_alternative(init)); + } + + template Result call(const char* name, const auto& cmd, const char* expected) + { + auto request = encode_request(name, cmd); + std::vector frame; + handler(request, [&frame](std::vector response) { frame = std::move(response); }); + return decode_response(frame, expected); + } +}; + +template const Resp& ok(const Result& result) +{ + if (const auto* error = std::get_if(&result)) { + ADD_FAILURE() << "service error: " << *error; + static Resp empty{}; + return empty; + } + return std::get(result); +} + +template std::string error_of(const Result& result) +{ + const auto* error = std::get_if(&result); + return error ? *error : std::string(""); +} + +template W bytes_of(const fr& value) +{ + W w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + +fr fr_of(const auto& w) +{ + return fr::serialize_from_buffer(w.data()); +} + +/** Init the FFI entry's shared world state at the canonical genesis (a later Init restarts it). */ +void init_ffi() +{ + ok(call_ffi("WsdbInit", canonical_init(), "WsdbInitResponse")); +} + +} // namespace + +TEST(WsdbRef, FfiEntryServesTheCanonicalGenesis) +{ + init_ffi(); + auto archive = + ok(call_ffi("WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 4, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(fr_of(archive.root), fr(GENESIS_ARCHIVE_ROOT)); + EXPECT_EQ(archive.size, 1); + EXPECT_EQ(archive.depth, ARCHIVE_HEIGHT); + + auto header = ok(call_ffi( + "WsdbGetLeafValue", + wire::WsdbGetLeafValue{ .treeId = 4, .revision = LATEST_COMMITTED, .leafIndex = 0 }, + "WsdbGetLeafValueResponse")); + ASSERT_TRUE(header.value.has_value()); + EXPECT_EQ(fr_of(*header.value), fr(GENESIS_BLOCK_HEADER_HASH)); + + auto initial = ok(call_ffi( + "WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}, "WsdbGetInitialStateReferenceResponse")); + ASSERT_EQ(initial.state.size(), 4); + bool saw_nullifier_tree = false; + for (const auto& tree : initial.state) { + if (tree.treeId == 0) { + saw_nullifier_tree = true; + EXPECT_EQ(fr_of(tree.root), fr(GENESIS_NULLIFIER_TREE_ROOT)); + EXPECT_EQ(tree.size, 128); + } + } + EXPECT_TRUE(saw_nullifier_tree); +} + +TEST(WsdbRef, FfiEntryAnswersBadRequestsWithErrors) +{ + init_ffi(); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string("WsdbNoSuchCommand")); + pk.pack_map(0); + uint8_t* output = nullptr; + size_t output_len = 0; + wsdb_ref_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); + std::vector frame(output, output + output_len); + wsdb_ref_ipc_ffi_free(output); + EXPECT_EQ(error_of(decode_response(frame, "")), + "unknown command: WsdbNoSuchCommand"); + + EXPECT_EQ(error_of(call_ffi( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 9, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")), + "Unknown tree id 9"); +} + +TEST(WsdbRef, ForksAndHistoryOverTheWire) +{ + Service service; + auto fork = ok(service.call( + "WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }, "WsdbCreateForkResponse")); + EXPECT_EQ(fork.forkId, 1); + ok(service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ .treeId = 1, .leaves = { bytes_of(fr(42)) }, .forkId = fork.forkId }, + "WsdbAppendLeavesResponse")); + auto on_fork = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = fork.forkId, .blockNumber = LATEST, .includeUncommitted = true } }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(on_fork.size, 1); + auto canonical = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 1, .revision = LATEST_UNCOMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(canonical.size, 0); + + EXPECT_EQ(error_of(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = 3, .blockNumber = LATEST, .includeUncommitted = true } }, + "WsdbGetTreeInfoResponse")), + "Fork not found"); + EXPECT_NE( + error_of(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = 0, .blockNumber = 5, .includeUncommitted = false } }, + "WsdbGetTreeInfoResponse")) + .find("latest block is 0"), + std::string::npos); + EXPECT_EQ(error_of(service.call( + "WsdbGetLeafValue", + wire::WsdbGetLeafValue{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .leafIndex = 0 }, + "WsdbGetLeafValueResponse")), + "Unsupported tree type for get_leaf_value"); + EXPECT_EQ(error_of(service.call( + "WsdbRevertCheckpoint", wire::WsdbRevertCheckpoint{ .forkId = 0 }, "WsdbRevertCheckpointResponse")), + "Cannot revert without a checkpoint"); + EXPECT_NE(error_of(service.call( + "WsdbCopyStores", + wire::WsdbCopyStores{ .dstPath = "/tmp/x", .compact = std::nullopt }, + "WsdbCopyStoresResponse")) + .find("not supported"), + std::string::npos); +} + +TEST(WsdbRef, InitSetsTreeHeightsAndInitialSizes) +{ + Service service; + auto heights = protocol_tree_heights(); + heights[1].height = 3; + ok(service.call( + "WsdbInit", + wire::WsdbInit{ .treeHeights = heights, + .treePrefill = { { .treeId = 0, .size = 4 }, { .treeId = 2, .size = 8 } }, + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitResponse")); + auto tree_info = [&](uint32_t tree_id) { + return ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = tree_id, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + }; + EXPECT_EQ(tree_info(0).size, 4); + EXPECT_EQ(tree_info(2).size, 8); + EXPECT_EQ(tree_info(1).depth, 3); + // A three-level note hash tree holds eight leaves. + auto append = [&](size_t count) { + return service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ + .treeId = 1, .leaves = std::vector(count, bytes_of(fr(1))), .forkId = 0 }, + "WsdbAppendLeavesResponse"); + }; + ok(append(8)); + EXPECT_FALSE(std::holds_alternative(append(1))); + + auto rejected = [&](std::vector tree_heights, std::vector prefill) { + return error_of(service.call("WsdbInit", + wire::WsdbInit{ .treeHeights = std::move(tree_heights), + .treePrefill = std::move(prefill), + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = 0 }, + "WsdbInitResponse")); + }; + const std::vector both = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }; + EXPECT_NE(rejected({ heights.begin(), heights.end() - 1 }, both).find("each of the five trees"), std::string::npos); + EXPECT_NE(rejected(protocol_tree_heights(), { both[0] }).find("nullifier and public data"), std::string::npos); + EXPECT_NE( + rejected(protocol_tree_heights(), { both[0], both[1], { .treeId = 1, .size = 4 } }).find("not an indexed tree"), + std::string::npos); + EXPECT_NE(rejected(protocol_tree_heights(), { { .treeId = 0, .size = 1 }, both[1] }).find("initial size > 1"), + std::string::npos); +} + +TEST(WsdbRef, ASecondInitRestartsAtTheGivenGenesis) +{ + Service service; + auto nullifier_root = [&]() { + auto info = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 0, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + return fr_of(info.root); + }; + EXPECT_EQ(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); + ok(service.call( + "WsdbInit", + wire::WsdbInit{ .treeHeights = protocol_tree_heights(), + .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(5)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitResponse")); + EXPECT_NE(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); + auto slot = ok(service.call( + "WsdbFindPublicDataLeafIndices", + wire::WsdbFindPublicDataLeafIndices{ .revision = LATEST_COMMITTED, + .leaves = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(0)) } }, + .startIndex = 0 }, + "WsdbFindPublicDataLeafIndicesResponse")); + EXPECT_EQ(slot.indices, (std::vector>{ 127 })); +} + +TEST(WsdbRef, InsertsNullifiersWithWitnessesAndRejectsDuplicates) +{ + Service service; + const fr nullifier(0xabcdef); + auto inserted = ok(service.call( + "WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = { { .nullifier = bytes_of(nullifier) } }, + .forkId = 0 }, + "WsdbSequentialInsertNullifierResponse")); + ASSERT_EQ(inserted.result.insertionWitnessData.size(), 1); + EXPECT_EQ(inserted.result.insertionWitnessData[0].index, 128); + EXPECT_EQ(inserted.result.lowLeafWitnessData[0].path.size(), NULLIFIER_TREE_HEIGHT); + + auto found = ok(service.call( + "WsdbFindNullifierLeafIndices", + wire::WsdbFindNullifierLeafIndices{ .revision = LATEST_UNCOMMITTED, + .leaves = { { .nullifier = bytes_of(nullifier) } }, + .startIndex = 0 }, + "WsdbFindNullifierLeafIndicesResponse")); + ASSERT_EQ(found.indices.size(), 1); + EXPECT_EQ(found.indices[0], 128); + + // Rejected as a whole: the fresh nullifier in the same batch is not inserted either. + const fr fresh(0x777); + auto duplicate = service.call( + "WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = { { .nullifier = bytes_of(fresh) }, + { .nullifier = bytes_of(nullifier) } }, + .forkId = 0 }, + "WsdbSequentialInsertNullifierResponse"); + EXPECT_NE(error_of(duplicate).find("is already present"), std::string::npos); + auto low = ok(service.call( + "WsdbFindLowLeaf", + wire::WsdbFindLowLeaf{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .key = bytes_of(fresh) }, + "WsdbFindLowLeafResponse")); + EXPECT_FALSE(low.alreadyPresent); +} + +TEST(WsdbRef, UpdateArchiveChecksTheBlockState) +{ + Service service; + ok(service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ .treeId = 1, .leaves = { bytes_of(fr(42)) }, .forkId = 0 }, + "WsdbAppendLeavesResponse")); + auto state = ok( + service.call("WsdbGetStateReference", + wire::WsdbGetStateReference{ .revision = LATEST_UNCOMMITTED }, + "WsdbGetStateReferenceResponse")); + + auto stale = ok(service.call( + "WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}, "WsdbGetInitialStateReferenceResponse")); + EXPECT_EQ( + error_of(service.call( + "WsdbUpdateArchive", + wire::WsdbUpdateArchive{ + .blockStateRef = stale.state, .blockHeaderHash = bytes_of(fr(1)), .forkId = 0 }, + "WsdbUpdateArchiveResponse")), + "Can't update archive tree: Block state does not match world state"); + + ok(service.call( + "WsdbUpdateArchive", + wire::WsdbUpdateArchive{ + .blockStateRef = state.state, .blockHeaderHash = bytes_of(fr(1)), .forkId = 0 }, + "WsdbUpdateArchiveResponse")); + auto archive = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 4, .revision = LATEST_UNCOMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(archive.size, 2); +} + +// wasm does not find main() in gtest_main (see ecc_tests' secp256r1.test.cpp). +#ifdef __wasm__ +GTEST_API_ int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp new file mode 100644 index 00000000000..50cb374cdf1 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp @@ -0,0 +1,851 @@ +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" + +#include +#include +#include +#include +#include +#include + +namespace bb::wsdb_ref { + +using world_state::BatchInsertionResult; +using world_state::GenesisConfig; +using world_state::MerkleTreeId; +using world_state::ReferenceWorldState; +using world_state::TreeState; +using world_state::WorldStateRevision; +using FF = bb::fr; +using crypto::merkle_tree::IndexedLeaf; +using crypto::merkle_tree::LeafUpdateWitnessData; +using crypto::merkle_tree::NullifierLeafValue; +using crypto::merkle_tree::PublicDataLeafValue; +using crypto::merkle_tree::SequentialInsertionResult; + +namespace { + +/** Answers with what `work` returns, or with the error it throws. */ +template void serve(const Responder& respond, Work&& work) +{ + std::optional response; + try { + response = work(); + } catch (const std::exception& e) { + respond.error(e.what()); + return; + } + respond.ok(*response); +} + +// Field elements cross the wire as 32 canonical bytes under whichever nominal alias the field declares. +template FF to_fr(const W& w) +{ + return FF::serialize_from_buffer(w.data()); +} + +template W from_fr(const FF& f) +{ + W w{}; + FF::serialize_to_buffer(f, w.data()); + return w; +} + +std::vector path_to_wire(const std::vector& path) +{ + std::vector out; + out.reserve(path.size()); + for (const auto& node : path) { + out.push_back(from_fr(node)); + } + return out; +} + +MerkleTreeId tree_id_of(uint32_t id) +{ + if (id > static_cast(MerkleTreeId::ARCHIVE)) { + throw std::runtime_error("Unknown tree id " + std::to_string(id)); + } + return static_cast(id); +} + +bool is_append_only(MerkleTreeId id) +{ + return id == MerkleTreeId::NOTE_HASH_TREE || id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE || + id == MerkleTreeId::ARCHIVE; +} + +void require_append_only(MerkleTreeId id, const char* command) +{ + if (!is_append_only(id)) { + throw std::runtime_error(std::string("Unsupported tree type for ") + command); + } +} + +WorldStateRevision revision_of(const wire::WorldStateRevision& w) +{ + return WorldStateRevision{ .forkId = w.forkId, + .blockNumber = w.blockNumber, + .includeUncommitted = w.includeUncommitted }; +} + +std::vector state_to_wire(const std::vector& state) +{ + std::vector out; + out.reserve(state.size()); + for (const auto& tree : state) { + out.push_back( + { .treeId = static_cast(tree.tree_id), .root = from_fr(tree.root), .size = tree.size }); + } + return out; +} + +std::vector state_from_wire(const std::vector& state) +{ + std::vector out; + out.reserve(state.size()); + for (const auto& tree : state) { + out.push_back({ .tree_id = tree_id_of(tree.treeId), .root = to_fr(tree.root), .size = tree.size }); + } + return out; +} + +NullifierLeafValue from_wire(const wire::NullifierLeafValue& leaf) +{ + return NullifierLeafValue(to_fr(leaf.nullifier)); +} + +PublicDataLeafValue from_wire(const wire::PublicDataLeafValue& leaf) +{ + return PublicDataLeafValue(to_fr(leaf.slot), to_fr(leaf.value)); +} + +template std::vector all_from_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(from_wire(item)); + } + return out; +} + +std::vector frs_from_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(to_fr(item)); + } + return out; +} + +wire::NullifierLeafValue to_wire(const NullifierLeafValue& leaf) +{ + return { .nullifier = from_fr(leaf.nullifier) }; +} + +wire::PublicDataLeafValue to_wire(const PublicDataLeafValue& leaf) +{ + return { .slot = from_fr(leaf.slot), .value = from_fr(leaf.value) }; +} + +wire::IndexedNullifierLeafValue to_wire(const IndexedLeaf& leaf) +{ + return { .leaf = to_wire(leaf.leaf), .nextIndex = leaf.nextIndex, .nextKey = from_fr(leaf.nextKey) }; +} + +wire::IndexedPublicDataLeafValue to_wire(const IndexedLeaf& leaf) +{ + return { .leaf = to_wire(leaf.leaf), .nextIndex = leaf.nextIndex, .nextKey = from_fr(leaf.nextKey) }; +} + +wire::NullifierLeafUpdateWitnessData to_wire(const LeafUpdateWitnessData& w) +{ + return { .leaf = to_wire(w.leaf), .index = w.index, .path = path_to_wire(w.path) }; +} + +wire::PublicDataLeafUpdateWitnessData to_wire(const LeafUpdateWitnessData& w) +{ + return { .leaf = to_wire(w.leaf), .index = w.index, .path = path_to_wire(w.path) }; +} + +template std::vector all_to_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(to_wire(item)); + } + return out; +} + +template +WireResult batch_to_wire(const BatchInsertionResult& result) +{ + std::vector sorted; + sorted.reserve(result.sorted_leaves.size()); + for (const auto& [leaf, index] : result.sorted_leaves) { + sorted.push_back({ .leaf = to_wire(leaf), .index = index }); + } + return { .lowLeafWitnessData = all_to_wire(result.low_leaf_witness_data), + .sortedLeaves = std::move(sorted), + .subtreePath = path_to_wire(result.subtree_path) }; +} + +template +WireResult sequential_to_wire(const SequentialInsertionResult& result) +{ + return { .lowLeafWitnessData = all_to_wire(result.low_leaf_witness_data), + .insertionWitnessData = all_to_wire(result.insertion_witness_data) }; +} + +wire::WorldStateStatusSummary summary_to_wire(const world_state::StatusSummary& status) +{ + return { .unfinalizedBlockNumber = status.unfinalized_block_number, + .finalizedBlockNumber = status.finalized_block_number, + .oldestHistoricalBlock = status.oldest_historical_block, + .treesAreSynched = status.trees_are_synched }; +} + +/** A node's full status: the summary, each tree's meta, and empty store statistics (there are no stores). */ +wire::WorldStateStatusFull full_status(const ReferenceWorldState& world_state) +{ + const auto meta = world_state.tree_meta(); + auto tree_meta = [&](MerkleTreeId id) { + const auto& m = meta[static_cast(id)]; + return wire::TreeMeta{ .name = world_state::getMerkleTreeName(id), + .depth = m.depth, + .size = m.size, + .committedSize = m.committed_size, + .root = from_fr(m.root), + .initialSize = m.initial_size, + .initialRoot = from_fr(m.initial_root), + .oldestHistoricBlock = m.oldest_historic_block, + .unfinalizedBlockHeight = m.unfinalized_block_height, + .finalizedBlockHeight = m.finalized_block_height }; + }; + auto tree_stats = [](MerkleTreeId id) { + auto db = [&](const char* suffix) { + return wire::DBStats{ .name = world_state::getMerkleTreeName(id) + suffix, + .numDataItems = 0, + .totalUsedSize = 0 }; + }; + return wire::TreeDBStats{ .mapSize = 0, + .physicalFileSize = 0, + .blocksDBStats = db(" blocks"), + .nodesDBStats = db(" nodes"), + .leafPreimagesDBStats = db(" leaf preimages"), + .leafIndicesDBStats = db(" leaf indices"), + .blockIndicesDBStats = db(" block indices") }; + }; + return { .summary = summary_to_wire(world_state.status()), + .dbStats = { .noteHashTreeStats = tree_stats(MerkleTreeId::NOTE_HASH_TREE), + .messageTreeStats = tree_stats(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archiveTreeStats = tree_stats(MerkleTreeId::ARCHIVE), + .publicDataTreeStats = tree_stats(MerkleTreeId::PUBLIC_DATA_TREE), + .nullifierTreeStats = tree_stats(MerkleTreeId::NULLIFIER_TREE) }, + .meta = { .noteHashTreeMeta = tree_meta(MerkleTreeId::NOTE_HASH_TREE), + .messageTreeMeta = tree_meta(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archiveTreeMeta = tree_meta(MerkleTreeId::ARCHIVE), + .publicDataTreeMeta = tree_meta(MerkleTreeId::PUBLIC_DATA_TREE), + .nullifierTreeMeta = tree_meta(MerkleTreeId::NULLIFIER_TREE) } }; +} + +template +std::vector> find_paths(const ReferenceWorldState& world_state, + const WorldStateRevision& revision, + MerkleTreeId tree_id, + const std::vector& keys) +{ + std::vector> paths; + paths.reserve(keys.size()); + for (const auto& key : keys) { + auto index = world_state.find_leaf_index(revision, tree_id, key, 0); + if (!index.has_value()) { + paths.emplace_back(std::nullopt); + continue; + } + paths.emplace_back(wire::SiblingPathAndIndex{ + .index = *index, .path = path_to_wire(world_state.get_sibling_path(revision, tree_id, *index)) }); + } + return paths; +} + +template +std::vector> find_indices(const ReferenceWorldState& world_state, + const WorldStateRevision& revision, + MerkleTreeId tree_id, + const std::vector& keys, + uint64_t start) +{ + std::vector> indices; + indices.reserve(keys.size()); + for (const auto& key : keys) { + indices.push_back(world_state.find_leaf_index(revision, tree_id, key, start)); + } + return indices; +} + +/** + * An indexed leaf for a leaf-value read, which a node answers with null on any failure (an unknown block + * included) except an unknown fork. + */ +template auto leaf_or_null(const ReferenceWorldState& world_state, uint64_t fork_id, Read read) +{ + if (!world_state.has_fork(fork_id)) { + throw std::runtime_error("Fork not found"); + } + try { + return read(); + } catch (const std::exception&) { + return decltype(read())(std::nullopt); + } +} + +template std::vector keys_of(const std::vector& leaves) +{ + std::vector keys; + keys.reserve(leaves.size()); + for (const auto& leaf : leaves) { + keys.push_back(leaf.get_key()); + } + return keys; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Tree state +// --------------------------------------------------------------------------- + +void handle_get_tree_info(WsdbRefContext& ctx, + wire::WsdbGetTreeInfo&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetTreeInfoResponse { + auto id = tree_id_of(cmd.treeId); + auto info = ctx.world_state->get_tree_info(revision_of(cmd.revision), id); + return { .treeId = cmd.treeId, + .root = from_fr(info.root), + .size = info.size, + .depth = ctx.world_state->tree_height(id) }; + }); +} + +void handle_get_state_reference(WsdbRefContext& ctx, + wire::WsdbGetStateReference&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetStateReferenceResponse { + return { .state = state_to_wire(ctx.world_state->get_state_reference(revision_of(cmd.revision))) }; + }); +} + +void handle_get_initial_state_reference(WsdbRefContext& ctx, + wire::WsdbGetInitialStateReference&& /*cmd*/, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetInitialStateReferenceResponse { + return { .state = state_to_wire(ctx.world_state->get_initial_state_reference()) }; + }); +} + +// --------------------------------------------------------------------------- +// Leaves +// --------------------------------------------------------------------------- + +void handle_get_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetLeafValue&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetLeafValueResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "get_leaf_value"); + auto value = ctx.world_state->get_leaf_value(revision_of(cmd.revision), id, cmd.leafIndex); + return { .value = value.has_value() ? std::optional(from_fr(*value)) : std::nullopt }; + }); +} + +void handle_get_public_data_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafValue&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetPublicDataLeafValueResponse { + auto leaf = leaf_or_null(*ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state->get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + }); + return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; + }); +} + +void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafValue&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetNullifierLeafValueResponse { + auto leaf = leaf_or_null(*ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state->get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + }); + return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; + }); +} + +void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafPreimage&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetPublicDataLeafPreimageResponse { + // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. + auto leaf = ctx.world_state->get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + if (!leaf.has_value()) { + throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); + } + return { .preimage = to_wire(*leaf) }; + }); +} + +void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafPreimage&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetNullifierLeafPreimageResponse { + // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. + auto leaf = ctx.world_state->get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + if (!leaf.has_value()) { + throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); + } + return { .preimage = to_wire(*leaf) }; + }); +} + +void handle_get_sibling_path(WsdbRefContext& ctx, + wire::WsdbGetSiblingPath&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetSiblingPathResponse { + return { .path = path_to_wire(ctx.world_state->get_sibling_path( + revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndex)) }; + }); +} + +void handle_get_block_numbers_for_leaf_indices(WsdbRefContext& ctx, + wire::WsdbGetBlockNumbersForLeafIndices&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetBlockNumbersForLeafIndicesResponse { + return { .blockNumbers = ctx.world_state->get_block_numbers_for_leaf_indices( + revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndices) }; + }); +} + +// --------------------------------------------------------------------------- +// Searches +// --------------------------------------------------------------------------- + +void handle_find_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindLeafIndices&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindLeafIndicesResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "find_leaf_indices"); + return { .indices = find_indices( + *ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves), cmd.startIndex) }; + }); +} + +void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindPublicDataLeafIndices&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindPublicDataLeafIndicesResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { + .indices = find_indices( + *ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys, cmd.startIndex) + }; + }); +} + +void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindNullifierLeafIndices&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindNullifierLeafIndicesResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .indices = find_indices( + *ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys, cmd.startIndex) }; + }); +} + +void handle_find_low_leaf(WsdbRefContext& ctx, + wire::WsdbFindLowLeaf&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindLowLeafResponse { + auto low = ctx.world_state->find_low_leaf(revision_of(cmd.revision), tree_id_of(cmd.treeId), to_fr(cmd.key)); + return { .alreadyPresent = low.is_already_present, .index = low.index }; + }); +} + +void handle_find_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindSiblingPathsResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "find_sibling_paths"); + return { .paths = find_paths(*ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves)) }; + }); +} + +void handle_find_public_data_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindPublicDataSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindPublicDataSiblingPathsResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .paths = + find_paths(*ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys) }; + }); +} + +void handle_find_nullifier_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindNullifierSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindNullifierSiblingPathsResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .paths = find_paths(*ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys) }; + }); +} + +// --------------------------------------------------------------------------- +// Writes +// --------------------------------------------------------------------------- + +void handle_append_leaves(WsdbRefContext& ctx, + wire::WsdbAppendLeaves&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbAppendLeavesResponse { + auto leaves = frs_from_wire(cmd.leaves); + ctx.world_state->append_leaves(cmd.forkId, tree_id_of(cmd.treeId), leaves); + return {}; + }); +} + +void handle_append_public_data_leaves(WsdbRefContext& ctx, + wire::WsdbAppendPublicDataLeaves&& cmd, + Responder respond) +{ + // As on a node: a batch insertion without its witnesses. + serve(respond, [&]() -> wire::WsdbAppendPublicDataLeavesResponse { + auto leaves = all_from_wire(cmd.leaves); + ctx.world_state->batch_insert_public_data(cmd.forkId, leaves, 0); + return {}; + }); +} + +void handle_append_nullifier_leaves(WsdbRefContext& ctx, + wire::WsdbAppendNullifierLeaves&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbAppendNullifierLeavesResponse { + auto leaves = all_from_wire(cmd.leaves); + ctx.world_state->batch_insert_nullifiers(cmd.forkId, leaves, 0); + return {}; + }); +} + +void handle_batch_insert_public_data(WsdbRefContext& ctx, + wire::WsdbBatchInsertPublicData&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbBatchInsertPublicDataResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state->batch_insert_public_data(cmd.forkId, leaves, cmd.subtreeDepth); + return { .result = batch_to_wire(result) }; + }); +} + +void handle_batch_insert_nullifier(WsdbRefContext& ctx, + wire::WsdbBatchInsertNullifier&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbBatchInsertNullifierResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state->batch_insert_nullifiers(cmd.forkId, leaves, cmd.subtreeDepth); + return { .result = batch_to_wire(result) }; + }); +} + +void handle_sequential_insert_public_data(WsdbRefContext& ctx, + wire::WsdbSequentialInsertPublicData&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbSequentialInsertPublicDataResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state->sequential_insert_public_data(cmd.forkId, leaves); + return { + .result = + sequential_to_wire( + result) + }; + }); +} + +void handle_sequential_insert_nullifier(WsdbRefContext& ctx, + wire::WsdbSequentialInsertNullifier&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbSequentialInsertNullifierResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state->sequential_insert_nullifiers(cmd.forkId, leaves); + return { .result = + sequential_to_wire( + result) }; + }); +} + +void handle_update_archive(WsdbRefContext& ctx, + wire::WsdbUpdateArchive&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbUpdateArchiveResponse { + ctx.world_state->update_archive(cmd.forkId, state_from_wire(cmd.blockStateRef), to_fr(cmd.blockHeaderHash)); + return {}; + }); +} + +// --------------------------------------------------------------------------- +// The canonical chain +// --------------------------------------------------------------------------- + +void handle_commit(WsdbRefContext& ctx, wire::WsdbCommit&& /*cmd*/, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCommitResponse { + ctx.world_state->commit(); + return { .status = full_status(*ctx.world_state) }; + }); +} + +void handle_rollback(WsdbRefContext& ctx, wire::WsdbRollback&& /*cmd*/, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRollbackResponse { + ctx.world_state->rollback(); + return {}; + }); +} + +void handle_sync_block(WsdbRefContext& ctx, wire::WsdbSyncBlock&& cmd, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbSyncBlockResponse { + auto note_hashes = frs_from_wire(cmd.paddedNoteHashes); + auto messages = frs_from_wire(cmd.paddedL1ToL2Messages); + auto nullifiers = all_from_wire(cmd.paddedNullifiers); + auto public_data = all_from_wire(cmd.publicDataWrites); + auto optional_fr = [](const std::optional& w) { + return w.has_value() ? std::optional(to_fr(*w)) : std::nullopt; + }; + ctx.world_state->sync_block(state_from_wire(cmd.blockStateRef), + to_fr(cmd.blockHeaderHash), + note_hashes, + messages, + nullifiers, + public_data, + optional_fr(cmd.expectedArchiveRoot), + optional_fr(cmd.expectedPreviousArchiveRoot)); + return { .status = full_status(*ctx.world_state) }; + }); +} + +void handle_create_fork(WsdbRefContext& ctx, + wire::WsdbCreateFork&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCreateForkResponse { + auto block = cmd.latest ? std::nullopt : std::optional(cmd.blockNumber); + return { .forkId = ctx.world_state->create_fork(block) }; + }); +} + +void handle_delete_fork(WsdbRefContext& ctx, + wire::WsdbDeleteFork&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbDeleteForkResponse { + ctx.world_state->delete_fork(cmd.forkId); + return {}; + }); +} + +void handle_finalize_blocks(WsdbRefContext& ctx, + wire::WsdbFinalizeBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFinalizeBlocksResponse { + ctx.world_state->finalize_blocks(cmd.toBlockNumber); + return { .status = summary_to_wire(ctx.world_state->status()) }; + }); +} + +void handle_unwind_blocks(WsdbRefContext& ctx, + wire::WsdbUnwindBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbUnwindBlocksResponse { + ctx.world_state->unwind_blocks(cmd.toBlockNumber); + return { .status = full_status(*ctx.world_state) }; + }); +} + +void handle_remove_historical_blocks(WsdbRefContext& ctx, + wire::WsdbRemoveHistoricalBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRemoveHistoricalBlocksResponse { + ctx.world_state->remove_historical_blocks(cmd.toBlockNumber); + return { .status = full_status(*ctx.world_state) }; + }); +} + +void handle_get_status(WsdbRefContext& ctx, + wire::WsdbGetStatus&& /*cmd*/, + Responder respond) +{ + serve(respond, + [&]() -> wire::WsdbGetStatusResponse { return { .status = summary_to_wire(ctx.world_state->status()) }; }); +} + +// --------------------------------------------------------------------------- +// Checkpoints +// --------------------------------------------------------------------------- + +void handle_create_checkpoint(WsdbRefContext& ctx, + wire::WsdbCreateCheckpoint&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCreateCheckpointResponse { + ctx.world_state->create_checkpoint(cmd.forkId); + return {}; + }); +} + +void handle_commit_checkpoint(WsdbRefContext& ctx, + wire::WsdbCommitCheckpoint&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCommitCheckpointResponse { + ctx.world_state->commit_checkpoint(cmd.forkId); + return {}; + }); +} + +void handle_revert_checkpoint(WsdbRefContext& ctx, + wire::WsdbRevertCheckpoint&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRevertCheckpointResponse { + ctx.world_state->revert_checkpoint(cmd.forkId); + return {}; + }); +} + +void handle_commit_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbCommitAllCheckpoints&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCommitAllCheckpointsResponse { + ctx.world_state->commit_all_checkpoints(cmd.forkId); + return {}; + }); +} + +void handle_revert_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbRevertAllCheckpoints&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRevertAllCheckpointsResponse { + ctx.world_state->revert_all_checkpoints(cmd.forkId); + return {}; + }); +} + +// --------------------------------------------------------------------------- +// Stores and genesis +// --------------------------------------------------------------------------- + +void handle_copy_stores(WsdbRefContext& /*ctx*/, + wire::WsdbCopyStores&& /*cmd*/, + Responder respond) +{ + respond.error("CopyStores is not supported: wsdb-ref keeps its state in memory, with no stores to copy"); +} + +void handle_init(WsdbRefContext& ctx, wire::WsdbInit&& cmd, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbInitResponse { + GenesisConfig genesis; + std::array has_height{}; + for (const auto& [tree_id, height] : cmd.treeHeights) { + auto id = tree_id_of(tree_id); + if (has_height[static_cast(id)]) { + throw std::runtime_error("treeHeights names tree " + std::to_string(tree_id) + " twice"); + } + has_height[static_cast(id)] = true; + genesis.tree_heights[static_cast(id)] = height; + } + if (std::ranges::find(has_height, false) != has_height.end()) { + throw std::runtime_error("treeHeights must name each of the five trees"); + } + std::optional nullifier_size; + std::optional public_data_size; + for (const auto& [tree_id, size] : cmd.treePrefill) { + auto id = tree_id_of(tree_id); + if (id != MerkleTreeId::NULLIFIER_TREE && id != MerkleTreeId::PUBLIC_DATA_TREE) { + throw std::runtime_error("treePrefill names tree " + std::to_string(tree_id) + + ", which is not an indexed tree"); + } + auto& slot = id == MerkleTreeId::NULLIFIER_TREE ? nullifier_size : public_data_size; + if (slot.has_value()) { + throw std::runtime_error("treePrefill names tree " + std::to_string(tree_id) + " twice"); + } + slot = size; + } + if (!nullifier_size || !public_data_size) { + throw std::runtime_error("treePrefill must give the nullifier and public data trees their sizes"); + } + genesis.nullifier_tree_initial_size = *nullifier_size; + genesis.public_data_tree_initial_size = *public_data_size; + genesis.prefilled_public_data = all_from_wire(cmd.prefilledPublicData); + for (const auto& nullifier : cmd.prefilledNullifiers) { + genesis.prefilled_nullifiers.emplace_back(to_fr(nullifier)); + } + genesis.genesis_timestamp = cmd.genesisTimestamp; + genesis.header_generator_point = FF(cmd.initialHeaderGeneratorPoint); + if (ctx.world_state.has_value()) { + ctx.world_state->reset(genesis); + } else { + ctx.world_state.emplace(genesis); + } + return {}; + }); +} + +// The in-process FFI entry's context (generated/wsdb_ffi.hpp): one reference world state per loaded +// module, created by Init, shared by every call as over one transport connection. +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static WsdbRefContext ctx; + static AsyncDispatchHandler handler = make_wsdb_handler(ctx); + return handler; +} + +} // namespace bb::wsdb_ref diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp new file mode 100644 index 00000000000..c25a3d33bd6 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp @@ -0,0 +1,71 @@ +#pragma once +/** + * @file wsdb_ref_handlers.hpp + * @brief The node world state's contract (wsdb_schema.jsonc) served by the in-memory + * reference world state. + * + * The generated dispatch (generated/wsdb_dispatch.hpp) declares template handle_; the + * overloads below, for Ctx = WsdbRefContext, are preferred at its instantiation in + * make_wsdb_handler. Every handler answers synchronously, and a request that fails is + * answered with its error. CopyStores, which copies a node's LMDB stores, is the one command answered + * with an error unconditionally. + */ +#include "barretenberg/world_state_reference/reference_world_state.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_dispatch.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_types.hpp" + +#include + +namespace bb::wsdb_ref { + +/** @brief The state a wsdb-ref service runs over: one reference world state, which Init creates (and a later + * Init replaces). Every other command assumes it exists. */ +struct WsdbRefContext { + std::optional world_state; +}; + +// clang-format off +void handle_get_tree_info(WsdbRefContext& ctx, wire::WsdbGetTreeInfo&& cmd, Responder respond); +void handle_get_state_reference(WsdbRefContext& ctx, wire::WsdbGetStateReference&& cmd, Responder respond); +void handle_get_initial_state_reference(WsdbRefContext& ctx, wire::WsdbGetInitialStateReference&& cmd, Responder respond); +void handle_get_leaf_value(WsdbRefContext& ctx, wire::WsdbGetLeafValue&& cmd, Responder respond); +void handle_get_public_data_leaf_value(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafValue&& cmd, Responder respond); +void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafValue&& cmd, Responder respond); +void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafPreimage&& cmd, Responder respond); +void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafPreimage&& cmd, Responder respond); +void handle_get_sibling_path(WsdbRefContext& ctx, wire::WsdbGetSiblingPath&& cmd, Responder respond); +void handle_get_block_numbers_for_leaf_indices(WsdbRefContext& ctx, wire::WsdbGetBlockNumbersForLeafIndices&& cmd, Responder respond); +void handle_find_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindLeafIndices&& cmd, Responder respond); +void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindPublicDataLeafIndices&& cmd, Responder respond); +void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindNullifierLeafIndices&& cmd, Responder respond); +void handle_find_low_leaf(WsdbRefContext& ctx, wire::WsdbFindLowLeaf&& cmd, Responder respond); +void handle_find_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindSiblingPaths&& cmd, Responder respond); +void handle_find_public_data_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindPublicDataSiblingPaths&& cmd, Responder respond); +void handle_find_nullifier_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindNullifierSiblingPaths&& cmd, Responder respond); +void handle_append_leaves(WsdbRefContext& ctx, wire::WsdbAppendLeaves&& cmd, Responder respond); +void handle_append_public_data_leaves(WsdbRefContext& ctx, wire::WsdbAppendPublicDataLeaves&& cmd, Responder respond); +void handle_append_nullifier_leaves(WsdbRefContext& ctx, wire::WsdbAppendNullifierLeaves&& cmd, Responder respond); +void handle_batch_insert_public_data(WsdbRefContext& ctx, wire::WsdbBatchInsertPublicData&& cmd, Responder respond); +void handle_batch_insert_nullifier(WsdbRefContext& ctx, wire::WsdbBatchInsertNullifier&& cmd, Responder respond); +void handle_sequential_insert_public_data(WsdbRefContext& ctx, wire::WsdbSequentialInsertPublicData&& cmd, Responder respond); +void handle_sequential_insert_nullifier(WsdbRefContext& ctx, wire::WsdbSequentialInsertNullifier&& cmd, Responder respond); +void handle_update_archive(WsdbRefContext& ctx, wire::WsdbUpdateArchive&& cmd, Responder respond); +void handle_commit(WsdbRefContext& ctx, wire::WsdbCommit&& cmd, Responder respond); +void handle_rollback(WsdbRefContext& ctx, wire::WsdbRollback&& cmd, Responder respond); +void handle_sync_block(WsdbRefContext& ctx, wire::WsdbSyncBlock&& cmd, Responder respond); +void handle_create_fork(WsdbRefContext& ctx, wire::WsdbCreateFork&& cmd, Responder respond); +void handle_delete_fork(WsdbRefContext& ctx, wire::WsdbDeleteFork&& cmd, Responder respond); +void handle_finalize_blocks(WsdbRefContext& ctx, wire::WsdbFinalizeBlocks&& cmd, Responder respond); +void handle_unwind_blocks(WsdbRefContext& ctx, wire::WsdbUnwindBlocks&& cmd, Responder respond); +void handle_remove_historical_blocks(WsdbRefContext& ctx, wire::WsdbRemoveHistoricalBlocks&& cmd, Responder respond); +void handle_get_status(WsdbRefContext& ctx, wire::WsdbGetStatus&& cmd, Responder respond); +void handle_create_checkpoint(WsdbRefContext& ctx, wire::WsdbCreateCheckpoint&& cmd, Responder respond); +void handle_commit_checkpoint(WsdbRefContext& ctx, wire::WsdbCommitCheckpoint&& cmd, Responder respond); +void handle_revert_checkpoint(WsdbRefContext& ctx, wire::WsdbRevertCheckpoint&& cmd, Responder respond); +void handle_commit_all_checkpoints(WsdbRefContext& ctx, wire::WsdbCommitAllCheckpoints&& cmd, Responder respond); +void handle_revert_all_checkpoints(WsdbRefContext& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); +void handle_copy_stores(WsdbRefContext& ctx, wire::WsdbCopyStores&& cmd, Responder respond); +void handle_init(WsdbRefContext& ctx, wire::WsdbInit&& cmd, Responder respond); +// clang-format on + +} // namespace bb::wsdb_ref diff --git a/native-packages/wsdb/wsdb_schema.jsonc b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc similarity index 92% rename from native-packages/wsdb/wsdb_schema.jsonc rename to barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc index cea021eff17..e3fdf4f02d2 100644 --- a/native-packages/wsdb/wsdb_schema.jsonc +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc @@ -17,6 +17,14 @@ "blockNumber": "BlockNumber", "includeUncommitted": "bool" }, + "TreeHeight": { + "treeId": "MerkleTreeId", + "height": "u32" + }, + "TreePrefill": { + "treeId": "MerkleTreeId", + "size": "LeafIndex" + }, "TreeStateReference": { "treeId": "MerkleTreeId", "root": "Fr", @@ -472,6 +480,22 @@ "compact": "bool?" }, "response": {} + }, + // The first command: the genesis the world state starts from, before any block. Anything sent before it is + // undefined. treeHeights names each of the five trees once; treePrefill gives each indexed tree (nullifier, + // public data) its initial size, prefill included. What a second Init does is the service's own: wsdb-ref + // restarts at the new genesis. A service whose genesis is fixed when it is created (aztec-wsdb) answers Init + // with an error. + "Init": { + "request": { + "treeHeights": "TreeHeight[]", + "treePrefill": "TreePrefill[]", + "prefilledPublicData": "PublicDataLeafValue[]", + "prefilledNullifiers": "Fr[]", + "genesisTimestamp": "u64", + "initialHeaderGeneratorPoint": "u32" + }, + "response": {} } } } diff --git a/barretenberg/ts/.gitignore b/barretenberg/ts/.gitignore index ecde99a3073..177321fcb6f 100644 --- a/barretenberg/ts/.gitignore +++ b/barretenberg/ts/.gitignore @@ -10,6 +10,7 @@ bb.js/*.tsbuildinfo bb-avm-sim/ bb.js-api/ cdb/ +wsdb-ref/ .tsbuildinfo* *.tsbuildinfo *.log diff --git a/barretenberg/ts/.rebuild_patterns b/barretenberg/ts/.rebuild_patterns index 7c88bc2f15a..d7bc14a2e98 100644 --- a/barretenberg/ts/.rebuild_patterns +++ b/barretenberg/ts/.rebuild_patterns @@ -6,3 +6,4 @@ ^barretenberg/cpp/src/barretenberg/bbapi/bb_schema\.json$ ^barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants\.json$ ^barretenberg/cpp/src/barretenberg/cdb/cdb_schema\.json$ +^barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema\.jsonc$ diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index fe148f8f1c2..c47e3bc67c5 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -7,6 +7,9 @@ BB_AVM_SIM_BINARY=bb-avm-sim BB_AVM_SIM_PACKAGE=@aztec-foundation/bb-avm-sim CDB_PACKAGE=@aztec-foundation/cdb BB_JS_API_PACKAGE=@aztec-foundation/bb.js-api +WSDB_REF_PACKAGE=@aztec-foundation/wsdb-ref +WSDB_REF_ADDON=wsdb_ref_napi.node +WSDB_REF_BINARY=wsdb-ref hash=$(hash_str \ $(bb.js/bootstrap.sh hash) \ @@ -80,13 +83,34 @@ function generate_bb_js_api_package { --package-wasm-threads-module "barretenberg-threads.wasm=$ROOT/barretenberg/cpp/build-wasm-threads/bin/barretenberg.wasm" } -# bb-avm-sim, cdb and bb.js-api are gitignored workspaces declared in package.json, so +# The in-memory reference world state over the node world state's contract, fully generated: a spawned +# wsdb-ref process over a socket or shared memory (node), in-process through its wasm module (node and +# browsers), or in-process through its Node-API addon (node). All three run the dispatch the bb build +# generates from the same schema (cpp/.../wsdb_ref). +function generate_wsdb_ref_package { + node --experimental-strip-types --no-warnings \ + "$ROOT/ipc-codegen/src/generate.ts" \ + --schema "$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" \ + --lang ts \ + --package "$ROOT/barretenberg/ts/wsdb-ref" \ + --ffi-name wsdb_ref \ + --package-name "$WSDB_REF_PACKAGE" \ + --package-transports uds,shm,napi,wasm \ + --package-ipc-path-args 'msgpack,run,--input,{path}' \ + --binary-name "$WSDB_REF_BINARY" \ + --package-napi-addon "$WSDB_REF_ADDON" \ + --package-napi-addon-env-var WSDB_REF_NAPI_PATH \ + --package-wasm-module "wsdb-ref.wasm=$ROOT/barretenberg/cpp/build-wasm/bin/wsdb-ref.wasm" +} + +# bb-avm-sim, cdb, bb.js-api and wsdb-ref are gitignored workspaces declared in package.json, so # `yarn install --immutable` fails against the committed lockfile unless all exist. # Generate them together before installing, whichever one we're about to build. function generate_packages { generate_bb_avm_sim_package generate_cdb_package generate_bb_js_api_package + generate_wsdb_ref_package } function copy_bb_js_api_native { @@ -163,6 +187,44 @@ function build_bb_js { (cd bb.js && ./bootstrap.sh) } +function copy_wsdb_ref_native { + local target_dir="wsdb-ref/build/$(arch)-$(os)" + mkdir -p "$target_dir" + cp "$ROOT/barretenberg/cpp/build/lib/$WSDB_REF_ADDON" "$target_dir/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build/bin/$WSDB_REF_BINARY" "$target_dir/$WSDB_REF_BINARY" +} + +function copy_wsdb_ref_cross { + if [ -n "${1:-}" ]; then + local cross_arch="$1" + mkdir -p "wsdb-ref/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/$WSDB_REF_BINARY" "wsdb-ref/build/$cross_arch/$WSDB_REF_BINARY" + elif semver check "${REF_NAME:-}" && [ "$(arch)" == "amd64" ]; then + for cross_arch in arm64-linux amd64-macos arm64-macos; do + mkdir -p "wsdb-ref/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/$WSDB_REF_BINARY" \ + "wsdb-ref/build/$cross_arch/$WSDB_REF_BINARY" + done + else + echo "This task is expected to be run with an explicit arch or in an x86 release context." + fi +} + +function prepare_wsdb_ref_arch_packages { + yarn workspace "$WSDB_REF_PACKAGE" run prepare_arch_packages "$@" +} + +function build_wsdb_ref { + echo_header "wsdb-ref package build" + generate_packages + copy_wsdb_ref_native + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages +} + function build_bb_avm_sim { echo_header "bb-avm-sim package build" generate_packages @@ -224,13 +286,19 @@ function build { build_cdb } -# `test_cmds bb_avm_sim` lists the bb-avm-sim package's tests alone: they need its build -# (bb-avm-sim), which bb.js's tests do not. +# `test_cmds bb_avm_sim` and `test_cmds wsdb_ref` list those packages' tests alone: they need their +# builds (bb-avm-sim, bb-wsdb-ref), which bb.js's tests do not. function test_cmds { case "${1:-}" in bb_avm_sim) echo "$hash barretenberg/ts/scripts/bb_avm_sim_test.sh" ;; + wsdb_ref) + local backend + for backend in process-uds process-shm napi wasm; do + echo "$hash barretenberg/ts/scripts/wsdb_ref_test.sh $backend" + done + ;; *) (cd bb.js && ./bootstrap.sh test_cmds) ;; @@ -250,6 +318,14 @@ function cross_copy_bb_js { cross_copy_bb_js_api "$@" } +function cross_copy_wsdb_ref { + generate_packages + copy_wsdb_ref_cross "$@" + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages +} + function cross_copy_bb_avm_sim { generate_packages copy_bb_avm_sim_cross "$@" @@ -279,6 +355,12 @@ function get_projects { if [ -d cdb ]; then echo "$PWD/cdb" fi + if [ -d wsdb-ref ]; then + for package_dir in wsdb-ref/packages/*; do + [ -d "$package_dir" ] && echo "$PWD/$package_dir" + done + echo "$PWD/wsdb-ref" + fi } function release_bb_avm_sim { @@ -294,6 +376,22 @@ function release_bb_avm_sim { (cd bb-avm-sim && retry "deploy_npm ${REF_NAME#v}") } +function release_wsdb_ref { + generate_packages + copy_wsdb_ref_native + copy_wsdb_ref_cross + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages + local package_dir + for package_dir in wsdb-ref/packages/*; do + # A platform whose addon was not built has only the generated manifest: nothing to publish. + [ -f "$package_dir/$WSDB_REF_ADDON" ] || continue + (cd "$package_dir" && retry "deploy_npm ${REF_NAME#v}") + done + (cd wsdb-ref && retry "deploy_npm ${REF_NAME#v}") +} + function release_cdb { generate_packages npm_install_deps "$IPC_RUNTIME_PKG" @@ -328,12 +426,14 @@ function release { (cd bb.js && ./bootstrap.sh release) release_bb_avm_sim release_cdb + release_wsdb_ref release_bb_bin } export -f generate_bb_avm_sim_package copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages export -f generate_bb_js_api_package copy_bb_js_api_native copy_bb_js_api_cross prepare_bb_js_api_arch_packages build_bb_js_api_ts build_bb_js_api export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim cross_copy_bb_js_api release release_cdb release_bb_js_api +export -f generate_wsdb_ref_package copy_wsdb_ref_native copy_wsdb_ref_cross prepare_wsdb_ref_arch_packages build_wsdb_ref cross_copy_wsdb_ref release_wsdb_ref case "$cmd" in "") diff --git a/barretenberg/ts/package.json b/barretenberg/ts/package.json index ab6588c9187..022fc779009 100644 --- a/barretenberg/ts/package.json +++ b/barretenberg/ts/package.json @@ -12,7 +12,9 @@ "bb.js-api/packages/*", "bb-avm-sim", "bb-avm-sim/packages/*", - "cdb" + "cdb", + "wsdb-ref", + "wsdb-ref/packages/*" ], "resolutions": { "@aztec-foundation/ipc-runtime": "portal:../../ipc-runtime/ts" diff --git a/barretenberg/ts/scripts/wsdb_ref.test.mjs b/barretenberg/ts/scripts/wsdb_ref.test.mjs new file mode 100644 index 00000000000..32c9c206900 --- /dev/null +++ b/barretenberg/ts/scripts/wsdb_ref.test.mjs @@ -0,0 +1,162 @@ +// Tests of the generated @aztec-foundation/wsdb-ref package against one backend, from its build. +// Run through wsdb_ref_test.sh, which sets WSDB_REF_TEST_BACKEND to process-uds, process-shm (a spawned +// wsdb-ref binary over a socket or shared memory), napi or wasm. +// +// Every napi backend in a process shares one world state (node loads the addon once), so the +// tests read the genesis through committed reads, which writes never change, and write only fresh +// random keys. Each service is sent Init first, with the test's own genesis. +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { after, before, test } from 'node:test'; + +import { WsdbError, WsdbService, WsdbServiceSync } from '@aztec-foundation/wsdb-ref'; + +const BACKENDS = { + 'process-uds': { backend: 'process', process: { transport: 'uds' } }, + 'process-shm': { backend: 'process', process: { transport: 'shm' } }, + napi: { backend: 'napi' }, + wasm: { backend: 'wasm' }, +}; +const options = BACKENDS[process.env.WSDB_REF_TEST_BACKEND]; +assert.ok(options, `WSDB_REF_TEST_BACKEND must be one of ${Object.keys(BACKENDS).join(', ')}`); + +const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); +const constantsNr = readFileSync( + join(root, 'noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr'), + 'utf8', +); +function protocolU32(name) { + const match = constantsNr.match(new RegExp(`pub global ${name}: u32 = (\\d+);`)); + assert.ok(match, `${name} not found in constants.nr`); + return Number(match[1]); +} + +// The test's own genesis: protocol-sized trees with no prefilled leaves. The canonical genesis, with the protocol +// contracts' nullifiers and the roots constants.nr records, is checked by wsdb-ref's C++ tests. +const GENESIS = { + treeHeights: [ + { treeId: 0, height: 42 }, + { treeId: 1, height: 42 }, + { treeId: 2, height: 40 }, + { treeId: 3, height: 36 }, + { treeId: 4, height: 30 }, + ], + treePrefill: [ + { treeId: 0, size: 128 }, + { treeId: 2, size: 128 }, + ], + prefilledPublicData: [], + prefilledNullifiers: [], + genesisTimestamp: 0, + initialHeaderGeneratorPoint: protocolU32('DOM_SEP__BLOCK_HEADER_HASH'), +}; + +const NULLIFIER_TREE = 0; +const NOTE_HASH_TREE = 1; +const ARCHIVE = 4; +const LATEST = 0xffffffff; +const committed = { forkId: 0, blockNumber: LATEST, includeUncommitted: false }; +const uncommitted = { forkId: 0, blockNumber: LATEST, includeUncommitted: true }; + +const toBigInt = bytes => BigInt('0x' + Buffer.from(bytes).toString('hex')); +// A random field element: 31 random bytes stay below the modulus. +const randomFr = () => new Uint8Array([0, ...randomBytes(31)]); + +let service; +let genesisArchiveRoot; +before(async () => { + service = await WsdbService.create(options); + await service.init(GENESIS); +}); +after(async () => { + await service.destroy(); +}); + +test('serves the genesis Init gave it', async () => { + const nullifiers = await service.getTreeInfo({ treeId: NULLIFIER_TREE, revision: committed }); + assert.equal(nullifiers.size, 128); + assert.equal(nullifiers.depth, 42); + + const archive = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(archive.size, 1); + assert.equal(archive.depth, 30); + genesisArchiveRoot = toBigInt(archive.root); + + const header = await service.getLeafValue({ treeId: ARCHIVE, revision: committed, leafIndex: 0 }); + assert.ok(header.value, 'the genesis block header is in the archive'); + + const { state } = await service.getInitialStateReference({}); + assert.deepEqual(state.map(t => t.treeId).sort(), [0, 1, 2, 3]); +}); + +test('appended leaves are read back with membership witnesses', async () => { + const leaf = randomFr(); + await service.appendLeaves({ treeId: NOTE_HASH_TREE, leaves: [leaf], forkId: 0 }); + const { + indices: [index], + } = await service.findLeafIndices({ treeId: NOTE_HASH_TREE, revision: uncommitted, leaves: [leaf], startIndex: 0 }); + assert.equal(typeof index, 'number'); + const { value } = await service.getLeafValue({ treeId: NOTE_HASH_TREE, revision: uncommitted, leafIndex: index }); + assert.deepEqual(Buffer.from(value), Buffer.from(leaf)); + const { path } = await service.getSiblingPath({ treeId: NOTE_HASH_TREE, revision: uncommitted, leafIndex: index }); + assert.equal(path.length, 42); + // Committed reads still see the genesis. + const { indices } = await service.findLeafIndices({ + treeId: NOTE_HASH_TREE, + revision: committed, + leaves: [leaf], + startIndex: 0, + }); + assert.deepEqual(indices, [null]); +}); + +test('nullifier inserts return witnesses, and duplicates are the service saying no', async () => { + const nullifier = randomFr(); + const { result } = await service.sequentialInsertNullifier({ leaves: [{ nullifier }], forkId: 0 }); + assert.equal(result.lowLeafWitnessData.length, 1); + assert.equal(result.insertionWitnessData.length, 1); + const low = await service.findLowLeaf({ treeId: NULLIFIER_TREE, revision: uncommitted, key: nullifier }); + assert.equal(low.alreadyPresent, true); + + await assert.rejects(service.sequentialInsertNullifier({ leaves: [{ nullifier }], forkId: 0 }), error => { + assert.ok(error instanceof WsdbError); + assert.match(error.message, /is already present/); + return true; + }); +}); + +test('forks are separate world states, and bad requests are errors, not crashes', async () => { + const { forkId } = await service.createFork({ latest: true, blockNumber: 0 }); + await service.appendLeaves({ treeId: NOTE_HASH_TREE, leaves: [randomFr()], forkId }); + const onFork = await service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId } }); + const canonical = await service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: committed }); + assert.equal(onFork.size, canonical.size + 1); + await service.deleteFork({ forkId }); + + await assert.rejects( + service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId } }), + /Fork not found/, + ); + await assert.rejects( + service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, blockNumber: 7 } }), + /latest block is 0/, + ); + // The service still answers afterwards (a wasm module that aborted would not). + const info = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(info.size, 1); +}); + +test('the synchronous service, given the same Init, reaches the same genesis', async () => { + // A synchronous process client is always over shared memory. + const sync = await WsdbServiceSync.create({ backend: options.backend }); + try { + sync.init(GENESIS); + const archive = sync.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(toBigInt(archive.root), genesisArchiveRoot); + } finally { + sync.destroy(); + } +}); diff --git a/barretenberg/ts/scripts/wsdb_ref_test.sh b/barretenberg/ts/scripts/wsdb_ref_test.sh new file mode 100755 index 00000000000..a146bdc9ebe --- /dev/null +++ b/barretenberg/ts/scripts/wsdb_ref_test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Runs the @aztec-foundation/wsdb-ref package tests against one backend: process-uds, process-shm, napi or wasm. +# Needs the package built (make bb-wsdb-ref, or ./bootstrap.sh build_wsdb_ref here). +set -euo pipefail +backend=${1:?usage: wsdb_ref_test.sh } +cd "$(dirname "$0")/.." +WSDB_REF_TEST_BACKEND="$backend" node --test --test-reporter=spec scripts/wsdb_ref.test.mjs diff --git a/barretenberg/ts/yarn.lock b/barretenberg/ts/yarn.lock index ef513e4233d..63d65c5327c 100644 --- a/barretenberg/ts/yarn.lock +++ b/barretenberg/ts/yarn.lock @@ -169,6 +169,57 @@ __metadata: languageName: node linkType: soft +"@aztec-foundation/wsdb-ref-darwin-arm64@npm:0.1.0, @aztec-foundation/wsdb-ref-darwin-arm64@workspace:wsdb-ref/packages/wsdb-ref-darwin-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-darwin-arm64@workspace:wsdb-ref/packages/wsdb-ref-darwin-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-darwin-x64@npm:0.1.0, @aztec-foundation/wsdb-ref-darwin-x64@workspace:wsdb-ref/packages/wsdb-ref-darwin-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-darwin-x64@workspace:wsdb-ref/packages/wsdb-ref-darwin-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-linux-arm64@npm:0.1.0, @aztec-foundation/wsdb-ref-linux-arm64@workspace:wsdb-ref/packages/wsdb-ref-linux-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-linux-arm64@workspace:wsdb-ref/packages/wsdb-ref-linux-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-linux-x64@npm:0.1.0, @aztec-foundation/wsdb-ref-linux-x64@workspace:wsdb-ref/packages/wsdb-ref-linux-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-linux-x64@workspace:wsdb-ref/packages/wsdb-ref-linux-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref@workspace:wsdb-ref": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref@workspace:wsdb-ref" + dependencies: + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" + "@aztec-foundation/wsdb-ref-darwin-arm64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-darwin-x64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-linux-arm64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-linux-x64": "npm:0.1.0" + "@types/node": "npm:^22.15.17" + msgpackr: "npm:^1.11.2" + tslib: "npm:^2.4.0" + typescript: "npm:^5.3.3" + dependenciesMeta: + "@aztec-foundation/wsdb-ref-darwin-arm64": + optional: true + "@aztec-foundation/wsdb-ref-darwin-x64": + optional: true + "@aztec-foundation/wsdb-ref-linux-arm64": + optional: true + "@aztec-foundation/wsdb-ref-linux-x64": + optional: true + bin: + wsdb-ref: ./dest/bin.js + languageName: unknown + linkType: soft + "@aztec/barretenberg-ts-packages@workspace:.": version: 0.0.0-use.local resolution: "@aztec/barretenberg-ts-packages@workspace:." diff --git a/ipc-codegen/README.md b/ipc-codegen/README.md index 3b4730433da..42b003d1ad4 100644 --- a/ipc-codegen/README.md +++ b/ipc-codegen/README.md @@ -106,10 +106,12 @@ node --experimental-strip-types --no-warnings \ |---|---| | `--server` | Emit server dispatch (matches request name to handler, deserialises, calls handler, serialises response). Pair it with an `ipc::IpcServer` from ipc-runtime. | | `--client` | Emit the typed client: one method per command over a byte backend, the same shape in every language — `Api` (Rust), `Api` (C++), `Client(Backend)` (Zig), `AsyncApi`/`SyncApi` over `IpcClientAsync`/`IpcClientSync` (TS). A backend takes the msgpack request bytes and returns the response bytes: ipc-runtime's transport (C++: `ipc_codegen::IpcBackend`), the service in-process (`--ffi`), or your own. | -| `--package ` | TS only. Emit a complete client package: the generated API over every backend the package has (`--package-transports`), with a `Service.create(options)` that picks one by default (the spawned process when the binary resolves, else the wasm module), can be forced to one, or takes a backend object of the consumer's own; one entry per host (node, `browser`, `react-native`) so each host sees only the backends that exist there; the binary resolved from an override path, an environment variable, or the installed per-platform arch package (this package's optional dependencies). With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. | +| `--package ` | TS only. Emit a complete client package: the generated API over every backend the package has (`--package-transports`), with a `Service.create(options)` that picks one by default (the first of the spawned process, the napi addon and the wasm module that is available, falling back along that order), can be forced to one, or takes a backend object of the consumer's own; one entry per host (node, `browser`, `react-native`) so each host sees only the backends that exist there; the binary resolved from an override path, an environment variable, or the installed per-platform arch package (this package's optional dependencies). With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. | | `--uds` | Rust/Zig only. Copies the `Backend` trait template (and `error.rs` for Rust) into `` so consumers can plug ipc-runtime — or any custom transport — behind the generated client. The flag name is historical: the trait is transport-agnostic. | | `--ffi` | In-process FFI, both directions of the contract in `SCHEMA_SPEC.md` ("FFI entry"). With `--client` (Rust/Zig/C++): generates the FFI backend (`ffi_backend.{rs,zig}`, `_ffi_backend.hpp`), which calls a linked library's `_ipc_ffi_entry`. With `--server` (Rust/Zig/C++): emits the entry itself — `_ffi.rs` with an `export__ffi!` macro over a `Handler + Default` type, `_ffi.zig` with `exportFfi(Handler)` over a default-initialised handler, or `_ffi.{hpp,cpp}` defining `_ipc_ffi_entry`/`_alloc`/`_free` over the generated dispatch (the service defines `ipc_ffi_dispatcher()`). Each example's FFI test runs the generated client over the FFI backend against its own entry, in-process. The service prefix on the symbols lets several services be linked into one binary. A wasm reactor built from these is what the TS `wasm` transport runs. | -| `--package-transports ` | TS `--package` only. Comma-separated transports the package offers: `uds`, `shm` (spawned process) and `wasm` (the service's wasm module, run in-process through `@aztec-foundation/ipc-runtime/wasm`; adds a browser entry). | +| `--napi` | C++ with `--server --ffi`. Also emits `_napi.cpp`, a Node-API addon over the FFI entry: `call(bytes)` on the calling thread and `callAsync(bytes)` on the libuv thread pool, serialized as the entry requires. It needs only the FFI header and `node-addon-api`; build it as a shared library named `*.node`. It is what the TS `napi` transport loads. | +| `--package-transports ` | TS `--package` only. Comma-separated transports the package offers: `uds`, `shm` (spawned process), `napi` (the service's Node-API addon, run in-process in node) and `wasm` (the service's wasm module, run in-process through `@aztec-foundation/ipc-runtime/wasm`; adds a browser entry). A package without `uds`/`shm` has no binary and no `bin`. | +| `--package-napi-addon `, `--package-napi-addon-env-var ` | TS `napi` transport. File name of the addon the owning project builds from `--napi` and stages into the arch packages (listed under `ipcRuntime.nativeFiles` in `package.json`, which `ipc-runtime-prepare-arch-packages` reads), and the environment variable that overrides its path (default `_PATH`). Node loads an addon once per process, so every napi backend in a process shares one service context. | | `--package-wasm-module =`, `--package-wasm-threads-module =` | TS `wasm` transport. The single-thread and threads builds at ``, installed into the package's `wasm/` directory as ``; at least one is required. Generation reads each module's memory import and records it in the package, since a host must create an imported memory matching it and JS cannot read it from a compiled module. | | `--package-wasm-wide-module =`, `--package-wasm-wide-threads-module =` | TS `wasm` transport. Optional wide-arithmetic builds of the two, installed the same way; the package runs one in place of its counterpart where the engine validates the wide-arithmetic proposal. | @@ -272,6 +274,11 @@ close to what it describes and tracks with that code. Whenever the wire contract changes, edit the schema, regenerate the bindings, and commit the diff. Both sides regenerate from the same committed schema, so they stay byte-compatible. +A schema may `imports` other schemas' aliases and types, so a type is written +once, and may `extends` another service's schema, becoming a strict superset of +its contract (see `SCHEMA_SPEC.md`). Code typed against a parent schema's +generated interface then accepts the client of any schema that extends it. + Each generated file embeds a `SCHEMA_HASH` (a hash of the committed schema) so callers can detect at connection time that their bindings predate the server. diff --git a/ipc-codegen/SCHEMA_SPEC.md b/ipc-codegen/SCHEMA_SPEC.md index 213964104d2..fa5dbcbbe89 100644 --- a/ipc-codegen/SCHEMA_SPEC.md +++ b/ipc-codegen/SCHEMA_SPEC.md @@ -224,10 +224,13 @@ void* _ipc_ffi_alloc(size_t size); void _ipc_ffi_free(void* ptr); ``` -`` is the schema's `service` in snake_case (`Bb` → `bb_ipc_ffi_entry`; -a positional schema uses the generator's type prefix, or the bare names when -there is none). The prefix is what lets several services be linked into one -binary. `input` is exactly the request payload above — no framing envelope, +`` is the implementation's FFI name: by default the schema's `service` in +snake_case (`Bb` → `bb_ipc_ffi_entry`; a positional schema uses the generator's +type prefix, or the bare names when there is none). The prefix is what lets +several services be linked into one binary. The schema's `service` names the wire +protocol, so when several implementations serve one schema each passes its own +`--ffi-name` (`wsdb_ref_ipc_ffi_entry`, `wsdb_labs_ipc_ffi_entry`) and they can +be linked together. `input` is exactly the request payload above — no framing envelope, since an in-process call has no transport — and `*output` receives the response payload in a buffer from `_ipc_ffi_alloc()` that the caller releases with `_ipc_ffi_free()` (both malloc/free-compatible). Input buffers the caller diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index f00f991ab63..503ca1634eb 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -32,6 +32,8 @@ export interface CppCodegenOptions { * and the server dispatch deserializes into wire types then converts to domain types. */ wireNamespace?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class CppCodegen { @@ -700,12 +702,13 @@ void serve(const std::string& input_path, Ctx& ctx) * symbols, and the one hook the service defines to hand over its dispatcher. */ /** - * Exported symbol names carry the service (`bb_ipc_ffi_entry`), so libraries of several services - * can be linked into one binary. A schema with no service/prefix keeps the bare names. + * Exported symbol names carry the implementation's FFI name (`bb_ipc_ffi_entry`), so libraries of + * several services, or several implementations of one schema, can be linked into one binary. A + * schema with no service/prefix and no FFI name keeps the bare names. */ private ffiSymbol(name: string): string { - const prefix = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; - return `${prefix}${name}`; + const ffiName = this.opts.ffiName ?? toSnakeCase(this.opts.prefix); + return ffiName ? `${ffiName}_${name}` : name; } generateFfiHeader(): string { @@ -819,6 +822,129 @@ IPC_FFI_EXPORT void ${entry}(const uint8_t* input, size_t input_len, uint8_t** o *output = out; *output_len = response.size(); } +`; + } + + /** + * A Node-API addon over the FFI entry (generateFfiHeader/generateFfiSource): the in-process + * native backend a generated TS package's `napi` transport loads. It adds nothing to the + * contract — bytes in, bytes out through `_ipc_ffi_entry` — so it depends only on + * the FFI header, never on the service's types or handlers. + */ + generateNapiSource(): string { + const { prefix } = this.opts; + const ffiHeader = `${toSnakeCase(prefix)}_ffi.hpp`; + const entry = this.ffiSymbol("ipc_ffi_entry"); + const free = this.ffiSymbol("ipc_ffi_free"); + const moduleName = `${toSnakeCase(prefix) || "ipc"}_napi`; + + return `// AUTOGENERATED FILE - DO NOT EDIT +// Node-API addon for ${prefix}: the in-process FFI entry exposed to JavaScript. +// +// call(input: Uint8Array): Buffer runs on the calling thread +// callAsync(input: Uint8Array): Promise runs on the libuv thread pool +// +// The FFI entry is not thread-safe, so every call, from either form, holds one mutex: async +// calls queue behind each other and behind any synchronous call in progress. +#include "${ffiHeader}" + +#include + +#include +#include +#include +#include + +namespace { + +std::mutex& entry_mutex() +{ + static std::mutex mutex; + return mutex; +} + +std::vector run_entry(const uint8_t* input, size_t input_len) +{ + uint8_t* output = nullptr; + size_t output_len = 0; + { + std::lock_guard lock(entry_mutex()); + ${entry}(input, input_len, &output, &output_len); + } + std::vector response(output, output + output_len); + ${free}(output); + return response; +} + +Napi::Uint8Array input_arg(const Napi::CallbackInfo& info) +{ + if (info.Length() != 1 || !info[0].IsTypedArray() || + info[0].As().TypedArrayType() != napi_uint8_array) { + NAPI_THROW(Napi::TypeError::New(info.Env(), "expected one Uint8Array argument"), Napi::Uint8Array()); + } + return info[0].As(); +} + +Napi::Value call(const Napi::CallbackInfo& info) +{ + Napi::Uint8Array input = input_arg(info); + if (info.Env().IsExceptionPending()) { + return info.Env().Undefined(); + } + std::vector response = run_entry(input.Data(), input.ByteLength()); + return Napi::Buffer::Copy(info.Env(), response.data(), response.size()); +} + +class CallWorker : public Napi::AsyncWorker { + public: + CallWorker(Napi::Env env, const Napi::Uint8Array& input) + : Napi::AsyncWorker(env) + , deferred_(Napi::Promise::Deferred::New(env)) + , input_(input.Data(), input.Data() + input.ByteLength()) + {} + + Napi::Promise promise() const { return deferred_.Promise(); } + + void Execute() override { response_ = run_entry(input_.data(), input_.size()); } + + void OnOK() override + { + deferred_.Resolve(Napi::Buffer::Copy(Env(), response_.data(), response_.size())); + } + + void OnError(const Napi::Error& error) override { deferred_.Reject(error.Value()); } + + private: + Napi::Promise::Deferred deferred_; + // Copied: the caller's buffer may be reused or collected before the worker runs. + std::vector input_; + std::vector response_; +}; + +Napi::Value call_async(const Napi::CallbackInfo& info) +{ + Napi::Uint8Array input = input_arg(info); + if (info.Env().IsExceptionPending()) { + return info.Env().Undefined(); + } + auto* worker = new CallWorker(info.Env(), input); + Napi::Promise promise = worker->promise(); + worker->Queue(); // the worker deletes itself once it has settled the promise + return promise; +} + +Napi::Object init(Napi::Env env, Napi::Object exports) +{ + exports.Set("service", Napi::String::New(env, "${prefix}")); + exports.Set("call", Napi::Function::New(env, call, "call")); + exports.Set("callAsync", Napi::Function::New(env, call_async, "callAsync")); + return exports; +} + +} // namespace + +// NOLINTNEXTLINE +NODE_API_MODULE(${moduleName}, init) `; } } diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index 3e1fdcb5cec..dfd3c951391 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -74,6 +74,8 @@ interface Args { packageTransports: string; packageIpcPathArgs: string; packageWasmModule: string; + packageNapiAddon: string; + packageNapiAddonEnvVar: string; packageWasmThreadsModule: string; packageWasmWideModule: string; packageWasmWideThreadsModule: string; @@ -83,6 +85,8 @@ interface Args { cppIncludeDir: string; uds: boolean; ffi: boolean; + napi: boolean; + ffiName: string; stripMethodPrefix: boolean; stripTypePrefix: boolean; } @@ -106,7 +110,7 @@ Optional: --package-name TS package name for --package --binary-name Native service binary name for --package --binary-env-var Env var overriding the binary path for --package - --package-transports Comma-separated transports for --package (uds,shm,wasm) + --package-transports Comma-separated transports for --package (uds,shm,napi,wasm) --package-ipc-path-args Comma-separated binary args for IPC path; use {path} --package-wasm-module = @@ -121,6 +125,12 @@ Optional: --package-wasm-wide-module =, --package-wasm-wide-threads-module = wasm transport: wide-arithmetic builds of the two, likewise, run instead where the engine supports the proposal + --package-napi-addon + napi transport: file name of the Node-API addon built from + the C++ --napi output, shipped in the arch packages + --package-napi-addon-env-var + napi transport: env var overriding the addon path + (default: _PATH) --ipc-runtime-dependency package.json dependency spec for @aztec-foundation/ipc-runtime --prefix Type prefix (auto-detected when >= 2 commands share one) @@ -133,6 +143,13 @@ Optional: --ffi In-process FFI (rust, zig, cpp). With --client: the FFI client backend. With --server: the exported FFI entry (_ipc_ffi_entry) over the dispatch + --ffi-name The name the FFI symbols carry (_ipc_ffi_entry, and the + wasm export a TS package's wasm transport calls). Defaults + to the schema's service in snake_case; set it when several + implementations serve one schema, so each exports its own + --napi With --server --ffi (cpp): also emit _napi.cpp, a + Node-API addon over the FFI entry, for a TS package's + napi transport (--package-transports napi) --cpp-namespace C++ namespace (e.g. my::ns) --cpp-wire-namespace Wire types sub-namespace (default: wire) --cpp-include-dir Include path for generated dir (e.g. myservice/generated) @@ -155,6 +172,8 @@ function parseArgs(argv: string[]): Args { packageTransports: "uds", packageIpcPathArgs: "--socket,{path}", packageWasmModule: "", + packageNapiAddon: "", + packageNapiAddonEnvVar: "", packageWasmThreadsModule: "", packageWasmWideModule: "", packageWasmWideThreadsModule: "", @@ -164,6 +183,8 @@ function parseArgs(argv: string[]): Args { cppIncludeDir: "", uds: false, ffi: false, + napi: false, + ffiName: "", stripMethodPrefix: false, stripTypePrefix: false, }; @@ -215,6 +236,12 @@ function parseArgs(argv: string[]): Args { case "--package-ipc-path-args": args.packageIpcPathArgs = takeValue(); break; + case "--package-napi-addon": + args.packageNapiAddon = takeValue(); + break; + case "--package-napi-addon-env-var": + args.packageNapiAddonEnvVar = takeValue(); + break; case "--package-wasm-module": args.packageWasmModule = takeValue(); break; @@ -245,6 +272,12 @@ function parseArgs(argv: string[]): Args { case "--ffi": args.ffi = true; break; + case "--napi": + args.napi = true; + break; + case "--ffi-name": + args.ffiName = takeValue(); + break; case "--strip-method-prefix": args.stripMethodPrefix = true; break; @@ -290,6 +323,12 @@ function parseArgs(argv: string[]): Args { ); process.exit(1); } + if (args.napi && !(args.lang === "cpp" && args.server && args.ffi)) { + console.error( + `--napi emits a Node-API addon over the C++ FFI entry; pass --lang cpp --server --ffi`, + ); + process.exit(1); + } if (args.ffi && !args.server && !args.client) { console.error( `--ffi emits the client FFI backend (--client) and/or the exported FFI entry (--server)`, @@ -483,6 +522,9 @@ function generate(args: Args) { // the bare command. Positional schemas keep the legacy --prefix/--strip flags. const prefix = service || args.prefix || detectPrefix(compiled); const stripMethodPrefix = service ? true : args.stripMethodPrefix; + // The schema's service names the wire protocol (it prefixes every tag), so it is shared by every + // implementation of the schema. The FFI symbols name one implementation. + const ffiName = args.ffiName || toSnakeCase(prefix); console.log( `Schema: ${absSchema} (${compiled.commands.length} commands, prefix=${prefix})`, @@ -563,13 +605,29 @@ function generate(args: Args) { // generated from, and lets consumers regenerate bindings. writePackage(schemaFileName, readFileSync(absSchema, "utf-8")); } else { - const binaryName = - args.binaryName || toSnakeCase(prefix).replace(/_/g, "-"); const transports = args.packageTransports .split(",") .map((t) => t.trim()) .filter(Boolean); + const known = ["uds", "shm", "napi", "wasm"]; + const unknown = transports.filter((t) => !known.includes(t)); + if (unknown.length > 0) { + console.error( + `--package-transports: unknown transport(s) ${unknown.join(", ")} (known: ${known.join(", ")})`, + ); + process.exit(1); + } const wasm = transports.includes("wasm"); + const napi = transports.includes("napi"); + const spawned = transports.some((t) => t === "uds" || t === "shm"); + // Only a package that spawns the service has a binary: no bin launcher otherwise. + const binaryName = spawned + ? args.binaryName || toSnakeCase(prefix).replace(/_/g, "-") + : ""; + if (napi && !args.packageNapiAddon) { + console.error(`--package-transports napi needs --package-napi-addon`); + process.exit(1); + } if ( wasm && !args.packageWasmModule && @@ -614,9 +672,12 @@ function generate(args: Args) { } const packageGen = new TypeScriptPackageCodegen({ prefix, + ffiName, packageName, binaryName, - binaryEnvVar: args.binaryEnvVar || defaultBinaryEnvVar(binaryName), + binaryEnvVar: binaryName + ? args.binaryEnvVar || defaultBinaryEnvVar(binaryName) + : "", ipcRuntimeDependency: args.ipcRuntimeDependency, transports, ipcPathArgs: args.packageIpcPathArgs @@ -627,6 +688,11 @@ function generate(args: Args) { wasmThreadsModule: wasmModules.threads, wasmWideModule: wasmModules.wideSingle, wasmWideThreadsModule: wasmModules.wideThreads, + napiAddon: napi ? args.packageNapiAddon : undefined, + napiAddonEnvVar: napi + ? args.packageNapiAddonEnvVar || + defaultBinaryEnvVar(args.packageNapiAddon.replace(/\.node$/, "")) + : undefined, }); writePackage("package.json", packageGen.generatePackageJson()); writePackage("tsconfig.json", packageGen.generateTsconfig()); @@ -636,10 +702,15 @@ function generate(args: Args) { "src/react-native.ts", packageGen.generateReactNativeIndex(), ); - writePackage("src/platform.ts", packageGen.generatePlatform()); - if (transports.some((t) => t !== "wasm")) { + if (spawned || napi) { + writePackage("src/platform.ts", packageGen.generatePlatform()); + } + if (spawned) { writePackage("src/process.ts", packageGen.generateProcess()); } + if (napi) { + writePackage("src/napi.ts", packageGen.generateNapi()); + } if (binaryName) { writePackage("src/bin.ts", packageGen.generateBin()); } @@ -657,6 +728,7 @@ function generate(args: Args) { case "rust": { const gen = new RustCodegen({ prefix, + ffiName, stripMethodPrefix: stripMethodPrefix, stripTypePrefix: args.stripTypePrefix, }); @@ -695,6 +767,7 @@ function generate(args: Args) { case "zig": { const gen = new ZigCodegen({ prefix, + ffiName, clientName: `${prefix}Client`, stripMethodPrefix: stripMethodPrefix, }); @@ -738,6 +811,7 @@ function generate(args: Args) { const gen = new CppCodegen({ namespace: ns, prefix, + ffiName, wireNamespace: wireNs, generatedIncludeDir: args.cppIncludeDir, stripMethodPrefix: stripMethodPrefix, @@ -770,6 +844,14 @@ function generate(args: Args) { gen.generateFfiHeader(), ), ); + if (args.napi) { + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_napi.cpp`, + gen.generateNapiSource(), + ), + ); + } cppFiles.push( writeFile( `${toSnakeCase(prefix)}_ffi.cpp`, diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts index 305546b1bd3..41e5eb9e732 100644 --- a/ipc-codegen/src/rust_codegen.ts +++ b/ipc-codegen/src/rust_codegen.ts @@ -30,6 +30,8 @@ export interface RustCodegenOptions { typesDocComment?: string; /** Module doc comment for api file */ apiDocComment?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class RustCodegen { @@ -60,6 +62,7 @@ export class RustCodegen { prefix, stripMethodPrefix: options?.stripMethodPrefix ?? false, stripTypePrefix: options?.stripTypePrefix ?? false, + ffiName: options?.ffiName ?? toSnakeCase(prefix), apiStructName: options?.apiStructName ?? `${name}Api`, backendImport: options?.backendImport ?? "super::backend::Backend", errorImport: options?.errorImport ?? `super::error::{IpcError, Result}`, @@ -927,8 +930,7 @@ pub fn handle_request(handler: &mut dyn Handler, request_bytes: &[u8]) -> Vec_ipc_ffi_entry`. */ + ffiName: string; packageName: string; binaryName: string; binaryEnvVar: string; @@ -23,6 +25,10 @@ export interface TypeScriptPackageOptions { /** wasm transport: wide-arithmetic builds of the two, run instead where the engine supports it. */ wasmWideModule?: PackageWasmModule; wasmWideThreadsModule?: PackageWasmModule; + /** napi transport: file name of the Node-API addon over the FFI entry, shipped in the arch packages. */ + napiAddon?: string; + /** napi transport: environment variable that overrides the addon's path. */ + napiAddonEnvVar?: string; } function className(prefix: string): string { @@ -33,6 +39,10 @@ function binaryFinderName(prefix: string): string { return `find${prefix}Binary`; } +function addonFinderName(prefix: string): string { + return `find${prefix}NapiAddon`; +} + function envName(binaryName: string): string { return `${binaryName.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_PATH`; } @@ -202,8 +212,29 @@ export class TypeScriptPackageCodegen { return this.opts.transports.includes("wasm"); } + private get napi(): boolean { + return this.opts.transports.includes("napi"); + } + private get processTransports(): string[] { - return this.opts.transports.filter((t) => t !== "wasm"); + return this.opts.transports.filter((t) => t !== "wasm" && t !== "napi"); + } + + /** Whether the package ships per-platform native files: the service binary and/or the addon. */ + private get native(): boolean { + return this.process || this.napi; + } + + /** The service's name in prose: its binary where it has one, else the package stem. */ + private get label(): string { + return this.opts.binaryName || packageStem(this.opts.packageName); + } + + private get nativeFiles(): string[] { + return [ + this.process ? this.opts.binaryName : "", + this.napi ? this.opts.napiAddon ?? "" : "", + ].filter(Boolean); } private get process(): boolean { @@ -228,8 +259,10 @@ export { SyncApi } from './generated/sync.js'; // Clean first: tsc would leave behind the output of a source file the generator no // longer emits, and these packages are a couple of seconds to compile. build: "rm -rf dest .tsbuildinfo && tsc -p tsconfig.json", - prepare_arch_packages: "ipc-runtime-prepare-arch-packages", }; + if (this.native) { + scripts.prepare_arch_packages = "ipc-runtime-prepare-arch-packages"; + } const entry = (name: string) => ({ types: `./dest/${name}.d.ts`, default: `./dest/${name}.js`, @@ -266,17 +299,26 @@ export { SyncApi } from './generated/sync.js'; }, files: ["dest/", ...(this.wasm ? ["wasm/"] : []), "README.md"], scripts, + // Read by ipc-runtime-prepare-arch-packages: native files the arch packages carry besides + // the `bin` binary (the napi transport's addon). + ...(this.napi + ? { ipcRuntime: { nativeFiles: [this.opts.napiAddon] } } + : {}), dependencies: { "@aztec-foundation/ipc-runtime": this.opts.ipcRuntimeDependency, msgpackr: "^1.11.2", tslib: "^2.4.0", }, - optionalDependencies: Object.fromEntries( - Object.values(archPackages).map((packageName) => [ - packageName, - "0.1.0", - ]), - ), + ...(this.native + ? { + optionalDependencies: Object.fromEntries( + Object.values(archPackages).map((packageName) => [ + packageName, + "0.1.0", + ]), + ), + } + : {}), devDependencies: { "@types/node": "^22.15.17", typescript: "^5.3.3", @@ -298,17 +340,20 @@ runServiceBinary(BINARY, '${this.opts.packageName}', process.argv.slice(2)); const pkg = { name: `${this.opts.packageName}-${suffix}`, version: "0.1.0", - description: `Native binary for ${this.opts.packageName} (${suffix})`, + description: `Native files for ${this.opts.packageName} (${suffix})`, license: "MIT", os: [os], cpu: [cpu], - files: [this.opts.binaryName], + files: this.nativeFiles, preferUnplugged: true, }; return JSON.stringify(pkg, null, 2) + "\n"; } generateArchPackageManifests(): Array<{ path: string; content: string }> { + if (!this.native) { + return []; + } const stem = packageStem(this.opts.packageName); return ARCH_PACKAGES.map(({ suffix, os, cpu }) => ({ path: `packages/${stem}-${suffix}/package.json`, @@ -376,10 +421,12 @@ ${syncSpawn}`; const { prefix } = this.opts; const process = this.process; const wasm = this.wasm; + const napi = this.napi; const backendType = backends.join(" | ") || "never"; const defaults = [ process && `the spawned process when the '${this.opts.binaryName}' binary resolves`, + napi && "the napi addon when it resolves", wasm && "the wasm module", ] .filter(Boolean) @@ -398,7 +445,7 @@ export interface ${prefix}CreateOptions { logger?: (msg: string) => void; /** Let node exit while the backend is alive (a process that watches its parent, or the wasm workers). */ unref?: boolean; -${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>;\n` : ""}} +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${napi ? ` napi?: ${prefix}NapiOptions;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>;\n` : ""}} /** Options for ${className(prefix)}Sync.create / createBackendSync. */ export interface ${prefix}CreateSyncOptions { @@ -408,15 +455,19 @@ export interface ${prefix}CreateSyncOptions { threads?: number; logger?: (msg: string) => void; unref?: boolean; -${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>;\n` : ""}} +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${napi ? ` napi?: ${prefix}NapiOptions;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>;\n` : ""}} `; } /** The service classes over createBackend/createBackendSync, with the direct constructors this entry offers. */ - private serviceClasses(opts: { process: boolean; wasm: boolean }): string { + private serviceClasses(opts: { + process: boolean; + wasm: boolean; + napi: boolean; + }): string { const { prefix } = this.opts; const svc = className(prefix); - const { process, wasm } = opts; + const { process, wasm, napi } = opts; return `/** * The ${prefix} service: the generated API over whichever backend \`create\` chose, forced, or was * given.${process ? " Process lifecycle stays inside the backend and never leaks onto this API." : ""} @@ -439,6 +490,15 @@ ${ ` : "" }${ + napi + ? ` + /** The service over the in-process napi addon (no fallback); calls run on the libuv pool. */ + static async napi(options: ${prefix}NapiOptions = {}): Promise<${svc}> { + return new ${svc}(await createNapiBackend(options)); + } +` + : "" + }${ wasm ? ` /** The service over the in-process wasm module (no fallback). */ @@ -493,6 +553,15 @@ ${ ` : "" }${ + napi + ? ` + /** The service over the in-process napi addon, on the calling thread (no fallback). */ + static async napi(options: ${prefix}NapiOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(createNapiBackendSync(options)); + } +` + : "" + }${ wasm ? ` /** The service over the single-threaded in-process wasm module (no fallback). */ @@ -511,10 +580,17 @@ ${ const findBinary = binaryFinderName(prefix); const process = this.process; const wasm = this.wasm; + const napi = this.napi; const shm = this.shm; - const backends = [process && "'process'", wasm && "'wasm'"].filter( - Boolean, - ) as string[]; + const findAddon = addonFinderName(prefix); + const backends = [ + process && "'process'", + napi && "'napi'", + wasm && "'wasm'", + ].filter(Boolean) as string[]; + const platformExports = [process && findBinary, napi && findAddon] + .filter(Boolean) + .join(", "); return `import { type IpcClientAsync, @@ -523,14 +599,13 @@ ${ } from '@aztec-foundation/ipc-runtime'; import { AsyncApi } from './generated/async.js'; import { SyncApi } from './generated/sync.js'; -${process ? `import { type ${prefix}ProcessOptions, spawnProcessBackend${shm ? ", spawnProcessBackendSync" : ""} } from './process.js';\n` : ""}import { ${findBinary} } from './platform.js'; -${wasm ? `import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js';\n` : ""} -${this.generatedExports()}${process ? "export * from './process.js';\n" : ""}${wasm ? "export * from './wasm.js';\n" : ""}export { ${findBinary} } from './platform.js'; +${process ? `import { type ${prefix}ProcessOptions, spawnProcessBackend${shm ? ", spawnProcessBackendSync" : ""} } from './process.js';\n` : ""}${platformExports ? `import { ${platformExports} } from './platform.js';\n` : ""}${napi ? `import { type ${prefix}NapiOptions, createNapiBackend, createNapiBackendSync } from './napi.js';\n` : ""}${wasm ? `import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js';\n` : ""} +${this.generatedExports()}${process ? "export * from './process.js';\n" : ""}${napi ? "export * from './napi.js';\n" : ""}${wasm ? "export * from './wasm.js';\n" : ""}${platformExports ? `export { ${platformExports} } from './platform.js';` : ""} ${this.createOptionTypes(backends)} /** * The backend \`${className(prefix)}.create\` would use for \`options\`, for facades that wrap the - * generated API themselves. Unset backend: ${process ? `the process when the binary resolves${wasm ? ", falling back to wasm if it cannot be spawned" : ""}` : "the wasm module"}. + * generated API themselves. ${backends.length > 1 ? `Unset backend: the first of ${backends.join(", ")} that is available,\n * falling back along that order when one cannot be created.` : `Unset backend: ${backends[0] ?? "none"}.`} */ export async function createBackend(options: ${prefix}CreateOptions = {}): Promise { const common = { threads: options.threads, logger: options.logger, unref: options.unref }; @@ -546,6 +621,14 @@ ${ ` : "" }${ + napi + ? ` napi: { + available: () => ${findAddon}(options.napi?.addonPath) !== null, + create: () => createNapiBackend(options.napi), + }, +` + : "" + }${ wasm ? ` wasm: { create: () => createWasmBackend({ ...common, ...options.wasm }) }, ` @@ -553,7 +636,7 @@ ${ } }); } -/** The synchronous counterpart of createBackend${shm ? ": the process over shared memory" : ""}${shm && wasm ? ", else " : ""}${wasm ? "the single-threaded wasm module" : ""}. */ +/** The synchronous counterpart of createBackend${[shm && "the process over shared memory", napi && "the napi addon on the calling thread", wasm && "the single-threaded wasm module"].filter(Boolean).map((s, i) => (i === 0 ? ": " : ", else ") + s).join("")}. */ export async function createBackendSync(options: ${prefix}CreateSyncOptions = {}): Promise { const common = { threads: options.threads, logger: options.logger, unref: options.unref }; return pickServiceBackend(options.backend, { @@ -568,6 +651,14 @@ ${ ` : "" }${ + napi + ? ` napi: { + available: () => ${findAddon}(options.napi?.addonPath) !== null, + create: async () => createNapiBackendSync(options.napi), + }, +` + : "" + }${ wasm ? ` wasm: { create: () => createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }) }, ` @@ -575,7 +666,7 @@ ${ } }); } -${this.serviceClasses({ process, wasm })}`; +${this.serviceClasses({ process, wasm, napi })}`; } /** Browser entry: the service runs in-process as a wasm module; there is no process to spawn. */ @@ -629,7 +720,7 @@ export async function createBackendSync(options: ${prefix}CreateSyncOptions = {} return createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }); } -${this.serviceClasses({ process: false, wasm: true })}`; +${this.serviceClasses({ process: false, wasm: true, napi: false })}`; } /** @@ -709,8 +800,8 @@ export class ${svc}Sync extends SyncApi { : "undefined"; const wide = (key: string, m: PackageWasmModule | undefined) => m ? `\n ${key}: ${moduleEntry(m)},` : ""; - // The module's FFI symbols carry the service name (see the FFI entry in SCHEMA_SPEC.md). - const sym = prefix ? `${toSnakeCase(prefix)}_` : ""; + // The module's FFI symbols carry the implementation's FFI name (see the FFI entry in SCHEMA_SPEC.md). + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; const ffiExports = ` entry: '${sym}ipc_ffi_entry',`; return `import { @@ -726,7 +817,7 @@ export class ${svc}Sync extends SyncApi { export { sharedMemoryAvailable } from '@aztec-foundation/ipc-runtime/wasm'; -/** Options for running the ${this.opts.binaryName} wasm module in-process. */ +/** Options for running the ${this.label} wasm module in-process. */ export interface ${prefix}WasmOptions { /** * Threads to run with (1 = no worker threads; selects the single-thread module). Default: the @@ -783,7 +874,7 @@ export function resolveThreads(threads?: number): number { } /** - * The ${this.opts.binaryName} wasm module in-process: the main instance in a worker by default, wasi + * The ${this.label} wasm module in-process: the main instance in a worker by default, wasi * threads on further workers. The workers are ipc-runtime's own, spawned with the literal * expression bundlers detect, so they ship as worker chunks of the consuming application. */ @@ -836,6 +927,7 @@ ${ffiExports} */ generatePlatform(): string { const findBinary = binaryFinderName(this.opts.prefix); + const findAddon = addonFinderName(this.opts.prefix); const archPackages = archPackageNames(this.opts.packageName); // Keyed as `process.arch`-`process.platform` reads at runtime. const byPlatform = [ @@ -846,21 +938,23 @@ ${ffiExports} ] .map(([key, name]) => ` '${key}': '${name}',`) .join("\n"); - - return `import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { type ServiceBinary, findServiceBinary } from '@aztec-foundation/ipc-runtime'; - + const imports = [ + this.process && "type ServiceBinary", + this.napi && "type ServiceNativeFile", + "findServiceBinary", + ] + .filter(Boolean) + .join(", "); + const binary = this.process + ? ` /** This package's native binary, for ipc-runtime's resolver and process backends. */ export const BINARY: ServiceBinary = { name: '${this.opts.binaryName}', envVar: '${this.opts.binaryEnvVar}', - archPackages: { -${byPlatform} - }, + archPackages: ARCH_PACKAGES, ipcPathArgs: ${JSON.stringify(this.opts.ipcPathArgs)}, instancePrefix: '${toSnakeCase(this.opts.prefix)}', - packageDir: path.join(path.dirname(fileURLToPath(import.meta.url)), '..'), + packageDir: PACKAGE_DIR, }; /** @@ -871,18 +965,98 @@ ${byPlatform} export function ${findBinary}(customPath?: string): string | null { return findServiceBinary(BINARY, customPath); } +` + : ""; + const addon = this.napi + ? ` +/** This package's Node-API addon over the service's FFI entry, for the napi backend. */ +export const NAPI_ADDON: ServiceNativeFile = { + name: '${this.opts.napiAddon}', + envVar: '${this.opts.napiAddonEnvVar}', + archPackages: ARCH_PACKAGES, + packageDir: PACKAGE_DIR, +}; + +/** + * The '${this.opts.napiAddon}' addon to load: an explicit path if given, else + * \`${this.opts.napiAddonEnvVar}\`, else the installed arch package for this platform. Null when none + * of those yields an existing file. + */ +export function ${findAddon}(customPath?: string): string | null { + return findServiceBinary(NAPI_ADDON, customPath); +} +` + : ""; + + return `import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ${imports} } from '@aztec-foundation/ipc-runtime'; +/** The per-platform packages carrying this package's native files. */ +const ARCH_PACKAGES: Record = { +${byPlatform} +}; + +const PACKAGE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +${binary}${addon} export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; `; } + /** The napi transport (node only): the generated Node-API addon over the service's FFI entry. */ + generateNapi(): string { + const { prefix } = this.opts; + const findAddon = addonFinderName(prefix); + return `import { + NapiFfiBackend, + NapiFfiBackendSync, + loadNapiFfiAddon, +} from '@aztec-foundation/ipc-runtime'; +import { ${findAddon} } from './platform.js'; + +/** + * Options for the in-process napi backend. The addon is one per process: node caches a native + * module by path, and the FFI entry behind it holds one service context, so every napi backend + * loaded from the same file shares the service's state. + */ +export interface ${prefix}NapiOptions { + /** The addon to load instead of the installed one (else \`${this.opts.napiAddonEnvVar}\`, else the arch package). */ + addonPath?: string; +} + +function loadAddon(options: ${prefix}NapiOptions) { + const addonPath = ${findAddon}(options.addonPath); + if (!addonPath) { + throw new Error( + "${this.opts.packageName}: napi addon '${this.opts.napiAddon}' not found. Install the matching " + + "'${this.opts.packageName}-' package, set ${this.opts.napiAddonEnvVar}, or pass napi.addonPath.", + ); + } + return loadNapiFfiAddon(addonPath, '${prefix}'); +} + +/** The service in-process through its napi addon; each call runs on the libuv thread pool. */ +export async function createNapiBackend(options: ${prefix}NapiOptions = {}): Promise { + return new NapiFfiBackend(loadAddon(options)); +} + +/** The synchronous form: each call runs on, and blocks, the calling thread. */ +export function createNapiBackendSync(options: ${prefix}NapiOptions = {}): NapiFfiBackendSync { + return new NapiFfiBackendSync(loadAddon(options)); +} +`; + } + generateReadme(): string { const svc = className(this.opts.prefix); const process = this.process; const wasm = this.wasm; + const napi = this.napi; const backends = [ process && `- \`'process'\`: spawns the \`${this.opts.binaryName}\` binary (node) and talks to it over ${this.processTransports.join(" or ")}. The binary is resolved from \`${this.opts.binaryEnvVar}\`, an explicit \`process.binaryPath\`, or the installed arch package (one of this package's optional dependencies).`, + napi && + `- \`'napi'\`: runs the service in-process (node) through its Node-API addon, \`${this.opts.napiAddon}\`, a thin wrapper over the service's FFI entry. Asynchronous calls run on the libuv thread pool and the synchronous backend runs on the calling thread; either way calls are serialized. Node loads a native module once per process, so every napi backend in a process shares one service instance. The addon is resolved from \`${this.opts.napiAddonEnvVar}\`, an explicit \`napi.addonPath\`, or the installed arch package.`, wasm && `- \`'wasm'\`: runs the service's wasm module in-process (node and browsers) through \`@aztec-foundation/ipc-runtime/wasm\`: the main instance in a worker, wasi threads on further workers where a shared memory is available (node, or a browser page served with COOP/COEP headers), otherwise the single-thread module. The worker scripts and the module are referenced with \`new URL(..., import.meta.url)\`, so bundlers emit them as chunks and assets of the application, and only the module actually chosen is ever fetched (Vite users: exclude the package from \`optimizeDeps\`). The module ships uncompressed, which is what lets the browser stream it into \`WebAssembly.compileStreaming\` and cache the compiled code between visits; serve it with your host's own compression. Where that is not possible, \`wasm.module\` takes a compressed copy — or bytes, a \`Response\`, or an already compiled \`Module\`.`, `- an object: anything with \`call(bytes)\`/\`destroy()\`, for a transport of your own (a bridge to a natively linked library, for instance).`, @@ -891,6 +1065,7 @@ export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; .join("\n"); const defaultPolicy = [ process && `the process when the binary resolves`, + napi && `the napi addon when it resolves`, wasm && `the wasm module`, ] .filter(Boolean) @@ -900,6 +1075,7 @@ export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; : "(the process reads it from `HARDWARE_CONCURRENCY`/`RAYON_NUM_THREADS`)"; const syncForm = [ this.shm && "shared memory for a process", + napi && "the napi addon on the calling thread", wasm && "the single-threaded wasm module on the calling thread", ] .filter(Boolean) @@ -962,11 +1138,15 @@ The package shell (package.json, tsconfig, \`src/*.ts\`, scripts/) is generated; build through the owning project's \`./bootstrap.sh\`, which regenerates and then runs \`npm install --omit=optional && npm run build\`. -To prepare per-architecture binary packages: +${ + this.native + ? `To prepare per-architecture ${this.napi ? "packages" : "binary packages"}${this.napi ? ` (${this.nativeFiles.map((f) => `\`${f}\``).join(" and ")})` : ""}: \`\`\`sh -npm run prepare_arch_packages -- linux-x64=/path/to/${this.opts.binaryName} +npm run prepare_arch_packages -- ${this.nativeFiles.length === 1 && this.process ? `linux-x64=/path/to/${this.opts.binaryName}` : this.nativeFiles.map((f) => `linux-x64:${f}=/path/to/${f}`).join(" ")} \`\`\` -`; +` + : "" +}`; } } diff --git a/ipc-codegen/src/zig_codegen.ts b/ipc-codegen/src/zig_codegen.ts index 4032266451f..96c39ade5ff 100644 --- a/ipc-codegen/src/zig_codegen.ts +++ b/ipc-codegen/src/zig_codegen.ts @@ -29,6 +29,8 @@ export interface ZigCodegenOptions { stripMethodPrefix?: boolean; /** Client struct name (e.g., 'WsdbClient') */ clientName?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class ZigCodegen { @@ -40,6 +42,7 @@ export class ZigCodegen { prefix: options?.prefix ?? "", clientName: options?.clientName ?? "Client", stripMethodPrefix: options?.stripMethodPrefix ?? false, + ffiName: options?.ffiName ?? toSnakeCase(options?.prefix ?? ""), }; } @@ -48,7 +51,7 @@ export class ZigCodegen { * carry the service (`bb_ipc_ffi_entry`) so several services can be linked into one binary. */ generateFfiBackend(): string { - const sym = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; return `//! AUTOGENERATED - DO NOT EDIT //! FFI client backend for ${this.opts.prefix || "service"}: calls the service's in-process FFI //! entry directly (\`${sym}ipc_ffi_entry\`), with no process and no IPC. Link the library @@ -93,7 +96,7 @@ pub const FfiBackend = struct { */ generateFfi(): string { const { prefix } = this.opts; - const sym = prefix ? `${toSnakeCase(prefix)}_` : ""; + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; const serverFile = `${toSnakeCase(prefix)}_server.zig`; return `//! AUTOGENERATED - DO NOT EDIT //! In-process FFI entry for ${prefix || "service"}: the ipc-codegen FFI backend contract, with the diff --git a/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp b/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp index 1ac19615fb5..2ccff505295 100644 --- a/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp +++ b/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp @@ -12,6 +12,9 @@ */ #include "throw.hpp" +// msgpack-c 7.0.0 uses std::remove_volatile and friends without including ; libc++ in +// wasi-sdk does not provide it transitively. +#include #ifdef BB_NO_EXCEPTIONS #define try if (true) diff --git a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs index d2cb9e94f89..1f8ecbdf0f8 100755 --- a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs +++ b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs @@ -1,12 +1,16 @@ #!/usr/bin/env node -// Stage a generated client package's per-platform binary packages: the optional dependencies its -// binary resolver looks for. Run from the package root; the package name, binary name and version -// come from its own package.json, so this is the same script for every service. +// Stage a generated client package's per-platform packages: the optional dependencies its native +// resolver looks for. Run from the package root; the package name, the native files and the +// version come from its own package.json, so this is the same script for every service. // -// Usage: prepare_arch_packages [= ...] +// The native files are the `bin` binary (a package that spawns its service) and any file listed +// in `ipcRuntime.nativeFiles` (the napi transport's addon). +// +// Usage: prepare_arch_packages [= | := ...] // platform: linux-x64 | linux-arm64 | darwin-x64 | darwin-arm64, or the build directory name // (amd64-linux, arm64-linux, amd64-macos, arm64-macos) -// Without an argument for a platform, build// is used when it exists. +// = names the package's only native file, or its binary when it has several. +// Without an argument for a file, build// is used when it exists. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; @@ -22,53 +26,73 @@ const pkg = JSON.parse(readFileSync('package.json', 'utf8')); const stem = pkg.name.split('/').pop(); // A string bin names its command after the package, which is the form yarn rewrites a one-entry map to on install. const binaryName = typeof pkg.bin === 'string' ? stem : Object.keys(pkg.bin ?? {})[0]; -if (!binaryName) { - console.error(`prepare_arch_packages: ${pkg.name} declares no bin, so it ships no binary`); +const files = [...(binaryName ? [binaryName] : []), ...(pkg.ipcRuntime?.nativeFiles ?? [])]; +if (files.length === 0) { + console.error(`prepare_arch_packages: ${pkg.name} declares no bin and no ipcRuntime.nativeFiles, so it ships nothing native`); process.exit(1); } +// The file a bare `=` names. +const defaultFile = binaryName ?? files[0]; +// Keyed `:`. const overrides = new Map(); for (const arg of process.argv.slice(2)) { const eq = arg.indexOf('='); if (eq < 0) { - console.error('Usage: prepare_arch_packages [= ...]'); + console.error('Usage: prepare_arch_packages [= | := ...]'); console.error('Platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64'); + console.error(`Files: ${files.join(', ')}`); + process.exit(1); + } + const target = arg.slice(0, eq); + const colon = target.indexOf(':'); + const [platform, file] = colon < 0 ? [target, defaultFile] : [target.slice(0, colon), target.slice(colon + 1)]; + if (!files.includes(file)) { + console.error(`prepare_arch_packages: ${pkg.name} ships no native file '${file}' (it has: ${files.join(', ')})`); process.exit(1); } - overrides.set(arg.slice(0, eq), arg.slice(eq + 1)); + overrides.set(`${platform}:${file}`, arg.slice(eq + 1)); } for (const { buildDir, suffix, os, cpu } of PLATFORMS) { const name = `${pkg.name}-${suffix}`; const outDir = join('packages', `${stem}-${suffix}`); - const binaryPath = - overrides.get(suffix) ?? overrides.get(buildDir) ?? join('build', buildDir, binaryName); + const sources = files.map(file => ({ + file, + path: overrides.get(`${suffix}:${file}`) ?? overrides.get(`${buildDir}:${file}`) ?? join('build', buildDir, file), + })); + const present = sources.filter(({ path }) => existsSync(path)); - if (!existsSync(binaryPath)) { - console.log(`Skipping ${name}: no binary at ${binaryPath}`); + if (present.length === 0) { + console.log(`Skipping ${name}: none of ${sources.map(s => s.path).join(', ')} exists`); continue; } + for (const { path } of sources.filter(s => !present.includes(s))) { + console.log(`Warning: ${name} is missing ${path}; the backend that needs it will not resolve on ${suffix}`); + } rmSync(outDir, { recursive: true, force: true }); mkdirSync(outDir, { recursive: true }); - copyFileSync(binaryPath, join(outDir, binaryName)); - chmodSync(join(outDir, binaryName), 0o755); + for (const { file, path } of present) { + copyFileSync(path, join(outDir, file)); + chmodSync(join(outDir, file), 0o755); + } writeFileSync( join(outDir, 'package.json'), JSON.stringify( { name, version: pkg.version, - description: `Native binary for ${pkg.name} (${suffix})`, + description: `Native files for ${pkg.name} (${suffix})`, license: 'MIT', os: [os], cpu: [cpu], - files: [binaryName], + files: present.map(({ file }) => file), preferUnplugged: true, }, null, 2, ) + '\n', ); - console.log(`Staged ${name} from ${binaryPath}`); + console.log(`Staged ${name} from ${present.map(({ path }) => path).join(', ')}`); } diff --git a/ipc-runtime/ts/src/index.ts b/ipc-runtime/ts/src/index.ts index 85712c580a9..640e1987c20 100644 --- a/ipc-runtime/ts/src/index.ts +++ b/ipc-runtime/ts/src/index.ts @@ -34,7 +34,15 @@ export { type Platform, } from "./native_loader.js"; export { + type NapiFfiAddon, + NapiFfiBackend, + NapiFfiBackendSync, + loadNapiFfiAddon, +} from "./napi_ffi.js"; +export { + type ServiceBackendChoice, type ServiceBinary, + type ServiceNativeFile, type ServiceProcessOptions, findServiceBinary, pickServiceBackend, diff --git a/ipc-runtime/ts/src/napi_ffi.ts b/ipc-runtime/ts/src/napi_ffi.ts new file mode 100644 index 00000000000..4696c0f393f --- /dev/null +++ b/ipc-runtime/ts/src/napi_ffi.ts @@ -0,0 +1,84 @@ +import { createRequire } from "node:module"; +import * as path from "node:path"; +import type { IpcClientAsync, IpcClientSync } from "./types.js"; + +/** + * The surface of a Node-API addon generated by ipc-codegen's `--napi` over a service's FFI entry + * (`_napi.cpp`): bytes in, bytes out, on the calling thread or on the libuv pool. The + * addon serializes calls itself, as the FFI entry requires. + */ +export interface NapiFfiAddon { + /** The schema's service name the addon was generated for. */ + service: string; + call(input: Uint8Array): Uint8Array; + callAsync(input: Uint8Array): Promise; +} + +/** + * Load a generated FFI addon from `addonPath`. Node caches native modules by path, so every load of + * one file returns the same addon, and with it the same service state: the FFI entry has one + * context per loaded library, not one per backend. + */ +export function loadNapiFfiAddon( + addonPath: string, + expectedService?: string, +): NapiFfiAddon { + const require = createRequire(import.meta.url); + const addon = require(path.resolve(addonPath)) as NapiFfiAddon; + if ( + typeof addon?.call !== "function" || + typeof addon?.callAsync !== "function" + ) { + throw new Error( + `${addonPath}: not an ipc-codegen FFI addon (no call/callAsync)`, + ); + } + if (expectedService !== undefined && addon.service !== expectedService) { + throw new Error( + `${addonPath}: addon serves '${addon.service}', expected '${expectedService}'`, + ); + } + return addon; +} + +function asUint8Array(buffer: Uint8Array): Uint8Array { + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +/** Asynchronous backend over a generated FFI addon: each call runs on the libuv thread pool. */ +export class NapiFfiBackend implements IpcClientAsync { + private destroyed = false; + + constructor(private readonly addon: NapiFfiAddon) {} + + async call(input: Uint8Array): Promise { + if (this.destroyed) { + throw new Error(`${this.addon.service}: napi backend used after destroy()`); + } + return asUint8Array(await this.addon.callAsync(input)); + } + + /** The addon stays loaded (node cannot unload one); this only stops further calls. */ + destroy(): Promise { + this.destroyed = true; + return Promise.resolve(); + } +} + +/** Synchronous backend over a generated FFI addon: each call runs on, and blocks, the calling thread. */ +export class NapiFfiBackendSync implements IpcClientSync { + private destroyed = false; + + constructor(private readonly addon: NapiFfiAddon) {} + + call(input: Uint8Array): Uint8Array { + if (this.destroyed) { + throw new Error(`${this.addon.service}: napi backend used after destroy()`); + } + return asUint8Array(this.addon.call(input)); + } + + destroy(): void { + this.destroyed = true; + } +} diff --git a/ipc-runtime/ts/src/service.test.ts b/ipc-runtime/ts/src/service.test.ts new file mode 100644 index 00000000000..98f725520c3 --- /dev/null +++ b/ipc-runtime/ts/src/service.test.ts @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { pickServiceBackend } from "./service.js"; +import type { IpcClientAsync } from "./types.js"; + +function backend(name: string): IpcClientAsync & { name: string } { + return { + name, + call: () => Promise.resolve(new Uint8Array()), + destroy: () => Promise.resolve(), + }; +} + +const ok = (name: string, available?: boolean) => ({ + ...(available === undefined ? {} : { available: () => available }), + create: () => Promise.resolve(backend(name)), +}); +const failing = (available?: boolean) => ({ + ...(available === undefined ? {} : { available: () => available }), + create: (): Promise => Promise.reject(new Error("boom")), +}); + +async function picked( + wanted: Parameters[0], + choices: Omit[1], "label">, +): Promise { + const chosen = await pickServiceBackend(wanted as any, { + label: "svc", + ...(choices as any), + }); + return (chosen as any).name; +} + +test("unset: process first when its binary resolves", async () => { + assert.equal( + await picked(undefined, { process: ok("process", true), napi: ok("napi", true), wasm: ok("wasm") }), + "process", + ); +}); + +test("unset: napi when the process is unavailable", async () => { + assert.equal( + await picked(undefined, { process: ok("process", false), napi: ok("napi", true), wasm: ok("wasm") }), + "napi", + ); +}); + +test("unset: wasm when neither native backend is available", async () => { + assert.equal( + await picked(undefined, { process: ok("process", false), napi: ok("napi", false), wasm: ok("wasm") }), + "wasm", + ); +}); + +test("unset: a failing backend falls through to the next", async () => { + const logs: string[] = []; + const chosen = await pickServiceBackend(undefined, { + label: "svc", + napi: failing(true), + wasm: ok("wasm"), + logger: (m) => logs.push(m), + }); + assert.equal((chosen as any).name, "wasm"); + assert.match(logs[0]!, /svc napi unavailable \(boom\); falling back to wasm/); +}); + +test("unset: the last candidate is tried even when it reports unavailable", async () => { + assert.equal(await picked(undefined, { process: ok("process", false) }), "process"); +}); + +test("unset: a failure of the last candidate propagates", async () => { + await assert.rejects(picked(undefined, { napi: failing(true) }), /boom/); +}); + +test("a named backend is forced, with no fallback", async () => { + assert.equal(await picked("napi", { napi: ok("napi", false), wasm: ok("wasm") }), "napi"); + await assert.rejects(picked("napi", { napi: failing(true), wasm: ok("wasm") }), /boom/); +}); + +test("a named backend the host lacks is an error", async () => { + await assert.rejects(picked("napi", { wasm: ok("wasm") }), /svc: no such backend here: napi/); +}); + +test("a backend object is used as is", async () => { + const mine = backend("mine"); + const chosen = await pickServiceBackend(mine, { label: "svc", wasm: ok("wasm") }); + assert.equal(chosen, mine); +}); diff --git a/ipc-runtime/ts/src/service.ts b/ipc-runtime/ts/src/service.ts index 071d2447b7c..d67e9861d53 100644 --- a/ipc-runtime/ts/src/service.ts +++ b/ipc-runtime/ts/src/service.ts @@ -29,6 +29,15 @@ export interface ServiceBinary { packageDir: string; } +/** + * A per-platform native file a generated package ships in its arch packages: the service binary, + * or a Node-API addon over the service's FFI entry. Resolved the same way for both. + */ +export type ServiceNativeFile = Pick< + ServiceBinary, + "name" | "envVar" | "archPackages" | "packageDir" +>; + /** Options a caller may give when the service runs as a spawned process. */ export interface ServiceProcessOptions { binaryPath?: string; @@ -50,7 +59,7 @@ function platformKey(): string { return `${process.arch}-${process.platform}`; } -function archPackageDir(binary: ServiceBinary): string | null { +function archPackageDir(binary: ServiceNativeFile): string | null { const packageName = binary.archPackages[platformKey()]; if (!packageName) { return null; @@ -75,7 +84,7 @@ function archPackageDir(binary: ServiceBinary): string | null { * installed arch package for this platform. Null when none of those yields an existing file. */ export function findServiceBinary( - binary: ServiceBinary, + binary: ServiceNativeFile, customPath?: string, ): string | null { const explicit = customPath ?? process.env[binary.envVar]; @@ -195,43 +204,62 @@ export function runServiceBinary( process.exit(result.status ?? 1); } +/** One backend a host can offer, for pickServiceBackend. */ +export interface ServiceBackendChoice { + /** Whether the default policy should try it; absent means always (it is the last resort). */ + available?: () => boolean; + create: () => Promise; +} + /** - * Which backend to use, given what this host can offer. Unset: the process when its binary - * resolves, else wasm. A name forces one, with no fallback to the other. + * Which backend to use, given what this host can offer. Unset: the first of process, napi, wasm + * whose `available()` holds (or that has no such check), trying the next one if creating it + * fails; the last candidate is tried regardless. A name forces one, with no fallback. */ export async function pickServiceBackend< T extends IpcClientAsync | IpcClientSync, >( - wanted: "process" | "wasm" | T | undefined, + wanted: "process" | "napi" | "wasm" | T | undefined, choices: { label: string; - process?: { available: () => boolean; create: () => Promise }; - wasm?: { create: () => Promise }; + process?: ServiceBackendChoice; + napi?: ServiceBackendChoice; + wasm?: ServiceBackendChoice; logger?: (msg: string) => void; }, ): Promise { if (typeof wanted === "object") { return wanted; } - const { process: proc, wasm } = choices; - if ( - proc && - (wanted === "process" || - (wanted === undefined && (!wasm || proc.available()))) - ) { + const candidates = (["process", "napi", "wasm"] as const) + .map((name) => ({ name, choice: choices[name] })) + .filter( + (c): c is { name: "process" | "napi" | "wasm"; choice: ServiceBackendChoice } => + c.choice !== undefined, + ); + if (wanted !== undefined) { + const forced = candidates.find((c) => c.name === wanted); + if (!forced) { + throw new Error(`${choices.label}: no such backend here: ${String(wanted)}`); + } + return forced.choice.create(); + } + for (let i = 0; i < candidates.length; i++) { + const { name, choice } = candidates[i]!; + const last = i === candidates.length - 1; + if (!last && choice.available && !choice.available()) { + continue; + } try { - return await proc.create(); + return await choice.create(); } catch (err) { - if (wanted === "process" || !wasm) { + if (last) { throw err; } choices.logger?.( - `${choices.label} process unavailable (${(err as Error).message}); falling back to wasm`, + `${choices.label} ${name} unavailable (${(err as Error).message}); falling back to ${candidates[i + 1]!.name}`, ); } } - if (wasm && (wanted === undefined || wanted === "wasm")) { - return wasm.create(); - } - throw new Error(`${choices.label}: no such backend here: ${String(wanted)}`); + throw new Error(`${choices.label}: no backend available`); } diff --git a/native-packages/kvdb/.gitignore b/native-packages/kvdb/.gitignore index a0e6495b6eb..c7dd2d6e11c 100644 --- a/native-packages/kvdb/.gitignore +++ b/native-packages/kvdb/.gitignore @@ -1,4 +1,5 @@ cpp/build/ +cpp/build-*/ ts/dest/ ts/build/ ts/packages/ diff --git a/native-packages/kvdb/bootstrap.sh b/native-packages/kvdb/bootstrap.sh index a5efdcce92a..2afe3350211 100755 --- a/native-packages/kvdb/bootstrap.sh +++ b/native-packages/kvdb/bootstrap.sh @@ -10,8 +10,8 @@ hash=$(hash_str \ $(../lmdblib/bootstrap.sh hash) \ $(cache_content_hash .rebuild_patterns)) -# Build the NAPI addon. kvdb is barretenberg-free: it links only the lmdblib -# sibling package's archives (+ lmdb, node-addon-api). lmdblib is assumed prebuilt. +# Build the NAPI addon. kvdb is barretenberg-free: it builds and links only the lmdblib +# sibling package (+ lmdb, node-addon-api). function build_native { local build_dir="cpp/build" CC=$(which clang) CXX=$(which clang++) cmake -S cpp -B "$build_dir" -G Ninja >/dev/null @@ -21,6 +21,15 @@ function build_native { cp "$build_dir/lib/$NAPI_BINARY" "$target_dir/$NAPI_BINARY" } +# Cross-compile the addon for another platform with zig (cpp/CMakePresets.json). +function build_cross { + local platform=$1 + echo_header "kvdb build_cross $platform" + (cd cpp && cmake --preset "$platform" >/dev/null && cmake --build --preset "$platform") + mkdir -p "ts/build/$platform" + cp "cpp/build-$platform/lib/$NAPI_BINARY" "ts/build/$platform/$NAPI_BINARY" +} + function build { echo_header "kvdb build" build_native @@ -46,11 +55,14 @@ function test { } function clean { - rm -rf ts/dest ts/build ts/packages ts/node_modules node_modules cpp/build + rm -rf ts/dest ts/build ts/packages ts/node_modules node_modules cpp/build cpp/build-* } function release { build_native + for platform in arm64-linux amd64-macos arm64-macos; do + build_cross $platform + done npm_install_deps yarn build (cd ts && ./scripts/prepare_arch_packages.sh) @@ -60,7 +72,7 @@ function release { (cd ts && retry "deploy_npm ${REF_NAME#v}") } -export -f build_native build test_cmds test clean release +export -f build_native build_cross build test_cmds test clean release case "$cmd" in "") diff --git a/native-packages/kvdb/cpp/CMakeLists.txt b/native-packages/kvdb/cpp/CMakeLists.txt index d3ceeda004c..755202d977c 100644 --- a/native-packages/kvdb/cpp/CMakeLists.txt +++ b/native-packages/kvdb/cpp/CMakeLists.txt @@ -1,8 +1,8 @@ # Standalone build for the kvdb Node NAPI addon (nodejs_module.node). # # This package owns the kv-store native backend (the LMDBStore NAPI wrapper) that -# barretenberg no longer hosts. It is barretenberg-free: it links only the lmdblib -# sibling package's archives + lmdb + the Node addon runtime. It uses msgpack-c +# barretenberg no longer hosts. It is barretenberg-free: it builds and links only the +# lmdblib sibling package + lmdb + the Node addon runtime. It uses msgpack-c # directly (no bb serialization framework) and vendors the small message header / # dispatcher it needs under src/messaging. @@ -14,35 +14,21 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -# Default to Release (-O3 -DNDEBUG) to match the prebuilt liblmdblib.a it links: the -# shared lmdblib templated/inline headers must be compiled the same way in both, or -# their inline codegen diverges (ODR). +# Default to Release (-O3 -DNDEBUG), as lmdblib does: the shared lmdblib templated/inline +# headers must be compiled the same way in both, or their inline codegen diverges (ODR). if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() add_definitions(-DNAPI_VERSION=9) -# --------------------------------------------------------------------------- -# lmdblib sibling package build outputs (the only native dependency). -# --------------------------------------------------------------------------- -get_filename_component(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) -set(LMDBLIB_PKG "${REPO_ROOT}/native-packages/lmdblib") -set(LMDBLIB_SRC "${LMDBLIB_PKG}/cpp/src") -set(LMDBLIB_LIB "${LMDBLIB_PKG}/cpp/build/lib/liblmdblib.a") -set(LMDB_INCLUDE "${LMDBLIB_PKG}/cpp/build/_deps/lmdb-src/libraries/liblmdb") -set(LMDB_LIB "${LMDBLIB_PKG}/cpp/build/lib/liblmdb.a") -# msgpack-c headers (header-only) come transitively from lmdblib's build: lmdblib fetches -# msgpack-c and compiles liblmdblib.a against it, so the wire ABI is single-sourced rather -# than re-fetched here. (Matches how the wsdb package consumes msgpack.) -set(MSGPACK_INCLUDE "${LMDBLIB_PKG}/cpp/build/_deps/msgpack-src/include") - -foreach(artifact "${LMDBLIB_LIB}" "${LMDB_LIB}" "${MSGPACK_INCLUDE}") - if(NOT EXISTS "${artifact}") - message(FATAL_ERROR "Missing prebuilt artifact: ${artifact}\n" - "Build lmdblib first (cd native-packages/lmdblib && ./bootstrap.sh).") - endif() -endforeach() +# The lmdblib sibling package (the lmdb C++ wrapper, with the lmdb and msgpack-c sources it fetches), +# built here for this toolchain and target. +include(FetchContent) +get_filename_component(LMDBLIB "${CMAKE_CURRENT_SOURCE_DIR}/../../lmdblib/cpp" ABSOLUTE) +set(LMDBLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE) +add_subdirectory(${LMDBLIB} ${CMAKE_BINARY_DIR}/_deps/lmdblib) +FetchContent_GetProperties(msgpack) # --------------------------------------------------------------------------- # Node addon-api / node-api headers (resolved from this package's node_modules). @@ -64,7 +50,7 @@ execute_process( string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR}) string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR ${NODE_API_HEADERS_DIR}) -# libc++ and the x86_64 Skylake target match lmdblib's archive. Static +# libc++ and the x86_64 Skylake target match lmdblib's. Static # libc++/libgcc keep the addon self-contained (no libc++.so.1 runtime dependency). add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") @@ -77,9 +63,8 @@ add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/src - ${LMDBLIB_SRC} - ${MSGPACK_INCLUDE} - ${LMDB_INCLUDE} + ${LMDBLIB}/src + ${msgpack_SOURCE_DIR}/include ) set(KVDB_SOURCES @@ -91,7 +76,7 @@ add_library(nodejs_module SHARED ${KVDB_SOURCES}) set_target_properties(nodejs_module PROPERTIES PREFIX "" SUFFIX ".node") target_include_directories(nodejs_module PRIVATE ${NODE_API_HEADERS_DIR} ${NODE_ADDON_API_DIR}) -target_link_libraries(nodejs_module PRIVATE ${LMDBLIB_LIB} ${LMDB_LIB} pthread) +target_link_libraries(nodejs_module PRIVATE lmdblib lmdb pthread) # On macOS, Node.js N-API symbols are provided by the runtime, not at link time. if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") diff --git a/native-packages/kvdb/cpp/CMakePresets.json b/native-packages/kvdb/cpp/CMakePresets.json new file mode 100644 index 00000000000..03bfc94362b --- /dev/null +++ b/native-packages/kvdb/cpp/CMakePresets.json @@ -0,0 +1,91 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cross-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "environment": { + "CC": "zig cc", + "CXX": "zig c++", + "CFLAGS": "-g0", + "CXXFLAGS": "-g0" + }, + "cacheVariables": { + "CMAKE_AR": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ar.sh", + "CMAKE_RANLIB": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ranlib.sh", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "arm64-linux", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-linux-gnu.2.35", + "CXX": "zig c++ -target aarch64-linux-gnu.2.35", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux" + } + }, + { + "name": "amd64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target x86_64-macos -mcpu=baseline", + "CXX": "zig c++ -target x86_64-macos -mcpu=baseline", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "x86_64" + } + }, + { + "name": "arm64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-macos -mcpu=apple_a14", + "CXX": "zig c++ -target aarch64-macos -mcpu=apple_a14", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "aarch64" + } + } + ], + "buildPresets": [ + { + "name": "arm64-linux", + "configurePreset": "arm64-linux", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + }, + { + "name": "amd64-macos", + "configurePreset": "amd64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + }, + { + "name": "arm64-macos", + "configurePreset": "arm64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + } + ] +} diff --git a/native-packages/lmdblib/cpp/CMakeLists.txt b/native-packages/lmdblib/cpp/CMakeLists.txt index 61d611fcc86..f000c0b08d8 100644 --- a/native-packages/lmdblib/cpp/CMakeLists.txt +++ b/native-packages/lmdblib/cpp/CMakeLists.txt @@ -40,17 +40,17 @@ set(LMDB_DIR ${lmdb_SOURCE_DIR}/libraries/liblmdb) add_library(lmdb STATIC ${LMDB_DIR}/mdb.c ${LMDB_DIR}/midl.c) target_include_directories(lmdb PUBLIC ${LMDB_DIR}) -# msgpack-c: header-only. Fetch the source and use its include directory directly -# (we don't build msgpack-c's own CMake targets/tests). +# msgpack-c: header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so this only +# fetches it (never msgpack-c's own targets/tests). Declared as barretenberg's is, so a build that +# includes both shares one. FetchContent_Declare( msgpack GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include ) -FetchContent_GetProperties(msgpack) -if(NOT msgpack_POPULATED) - FetchContent_Populate(msgpack) -endif() +FetchContent_MakeAvailable(msgpack) set(MSGPACK_INCLUDE ${msgpack_SOURCE_DIR}/include) # msgpack-c is used header-only with exceptions (no boost). Compiled with libc++ and diff --git a/native-packages/wsdb/.gitignore b/native-packages/wsdb/.gitignore index c44a028d9a2..60aa617d1ad 100644 --- a/native-packages/wsdb/.gitignore +++ b/native-packages/wsdb/.gitignore @@ -1,5 +1,6 @@ ts/ cpp/build/ +cpp/build-*/ # ipc-codegen emits the server dispatch into a generated/ subdir at build time. **/generated/ .yarn/* diff --git a/native-packages/wsdb/.rebuild_patterns b/native-packages/wsdb/.rebuild_patterns index 66783cd75ab..28d60043a40 100644 --- a/native-packages/wsdb/.rebuild_patterns +++ b/native-packages/wsdb/.rebuild_patterns @@ -1,7 +1,6 @@ ^native-packages/wsdb/bootstrap\.sh$ ^native-packages/wsdb/package\.json$ ^native-packages/wsdb/\.rebuild_patterns$ -^native-packages/wsdb/wsdb_schema\.jsonc$ ^native-packages/wsdb/cpp/ ^native-packages/lmdblib/ ^native-packages/wsdb/foundation\.pin$ diff --git a/native-packages/wsdb/README.md b/native-packages/wsdb/README.md index 414e9012320..904fdab2fa2 100644 --- a/native-packages/wsdb/README.md +++ b/native-packages/wsdb/README.md @@ -1,21 +1,36 @@ # aztec-wsdb The world-state DB service: a persistent, content-addressed merkle store behind an IPC contract -(`wsdb_schema.jsonc`). `cpp/` is the C++ server and its tests, `ts/` the generated TypeScript +(`wsdb_schema.jsonc`, owned by barretenberg's wsdb-ref in `barretenberg/cpp/src/barretenberg/wsdb_ref/`). `cpp/` is the C++ server and its tests, `ts/` the generated TypeScript client package (`@aztec-foundation/wsdb`) that launches the server binary. ## Dependencies -wsdb compiles no barretenberg header. Everything it needs from outside this directory comes from -one aztec-packages release, pinned by tag in `foundation.pin` (`cpp/CMakeLists.txt`): +wsdb's own code includes no barretenberg header. While it lives in aztec-packages it builds against this +checkout; only the protocol constants come from the release pinned by tag in `foundation.pin` +(`cpp/CMakeLists.txt`): | Dependency | Source | |---|---| -| barretenberg (`bb_poseidon2_hash*`, and the reference world state's `bb_wsref_*` C ABI for the conformance test) | `barretenberg-static--.tar.gz` from the barretenberg GitHub release of the tag | -| ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch) | a sparse partial clone of aztec-packages at the tag, holding only `ipc-runtime/cpp` and `ipc-codegen` (a few MB) | +| barretenberg's wsdb-ref library: `bb_poseidon2_hash*`, and wsdb-ref behind its FFI entry (`wsdb_ref_ipc_ffi_entry`) for the differential test. wsdb calls only those C functions | built from source in this build, from the working tree's `barretenberg/cpp/src/barretenberg/wsdb_ref/standalone` | +| ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch, FFI entry and client) | the working tree | +| the wire contract (`wsdb_schema.jsonc`) | the working tree, `barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc` | | protocol constants (`aztec_constants.hpp`) | `npx @aztec-foundation/constants-codegen@`, which embeds its release's `constants.nr` | -The lmdblib sibling package (`../lmdblib`) is always built from source. The TypeScript client is -generated by `bootstrap.sh` from the same ipc-codegen. +When native-packages leaves aztec-packages, all of these come from the pinned release again (see the +NOTE in `cpp/CMakeLists.txt`). + +aztec-wsdb serves the schema over IPC (the `aztec-wsdb` binary) and in-process through its FFI entry, +`wsdb_labs_ipc_ffi_entry`, opened with `wsdb_labs_ffi_open` (`wsdb_ffi_service.hpp`). The differential +test, `wsdb_ref_diff.test.cpp`, drives aztec-wsdb and wsdb-ref through their FFI entries with the +generated typed client and requires both to answer every request alike. + +The lmdblib sibling package (`../lmdblib`) is built from source as part of this build too. Both it +and wsdb-ref declare the same msgpack-c, so the build has one. The +TypeScript client is generated by `bootstrap.sh` from the same ipc-codegen. + +The release publishes aztec-wsdb for linux-x64, linux-arm64, darwin-x64 and darwin-arm64. The host +binary is `build_native`; the other three are `build_cross `, zig cross builds from +`cpp/CMakePresets.json`. `foundation.pin` is the tag and nothing else; edit it to move the pin. diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 69109e3f66f..f76fd45c858 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -5,21 +5,18 @@ source $(git rev-parse --show-toplevel)/ci3/source_bootstrap ROOT=$(git rev-parse --show-toplevel) PKG="$ROOT/native-packages/wsdb" WSDB_BINARY=aztec-wsdb -# This package owns the wire contract; both the TS client and the C++ server -# codegen from this schema. -WSDB_SCHEMA="$PKG/wsdb_schema.jsonc" +# The wire contract, owned by barretenberg's wsdb-ref; both the TS client and the C++ server codegen +# from it. Read from the working tree for now: see the NOTE in cpp/CMakeLists.txt for where it comes +# from once native-packages leaves aztec-packages. +WSDB_SCHEMA="$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" -# Everything outside this package comes from the release pinned in foundation.pin (see -# cpp/CMakeLists.txt), so the build's identity is this package plus the lmdblib sibling. -hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) +# While wsdb lives in aztec-packages it builds against this checkout's barretenberg (the wsdb-ref +# sources), ipc-runtime and ipc-codegen (see cpp/CMakeLists.txt), so the build follows their hashes. +hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(../../barretenberg/cpp/bootstrap.sh hash) \ + $(../../ipc-codegen/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) -# ipc-codegen from the sparse clone cpp/CMakeLists.txt makes at configure time. -IPC_CODEGEN_DIR="$PKG/cpp/build/_deps/aztec-packages-src/ipc-codegen" +IPC_CODEGEN_DIR="$ROOT/ipc-codegen" -# Note this runs the codegen from the pinned release, not the working tree, so its flags must be -# what that release accepts. --client and --out are implied by --package in later versions and are -# still accepted there, but the pinned one requires --out; do not drop them until foundation.pin -# moves past the release that made them optional. function generate_ts_package { node --experimental-strip-types --no-warnings \ "$IPC_CODEGEN_DIR/src/generate.ts" \ @@ -34,7 +31,7 @@ function generate_ts_package { --package-ipc-path-args 'msgpack,run,--input,{path}' } -# Build the standalone aztec-wsdb binary against the release pinned in foundation.pin. +# Build the standalone aztec-wsdb binary. function build_native { local build_dir="cpp/build" CC=$(which clang) CXX=$(which clang++) cmake -S cpp -B "$build_dir" -G Ninja >/dev/null @@ -44,13 +41,22 @@ function build_native { cp "$build_dir/bin/$WSDB_BINARY" "$target_dir/$WSDB_BINARY" } +# Cross-compile aztec-wsdb for a non-host release platform with zig (cpp/CMakePresets.json). +function build_cross { + local platform=$1 + echo_header "wsdb build_cross $platform" + (cd cpp && cmake --preset "$platform" >/dev/null && cmake --build --preset "$platform") + mkdir -p "ts/build/$platform" + cp "cpp/build-$platform/bin/$WSDB_BINARY" "ts/build/$platform/$WSDB_BINARY" +} + function build { echo_header "wsdb build" build_native generate_ts_package npm_install_deps yarn build - (cd ts && ./scripts/prepare_arch_packages.sh "$(arch)-$(os)=build/$(arch)-$(os)/$WSDB_BINARY") + (cd ts && yarn prepare_arch_packages "$(arch)-$(os)=build/$(arch)-$(os)/$WSDB_BINARY") } # Emit test commands for the CI test engine. @@ -81,22 +87,25 @@ function test { } function clean { - rm -rf ts node_modules cpp/build + rm -rf ts node_modules cpp/build cpp/build-* } function release { build_native + for platform in arm64-linux amd64-macos arm64-macos; do + build_cross $platform + done generate_ts_package npm_install_deps yarn build - (cd ts && ./scripts/prepare_arch_packages.sh) + (cd ts && yarn prepare_arch_packages) for package_dir in ts/packages/*; do (cd "$package_dir" && retry "deploy_npm ${REF_NAME#v}") done (cd ts && retry "deploy_npm ${REF_NAME#v}") } -export -f generate_ts_package build_native build test_cmds test bench_cmds bench clean release +export -f generate_ts_package build_native build_cross build test_cmds test bench_cmds bench clean release case "$cmd" in "") diff --git a/native-packages/wsdb/cpp/CMakeLists.txt b/native-packages/wsdb/cpp/CMakeLists.txt index 1e05a9f9cda..2f0f87dad24 100644 --- a/native-packages/wsdb/cpp/CMakeLists.txt +++ b/native-packages/wsdb/cpp/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.24) -# aztec-wsdb: the world-state DB service. Compiles no barretenberg header; everything it needs from -# outside this directory comes from one aztec-packages release, pinned by tag in ../foundation.pin. +# aztec-wsdb: the world-state DB service. Its own code includes no barretenberg header. What it needs +# from outside this directory (barretenberg's wsdb-ref library, ipc-runtime, ipc-codegen and the +# schema) comes from this aztec-packages checkout; only the protocol constants come from the release +# pinned by tag in ../foundation.pin. +# NOTE: when native-packages moves out of aztec-packages, take all of it from the pinned release +# again: FetchContent wsdb-ref's standalone project, ipc-runtime/cpp, ipc-codegen and the schema from a +# sparse clone of aztec-packages at the tag. project(aztec_wsdb CXX C) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -12,81 +17,91 @@ endif() set(pkg "${CMAKE_CURRENT_SOURCE_DIR}/..") set(deps "${CMAKE_BINARY_DIR}/_deps") -# --- Dependencies ------------------------------------------------------------------------------- -# barretenberg's static library (the poseidon2 c_bind and the reference world state's C ABI) from -# the GitHub release of the pinned tag, and a sparse clone of aztec-packages at that tag for -# ipc-runtime/cpp (compiled in) and ipc-codegen (run with node). -file(READ "${pkg}/foundation.pin" tag) -string(STRIP "${tag}" tag) -string(REGEX REPLACE "^v" "" version "${tag}") -string(TOLOWER "${CMAKE_SYSTEM_NAME}" os) -string(REPLACE "x86_64" "amd64" arch "${CMAKE_SYSTEM_PROCESSOR}") -string(REPLACE "aarch64" "arm64" arch "${arch}") -include(FetchContent) -FetchContent_Declare(aztec-packages - DOWNLOAD_COMMAND git clone --quiet --filter=blob:none --depth 1 --sparse --branch "${tag}" - https://github.com/AztecProtocol/aztec-packages.git - COMMAND git -C sparse-checkout set ipc-runtime/cpp ipc-codegen) -FetchContent_Declare(barretenberg - URL "https://github.com/AztecProtocol/barretenberg/releases/download/${tag}/barretenberg-static-${arch}-${os}.tar.gz") -FetchContent_MakeAvailable(aztec-packages barretenberg) -set(foundation ${aztec-packages_SOURCE_DIR}) -set(bb_archive ${barretenberg_SOURCE_DIR}/libbb-external.a) -# The lmdblib sibling package: the lmdb C++ wrapper plus the lmdb and msgpack-c sources it fetched. -set(lmdblib "${pkg}/../lmdblib/cpp") -# --- Build -------------------------------------------------------------------------------------- -# Match barretenberg's archive build (-march=skylake on x86_64, libc++, Release). +# --- Toolchain ---------------------------------------------------------------------------------- +# Set before the dependencies so they are compiled the same way: libc++, and barretenberg's +# -march=skylake on x86_64. add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") add_compile_options(-march=skylake) endif() -add_compile_options(-fconstexpr-steps=100000000 -fbracket-depth=1024 -stdlib=libc++ +add_compile_options(-fconstexpr-steps=100000000 -fbracket-depth=1024 $<$:-stdlib=libc++> -Wno-deprecated-declarations -Wno-missing-field-initializers) add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) -include_directories(src ${lmdblib}/src ${lmdblib}/build/_deps/msgpack-src/include - ${lmdblib}/build/_deps/lmdb-src/libraries/liblmdb) + +# --- Dependencies ------------------------------------------------------------------------------- +include(FetchContent) +file(READ "${pkg}/foundation.pin" tag) +string(STRIP "${tag}" tag) +string(REGEX REPLACE "^v" "" version "${tag}") +set(foundation "${pkg}/../..") +# barretenberg's wsdb-ref library, built from source here: the poseidon2 C bindings, and wsdb-ref (the +# reference world state) behind its FFI entry, which the differential test drives. +FetchContent_Declare(wsdb_ref SOURCE_DIR ${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone) +FetchContent_MakeAvailable(wsdb_ref) +option(WSDB_BUILD_TESTS "Build wsdb_tests and wsdb_bench" ON) +# The lmdblib sibling package (the lmdb C++ wrapper, with the lmdb and msgpack-c sources it fetches), +# built here for this toolchain and target. +set(lmdblib "${pkg}/../lmdblib/cpp") +set(LMDBLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE) +add_subdirectory(${lmdblib} ${deps}/lmdblib) +FetchContent_GetProperties(msgpack) +# --- Build -------------------------------------------------------------------------------------- +include_directories(src ${lmdblib}/src ${msgpack_SOURCE_DIR}/include) set(IPC_RUNTIME_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(IPC_RUNTIME_BUILD_NAPI OFF CACHE BOOL "" FORCE) add_subdirectory(${foundation}/ipc-runtime/cpp ${deps}/ipc-runtime) -# The generated server dispatch and wire types (../wsdb_schema.jsonc owns the wire contract), and +# The wire contract is wsdb_schema.jsonc, owned by barretenberg's wsdb-ref (the reference +# implementation). +set(wsdb_schema "${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc") + +# The generated server dispatch, wire types, in-process FFI entry (wsdb_labs_ipc_ffi_entry; its symbols +# carry this implementation's name, as wsdb-ref's carry wsdb_ref) and typed client from that schema, and # the protocol constants from @aztec-foundation/constants-codegen at the pinned version; both are # regenerated whenever configure runs, which their inputs below trigger. execute_process( COMMAND node --experimental-strip-types --no-warnings ${foundation}/ipc-codegen/src/generate.ts - --schema ${pkg}/wsdb_schema.jsonc --lang cpp --out ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/generated - --server --cpp-namespace azteclabs::wsdb --cpp-include-dir wsdb/generated + --schema ${wsdb_schema} --lang cpp --out ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/generated + --server --client --ffi --ffi-name wsdb_labs --cpp-namespace azteclabs::wsdb + --cpp-include-dir wsdb/generated COMMAND_ERROR_IS_FATAL ANY) execute_process( COMMAND "${CMAKE_COMMAND}" -E env "WSDB_FOUNDATION_VERSION=${version}" "${pkg}/scripts/remake-constants.sh" COMMAND_ERROR_IS_FATAL ANY) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - ${pkg}/wsdb_schema.jsonc ${pkg}/foundation.pin ${pkg}/scripts/constants-codegen/cpp.json) + ${wsdb_schema} ${pkg}/foundation.pin ${pkg}/scripts/constants-codegen/cpp.json) +file(GLOB_RECURSE ipc_codegen_sources CONFIGURE_DEPENDS ${foundation}/ipc-codegen/src/*.ts + ${foundation}/ipc-codegen/templates/cpp/*) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${ipc_codegen_sources}) set(CLI11_BUILD_TESTS OFF CACHE INTERNAL "") set(CLI11_BUILD_EXAMPLES OFF CACHE INTERNAL "") set(CLI11_BUILD_DOCS OFF CACHE INTERNAL "") FetchContent_Declare(cli11 GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git GIT_TAG v2.4.2 GIT_SHALLOW TRUE) -set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) -FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.13.0 GIT_SHALLOW TRUE) -set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) -set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE) -FetchContent_Declare(benchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.8.3 GIT_SHALLOW TRUE) -FetchContent_MakeAvailable(cli11 googletest benchmark) +FetchContent_MakeAvailable(cli11) file(GLOB_RECURSE sources CONFIGURE_DEPENDS src/*.cpp) file(GLOB_RECURSE tests CONFIGURE_DEPENDS src/*.test.cpp) file(GLOB_RECURSE benches CONFIGURE_DEPENDS src/*.bench.cpp) list(REMOVE_ITEM sources ${tests} ${benches}) -set(libs ${bb_archive} ${lmdblib}/build/lib/liblmdblib.a ipc_runtime ${lmdblib}/build/lib/liblmdb.a pthread) +set(libs wsdb_ref lmdblib ipc_runtime lmdb pthread) add_executable(aztec-wsdb ${sources}) target_link_libraries(aztec-wsdb PRIVATE ${libs} CLI11::CLI11) -list(REMOVE_ITEM sources ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/main.cpp) -add_executable(wsdb_tests ${sources} ${tests}) -target_link_libraries(wsdb_tests PRIVATE ${libs} CLI11::CLI11 gmock_main gmock gtest) +if(WSDB_BUILD_TESTS) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.13.0 GIT_SHALLOW TRUE) + set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE) + FetchContent_Declare(benchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.8.3 GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(googletest benchmark) -add_executable(wsdb_bench ${sources} ${benches}) -target_link_libraries(wsdb_bench PRIVATE ${libs} CLI11::CLI11 benchmark::benchmark_main) + list(REMOVE_ITEM sources ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/main.cpp) + add_executable(wsdb_tests ${sources} ${tests}) + target_link_libraries(wsdb_tests PRIVATE ${libs} CLI11::CLI11 gmock_main gmock gtest) + + add_executable(wsdb_bench ${sources} ${benches}) + target_link_libraries(wsdb_bench PRIVATE ${libs} CLI11::CLI11 benchmark::benchmark_main) +endif() diff --git a/native-packages/wsdb/cpp/CMakePresets.json b/native-packages/wsdb/cpp/CMakePresets.json new file mode 100644 index 00000000000..0e6f6b2df1d --- /dev/null +++ b/native-packages/wsdb/cpp/CMakePresets.json @@ -0,0 +1,92 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cross-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "environment": { + "CC": "zig cc", + "CXX": "zig c++", + "CFLAGS": "-g0", + "CXXFLAGS": "-g0" + }, + "cacheVariables": { + "CMAKE_AR": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ar.sh", + "CMAKE_RANLIB": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ranlib.sh", + "CMAKE_BUILD_TYPE": "Release", + "WSDB_BUILD_TESTS": "OFF" + } + }, + { + "name": "arm64-linux", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-linux-gnu.2.35", + "CXX": "zig c++ -target aarch64-linux-gnu.2.35", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux" + } + }, + { + "name": "amd64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target x86_64-macos -mcpu=baseline", + "CXX": "zig c++ -target x86_64-macos -mcpu=baseline", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "x86_64" + } + }, + { + "name": "arm64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-macos -mcpu=apple_a14", + "CXX": "zig c++ -target aarch64-macos -mcpu=apple_a14", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "aarch64" + } + } + ], + "buildPresets": [ + { + "name": "arm64-linux", + "configurePreset": "arm64-linux", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + }, + { + "name": "amd64-macos", + "configurePreset": "amd64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + }, + { + "name": "arm64-macos", + "configurePreset": "arm64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + } + ] +} diff --git a/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp b/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp index 2b3c6d6d031..66df2fda92e 100644 --- a/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp +++ b/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp @@ -359,6 +359,14 @@ void ContentAddressedIndexedTree::get_leaf(const index_t& requestContext.root = store_->get_current_root(*tx, includeUncommitted); std::optional leaf_hash = find_leaf_hash(index, requestContext, *tx, false); if (!leaf_hash.has_value()) { + // An unwritten subtree below the tree's size holds only empty leaves (a batch's padding). + TreeMeta meta; + store_->get_meta(meta, *tx, includeUncommitted); + if (index < meta.size) { + response.success = true; + response.inner.indexed_leaf = IndexedLeafValueType::empty(); + return; + } response.success = false; response.message = "Failed to find leaf hash for current root"; return; @@ -402,6 +410,12 @@ void ContentAddressedIndexedTree::get_leaf(const index_t& requestContext.root = blockData.root; std::optional leaf_hash = find_leaf_hash(index, requestContext, *tx, false); if (!leaf_hash.has_value()) { + // An unwritten subtree below the block's tree size holds only empty leaves (a batch's padding). + if (index < blockData.size) { + response.success = true; + response.inner.indexed_leaf = IndexedLeafValueType::empty(); + return; + } response.success = false; response.message = format("Failed to find leaf hash for root of block ", blockNumber); return; diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.cpp b/native-packages/wsdb/cpp/src/world_state/world_state.cpp index 70486a0c75b..c367b513d2a 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.cpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.cpp @@ -594,6 +594,9 @@ std::pair WorldState::commit(WorldStateStatusFull& status) } signal.wait_for_level(0); + if (success) { + populate_status_summary(status); + } return std::make_pair(success.load(), message); } diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.hpp b/native-packages/wsdb/cpp/src/world_state/world_state.hpp index 078031fbddc..eb7dd61b8ce 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.hpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.hpp @@ -361,6 +361,14 @@ class WorldState { static void populate_status_summary(WorldStateStatusFull& status); + /** + * Runs `insert(on_done)` on `tree` inside a checkpoint of that tree alone, committed if the insertion succeeds and + * reverted if it fails. An indexed insertion writes leaf preimages and key indices to the tree's cache before it + * finds a duplicate key, so without the checkpoint a failed insertion would leave the cache out of step with the + * tree. + */ + template static void insert_atomically(TreeType& tree, Insert&& insert); + template void commit_tree(TreeDBStats& dbStats, Signal& signal, @@ -678,6 +686,44 @@ template void WorldState::append_leaves(MerkleTreeId id, const std: } } +template void WorldState::insert_atomically(TreeType& tree, Insert&& insert) +{ + auto await_response = [](auto&& call) { + Signal signal; + bool success = true; + std::string message; + call([&](const auto& response) { + success = response.success; + message = response.message; + signal.signal_level(0); + }); + signal.wait_for_level(); + return std::make_pair(success, message); + }; + + auto [checkpointed, checkpoint_error] = await_response([&](auto on_done) { + tree.checkpoint([&, on_done](TypedResponse& response) { on_done(response); }); + }); + if (!checkpointed) { + throw std::runtime_error(checkpoint_error); + } + auto [inserted, insert_error] = await_response(insert); + auto [ended, end_error] = await_response([&](auto on_done) { + auto callback = [&, on_done](const Response& response) { on_done(response); }; + if (inserted) { + tree.commit_checkpoint(callback); + } else { + tree.revert_checkpoint(callback); + } + }); + if (!inserted) { + throw std::runtime_error(insert_error); + } + if (!ended) { + throw std::runtime_error(end_error); + } +} + template BatchInsertionResult WorldState::batch_insert_indexed_leaves(MerkleTreeId id, const std::vector& leaves, @@ -690,31 +736,20 @@ BatchInsertionResult WorldState::batch_insert_indexed_leaves(MerkleTreeId id, Fork::SharedPtr fork = retrieve_fork(fork_id); - Signal signal; BatchInsertionResult result; const auto& wrapper = std::get>(fork->_trees.at(id)); - bool success = true; - std::string error_msg; - - wrapper.tree->add_or_update_values( - leaves, subtree_depth, [&](const TypedResponse>& response) { - if (response.success) { - result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; - result.sortedLeaves = *response.inner.sortedLeaves; - result.subtreePath = response.inner.subtreePath; - } else { - success = false; - error_msg = response.message; - } - signal.signal_level(0); - }); - - signal.wait_for_level(); - - if (!success) { - throw std::runtime_error(error_msg); - } + insert_atomically(*wrapper.tree, [&](auto on_done) { + wrapper.tree->add_or_update_values( + leaves, subtree_depth, [&, on_done](const TypedResponse>& response) { + if (response.success) { + result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; + result.sortedLeaves = *response.inner.sortedLeaves; + result.subtreePath = response.inner.subtreePath; + } + on_done(response); + }); + }); return result; } @@ -730,30 +765,19 @@ SequentialInsertionResult WorldState::insert_indexed_leaves(MerkleTreeId id, Fork::SharedPtr fork = retrieve_fork(fork_id); - Signal signal; SequentialInsertionResult result; const auto& wrapper = std::get>(fork->_trees.at(id)); - bool success = true; - std::string error_msg; - - wrapper.tree->add_or_update_values_sequentially( - leaves, [&](const TypedResponse>& response) { - if (response.success) { - result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; - result.insertionWitnessData = *response.inner.insertionWitnessData; - } else { - success = false; - error_msg = response.message; - } - - signal.signal_level(0); - }); - signal.wait_for_level(); - - if (!success) { - throw std::runtime_error(error_msg); - } + insert_atomically(*wrapper.tree, [&](auto on_done) { + wrapper.tree->add_or_update_values_sequentially( + leaves, [&, on_done](const TypedResponse>& response) { + if (response.success) { + result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; + result.insertionWitnessData = *response.inner.insertionWitnessData; + } + on_done(response); + }); + }); return result; } diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp b/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp index faa2fea98da..570945e712f 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp @@ -591,6 +591,71 @@ TEST_F(WorldStateTest, NullifierBatchInsert) } } +TEST_F(WorldStateTest, FailedIndexedInsertionsLeaveTheForkUnchanged) +{ + // Failed insertions that repeat 150 sit between inserting 150 and 160: the later witnesses and state must match a + // world state that never saw them. + std::string clean_dir = random_temp_directory(); + std::filesystem::create_directories(clean_dir); + { + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + WorldState clean( + thread_pool_size, clean_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + auto id = MerkleTreeId::NULLIFIER_TREE; + ws.batch_insert_indexed_leaves(id, { NullifierLeafValue(150) }, 0); + clean.batch_insert_indexed_leaves(id, { NullifierLeafValue(150) }, 0); + + EXPECT_THROW(ws.batch_insert_indexed_leaves( + id, { NullifierLeafValue(170), NullifierLeafValue(150) }, 1), + std::runtime_error); + EXPECT_THROW( + ws.insert_indexed_leaves(id, { NullifierLeafValue(180), NullifierLeafValue(150) }), + std::runtime_error); + + auto batch = ws.batch_insert_indexed_leaves(id, { NullifierLeafValue(160) }, 0); + auto clean_batch = clean.batch_insert_indexed_leaves(id, { NullifierLeafValue(160) }, 0); + EXPECT_EQ(batch.lowLeafWitnessData, clean_batch.lowLeafWitnessData); + auto sequential = ws.insert_indexed_leaves(id, { NullifierLeafValue(175) }); + auto clean_sequential = clean.insert_indexed_leaves(id, { NullifierLeafValue(175) }); + EXPECT_EQ(sequential.lowLeafWitnessData, clean_sequential.lowLeafWitnessData); + EXPECT_EQ(sequential.insertionWitnessData, clean_sequential.insertionWitnessData); + EXPECT_EQ(ws.get_state_reference(uncommitted_revision()), clean.get_state_reference(uncommitted_revision())); + } + std::filesystem::remove_all(clean_dir); +} + +TEST_F(WorldStateTest, CommitReportsTheStatusSummary) +{ + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, { fr(42) }); + + WorldStateStatusFull status; + status.summary = { + .unfinalizedBlockNumber = 99, .finalizedBlockNumber = 99, .oldestHistoricalBlock = 99, .treesAreSynched = false + }; + EXPECT_TRUE(ws.commit(status).first); + + WorldStateStatusSummary expected; + ws.get_status_summary(expected); + EXPECT_EQ(status.summary, expected); +} + +TEST_F(WorldStateTest, EmptySlotsOfABatchReadAsTheEmptyLeaf) +{ + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + auto id = MerkleTreeId::NULLIFIER_TREE; + // A batch of zeros fills slots 128 to 131 with empty leaves; no node is stored beneath their subtree's root. + ws.batch_insert_indexed_leaves(id, std::vector(4, NullifierLeafValue(0)), 2); + WorldStateStatusFull status; + ws.commit(status); + + WorldStateRevision block_1{ .forkId = CANONICAL_FORK_ID, .blockNumber = 1, .includeUncommitted = false }; + for (const auto& revision : { uncommitted_revision(), committed_revision(), block_1 }) { + EXPECT_EQ(ws.get_indexed_leaf(revision, id, 130), IndexedLeaf::empty()); + EXPECT_THROW(ws.get_indexed_leaf(revision, id, 132), std::runtime_error); + } +} + TEST_F(WorldStateTest, PublicDataTree) { WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); diff --git a/native-packages/wsdb/cpp/src/wsdb/cli.cpp b/native-packages/wsdb/cpp/src/wsdb/cli.cpp index b655b74e9d1..12a1cb375da 100644 --- a/native-packages/wsdb/cpp/src/wsdb/cli.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/cli.cpp @@ -5,6 +5,7 @@ #include "world_state/world_state.hpp" #include "wsdb/generated/wsdb_ipc_server.hpp" #include "wsdb/wsdb_ipc_server.hpp" +#include "wsdb/wsdb_service.hpp" #include #include @@ -36,42 +37,8 @@ int parse_and_run_wsdb(int argc, char* argv[]) msgpack_run_command->add_option( "-i,--input", input_path, "IPC socket/shm path (.sock for UDS, .shm for shared memory)"); - std::string data_dir; - msgpack_run_command->add_option("-d,--data-dir", data_dir, "Data directory for LMDB stores")->required(); - - // Tree heights (JSON map: treeId -> height) - std::string tree_heights_json; - msgpack_run_command->add_option("--tree-heights", tree_heights_json, "Tree heights as JSON: {0:40,1:32,...}"); - - // Tree prefill sizes - std::string tree_prefill_json; - msgpack_run_command->add_option( - "--tree-prefill", tree_prefill_json, "Tree prefill sizes as JSON: {0:128,2:128,...}"); - - // Map sizes (KB) - std::string map_sizes_json; - msgpack_run_command->add_option("--map-sizes", map_sizes_json, "LMDB map sizes in KB as JSON: {0:1024,...}"); - - uint32_t threads = 16; - msgpack_run_command->add_option("-t,--threads", threads, "Thread pool size (default: 16)") - ->check(CLI::PositiveNumber); - - uint32_t initial_header_generator_point = 0; - msgpack_run_command->add_option( - "--initial-header-generator-point", initial_header_generator_point, "Header generator point (default: 0)"); - - // Prefilled public data as JSON array of [slot_hex, value_hex] pairs - std::string prefilled_public_data_json; - msgpack_run_command->add_option( - "--prefilled-public-data", prefilled_public_data_json, "Prefilled public data as JSON array"); - - // Prefilled nullifiers as JSON array of nullifier_hex strings - std::string prefilled_nullifiers_json; - msgpack_run_command->add_option( - "--prefilled-nullifiers", prefilled_nullifiers_json, "Prefilled genesis nullifiers as JSON array"); - - uint64_t genesis_timestamp = 0; - msgpack_run_command->add_option("--genesis-timestamp", genesis_timestamp, "Genesis block timestamp (default: 0)"); + WsdbConfig config; + add_wsdb_config_options(*msgpack_run_command, config); size_t request_ring_size = 1024 * 1024; msgpack_run_command @@ -95,18 +62,7 @@ int parse_and_run_wsdb(int argc, char* argv[]) try { if (msgpack_run_command->parsed()) { - return execute_wsdb_server(input_path, - data_dir, - tree_heights_json, - tree_prefill_json, - map_sizes_json, - threads, - initial_header_generator_point, - prefilled_public_data_json, - prefilled_nullifiers_json, - genesis_timestamp, - request_ring_size, - response_ring_size); + return execute_wsdb_server(input_path, config, request_ring_size, response_ring_size); } } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << '\n'; diff --git a/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp b/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp deleted file mode 100644 index e783063bebc..00000000000 --- a/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp +++ /dev/null @@ -1,313 +0,0 @@ -#include "common/aztec_constants.hpp" -#include "field/field_element.hpp" -#include "merkle_tree/fixtures.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "merkle_tree/response.hpp" -#include "world_state/types.hpp" -#include "world_state/world_state.hpp" -#include "wsdb/reference_merkle_db.hpp" -#include -#include -#include -#include -#include -#include - -// This test is the conformance gate for this package's WorldState against barretenberg's -// in-memory reference world state (world_state_reference), reached through its flat C ABI -// (see reference_merkle_db.hpp) so no barretenberg headers are compiled here. The reference -// is the spec for the AVM-facing tree semantics: both sides are driven through an identical -// sequence of operations and must agree, after every step, on roots, sibling paths, low-leaf -// lookups and indexed-leaf preimages. Any divergence here is a conformance bug in this -// package (or a spec change in the reference that this package has not caught up with). - -using namespace azteclabs::wsdb::world_state; -using namespace azteclabs::wsdb::merkle_tree; -using azteclabs::wsdb::fr; -using azteclabs::wsdb::ReferenceMerkleDB; - -namespace { - -constexpr size_t NULLIFIER_PREFILL = 128; -constexpr size_t PUBLIC_DATA_PREFILL = 128; - -class ReferenceConformanceTest : public ::testing::Test { - protected: - void SetUp() override - { - data_dir = random_temp_directory(); - std::filesystem::create_directories(data_dir); - - std::unordered_map tree_heights{ - { MerkleTreeId::NULLIFIER_TREE, NULLIFIER_TREE_HEIGHT }, - { MerkleTreeId::NOTE_HASH_TREE, NOTE_HASH_TREE_HEIGHT }, - { MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_TREE_HEIGHT }, - { MerkleTreeId::L1_TO_L2_MESSAGE_TREE, L1_TO_L2_MSG_TREE_HEIGHT }, - { MerkleTreeId::ARCHIVE, ARCHIVE_HEIGHT }, - }; - std::unordered_map tree_prefill{ - { MerkleTreeId::NULLIFIER_TREE, NULLIFIER_PREFILL }, - { MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_PREFILL }, - }; - - ws = std::make_unique(/*thread_pool_size=*/1, - data_dir, - /*map_size=*/10240, - tree_heights, - tree_prefill, - DOM_SEP__BLOCK_HEADER_HASH); - - ref = std::make_unique(NULLIFIER_PREFILL, PUBLIC_DATA_PREFILL); - } - - void TearDown() override - { - ws.reset(); - std::filesystem::remove_all(data_dir); - } - - static WorldStateRevision revision() { return uncommitted_revision(); } - - void expect_roots_equal() - { - for (MerkleTreeId tree_id : { MerkleTreeId::NULLIFIER_TREE, - MerkleTreeId::NOTE_HASH_TREE, - MerkleTreeId::PUBLIC_DATA_TREE, - MerkleTreeId::L1_TO_L2_MESSAGE_TREE }) { - const auto snapshot = ref->get_snapshot(tree_id); - const auto info = ws->get_tree_info(revision(), tree_id); - EXPECT_EQ(snapshot.root, info.meta.root) << "root mismatch for tree " << static_cast(tree_id); - EXPECT_EQ(snapshot.next_available_leaf_index, info.meta.size) - << "size mismatch for tree " << static_cast(tree_id); - } - } - - void expect_sibling_path_equal(MerkleTreeId tree_id, index_t leaf_index) - { - EXPECT_EQ(ref->get_sibling_path(tree_id, leaf_index), ws->get_sibling_path(revision(), tree_id, leaf_index)) - << "sibling path mismatch, tree " << static_cast(tree_id) << " index " << leaf_index; - } - - void expect_leaf_value_equal(MerkleTreeId tree_id, index_t leaf_index) - { - auto ws_leaf = ws->get_leaf(revision(), tree_id, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_leaf_value(tree_id, leaf_index), ws_leaf.value()) - << "leaf value mismatch, tree " << static_cast(tree_id) << " index " << leaf_index; - } - - void expect_low_leaf_equal(MerkleTreeId tree_id, const fr& key) - { - EXPECT_EQ(ref->get_low_indexed_leaf(tree_id, key), ws->find_low_leaf_index(revision(), tree_id, key)); - } - - void expect_nullifier_preimage_equal(index_t leaf_index) - { - auto ws_leaf = ws->get_indexed_leaf(revision(), MerkleTreeId::NULLIFIER_TREE, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_nullifier_preimage(leaf_index), ws_leaf.value()) - << "nullifier preimage mismatch at index " << leaf_index; - } - - void expect_public_data_preimage_equal(index_t leaf_index) - { - auto ws_leaf = - ws->get_indexed_leaf(revision(), MerkleTreeId::PUBLIC_DATA_TREE, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_public_data_preimage(leaf_index), ws_leaf.value()) - << "public data preimage mismatch at index " << leaf_index; - } - - std::string data_dir; - std::unique_ptr ws; - std::unique_ptr ref; -}; - -// Genesis state must already match: the indexed trees are prefilled with an ascending linked chain of -// padding leaves, the append-only trees start empty. -TEST_F(ReferenceConformanceTest, GenesisMatches) -{ - expect_roots_equal(); - - // Indexed-tree genesis preimages and sibling paths. (get_leaf_value is only ever called on the - // append-only trees in production, so we don't cross-check leaf values for the indexed trees.) - for (index_t i : { index_t(0), index_t(1), index_t(63), index_t(127) }) { - expect_nullifier_preimage_equal(i); - expect_public_data_preimage_equal(i); - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, i); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, i); - } - - // Append-only genesis (empty trees) sibling paths. - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 0); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); - - // Low-leaf lookups over the genesis chain (present and absent keys). - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(1)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(127)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(500)); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(42)); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(5000)); -} - -TEST_F(ReferenceConformanceTest, AppendNoteHashes) -{ - std::vector note_hashes{ fr(111), fr(222), fr(333) }; - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - - expect_roots_equal(); - for (index_t i = 0; i < 4; ++i) { - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, i); - } - for (index_t i = 0; i < 3; ++i) { - expect_leaf_value_equal(MerkleTreeId::NOTE_HASH_TREE, i); - } - - // Append to L1->L2 as well. - std::vector msgs{ fr(7), fr(8) }; - ws->append_leaves(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, msgs); - ref->append_leaves(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, msgs); - expect_roots_equal(); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 1); - expect_leaf_value_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); -} - -TEST_F(ReferenceConformanceTest, PadNoteHashTree) -{ - std::vector note_hashes{ fr(111), fr(222), fr(333) }; - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - - // The AVM pads the note-hash tree to a multiple of MAX_NOTE_HASHES_PER_TX; on the world state that - // is an append of zero leaves, on the reference a pad_tree. - size_t padding = MAX_NOTE_HASHES_PER_TX - (note_hashes.size() % MAX_NOTE_HASHES_PER_TX); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector(padding, fr(0))); - ref->pad_tree(MerkleTreeId::NOTE_HASH_TREE, padding); - - expect_roots_equal(); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 2); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, padding + 2); -} - -TEST_F(ReferenceConformanceTest, InsertNullifiers) -{ - // Keys must exceed the genesis padding range [0, 128) so each is a genuine insertion; nullifiers - // are not updateable, so re-inserting an existing key would (faithfully) throw on both sides. - for (const fr& nullifier : { fr(1000), fr(500), fr(1000000), fr(750) }) { - // The world state's low-leaf witness captures the pre-insertion state; check the reference - // agrees on it before mutating. - const auto low = ref->get_low_indexed_leaf(MerkleTreeId::NULLIFIER_TREE, nullifier); - const index_t new_index = ref->get_snapshot(MerkleTreeId::NULLIFIER_TREE).next_available_leaf_index; - - auto ws_result = ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { nullifier } }); - ref->insert_nullifier(nullifier); - - ASSERT_FALSE(ws_result.lowLeafWitnessData.empty()); - EXPECT_EQ(low.index, ws_result.lowLeafWitnessData[0].index); - ASSERT_FALSE(ws_result.insertionWitnessData.empty()); - EXPECT_EQ(new_index, ws_result.insertionWitnessData[0].index); - - expect_roots_equal(); - // The newly inserted leaf and the (mutated) low leaf. - expect_nullifier_preimage_equal(new_index); - expect_nullifier_preimage_equal(low.index); - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, new_index); - } - - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(500)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(600)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(1000)); -} - -TEST_F(ReferenceConformanceTest, InsertAndUpdatePublicData) -{ - // First insertion of a fresh slot. - const index_t new_index = ref->get_snapshot(MerkleTreeId::PUBLIC_DATA_TREE).next_available_leaf_index; - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(900), fr(11) } }); - ref->insert_public_data(fr(900), fr(11)); - expect_roots_equal(); - expect_public_data_preimage_equal(new_index); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, new_index); - - // Update of the same slot (public-data leaves are updateable; this goes through the "already present" - // branch and mutates the existing leaf in place rather than appending). - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(900), fr(99) } }); - ref->insert_public_data(fr(900), fr(99)); - expect_roots_equal(); - expect_public_data_preimage_equal(new_index); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(900)); -} - -// Exercises the full checkpoint protocol (create / commit / revert) and confirms the reference tracks -// roots in lockstep with the world state across nested checkpoints. -TEST_F(ReferenceConformanceTest, Checkpoints) -{ - EXPECT_EQ(ref->get_checkpoint_id(), 0u); - - // Outer checkpoint, then a mutation. - ws->checkpoint(CANONICAL_FORK_ID); - ref->create_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 1u); - - ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { fr(4242) } }); - ref->insert_nullifier(fr(4242)); - expect_roots_equal(); - - // Nested checkpoint, mutate, then revert it: state returns to the post-insert snapshot. - ws->checkpoint(CANONICAL_FORK_ID); - ref->create_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 2u); - - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ fr(7) }); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ fr(7) }); - expect_roots_equal(); - - ws->revert_checkpoint(CANONICAL_FORK_ID); - ref->revert_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 1u); - expect_roots_equal(); - // The note-hash append was rolled back. - EXPECT_EQ(ref->get_snapshot(MerkleTreeId::NOTE_HASH_TREE).next_available_leaf_index, 0u); - - // Commit the outer checkpoint: the nullifier insertion is kept. - ws->commit_checkpoint(CANONICAL_FORK_ID); - ref->commit_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 0u); - expect_roots_equal(); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(4242)); -} - -// A combined sequence mirroring the AVM fuzzer's genesis seeding, asserting equality at every step. -TEST_F(ReferenceConformanceTest, MixedSequence) -{ - // Register-contract-style nullifier inserts. - for (const fr& nullifier : { fr(0x1111), fr(0x2222) }) { - ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { nullifier } }); - ref->insert_nullifier(nullifier); - expect_roots_equal(); - } - - // Fee-payer / public-data writes. - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(0xABCD), fr(123456) } }); - ref->insert_public_data(fr(0xABCD), fr(123456)); - expect_roots_equal(); - - // Note hashes + padding. - std::vector note_hashes{ fr(1), fr(2) }; - size_t padding = MAX_NOTE_HASHES_PER_TX - (note_hashes.size() % MAX_NOTE_HASHES_PER_TX); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector(padding, fr(0))); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->pad_tree(MerkleTreeId::NOTE_HASH_TREE, padding); - expect_roots_equal(); - - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, NULLIFIER_PREFILL); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_PREFILL); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 0); - expect_nullifier_preimage_equal(NULLIFIER_PREFILL); - expect_public_data_preimage_equal(PUBLIC_DATA_PREFILL); -} - -} // namespace diff --git a/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp b/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp deleted file mode 100644 index ee81b013a38..00000000000 --- a/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp +++ /dev/null @@ -1,169 +0,0 @@ -#pragma once - -#include "common/aztec_constants.hpp" -#include "field/field_element.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "merkle_tree/merkle_tree_id.hpp" -#include "merkle_tree/response.hpp" -#include -#include -#include -#include - -// Flat C ABI into barretenberg's in-memory reference world state (defined in -// libbarretenberg.a, world_state_reference/memory_merkle_db_capi.cpp). The reference is -// the conformance spec for the AVM-facing tree semantics: roots, sibling paths, low-leaf -// lookups, indexed-leaf preimages and checkpointing. Field elements cross as 32 canonical -// bytes; tree ids are the shared MerkleTreeId values. -extern "C" { -void* bb_wsref_create(size_t nullifier_tree_prefill, size_t public_data_tree_prefill); -void bb_wsref_destroy(void* handle); -void bb_wsref_get_snapshot(void* handle, uint8_t tree_id, uint8_t* out_root, uint64_t* out_next_index); -size_t bb_wsref_get_sibling_path(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out); -void bb_wsref_get_low_indexed_leaf( - void* handle, uint8_t tree_id, const uint8_t* key, uint8_t* out_is_present, uint64_t* out_index); -void bb_wsref_get_leaf_value(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out); -void bb_wsref_get_nullifier_preimage( - void* handle, uint64_t leaf_index, uint8_t* out_nullifier, uint64_t* out_next_index, uint8_t* out_next_key); -void bb_wsref_get_public_data_preimage(void* handle, - uint64_t leaf_index, - uint8_t* out_slot, - uint8_t* out_value, - uint64_t* out_next_index, - uint8_t* out_next_key); -void bb_wsref_insert_nullifier(void* handle, const uint8_t* nullifier); -void bb_wsref_insert_public_data(void* handle, const uint8_t* slot, const uint8_t* value); -void bb_wsref_append_leaves(void* handle, uint8_t tree_id, const uint8_t* leaves, size_t count); -void bb_wsref_pad_tree(void* handle, uint8_t tree_id, size_t num_leaves); -void bb_wsref_create_checkpoint(void* handle); -void bb_wsref_commit_checkpoint(void* handle); -void bb_wsref_revert_checkpoint(void* handle); -uint32_t bb_wsref_get_checkpoint_id(void* handle); -} - -namespace azteclabs::wsdb { - -/** - * @brief RAII wrapper over barretenberg's reference merkle DB, in wsdb's own types. - * - * Used by the conformance test to drive the reference and this package's WorldState - * through identical operations and compare results, without compiling any barretenberg - * headers (field elements cross the C ABI as 32 canonical bytes, which is FieldElement's - * own representation). - */ -class ReferenceMerkleDB { - public: - struct Snapshot { - fr root; - uint64_t next_available_leaf_index = 0; - bool operator==(const Snapshot&) const = default; - }; - - ReferenceMerkleDB(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) - : handle_(bb_wsref_create(nullifier_tree_prefill, public_data_tree_prefill)) - {} - ~ReferenceMerkleDB() { bb_wsref_destroy(handle_); } - ReferenceMerkleDB(const ReferenceMerkleDB&) = delete; - ReferenceMerkleDB& operator=(const ReferenceMerkleDB&) = delete; - - Snapshot get_snapshot(merkle_tree::MerkleTreeId tree_id) const - { - Snapshot snapshot; - bb_wsref_get_snapshot(handle_, id(tree_id), snapshot.root.data(), &snapshot.next_available_leaf_index); - return snapshot; - } - - merkle_tree::fr_sibling_path get_sibling_path(merkle_tree::MerkleTreeId tree_id, uint64_t leaf_index) const - { - std::vector buffer(static_cast(tree_height(tree_id)) * 32); - size_t count = bb_wsref_get_sibling_path(handle_, id(tree_id), leaf_index, buffer.data()); - merkle_tree::fr_sibling_path path(count); - for (size_t i = 0; i < count; ++i) { - std::memcpy(path[i].data(), buffer.data() + (i * 32), 32); - } - return path; - } - - merkle_tree::GetLowIndexedLeafResponse get_low_indexed_leaf(merkle_tree::MerkleTreeId tree_id, const fr& key) const - { - uint8_t is_present = 0; - uint64_t index = 0; - bb_wsref_get_low_indexed_leaf(handle_, id(tree_id), key.data(), &is_present, &index); - return { is_present != 0, index }; - } - - fr get_leaf_value(merkle_tree::MerkleTreeId tree_id, uint64_t leaf_index) const - { - fr value; - bb_wsref_get_leaf_value(handle_, id(tree_id), leaf_index, value.data()); - return value; - } - - merkle_tree::IndexedLeaf get_nullifier_preimage(uint64_t leaf_index) const - { - merkle_tree::NullifierLeafValue leaf; - uint64_t next_index = 0; - fr next_key; - bb_wsref_get_nullifier_preimage(handle_, leaf_index, leaf.nullifier.data(), &next_index, next_key.data()); - return { leaf, next_index, next_key }; - } - - merkle_tree::IndexedLeaf get_public_data_preimage(uint64_t leaf_index) const - { - merkle_tree::PublicDataLeafValue leaf; - uint64_t next_index = 0; - fr next_key; - bb_wsref_get_public_data_preimage( - handle_, leaf_index, leaf.slot.data(), leaf.value.data(), &next_index, next_key.data()); - return { leaf, next_index, next_key }; - } - - void insert_nullifier(const fr& nullifier) { bb_wsref_insert_nullifier(handle_, nullifier.data()); } - - void insert_public_data(const fr& slot, const fr& value) - { - bb_wsref_insert_public_data(handle_, slot.data(), value.data()); - } - - void append_leaves(merkle_tree::MerkleTreeId tree_id, const std::vector& leaves) - { - std::vector buffer(leaves.size() * 32); - for (size_t i = 0; i < leaves.size(); ++i) { - std::memcpy(buffer.data() + (i * 32), leaves[i].data(), 32); - } - bb_wsref_append_leaves(handle_, id(tree_id), buffer.data(), leaves.size()); - } - - void pad_tree(merkle_tree::MerkleTreeId tree_id, size_t num_leaves) - { - bb_wsref_pad_tree(handle_, id(tree_id), num_leaves); - } - - void create_checkpoint() { bb_wsref_create_checkpoint(handle_); } - void commit_checkpoint() { bb_wsref_commit_checkpoint(handle_); } - void revert_checkpoint() { bb_wsref_revert_checkpoint(handle_); } - uint32_t get_checkpoint_id() const { return bb_wsref_get_checkpoint_id(handle_); } - - private: - static uint8_t id(merkle_tree::MerkleTreeId tree_id) { return static_cast(tree_id); } - - static uint32_t tree_height(merkle_tree::MerkleTreeId tree_id) - { - switch (tree_id) { - case merkle_tree::MerkleTreeId::NULLIFIER_TREE: - return NULLIFIER_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::NOTE_HASH_TREE: - return NOTE_HASH_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::PUBLIC_DATA_TREE: - return PUBLIC_DATA_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - return L1_TO_L2_MSG_TREE_HEIGHT; - default: - return 0; - } - } - - void* handle_; -}; - -} // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp new file mode 100644 index 00000000000..aef02ab9b1d --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp @@ -0,0 +1,79 @@ +// The in-process aztec-wsdb behind the generated FFI entry (wsdb_labs_ipc_ffi_entry): the same WsdbService the IPC server runs, +// called on the caller's thread with no transport. The caller opens it once with the `msgpack run` options. +#include "wsdb/wsdb_ffi_service.hpp" +#include "wsdb/wsdb_service.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace azteclabs::wsdb { + +namespace { +std::unique_ptr& ffi_service() +{ + static std::unique_ptr service; + return service; +} +} // namespace + +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + // The FFI entry takes its response before it returns, but handlers may respond later from a pool thread, so the + // caller waits here. The caller's thread does the IPC reactor's job: reads run inline on it when idle. + static AsyncDispatchHandler handler = [](std::span request, RawRespond respond) { + auto& service = ffi_service(); + if (!service) { + respond(detail::make_error("aztec-wsdb FFI: call wsdb_labs_ffi_open first")); + return; + } + std::mutex mtx; + std::condition_variable cv; + std::optional> response; + service->handler()(request, [&](std::vector frame) { + std::lock_guard lock(mtx); + response = std::move(frame); + cv.notify_one(); + }); + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return response.has_value(); }); + respond(std::move(*response)); + }; + return handler; +} + +} // namespace azteclabs::wsdb + +IPC_FFI_EXPORT int wsdb_labs_ffi_open(int argc, const char* const* args) +{ + using namespace azteclabs::wsdb; + if (ffi_service()) { + std::cerr << "wsdb_labs_ffi_open: already open" << '\n'; + return 1; + } + CLI::App app{ "wsdb FFI" }; + WsdbConfig config; + add_wsdb_config_options(app, config); + std::vector argv{ "wsdb_ffi" }; + argv.insert(argv.end(), args, args + argc); + try { + app.parse(static_cast(argv.size()), argv.data()); + ffi_service() = std::make_unique(config, []() { return false; }); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } catch (const std::exception& e) { + std::cerr << "wsdb_labs_ffi_open: " << e.what() << '\n'; + return 1; + } + return 0; +} + +IPC_FFI_EXPORT void wsdb_labs_ffi_close() +{ + azteclabs::wsdb::ffi_service().reset(); +} diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp new file mode 100644 index 00000000000..8cd6e0993b6 --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp @@ -0,0 +1,16 @@ +#pragma once +/** + * @file wsdb_ffi_service.hpp + * @brief Opening and closing the in-process aztec-wsdb behind its generated FFI entry (wsdb_labs_ipc_ffi_entry). + */ +#include "wsdb/generated/wsdb_ffi.hpp" + +/** + * Opens the in-process wsdb. args are the `aztec-wsdb msgpack run` options other than the transport ones + * (e.g. "-d", "", "--tree-heights", "{0:42,...}", "-t", "4"). Returns 0 on success; prints and returns non-zero + * on bad options or if it is already open. + */ +IPC_FFI_EXPORT int wsdb_labs_ffi_open(int argc, const char* const* args); + +/** Closes the in-process wsdb, waiting for in-flight work. */ +IPC_FFI_EXPORT void wsdb_labs_ffi_close(); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index b9fb70a023b..70c435c9eba 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -543,8 +543,11 @@ void handle_update_archive(WsdbRequest& ctx, void handle_commit(WsdbRequest& ctx, wire::WsdbCommit&& /*cmd*/, Responder respond) { schedule_write(ctx, 0, std::move(respond), [&ctx]() -> wire::WsdbCommitResponse { - WorldStateStatusFull status; - ctx.world_state.commit(status); + WorldStateStatusFull status{}; + auto [committed, message] = ctx.world_state.commit(status); + if (!committed) { + throw std::runtime_error(message); + } return wire::WsdbCommitResponse{ .status = status }; }); } @@ -759,4 +762,10 @@ void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder< }); } +// aztec-wsdb opens its stores at its genesis when it is constructed, so it cannot take a genesis afterwards. +void handle_init(WsdbRequest& /*ctx*/, wire::WsdbInit&& /*cmd*/, Responder respond) +{ + respond.error("aztec-wsdb does not support Init: its genesis is fixed when the service is created"); +} + } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp index d3e4eb710ae..6c0d09afa6e 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp @@ -127,5 +127,6 @@ void handle_revert_all_checkpoints(WsdbRequest& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder respond); +void handle_init(WsdbRequest& ctx, wire::WsdbInit&& cmd, Responder respond); } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp index e4f09b236ab..c53d827f9d6 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp @@ -1,200 +1,19 @@ #include "wsdb/wsdb_ipc_server.hpp" -#include "common/log.hpp" -#include "common/thread_pool.hpp" #include "ipc_runtime/ipc_server.hpp" #include "ipc_runtime/serve_helper.hpp" #include "ipc_runtime/signal_handlers.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "world_state/world_state.hpp" -#include "wsdb/generated/wsdb_ipc_server.hpp" -#include "wsdb/wsdb_handlers.hpp" -#include "wsdb/wsdb_request.hpp" -#include "wsdb/wsdb_scheduler.hpp" -#include +#include "wsdb/wsdb_service.hpp" -#include -#include -#include #include -#include -#include #include -#include -#include namespace azteclabs::wsdb { -using namespace azteclabs::wsdb::world_state; -using namespace azteclabs::wsdb::merkle_tree; - -// --------------------------------------------------------------------------- -// Simple JSON-like parsing for config maps -// Parses "{0:1024,1:2048,...}" into unordered_map -// --------------------------------------------------------------------------- - -static std::unordered_map parse_tree_uint64_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - std::string cleaned; - for (char c : json) { - if (c != '{' && c != '}' && c != ' ') { - cleaned += c; - } - } - std::istringstream ss(cleaned); - std::string pair; - while (std::getline(ss, pair, ',')) { - auto colon_pos = pair.find(':'); - if (colon_pos != std::string::npos) { - auto key = static_cast(std::stoi(pair.substr(0, colon_pos))); - auto value = static_cast(std::stoull(pair.substr(colon_pos + 1))); - result[key] = value; - } - } - return result; -} - -static std::unordered_map parse_tree_uint32_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - auto u64_map = parse_tree_uint64_map(json); - for (const auto& [k, v] : u64_map) { - result[k] = static_cast(v); - } - return result; -} - -static std::unordered_map parse_tree_index_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - auto u64_map = parse_tree_uint64_map(json); - for (const auto& [k, v] : u64_map) { - result[k] = static_cast(v); - } - return result; -} - -// --------------------------------------------------------------------------- -// Parse prefilled public data from JSON: [["slot_hex","value_hex"],...] -// Each hex string is a 64-char (32-byte) hex-encoded field element. -// --------------------------------------------------------------------------- - -static fr hex_to_fr(const std::string& hex) -{ - std::string cleaned = hex; - if (cleaned.size() >= 2 && cleaned[0] == '0' && (cleaned[1] == 'x' || cleaned[1] == 'X')) { - cleaned = cleaned.substr(2); - } - return fr(cleaned); -} - -static std::vector parse_prefilled_public_data(const std::string& json) -{ - std::vector result; - if (json.empty() || json == "[]") { - return result; - } - - // Simple state-machine parser for [["hex","hex"],["hex","hex"],...] - std::vector hex_values; - std::string current; - bool in_string = false; - - for (char c : json) { - if (c == '"') { - in_string = !in_string; - } else if (in_string) { - current += c; - } else if ((c == ',' || c == ']') && !current.empty()) { - hex_values.push_back(std::move(current)); - current.clear(); - } - } - - // hex_values should have pairs: slot, value, slot, value, ... - if (hex_values.size() % 2 != 0) { - std::cerr << "Warning: odd number of hex values in prefilled public data, ignoring last" << '\n'; - } - for (size_t i = 0; i + 1 < hex_values.size(); i += 2) { - result.emplace_back(hex_to_fr(hex_values[i]), hex_to_fr(hex_values[i + 1])); - } - return result; -} - -// --------------------------------------------------------------------------- -// Parse prefilled nullifiers from JSON: ["nullifier_hex",...] -// Each hex string is a 64-char (32-byte) hex-encoded field element. -// --------------------------------------------------------------------------- - -static std::vector parse_prefilled_nullifiers(const std::string& json) -{ - std::vector result; - if (json.empty() || json == "[]") { - return result; - } - - std::string current; - bool in_string = false; - - for (char c : json) { - if (c == '"') { - in_string = !in_string; - } else if (in_string) { - current += c; - } else if ((c == ',' || c == ']') && !current.empty()) { - result.push_back(hex_to_fr(current)); - current.clear(); - } - } - return result; -} - -// --------------------------------------------------------------------------- -// IPC server execution -// --------------------------------------------------------------------------- - int execute_wsdb_server(const std::string& input_path, - const std::string& data_dir, - const std::string& tree_heights_json, - const std::string& tree_prefill_json, - const std::string& map_sizes_json, - uint32_t threads, - uint32_t initial_header_generator_point, - const std::string& prefilled_public_data_json, - const std::string& prefilled_nullifiers_json, - uint64_t genesis_timestamp, + const WsdbConfig& config, size_t request_ring_size, size_t response_ring_size) { - const uint64_t DEFAULT_MAP_SIZE = 1024UL * 1024; - - // Parse config - auto tree_height = parse_tree_uint32_map(tree_heights_json); - auto tree_prefill = parse_tree_index_map(tree_prefill_json); - - std::unordered_map map_size{ - { world_state::MerkleTreeId::ARCHIVE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::NULLIFIER_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::NOTE_HASH_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::PUBLIC_DATA_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::L1_TO_L2_MESSAGE_TREE, DEFAULT_MAP_SIZE }, - }; - if (!map_sizes_json.empty()) { - auto parsed = parse_tree_uint64_map(map_sizes_json); - for (const auto& [k, v] : parsed) { - map_size[k] = v; - } - } - // Create the IPC server and listen BEFORE the (potentially slow) WorldState // construction: LMDB open and genesis prefill can take arbitrarily long on // a loaded machine. With the socket already listening, clients connect into @@ -227,63 +46,13 @@ int execute_wsdb_server(const std::string& input_path, } std::cerr << "aztec-wsdb listening on " << input_path << '\n'; - // Parse prefilled public data: JSON array of ["slot_hex","value_hex"] pairs - std::vector prefilled_public_data; - if (!prefilled_public_data_json.empty()) { - prefilled_public_data = parse_prefilled_public_data(prefilled_public_data_json); - std::cerr << "Parsed " << prefilled_public_data.size() << " prefilled public data entries" << '\n'; - } - - // Parse prefilled nullifiers: JSON array of "nullifier_hex" strings. The caller (TS world-state) passes the same - // canonical genesis nullifiers it seeds via the napi path, so the IPC genesis nullifier-tree root matches. - std::vector prefilled_nullifiers; - if (!prefilled_nullifiers_json.empty()) { - prefilled_nullifiers = parse_prefilled_nullifiers(prefilled_nullifiers_json); - std::cerr << "Parsed " << prefilled_nullifiers.size() << " prefilled nullifiers" << '\n'; - } - - // Create WorldState - std::cerr << "Creating WorldState at " << data_dir << " with " << threads << " threads" << '\n'; - auto ws = std::make_unique(threads, - data_dir, - map_size, - tree_height, - tree_prefill, - prefilled_public_data, - prefilled_nullifiers, - initial_header_generator_point, - genesis_timestamp); - - WsdbRequest request{ .world_state = *ws }; - + WsdbService service(config, [&server]() { return server->has_pending_request(); }); std::cerr << "aztec-wsdb IPC server ready" << '\n'; - // Dispatch pool: services requests concurrently so parallel reads aren't - // serialized through the single reactor thread. It is deliberately DISTINCT - // from WorldState's own intra-op pool (the `threads` arg above): mutating - // handlers enqueue subtasks onto that pool and wait() on them, so dispatching - // those handlers onto the SAME pool could deadlock (azteclabs::wsdb::ThreadPool is - // blocking and non-work-stealing). - // - // Sized from the caller-provided `threads` budget (the same value used for - // the WorldState pool), NOT std::thread::hardware_concurrency() — the latter - // ignores cgroup CPU limits and reports the host core count (e.g. 192 in a - // 2-CPU CI container), which would spawn a huge pool per wsdb process and - // exhaust the per-UID thread limit (pthread_create EAGAIN). - uint32_t dispatch_threads = std::max(2, threads); - azteclabs::wsdb::ThreadPool dispatch_pool(dispatch_threads); - - // Server-side ordering: the database, not the client, guarantees per-fork - // read/write consistency. Each handler hands its work to this scheduler via - // schedule_read / schedule_write (which run reads concurrently and serialize - // writes per fork — see WsdbScheduler), so the context carries it. - auto scheduler = std::make_shared(dispatch_pool, *server); - request.scheduler = scheduler.get(); - // Async dispatch: the reactor reads each request and hands it to the // generated handler with a respond callback; the handler decodes, schedules // its work, and responds when done (possibly from a pool thread). - auto handler = make_wsdb_handler(request); + auto& handler = service.handler(); server->run_reactor([&handler](int /*client_id*/, std::span raw, ipc::IpcServer::Respond respond) { handler(raw, std::move(respond)); }); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp index bf49b818487..cb51b4aedd8 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp @@ -1,27 +1,19 @@ #pragma once #include -#include #include namespace azteclabs::wsdb { +struct WsdbConfig; + /** * @brief Start the aztec-wsdb IPC server. * - * Creates a WorldState instance and runs the IPC server loop, dispatching - * incoming msgpack commands via the WsdbCommand NamedUnion. + * Listens on input_path, builds the WsdbService from config and runs the IPC reactor until shutdown. */ int execute_wsdb_server(const std::string& input_path, - const std::string& data_dir, - const std::string& tree_heights_json, - const std::string& tree_prefill_json, - const std::string& map_sizes_json, - uint32_t threads, - uint32_t initial_header_generator_point, - const std::string& prefilled_public_data_json, - const std::string& prefilled_nullifiers_json, - uint64_t genesis_timestamp, + const WsdbConfig& config, size_t request_ring_size, size_t response_ring_size); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp new file mode 100644 index 00000000000..fec192377d1 --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp @@ -0,0 +1,797 @@ +/** + * @file wsdb_ref_diff.test.cpp + * @brief aztec-wsdb against wsdb-ref, barretenberg's reference world state, request for request. + * + * Both serve wsdb_schema.jsonc and both are linked in, each behind its generated FFI entry: aztec-wsdb's + * (wsdb_labs_ipc_ffi_entry) from this package's sources, wsdb-ref's (wsdb_ref_ipc_ffi_entry) from barretenberg's + * static library. The test drives each through the generated typed client, WsdbApi. A seeded random run builds blocks + * the way a node does (effects on a fork, then SyncBlock), interleaved with reads at every revision, writes and + * checkpoints on forks, unwinds, finalization and history removal. A request must fail on both or on neither, and + * every success must be equal, apart from the store statistics a full status carries. Error messages are counted, not + * compared. + * + * WSDB_DIFF_SEEDS (default 4) and WSDB_DIFF_STEPS (default 400) size the run; WSDB_DIFF_TRACE prints every request. + */ +#include "common/aztec_constants.hpp" +#include "field/field_element.hpp" +#include "world_state/genesis_protocol_nullifiers.hpp" +#include "wsdb/generated/ipc_codegen/msgpack_adaptor.hpp" +#include "wsdb/generated/wsdb_client.hpp" +#include "wsdb/generated/wsdb_ffi_backend.hpp" +#include "wsdb/generated/wsdb_types.hpp" +#include "wsdb/wsdb_ffi_service.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// wsdb-ref's FFI entry, in barretenberg's static library. +extern "C" { +void wsdb_ref_ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len); +void wsdb_ref_ipc_ffi_free(void* ptr); +} + +using namespace azteclabs::wsdb; + +namespace { + +constexpr uint32_t LATEST = std::numeric_limits::max(); + +/** An ipc_codegen::Backend over wsdb-ref's FFI entry, as the generated FfiBackend is over aztec-wsdb's. */ +class WsdbRefBackend { + public: + std::vector call(std::span request) + { + uint8_t* out = nullptr; + size_t out_len = 0; + wsdb_ref_ipc_ffi_entry(request.data(), request.size(), &out, &out_len); + std::vector response(out, out + out_len); + wsdb_ref_ipc_ffi_free(out); + return response; + } +}; + +using RefApi = WsdbApi; +using LabsApi = WsdbApi; + +template W bytes_of(const fr& value) +{ + W w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + +fr fr_of(const auto& w) +{ + return fr::serialize_from_buffer(w.data()); +} + +/** A message or response as msgpack text, for failure messages and traces. */ +template std::string dump(const T& value) +{ + if constexpr (std::is_same_v) { + return "{}"; + } else { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack(value); + auto unpacked = msgpack::unpack(buf.data(), buf.size()); + std::ostringstream out; + out << unpacked.get(); + std::string text = out.str(); + return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; + } +} + +/** The indexed trees' initial sizes, prefill included: a run's one varied part of the genesis. */ +struct InitialSizes { + uint64_t nullifier; + uint64_t public_data; +}; + +/** aztec-wsdb's open options: this test's tree shapes and genesis, its stores in `data_dir`. */ +std::vector aztec_wsdb_options(const std::filesystem::path& data_dir, const InitialSizes& sizes) +{ + std::filesystem::create_directories(data_dir); + std::ostringstream nullifiers; + nullifiers << "["; + auto seeds = world_state::genesis_protocol_nullifiers(); + for (size_t i = 0; i < seeds.size(); ++i) { + std::ostringstream hex; + hex << seeds[i]; + nullifiers << (i > 0 ? "," : "") << "\"" << hex.str().substr(2) << "\""; + } + nullifiers << "]"; + std::ostringstream heights; + heights << "{0:" << NULLIFIER_TREE_HEIGHT << ",1:" << NOTE_HASH_TREE_HEIGHT << ",2:" << PUBLIC_DATA_TREE_HEIGHT + << ",3:" << L1_TO_L2_MSG_TREE_HEIGHT << ",4:" << ARCHIVE_HEIGHT << "}"; + return { + "-d", + data_dir.string(), + "-t", + "4", + "--tree-heights", + heights.str(), + "--tree-prefill", + "{0:" + std::to_string(sizes.nullifier) + ",2:" + std::to_string(sizes.public_data) + "}", + "--initial-header-generator-point", + std::to_string(DOM_SEP__BLOCK_HEADER_HASH), + "--prefilled-public-data", + R"([["00000000000000000000000000000000000000000000000000000000000003e8","0000000000000000000000000000000000000000000000000000000000000007"]])", + "--prefilled-nullifiers", + nullifiers.str() + }; +} + +/** aztec-wsdb in-process, open for one run: closed, with its stores, when the run ends. */ +class AztecWsdb { + public: + explicit AztecWsdb(const std::vector& options) + { + std::vector args; + for (const auto& option : options) { + args.push_back(option.c_str()); + } + if (wsdb_labs_ffi_open(static_cast(args.size()), args.data()) != 0) { + throw std::runtime_error("wsdb_labs_ffi_open failed"); + } + } + ~AztecWsdb() { wsdb_labs_ffi_close(); } + AztecWsdb(const AztecWsdb&) = delete; + AztecWsdb& operator=(const AztecWsdb&) = delete; + AztecWsdb(AztecWsdb&&) = delete; + AztecWsdb& operator=(AztecWsdb&&) = delete; +}; + +// Sends one command to both: `BOTH(get_tree_info, cmd)` calls api.get_tree_info(cmd) on each, building the command +// once so both get the same request. +#define BOTH(method, ...) \ + both( \ + #method, \ + [](auto& api, auto&&... args) { return api.method(std::forward(args)...); } __VA_OPT__(, ) \ + __VA_ARGS__) + +/** One differential run: a random but node-shaped workload, every request answered by both. */ +class DiffRun { + public: + DiffRun(const InitialSizes& sizes, uint64_t seed) + : sizes_(sizes) + , rng_(seed) + {} + + size_t requests = 0; + size_t errors = 0; + size_t failed_writes_reverted = 0; + std::vector message_mismatches; + + /** + * Sends one command to both through `call(api, cmd...)` and compares the answers. Returns the reference's + * response, or nullopt when it failed; a command answering nothing returns std::monostate. + */ + template auto both(const std::string& name, Call call, const Cmd&... cmd) + { + using Resp = decltype(call(ref_, Cmd(cmd)...)); + using Value = std::conditional_t, std::monostate, Resp>; + auto attempt = [&](auto& api) -> std::variant { + try { + if constexpr (std::is_void_v) { + call(api, Cmd(cmd)...); + return Value{}; + } else { + return call(api, Cmd(cmd)...); + } + } catch (const std::exception& e) { + return std::string(e.what()); + } + }; + auto ref = attempt(ref_); + auto labs = attempt(labs_); + ++requests; + auto request = [&]() { return name + " " + (std::string() + ... + dump(cmd)); }; + auto* ref_value = std::get_if(&ref); + auto* labs_value = std::get_if(&labs); + if (std::getenv("WSDB_DIFF_TRACE") != nullptr) { + std::cout << "step " << step_ << " " << request().substr(0, 600) + << (ref_value == nullptr ? " -> error: " + std::get(ref) + : name == "create_fork" || name == "get_status" ? " -> " + dump(*ref_value) + : "") + << "\n"; + } + if ((ref_value == nullptr) != (labs_value == nullptr)) { + ADD_FAILURE() << "step " << step_ << " " << request() << ": wsdb-ref " + << (ref_value == nullptr ? "failed: " + std::get(ref) : "succeeded") + << ", aztec-wsdb " + << (labs_value == nullptr ? "failed: " + std::get(labs) : "succeeded"); + } else if (ref_value == nullptr) { + ++errors; + if (std::get(ref) != std::get(labs)) { + message_mismatches.push_back(name + ": '" + std::get(ref) + "' vs '" + + std::get(labs) + "'"); + } + } else { + compare(*ref_value, *labs_value, request); + } + return ref_value == nullptr ? std::optional() : std::optional(std::move(*ref_value)); + } + + void run(size_t steps) + { + init(); + for (step_ = 0; step_ < steps && !::testing::Test::HasFailure(); ++step_) { + std::uniform_int_distribution pick(0, 99); + int choice = pick(rng_); + if (choice < 12) { + build_block(); + } else if (choice < 40) { + read(); + } else if (choice < 65) { + write_on_fork(); + } else if (choice < 72) { + checkpoint_op(); + } else if (choice < 78) { + fork_op(); + } else if (choice < 83) { + canonical_op(); + } else if (choice < 90) { + chain_op(); + } else { + read(); + } + } + } + + private: + RefApi ref_; + LabsApi labs_; + InitialSizes sizes_; + std::mt19937_64 rng_; + size_t step_ = 0; + std::vector forks_{ 0 }; + std::vector known_keys_; + std::vector known_note_hashes_; + std::vector slots_; + + static auto sorted_state(const std::vector& state) + { + std::vector> out; + for (const auto& tree : state) { + out.emplace_back(tree.treeId, fr_of(tree.root), tree.size); + } + std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { return std::get<0>(a) < std::get<0>(b); }); + return out; + } + + template void compare(const T& ref, const T& labs, const auto& request) + { + if constexpr (requires { ref.status.meta; }) { + compare_status(ref.status, labs.status, request); + } else if constexpr (requires { ref.state; }) { + // A node builds a state reference from an unordered map: the entries match, in any order. + EXPECT_EQ(sorted_state(ref.state), sorted_state(labs.state)) + << "step " << step_ << " " << request() << ": state references differ"; + } else if (!(ref == labs)) { + ADD_FAILURE() << "step " << step_ << ": responses differ\n request: " << request() + << "\n wsdb-ref: " << dump(ref) << "\n aztec-wsdb: " << dump(labs); + } + } + + void compare_status(const wire::WorldStateStatusFull& r, const wire::WorldStateStatusFull& n, const auto& request) + { + // Store statistics are a node's LMDB sizes; the summary and every tree's meta must match. + auto meta = [](const wire::TreeMeta& m) { + return std::make_tuple(m.depth, + m.size, + m.committedSize, + fr_of(m.root), + m.initialSize, + fr_of(m.initialRoot), + m.oldestHistoricBlock, + m.unfinalizedBlockHeight, + m.finalizedBlockHeight); + }; + auto what = [&]() { return "step " + std::to_string(step_) + " " + request(); }; + EXPECT_EQ(r.summary, n.summary) << what() << ": status summaries differ"; + EXPECT_EQ(meta(r.meta.nullifierTreeMeta), meta(n.meta.nullifierTreeMeta)) << what() << ": nullifier meta"; + EXPECT_EQ(meta(r.meta.noteHashTreeMeta), meta(n.meta.noteHashTreeMeta)) << what() << ": note hash meta"; + EXPECT_EQ(meta(r.meta.publicDataTreeMeta), meta(n.meta.publicDataTreeMeta)) << what() << ": public data meta"; + EXPECT_EQ(meta(r.meta.messageTreeMeta), meta(n.meta.messageTreeMeta)) << what() << ": message meta"; + EXPECT_EQ(meta(r.meta.archiveTreeMeta), meta(n.meta.archiveTreeMeta)) << what() << ": archive meta"; + } + + uint64_t uniform(uint64_t lo, uint64_t hi) { return std::uniform_int_distribution(lo, hi)(rng_); } + bool coin(int percent) { return uniform(0, 99) < static_cast(percent); } + + fr fresh_key() + { + fr key(uniform(1000, 1ULL << 62)); + known_keys_.push_back(key); + return key; + } + fr some_key() + { + return known_keys_.empty() || coin(30) ? fresh_key() : known_keys_[uniform(0, known_keys_.size() - 1)]; + } + fr some_slot() + { + if (slots_.empty() || coin(40)) { + slots_.push_back(fr(uniform(129, 5000))); + } + return slots_[uniform(0, slots_.size() - 1)]; + } + fr some_note_hash() + { + if (known_note_hashes_.empty() || coin(40)) { + return fr(uniform(1, 1ULL << 62)); + } + return known_note_hashes_[uniform(0, known_note_hashes_.size() - 1)]; + } + uint64_t some_fork() { return forks_[uniform(0, forks_.size() - 1)]; } + + uint32_t latest_block() + { + return static_cast(BOTH(get_status).value().status.unfinalizedBlockNumber); + } + + wire::WorldStateRevision some_revision() + { + uint32_t latest = latest_block(); + if (coin(30)) { + // Historical reads go to the canonical fork, as a node's clients send them. (Through another fork, + // aztec-wsdb caps index searches at that fork's own block: a combination nothing sends.) + return { .forkId = coin(10) ? 99ULL : 0ULL, + .blockNumber = static_cast(uniform(0, latest + 1)), + .includeUncommitted = coin(60) }; + } + return { .forkId = coin(10) ? 99 : some_fork(), .blockNumber = LATEST, .includeUncommitted = coin(60) }; + } + + void init() + { + // The genesis aztec-wsdb was started with: the protocol's tree heights, this run's initial sizes, the + // protocol nullifiers and one public-data slot. + wire::WsdbInit genesis{ .treeHeights = { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }, + .treePrefill = { { .treeId = 0, .size = sizes_.nullifier }, + { .treeId = 2, .size = sizes_.public_data } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(7)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; + for (const auto& seed : world_state::genesis_protocol_nullifiers()) { + genesis.prefilledNullifiers.push_back(bytes_of(seed)); + } + // aztec-wsdb takes its genesis when it is opened; wsdb-ref, from Init, which replaces any earlier state. + ref_.init(genesis); + slots_.push_back(fr(1000)); + BOTH(get_initial_state_reference); + } + + void read() + { + auto revision = some_revision(); + uint32_t tree = static_cast(uniform(0, 4)); + switch (uniform(0, 11)) { + case 0: + BOTH(get_tree_info, wire::WsdbGetTreeInfo{ .treeId = tree, .revision = revision }); + break; + case 1: + BOTH(get_state_reference, wire::WsdbGetStateReference{ .revision = revision }); + break; + case 2: + BOTH(get_leaf_value, + wire::WsdbGetLeafValue{ + .treeId = coin(80) ? 1u : 4u, .revision = revision, .leafIndex = uniform(0, 300) }); + break; + case 3: + BOTH(get_nullifier_leaf_preimage, + wire::WsdbGetNullifierLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 400) }); + BOTH(get_nullifier_leaf_value, + wire::WsdbGetNullifierLeafValue{ .revision = revision, .leafIndex = uniform(0, 400) }); + break; + case 4: + BOTH(get_public_data_leaf_preimage, + wire::WsdbGetPublicDataLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 300) }); + BOTH(get_public_data_leaf_value, + wire::WsdbGetPublicDataLeafValue{ .revision = revision, .leafIndex = uniform(0, 300) }); + break; + case 5: + BOTH(get_sibling_path, + wire::WsdbGetSiblingPath{ .treeId = tree, .revision = revision, .leafIndex = uniform(0, 400) }); + break; + case 6: + BOTH(find_leaf_indices, + wire::WsdbFindLeafIndices{ + .treeId = 1, + .revision = revision, + .leaves = { bytes_of(some_note_hash()), bytes_of(some_note_hash()) }, + .startIndex = uniform(0, 20) }); + break; + case 7: + BOTH(find_nullifier_leaf_indices, + wire::WsdbFindNullifierLeafIndices{ + .revision = revision, + .leaves = { { .nullifier = bytes_of(some_key()) }, + { .nullifier = bytes_of(fr(uniform(1, 127))) } }, + .startIndex = uniform(0, 140) }); + BOTH(find_public_data_leaf_indices, + wire::WsdbFindPublicDataLeafIndices{ + .revision = revision, + .leaves = { { .slot = bytes_of(some_slot()), + .value = bytes_of(fr(uniform(0, 3))) } }, + .startIndex = 0 }); + break; + case 8: + BOTH(find_low_leaf, + wire::WsdbFindLowLeaf{ .treeId = coin(50) ? 0u : 2u, + .revision = revision, + .key = bytes_of(coin(50) ? some_key() : some_slot()) }); + break; + case 9: + BOTH(find_sibling_paths, + wire::WsdbFindSiblingPaths{ + .treeId = 1, .revision = revision, .leaves = { bytes_of(some_note_hash()) } }); + BOTH(find_nullifier_sibling_paths, + wire::WsdbFindNullifierSiblingPaths{ + .revision = revision, .leaves = { { .nullifier = bytes_of(some_key()) } } }); + BOTH(find_public_data_sibling_paths, + wire::WsdbFindPublicDataSiblingPaths{ + .revision = revision, + .leaves = { { .slot = bytes_of(some_slot()), + .value = bytes_of(fr(0)) } } }); + break; + case 10: + BOTH(get_block_numbers_for_leaf_indices, + wire::WsdbGetBlockNumbersForLeafIndices{ + .treeId = tree, .revision = revision, .leafIndices = { 0, uniform(0, 140), uniform(0, 400) } }); + break; + default: + BOTH(get_status); + BOTH(get_initial_state_reference); + } + } + + /** Nullifiers, some zero; with `fresh_only`, none already inserted or repeated (so the insertion succeeds). */ + std::vector some_nullifiers(size_t count, bool allow_zero, bool fresh_only) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + fr key = allow_zero && coin(20) ? fr(0) : (fresh_only || coin(90) ? fresh_key() : some_key()); + out.push_back({ .nullifier = bytes_of(key) }); + } + return out; + } + + /** Public-data writes to distinct slots (a batch rejects a repeated slot). */ + std::vector distinct_public_data(size_t count) + { + std::vector out; + std::vector used; + while (out.size() < count) { + fr slot = some_slot(); + if (std::find(used.begin(), used.end(), slot) != used.end()) { + continue; + } + used.push_back(slot); + out.push_back({ .slot = bytes_of(slot), + .value = bytes_of(fr(uniform(1, 1000))) }); + } + return out; + } + + std::vector some_public_data(size_t count, bool allow_zero) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + if (allow_zero && coin(15)) { + out.push_back( + { .slot = bytes_of(fr(0)), .value = bytes_of(fr(0)) }); + continue; + } + out.push_back({ .slot = bytes_of(some_slot()), + .value = bytes_of(fr(uniform(1, 1000))) }); + } + return out; + } + + std::vector some_note_hashes(size_t count) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + fr value = coin(25) ? fr(0) : fr(uniform(1, 1ULL << 62)); + if (!value.is_zero()) { + known_note_hashes_.push_back(value); + } + out.push_back(bytes_of(value)); + } + return out; + } + + /** + * A fork other than the canonical one to write to, sometimes a new one. A failed write changes nothing on either + * side, so the fork stays in use after one. + */ + uint64_t other_fork() + { + std::vector others(forks_.begin() + 1, forks_.end()); + if (others.empty() || coin(15)) { + auto created = BOTH(create_fork, wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + forks_.push_back(created.value().forkId); + return forks_.back(); + } + return others[uniform(0, others.size() - 1)]; + } + + void write_on_fork() + { + uint64_t fork = coin(25) ? 0 : other_fork(); + if (fork == 0 && coin(50)) { + // Writes that cannot fail, so the canonical fork keeps growing. + BOTH(append_leaves, + wire::WsdbAppendLeaves{ + .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = 0 }); + BOTH(sequential_insert_nullifier, + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 3), true, true), + .forkId = 0 }); + BOTH(batch_insert_public_data, + wire::WsdbBatchInsertPublicData{ .leaves = distinct_public_data(4), .subtreeDepth = 2, .forkId = 0 }); + return; + } + // As the public processor does with each tx: a checkpoint around the write, reverted if the write fails. + bool guarded = coin(50); + if (guarded) { + BOTH(create_checkpoint, wire::WsdbCreateCheckpoint{ .forkId = fork }); + } + bool failed = false; + switch (uniform(0, 6)) { + case 0: + BOTH(append_leaves, + wire::WsdbAppendLeaves{ + .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = fork }); + break; + case 1: + failed = !BOTH(sequential_insert_nullifier, + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 4), true, false), + .forkId = fork }).has_value(); + break; + case 2: + failed = !BOTH(sequential_insert_public_data, + wire::WsdbSequentialInsertPublicData{ .leaves = some_public_data(uniform(0, 4), true), + .forkId = fork }).has_value(); + break; + case 3: + failed = !BOTH(batch_insert_nullifier, + wire::WsdbBatchInsertNullifier{ .leaves = some_nullifiers(4, true, false), + .subtreeDepth = static_cast(uniform(0, 2)), + .forkId = fork }).has_value(); + break; + case 4: + failed = !BOTH(batch_insert_public_data, + wire::WsdbBatchInsertPublicData{ .leaves = some_public_data(4, true), + .subtreeDepth = static_cast(uniform(0, 2)), + .forkId = fork }).has_value(); + break; + case 5: + failed = !BOTH(append_nullifier_leaves, + wire::WsdbAppendNullifierLeaves{ .leaves = some_nullifiers(uniform(1, 3), true, false), + .forkId = fork }).has_value(); + if (failed) { + break; + } + failed = !BOTH(append_public_data_leaves, + wire::WsdbAppendPublicDataLeaves{ .leaves = some_public_data(uniform(1, 3), true), .forkId = fork }).has_value(); + break; + default: { + auto state = BOTH(get_state_reference, + wire::WsdbGetStateReference{ + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }); + if (!state) { + break; + } + auto block_state = state->state; + if (coin(20)) { + block_state = BOTH(get_initial_state_reference).value().state; + } + BOTH(update_archive, + wire::WsdbUpdateArchive{ .blockStateRef = block_state, + .blockHeaderHash = bytes_of(fr(uniform(1, 1ULL << 62))), + .forkId = fork }); + } + } + if (guarded) { + if (failed) { + failed_writes_reverted++; + } + if (failed || coin(50)) { + BOTH(revert_checkpoint, wire::WsdbRevertCheckpoint{ .forkId = fork }); + } else { + BOTH(commit_checkpoint, wire::WsdbCommitCheckpoint{ .forkId = fork }); + } + } + } + + void checkpoint_op() + { + uint64_t fork = some_fork(); + switch (uniform(0, 4)) { + case 0: + case 1: + BOTH(create_checkpoint, wire::WsdbCreateCheckpoint{ .forkId = fork }); + break; + case 2: + if (coin(50)) { + BOTH(commit_checkpoint, wire::WsdbCommitCheckpoint{ .forkId = fork }); + } else { + BOTH(revert_checkpoint, wire::WsdbRevertCheckpoint{ .forkId = fork }); + } + break; + case 3: + BOTH(commit_all_checkpoints, wire::WsdbCommitAllCheckpoints{ .forkId = fork }); + break; + default: + BOTH(revert_all_checkpoints, wire::WsdbRevertAllCheckpoints{ .forkId = fork }); + } + } + + void fork_op() + { + if (coin(60) || forks_.size() < 2) { + bool latest = coin(70); + auto created = + BOTH(create_fork, + wire::WsdbCreateFork{ .latest = latest, + .blockNumber = static_cast(uniform(0, latest_block() + 1)) }); + if (created) { + forks_.push_back(created->forkId); + } + } else { + size_t at = uniform(0, forks_.size() - 1); + BOTH(delete_fork, wire::WsdbDeleteFork{ .forkId = forks_[at] }); + if (forks_[at] != 0) { + forks_.erase(forks_.begin() + static_cast(at)); + } + } + } + + void canonical_op() + { + // Direct writes to the canonical fork, then a commit or a rollback. + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 1, .leaves = some_note_hashes(2), .forkId = 0 }); + BOTH(sequential_insert_nullifier, + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(1, false, true), .forkId = 0 }); + if (coin(50)) { + BOTH(commit); + } else { + BOTH(rollback); + } + } + + void chain_op() + { + uint32_t latest = latest_block(); + switch (uniform(0, 2)) { + case 0: + BOTH(finalize_blocks, + wire::WsdbFinalizeBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); + break; + case 1: + BOTH(unwind_blocks, + wire::WsdbUnwindBlocks{ .toBlockNumber = static_cast(uniform(0, latest)) }); + break; + default: + BOTH(remove_historical_blocks, + wire::WsdbRemoveHistoricalBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); + } + // Forks a chain operation dropped are gone on both; forget them. + std::erase_if(forks_, [&](uint64_t fork) { + if (fork == 0) { + return false; + } + try { + ref_.get_tree_info(wire::WsdbGetTreeInfo{ + .treeId = 0, .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }); + return false; + } catch (const std::exception&) { + return true; + } + }); + } + + /** Builds a block as a node does: its effects on a fork at the tip, then the same effects synced to fork 0. */ + void build_block() + { + auto created = BOTH(create_fork, wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + uint64_t fork = created.value().forkId; + auto notes = some_note_hashes(coin(30) ? 64 : 8); + auto messages = coin(30) ? some_note_hashes(4) : std::vector{}; + auto nullifiers = some_nullifiers(coin(30) ? 64 : 8, true, true); + auto public_data = some_public_data(uniform(0, 4), false); + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 1, .leaves = notes, .forkId = fork }); + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 3, .leaves = messages, .forkId = fork }); + auto inserted = + BOTH(batch_insert_nullifier, + wire::WsdbBatchInsertNullifier{ + .leaves = nullifiers, .subtreeDepth = nullifiers.size() == 64 ? 6u : 3u, .forkId = fork }); + BOTH(sequential_insert_public_data, + wire::WsdbSequentialInsertPublicData{ .leaves = public_data, .forkId = fork }); + auto state = BOTH(get_state_reference, + wire::WsdbGetStateReference{ + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }) + .value(); + fr header_hash(uniform(1, 1ULL << 62)); + BOTH(update_archive, + wire::WsdbUpdateArchive{ .blockStateRef = state.state, + .blockHeaderHash = bytes_of(header_hash), + .forkId = fork }); + auto archive = BOTH(get_tree_info, + wire::WsdbGetTreeInfo{ + .treeId = 4, + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }) + .value(); + auto previous = BOTH(get_tree_info, + wire::WsdbGetTreeInfo{ + .treeId = 4, + .revision = { .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false } }) + .value(); + BOTH(delete_fork, wire::WsdbDeleteFork{ .forkId = fork }); + if (!inserted) { + return; + } + // Sometimes a block whose claimed state is wrong, which both must reject without committing. + if (coin(10)) { + state.state[0].size += 1; + } + BOTH(sync_block, + wire::WsdbSyncBlock{ .blockNumber = 0, + .blockStateRef = state.state, + .blockHeaderHash = bytes_of(header_hash), + .paddedNoteHashes = notes, + .paddedL1ToL2Messages = messages, + .paddedNullifiers = nullifiers, + .publicDataWrites = public_data, + .expectedArchiveRoot = coin(80) ? std::optional(archive.root) : std::nullopt, + .expectedPreviousArchiveRoot = + coin(80) ? std::optional(previous.root) : std::nullopt }); + } +}; + +} // namespace + +TEST(WsdbRefDiff, MatchesWsdbRefRequestForRequest) +{ + size_t seeds = std::getenv("WSDB_DIFF_SEEDS") ? std::stoul(std::getenv("WSDB_DIFF_SEEDS")) : 4; + size_t steps = std::getenv("WSDB_DIFF_STEPS") ? std::stoul(std::getenv("WSDB_DIFF_STEPS")) : 400; + for (uint64_t seed = 1; seed <= seeds && !HasFailure(); ++seed) { + auto dir = std::filesystem::temp_directory_path() / + ("wsdb-ref-diff-" + std::to_string(getpid()) + "-" + std::to_string(seed)); + { + // Odd seeds run the protocol's initial sizes, even ones others. + InitialSizes sizes = seed % 2 == 1 ? InitialSizes{ 128, 128 } : InitialSizes{ 64, 256 }; + AztecWsdb aztec_wsdb(aztec_wsdb_options(dir / "data", sizes)); + DiffRun run(sizes, seed); + run.run(steps); + std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors + << " answered with errors on both, " << run.message_mismatches.size() + << " with different messages, " << run.failed_writes_reverted + << " failed writes reverted by a checkpoint\n"; + for (size_t i = 0; i < std::min(run.message_mismatches.size(), 5); ++i) { + std::cout << " " << run.message_mismatches[i] << "\n"; + } + } + std::filesystem::remove_all(dir); + } +} diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp index 1a77644b3df..b24acd9f6b3 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp @@ -29,7 +29,6 @@ */ #include "common/thread_pool.hpp" -#include "ipc_runtime/ipc_server.hpp" #include #include @@ -44,9 +43,13 @@ namespace azteclabs::wsdb { class WsdbScheduler : public std::enable_shared_from_this { public: - WsdbScheduler(azteclabs::wsdb::ThreadPool& pool, ipc::IpcServer& server) + /** + * @param has_pending_request Whether another request is already waiting to be read; a read runs inline only + * when this is false and nothing is in flight. Called only on the reactor thread. + */ + WsdbScheduler(azteclabs::wsdb::ThreadPool& pool, std::function has_pending_request) : pool_(pool) - , server_(server) + , has_pending_request_(std::move(has_pending_request)) {} // A read on `fork`. Committed reads (independent snapshots) run concurrently @@ -94,7 +97,7 @@ class WsdbScheduler : public std::enable_shared_from_this { // nothing in flight this request is alone and ordering is moot. bool run_inline_if_idle(std::function& work) { - if (inflight_.load(std::memory_order_acquire) == 0 && !server_.has_pending_request()) { + if (inflight_.load(std::memory_order_acquire) == 0 && !has_pending_request_()) { work(); return true; } @@ -161,7 +164,7 @@ class WsdbScheduler : public std::enable_shared_from_this { } azteclabs::wsdb::ThreadPool& pool_; - ipc::IpcServer& server_; + std::function has_pending_request_; std::mutex mtx_; std::unordered_map lanes_; // per fork; references stable (never erased) std::atomic inflight_{ 0 }; // submitted-but-not-completed ops (queued + running) diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp new file mode 100644 index 00000000000..99f16e35912 --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp @@ -0,0 +1,235 @@ +#include "wsdb/wsdb_service.hpp" +#include "merkle_tree/indexed_leaf.hpp" +#include "wsdb/wsdb_handlers.hpp" +#include "wsdb/wsdb_scheduler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace azteclabs::wsdb { + +using namespace azteclabs::wsdb::world_state; +using namespace azteclabs::wsdb::merkle_tree; + +// --------------------------------------------------------------------------- +// Simple JSON-like parsing for config maps +// Parses "{0:1024,1:2048,...}" into unordered_map +// --------------------------------------------------------------------------- + +static std::unordered_map parse_tree_uint64_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + std::string cleaned; + for (char c : json) { + if (c != '{' && c != '}' && c != ' ') { + cleaned += c; + } + } + std::istringstream ss(cleaned); + std::string pair; + while (std::getline(ss, pair, ',')) { + auto colon_pos = pair.find(':'); + if (colon_pos != std::string::npos) { + auto key = static_cast(std::stoi(pair.substr(0, colon_pos))); + auto value = static_cast(std::stoull(pair.substr(colon_pos + 1))); + result[key] = value; + } + } + return result; +} + +static std::unordered_map parse_tree_uint32_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + auto u64_map = parse_tree_uint64_map(json); + for (const auto& [k, v] : u64_map) { + result[k] = static_cast(v); + } + return result; +} + +static std::unordered_map parse_tree_index_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + auto u64_map = parse_tree_uint64_map(json); + for (const auto& [k, v] : u64_map) { + result[k] = static_cast(v); + } + return result; +} + +// --------------------------------------------------------------------------- +// Parse prefilled public data from JSON: [["slot_hex","value_hex"],...] +// Each hex string is a 64-char (32-byte) hex-encoded field element. +// --------------------------------------------------------------------------- + +static fr hex_to_fr(const std::string& hex) +{ + std::string cleaned = hex; + if (cleaned.size() >= 2 && cleaned[0] == '0' && (cleaned[1] == 'x' || cleaned[1] == 'X')) { + cleaned = cleaned.substr(2); + } + return fr(cleaned); +} + +static std::vector parse_prefilled_public_data(const std::string& json) +{ + std::vector result; + if (json.empty() || json == "[]") { + return result; + } + + // Simple state-machine parser for [["hex","hex"],["hex","hex"],...] + std::vector hex_values; + std::string current; + bool in_string = false; + + for (char c : json) { + if (c == '"') { + in_string = !in_string; + } else if (in_string) { + current += c; + } else if ((c == ',' || c == ']') && !current.empty()) { + hex_values.push_back(std::move(current)); + current.clear(); + } + } + + // hex_values should have pairs: slot, value, slot, value, ... + if (hex_values.size() % 2 != 0) { + std::cerr << "Warning: odd number of hex values in prefilled public data, ignoring last" << '\n'; + } + for (size_t i = 0; i + 1 < hex_values.size(); i += 2) { + result.emplace_back(hex_to_fr(hex_values[i]), hex_to_fr(hex_values[i + 1])); + } + return result; +} + +// --------------------------------------------------------------------------- +// Parse prefilled nullifiers from JSON: ["nullifier_hex",...] +// Each hex string is a 64-char (32-byte) hex-encoded field element. +// --------------------------------------------------------------------------- + +static std::vector parse_prefilled_nullifiers(const std::string& json) +{ + std::vector result; + if (json.empty() || json == "[]") { + return result; + } + + std::string current; + bool in_string = false; + + for (char c : json) { + if (c == '"') { + in_string = !in_string; + } else if (in_string) { + current += c; + } else if ((c == ',' || c == ']') && !current.empty()) { + result.push_back(hex_to_fr(current)); + current.clear(); + } + } + return result; +} + + +void add_wsdb_config_options(CLI::App& app, WsdbConfig& config) +{ + app.add_option("-d,--data-dir", config.data_dir, "Data directory for LMDB stores")->required(); + app.add_option("--tree-heights", config.tree_heights_json, "Tree heights as JSON: {0:40,1:32,...}"); + app.add_option("--tree-prefill", config.tree_prefill_json, "Tree prefill sizes as JSON: {0:128,2:128,...}"); + app.add_option("--map-sizes", config.map_sizes_json, "LMDB map sizes in KB as JSON: {0:1024,...}"); + app.add_option("-t,--threads", config.threads, "Thread pool size (default: 16)")->check(CLI::PositiveNumber); + app.add_option("--initial-header-generator-point", + config.initial_header_generator_point, + "Header generator point (default: 0)"); + app.add_option( + "--prefilled-public-data", config.prefilled_public_data_json, "Prefilled public data as JSON array"); + app.add_option( + "--prefilled-nullifiers", config.prefilled_nullifiers_json, "Prefilled genesis nullifiers as JSON array"); + app.add_option("--genesis-timestamp", config.genesis_timestamp, "Genesis block timestamp (default: 0)"); +} + +static std::unique_ptr make_world_state(const WsdbConfig& config) +{ + const uint64_t DEFAULT_MAP_SIZE = 1024UL * 1024; + + auto tree_height = parse_tree_uint32_map(config.tree_heights_json); + auto tree_prefill = parse_tree_index_map(config.tree_prefill_json); + + std::unordered_map map_size{ + { world_state::MerkleTreeId::ARCHIVE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::NULLIFIER_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::NOTE_HASH_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::PUBLIC_DATA_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::L1_TO_L2_MESSAGE_TREE, DEFAULT_MAP_SIZE }, + }; + for (const auto& [k, v] : parse_tree_uint64_map(config.map_sizes_json)) { + map_size[k] = v; + } + + std::vector prefilled_public_data; + if (!config.prefilled_public_data_json.empty()) { + prefilled_public_data = parse_prefilled_public_data(config.prefilled_public_data_json); + std::cerr << "Parsed " << prefilled_public_data.size() << " prefilled public data entries" << '\n'; + } + + // The caller (TS world-state) passes the same canonical genesis nullifiers it seeds via the napi path, so the IPC + // genesis nullifier-tree root matches. + std::vector prefilled_nullifiers; + if (!config.prefilled_nullifiers_json.empty()) { + prefilled_nullifiers = parse_prefilled_nullifiers(config.prefilled_nullifiers_json); + std::cerr << "Parsed " << prefilled_nullifiers.size() << " prefilled nullifiers" << '\n'; + } + + std::cerr << "Creating WorldState at " << config.data_dir << " with " << config.threads << " threads" << '\n'; + return std::make_unique(config.threads, + config.data_dir, + map_size, + tree_height, + tree_prefill, + prefilled_public_data, + prefilled_nullifiers, + config.initial_header_generator_point, + config.genesis_timestamp); +} + +WsdbService::WsdbService(const WsdbConfig& config, std::function has_pending_request) + : world_state_(make_world_state(config)) + // Sized from the caller-provided `threads` budget, NOT std::thread::hardware_concurrency(): the latter ignores + // cgroup CPU limits and reports the host core count (e.g. 192 in a 2-CPU CI container), which would spawn a huge + // pool per wsdb process and exhaust the per-UID thread limit (pthread_create EAGAIN). + , dispatch_pool_(std::make_unique(std::max(2, config.threads))) + , request_{ .world_state = *world_state_ } + // Server-side ordering: the database, not the client, guarantees per-fork read/write consistency. Each handler + // hands its work to the scheduler via schedule_read / schedule_write, which run reads concurrently and serialize + // writes per fork. + , scheduler_(std::make_shared(*dispatch_pool_, std::move(has_pending_request))) +{ + request_.scheduler = scheduler_.get(); + handler_ = make_wsdb_handler(request_); +} + +WsdbService::~WsdbService() +{ + // Join the pool before the world state its in-flight work uses goes away. + dispatch_pool_.reset(); +} + +} // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp new file mode 100644 index 00000000000..d87c71cea1f --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp @@ -0,0 +1,76 @@ +#pragma once +/** + * @file wsdb_service.hpp + * @brief The wsdb service without a transport: the world state, the dispatch pool, the per-fork scheduler and the + * generated async handler over them. + * + * The IPC server feeds the handler from its reactor; the in-process FFI entry (wsdb_ffi_service.cpp) feeds it from the + * caller's thread. Both build it from the same WsdbConfig, which the `msgpack run` CLI and the FFI open call parse + * with the same options. + */ +#include "common/thread_pool.hpp" +#include "world_state/world_state.hpp" +#include "wsdb/generated/wsdb_dispatch.hpp" +#include "wsdb/wsdb_request.hpp" + +#include +#include +#include +#include + +namespace CLI { +class App; +} + +namespace azteclabs::wsdb { + +class WsdbScheduler; + +struct WsdbConfig { + std::string data_dir; + // Maps as "{treeId:value,...}". + std::string tree_heights_json; + std::string tree_prefill_json; + // LMDB map sizes in KB. + std::string map_sizes_json; + uint32_t threads = 16; + uint32_t initial_header_generator_point = 0; + // [["slot_hex","value_hex"],...] + std::string prefilled_public_data_json; + // ["nullifier_hex",...] + std::string prefilled_nullifiers_json; + uint64_t genesis_timestamp = 0; +}; + +/** Registers the WsdbConfig options on a CLI (sub)command. */ +void add_wsdb_config_options(CLI::App& app, WsdbConfig& config); + +class WsdbService { + public: + /** + * @param has_pending_request Whether another request is already waiting; reads run inline on the submitting + * thread only when this is false (see WsdbScheduler). + */ + WsdbService(const WsdbConfig& config, std::function has_pending_request); + ~WsdbService(); + + WsdbService(const WsdbService&) = delete; + WsdbService& operator=(const WsdbService&) = delete; + WsdbService(WsdbService&&) = delete; + WsdbService& operator=(WsdbService&&) = delete; + + /** Decodes one request, schedules its work and responds when done, possibly from a pool thread. Submissions + * must come from one thread at a time. */ + AsyncDispatchHandler& handler() { return handler_; } + + private: + std::unique_ptr world_state_; + // Distinct from WorldState's own pool: mutating handlers enqueue subtasks there and wait on them, so dispatching + // them onto the same blocking, non-work-stealing pool could deadlock. + std::unique_ptr dispatch_pool_; + WsdbRequest request_; + std::shared_ptr scheduler_; + AsyncDispatchHandler handler_; +}; + +} // namespace azteclabs::wsdb diff --git a/noir-projects/fnd/scripts/regenerate_genesis_constants.sh b/noir-projects/fnd/scripts/regenerate_genesis_constants.sh index 7449b80191e..8c6fad9a327 100755 --- a/noir-projects/fnd/scripts/regenerate_genesis_constants.sh +++ b/noir-projects/fnd/scripts/regenerate_genesis_constants.sh @@ -9,7 +9,8 @@ # 1. noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr # GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH, GENESIS_ARCHIVE_ROOT. # Source of truth: aztec_constants.hpp, ConstantsGen.sol and labs' constants.gen.ts all derive from it. -# 2. native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp +# 2. barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp +# native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp # The seed vector the C++ world-state test builds its genesis from. # 3. l1-contracts/test/fixtures/{empty,mixed,single_tx}_checkpoint_{1,2}.json (--fixtures) # Checkpoint 1 of each family starts from the genesis archive; checkpoint 2 chains off checkpoint 1. @@ -48,6 +49,7 @@ done constants_nr=noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr seeds_hpp=native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp +bb_seeds_hpp=barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp cpp_constants=barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp sol_constants=l1-contracts/src/core/libraries/ConstantsGen.sol ts_constants=labs/yarn-project/constants/src/constants.gen.ts @@ -90,19 +92,21 @@ echo " GENESIS_BLOCK_HEADER_HASH = $HEADER_HASH" echo " GENESIS_ARCHIVE_ROOT = $ARCHIVE_ROOT" echo " seeds = $(echo "$SEEDS" | wc -w)" +# The same seed vector for two C++ trees: wsdb's (its own field type) and barretenberg's reference world +# state, whose tests rebuild the canonical genesis from it. function write_seeds_header { - local file=$1 + local file=$1 field_include=$2 namespace=$3 field=$4 { - cat <<'EOF' + cat < -namespace azteclabs::wsdb::world_state { +namespace $namespace { /** * @brief The protocol contracts' registration nullifiers, seeded into the nullifier tree of a production genesis. @@ -113,12 +117,12 @@ namespace azteclabs::wsdb::world_state { * three roots below, and GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT in * constants.nr, so all of them are rewritten together and never on their own. */ -inline std::vector genesis_protocol_nullifiers() +inline std::vector<$field> genesis_protocol_nullifiers() { return { EOF - for seed in $SEEDS; do echo " fr(\"$seed\"),"; done - cat <<'EOF' + for seed in $SEEDS; do echo " $field(\"$seed\"),"; done + cat <"$file" } @@ -164,10 +164,14 @@ if [ "$check_only" = 1 ]; then grep -q "$2" "$constants_nr" || { echo " stale: $1 in $constants_nr"; stale=1; } done for seed in $SEEDS; do - grep -q "$seed" "$seeds_hpp" || { echo " stale: $seed missing from $seeds_hpp"; stale=1; } + for header in "$seeds_hpp" "$bb_seeds_hpp"; do + grep -q "$seed" "$header" || { echo " stale: $seed missing from $header"; stale=1; } + done done for root in "$NULLIFIER_ROOT" "$HEADER_HASH" "$ARCHIVE_ROOT"; do - grep -q "$root" "$seeds_hpp" || { echo " stale: $root missing from $seeds_hpp"; stale=1; } + for header in "$seeds_hpp" "$bb_seeds_hpp"; do + grep -q "$root" "$header" || { echo " stale: $root missing from $header"; stale=1; } + done done grep -q "$ARCHIVE_ROOT" l1-contracts/test/fixtures/empty_checkpoint_1.json || { echo " stale: l1-contracts/test/fixtures/*_checkpoint_*.json (rerun with --fixtures)"; stale=1; } @@ -178,7 +182,8 @@ fi phase "Update the pinned sources" node "$helper" write-constants "$constants_nr" "$NULLIFIER_ROOT" "$HEADER_HASH" "$ARCHIVE_ROOT" -write_seeds_header "$seeds_hpp" +write_seeds_header "$seeds_hpp" field/field_element.hpp azteclabs::wsdb::world_state fr +write_seeds_header "$bb_seeds_hpp" barretenberg/ecc/curves/bn254/fr.hpp bb::world_state bb::fr phase "Regenerate the derived constants" # constants-codegen embeds constants.nr at build time, so it must be rebuilt before the remake scripts run, or they @@ -216,11 +221,11 @@ if [ "$with_fixtures" = 1 ]; then fi phase "Stage" -git add "$constants_nr" "$seeds_hpp" +git add "$constants_nr" "$seeds_hpp" "$bb_seeds_hpp" if [ "$with_fixtures" = 1 ]; then git add l1-contracts/test/fixtures/; fi echo -git --no-pager diff --cached --stat -- "$constants_nr" "$seeds_hpp" l1-contracts/test/fixtures/ +git --no-pager diff --cached --stat -- "$constants_nr" "$seeds_hpp" "$bb_seeds_hpp" l1-contracts/test/fixtures/ echo if [ "$with_fixtures" = 1 ]; then echo "Done. Review the staged diff and commit."