diff --git a/Makefile b/Makefile index 3a864de4cc3..8cfa8115dce 100644 --- a/Makefile +++ b/Makefile @@ -149,21 +149,17 @@ bb-crs: bb-bbup: $(call build,$@,barretenberg/bbup) -# Yarn install for nodejs_module (needed by presets that build nodejs_module) -bb-cpp-yarn: - $(call run_command,$@,$(ROOT)/barretenberg/cpp,$(ROOT)/ci3/denoise 'cd src/barretenberg/nodejs_module && yarn --immutable') - # Format check (skipped if cache hit) bb-cpp-format-check: $(call build,$@,barretenberg/cpp,build_format_check) # BB C++ Native - Split into compilation and linking phases # Compilation phase: Build barretenberg + vm2_sim objects (can run in parallel with avm-transpiler) -bb-cpp-native-objects: bb-cpp-yarn +bb-cpp-native-objects: $(call build,$@,barretenberg/cpp,build_native_objects) # Linking phase: Link all native binaries (needs avm-transpiler) -bb-cpp-native: bb-cpp-native-objects avm-transpiler-native bb-cpp-yarn bb-cpp-format-check +bb-cpp-native: bb-cpp-native-objects avm-transpiler-native bb-cpp-format-check $(call build,$@,barretenberg/cpp,build_native) bb-cpp-chonk-inputs: @@ -178,25 +174,25 @@ bb-cpp-wasm-threads: $(call build,$@,barretenberg/cpp,build_preset wasm-threads) # Cross-compile object phases (parallel with avm-transpiler cross-compile) -bb-cpp-cross-arm64-linux-objects: bb-cpp-yarn +bb-cpp-cross-arm64-linux-objects: $(call build,$@,barretenberg/cpp,build_cross_objects arm64-linux) -bb-cpp-cross-amd64-macos-objects: bb-cpp-yarn +bb-cpp-cross-amd64-macos-objects: $(call build,$@,barretenberg/cpp,build_cross_objects amd64-macos) -bb-cpp-cross-arm64-macos-objects: bb-cpp-yarn +bb-cpp-cross-arm64-macos-objects: $(call build,$@,barretenberg/cpp,build_cross_objects arm64-macos) # Cross-compile for ARM64 Linux (release only) -bb-cpp-cross-arm64-linux: bb-cpp-native bb-cpp-cross-arm64-linux-objects avm-transpiler-cross-arm64-linux bb-cpp-yarn +bb-cpp-cross-arm64-linux: bb-cpp-native bb-cpp-cross-arm64-linux-objects avm-transpiler-cross-arm64-linux $(call build,$@,barretenberg/cpp,build_preset arm64-linux) # Cross-compile for AMD64 macOS (release only) -bb-cpp-cross-amd64-macos: bb-cpp-cross-arm64-linux bb-cpp-cross-amd64-macos-objects avm-transpiler-cross-amd64-macos bb-cpp-yarn +bb-cpp-cross-amd64-macos: bb-cpp-cross-arm64-linux bb-cpp-cross-amd64-macos-objects avm-transpiler-cross-amd64-macos $(call build,$@,barretenberg/cpp,build_preset amd64-macos) # Cross-compile for ARM64 macOS (release or CI_FULL) -bb-cpp-cross-arm64-macos: bb-cpp-cross-amd64-macos bb-cpp-cross-arm64-macos-objects avm-transpiler-cross-arm64-macos bb-cpp-yarn +bb-cpp-cross-arm64-macos: bb-cpp-cross-amd64-macos bb-cpp-cross-arm64-macos-objects avm-transpiler-cross-arm64-macos $(call build,$@,barretenberg/cpp,build_preset arm64-macos) # Cross-compile for AMD64 Windows (release only) diff --git a/barretenberg/.claude/skills/benchmark-avm/SKILL.md b/barretenberg/.claude/skills/benchmark-avm/SKILL.md index d5e3b55dd6e..569c346dda1 100644 --- a/barretenberg/.claude/skills/benchmark-avm/SKILL.md +++ b/barretenberg/.claude/skills/benchmark-avm/SKILL.md @@ -15,10 +15,9 @@ The benchmark is a jest test, `yarn-project/bb-prover/src/avm_proving_tests/avm_ The bulk test drives a real public-tx flow: it **simulates** the `AvmTest` contract against a live **world-state DB**, then **proves** with `bb-avm`. Since the world-state runs as a separate **IPC process** (`aztec-wsdb`) reached through two top-level components, `wsdb/` and `ipc-runtime/`, that are `portal:`-linked into yarn-project. A plain `./bootstrap.sh build yarn-project` does **not** wire these up (it serves cached artifacts), and the pinned `AvmTest` artifact and several `dest/` trees might go stale. The archaeology to discover all this is what turns a "quick bench" into hours; the recipe below is the distilled fast path. Run it from the repo root: ```bash -# 1. C++ binaries: AVM prover + native addon (@aztec/native) + IPC world-state DB +# 1. C++ binaries: AVM prover + IPC world-state DB (cd barretenberg/cpp && cmake --preset default -DAVM=ON \ - && cmake --build build --target bb-avm aztec-wsdb nodejs_module) -(cd barretenberg/ts && BUILD_CPP=0 ./scripts/copy_native.sh) # place nodejs_module.node where findNapiBinary() looks + && cmake --build build --target bb-avm aztec-wsdb) # 2. IPC world-state stack (portal deps; NOT set up by a yarn-project build). # wsdb/bootstrap.sh copies the aztec-wsdb binary built in step 1 into wsdb/ts/build//. @@ -46,7 +45,6 @@ Fast triage when the bulk test fails during setup: | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `TypeError: NativeWorldState is not a constructor` (or `BaseNativeWorldState`), thrown from `world-state/dest/native/native_world_state_instance.js` | Stale `world-state`/`@aztec/native` `dest/` — merge-train changed the native world-state API (`@aztec/native` no longer exports `NativeWorldState`) but the compiled JS is pre-merge | `yarn build` (step 4) to recompile `dest/`. A cached `./bootstrap.sh build yarn-project` will **not** fix this — it restores the stale build. | | `Cannot find module '@aztec-labs/wsdb'` / `'@aztec-foundation/ipc-runtime'` | The portal deps aren't installed | steps 2 + `yarn install` (step 4) | -| `NAPI binary not found for current platform` | `nodejs_module.node` not where `findNapiBinary()` looks | `(cd barretenberg/ts && BUILD_CPP=0 ./scripts/copy_native.sh)` | | Test **reverts**: `expect(result.revertCode.isOK()).toBe(true)` fails, log shows `Reverted code: Reverted` and `Total instructions executed: 0` | Stale `AvmTest` artifact — pre-merge bytecode embeds old constants/gas vs the rebuilt bb-avm/simulator, so the call reverts at dispatch | step 3 (recompile `avm_test_contract`) + `yarn workspace @aztec/noir-test-contracts.js generate` + `yarn build` | Note: `Total instructions executed: 0` prints even on a **healthy** passing run — that counter is a cosmetic metrics quirk, not a signal. The real health check is `revertCode` (OK vs Reverted) and a non-trivial `Proving (all)` time (a real bulk trace proves in ~4–5s; a truly empty one would be near-instant). diff --git a/barretenberg/.gitignore b/barretenberg/.gitignore index dca89bdb051..050c2d1512d 100644 --- a/barretenberg/.gitignore +++ b/barretenberg/.gitignore @@ -10,8 +10,7 @@ cmake-build-debug bench-out # Generated code from msgpack schema (run `yarn generate` in ts/) -rust/barretenberg-rs/src/generated_types.rs -rust/barretenberg-rs/src/api.rs +rust/barretenberg-rs/src/generated/ ts/src/cbind/generated/ # Codegen output dirs (ipc-codegen emits into a `generated/` subdir under each consumer) diff --git a/barretenberg/CLAUDE.md b/barretenberg/CLAUDE.md index 5c6da832e4b..29774c659e5 100644 --- a/barretenberg/CLAUDE.md +++ b/barretenberg/CLAUDE.md @@ -69,7 +69,7 @@ Bootstrap modes: - `./bootstrap.sh` => generate TypeScript bindings and build. See package.json for more fine-grained commands. Other commands: - `yarn build:esm` => the quickest way to rebuild, if only changes inside ts/ folder, and only testing yarn-project. -- `BUILD_CPP=1 scripts/copy_native.sh` => Ensures required cpp code is build (bb and nodejs_module) and copies into expected location. +- `BUILD_CPP=1 bb.js/scripts/copy_native.sh` => Builds the native bb binary and copies it to where bb.js expects it. ## Integration testing diff --git a/barretenberg/acir_tests/package.json b/barretenberg/acir_tests/package.json index d8ead2d5612..d0cddb1f814 100644 --- a/barretenberg/acir_tests/package.json +++ b/barretenberg/acir_tests/package.json @@ -15,6 +15,7 @@ "ws": "^8.17.1", "basic-ftp": "^5.2.0", "node-forge": "^1.3.2", - "serialize-javascript": "^7.0.3" + "serialize-javascript": "^7.0.3", + "@aztec-foundation/ipc-runtime": "portal:../../ipc-runtime/ts" } } diff --git a/barretenberg/acir_tests/yarn.lock b/barretenberg/acir_tests/yarn.lock index d6ea9a35c81..91e9dd9eb49 100644 --- a/barretenberg/acir_tests/yarn.lock +++ b/barretenberg/acir_tests/yarn.lock @@ -16,6 +16,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@portal:../../ts/bb.js::locator=bbjs-test%40workspace%3Abbjs-test" dependencies: + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" comlink: "npm:^4.4.1" commander: "npm:^12.1.0" idb-keyval: "npm:^6.2.1" @@ -32,6 +33,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@portal:../../ts/bb.js::locator=browser-test-app%40workspace%3Abrowser-test-app" dependencies: + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" comlink: "npm:^4.4.1" commander: "npm:^12.1.0" idb-keyval: "npm:^6.2.1" @@ -44,6 +46,12 @@ __metadata: languageName: node linkType: soft +"@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Facir-tests%40workspace%3A." + languageName: node + linkType: soft + "@aztec/acir-tests@workspace:.": version: 0.0.0-use.local resolution: "@aztec/acir-tests@workspace:." diff --git a/barretenberg/cpp/.gitignore b/barretenberg/cpp/.gitignore index 430b399a98c..03af6faa5eb 100644 --- a/barretenberg/cpp/.gitignore +++ b/barretenberg/cpp/.gitignore @@ -16,6 +16,7 @@ barretenberg_modules.dot barretenberg_modules.png src/barretenberg/bb/config.hpp src/barretenberg/avm/generated/ +src/barretenberg/bbapi/generated/ src/barretenberg/cdb/generated/ bench-out *.bak diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index ab8c78c3555..bcb0a8b9d32 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -543,7 +543,7 @@ { "name": "amd64-windows", "displayName": "Build for Windows amd64 with Zig", - "description": "Cross-compile for Windows x86_64 using Zig (MinGW). Excludes ipc/lmdb/world_state/nodejs_module (no Windows support).", + "description": "Cross-compile for Windows x86_64 using Zig (MinGW). Excludes world_state (no Windows support).", "inherits": "cross-base", "environment": { "CC": "zig cc -target x86_64-windows-gnu", @@ -801,25 +801,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "nodejs_module", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "nodejs_module", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "nodejs_module", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "nodejs_module", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 81256e04c8f..1c38af9a770 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -132,7 +132,7 @@ function build_native_objects { if ! cache_exists barretenberg-$native_preset-$hash.zst; then cmake --preset "$native_preset" targets=$(cmake --build --preset "$native_preset" --target help | awk -F: '$1 ~ /(_objects|_tests|_bench|_gen|.a)$/ && $1 !~ /^cmake_/{print $1}' | tr '\n' ' ') - cmake --build --preset "$native_preset" --target $targets nodejs_module + cmake --build --preset "$native_preset" --target $targets fi } diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index a850530a715..11967cf84c2 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -122,13 +122,9 @@ if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) add_subdirectory(barretenberg/world_state_reference) # NOTE: Do not conditionally base this on the AVM flag as it defines a necessary vm2_sim library. add_subdirectory(barretenberg/vm2) - add_subdirectory(barretenberg/ipc) add_subdirectory(barretenberg/vm2_wsdb) add_subdirectory(barretenberg/cdb) add_subdirectory(barretenberg/avm) - # nodejs_module hosts only the msgpack_client NAPI wrappers now (bb.js's SHM - # transport). The lmdb_store kv-store NAPI moved to native-packages/kvdb. - add_subdirectory(barretenberg/nodejs_module) endif() # Pull in ipc-runtime as a C++ dependency. Provides the `ipc_runtime` @@ -226,7 +222,7 @@ add_library( ) # bb-external: static library for external consumers (e.g. barretenberg-rs). -# Uses the core object list without lmdb/world_state — FFI consumers only need bbapi(). +# Uses the core object list without lmdb/world_state — FFI consumers only need ipc_ffi_entry(). # Built with -fvisibility=hidden; only WASM_EXPORT symbols remain visible. if(NOT WASM) # world_state_reference (the in-memory reference merkle DB and its bb_wsref_* C ABI) rides diff --git a/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt index 9fcf0636252..185425b29e6 100644 --- a/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt @@ -6,5 +6,5 @@ if(AVM_TRANSPILER_LIB) endif() if(NOT WASM AND NOT BB_LITE) - target_link_libraries(api_objects PRIVATE ipc) + target_link_libraries(api_objects PRIVATE ipc_runtime) endif() diff --git a/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp b/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp index 0abba6e00c3..eefdf533090 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp +++ b/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp @@ -1,23 +1,20 @@ #include "barretenberg/api/api_msgpack.hpp" -#include "barretenberg/bbapi/c_bind.hpp" +#include "barretenberg/bbapi/bbapi_handlers.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/generated/bb_dispatch.hpp" +#include "barretenberg/common/assert.hpp" #include "barretenberg/common/log.hpp" -#include "barretenberg/serialize/msgpack.hpp" #include #include #include +#include #include +#include #include #if !defined(__wasm__) && !defined(_WIN32) -#include "barretenberg/ipc/ipc_server.hpp" -#include -#include -#include -#ifdef __linux__ -#include -#elif defined(__APPLE__) -#include -#endif +#include "ipc_runtime/serve_helper.hpp" +#include "ipc_runtime/signal_handlers.hpp" #endif namespace bb { @@ -27,309 +24,108 @@ int process_msgpack_commands(std::istream& input_stream) // Redirect std::cout to stderr to prevent accidental writes to stdout auto* original_cout_buf = std::cout.rdbuf(); std::cout.rdbuf(std::cerr.rdbuf()); - - // Create an ostream that writes directly to stdout std::ostream stdout_stream(original_cout_buf); - // Process length-encoded msgpack buffers + // BBApiRequest lives across calls so IVC state (loaded circuit, + // accumulator, etc.) persists between Chonk* invocations. + bb::bbapi::BBApiRequest request; + auto handler = bb::bbapi::make_bb_handler(request); + while (!input_stream.eof()) { - // Read 4-byte length prefix in little-endian format uint32_t length = 0; input_stream.read(reinterpret_cast(&length), sizeof(length)); - if (input_stream.gcount() != sizeof(length)) { - // End of stream or incomplete length - break; + break; // EOF or incomplete length } - // Read the msgpack buffer std::vector buffer(length); input_stream.read(reinterpret_cast(buffer.data()), static_cast(length)); - if (input_stream.gcount() != static_cast(length)) { std::cerr << "Error: Incomplete msgpack buffer read" << '\n'; - // Restore original cout buffer before returning std::cout.rdbuf(original_cout_buf); return 1; } - msgpack::sbuffer response_buffer; + // The generated dispatch responds through a callback; handlers here + // complete synchronously, so capture the response and write it out. + // It turns a malformed envelope, an unknown command and a throwing + // handler into error frames itself, but a payload that fails to unpack or + // convert still throws: catch that here rather than let it take down a + // process that is mid-stream and owes the caller a response. + std::vector response; #ifndef BB_NO_EXCEPTIONS try { #endif - // Deserialize the msgpack buffer - // The buffer should contain a tuple of arguments (array) matching the bbapi function signature. - // Since bbapi(Command) takes one argument, we expect a 1-element array containing the Command. - auto unpacked = msgpack::unpack(reinterpret_cast(buffer.data()), buffer.size()); - auto obj = unpacked.get(); - - // First, expect an array (the tuple of arguments) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { - throw_or_abort("Expected an array of size 1 (tuple of arguments) for bbapi command deserialization"); - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - auto& tuple_arr = obj.via.array; - auto& command_obj = tuple_arr.ptr[0]; - - // Now access the Command itself, which should be an array of size 2 [command-name, payload] - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - if (command_obj.type != msgpack::type::ARRAY || command_obj.via.array.size != 2) { - throw_or_abort("Expected Command to be an array of size 2 [command-name, payload]"); - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - auto& command_arr = command_obj.via.array; - if (command_arr.ptr[0].type != msgpack::type::STR) { - throw_or_abort("Expected first element of Command to be a string (type name)"); - } - - // Convert to Command (which is a NamedUnion) - bb::bbapi::Command command; - command_obj.convert(command); - - // Execute the command - auto response = bbapi::bbapi(std::move(command)); - - // Serialize the response - msgpack::pack(response_buffer, response); + handler(buffer, [&response](std::vector r) { response = std::move(r); }); #ifndef BB_NO_EXCEPTIONS } catch (const std::exception& e) { - response_buffer.clear(); - bb::bbapi::CommandResponse response = bb::bbapi::ErrorResponse{ .message = e.what() }; - msgpack::pack(response_buffer, response); + response = bbapi::detail::make_error(e.what()); } #endif - // Write length-encoded response directly to stdout - uint32_t response_length = static_cast(response_buffer.size()); + auto response_length = static_cast(response.size()); stdout_stream.write(reinterpret_cast(&response_length), sizeof(response_length)); - stdout_stream.write(response_buffer.data(), static_cast(response_buffer.size())); + stdout_stream.write(reinterpret_cast(response.data()), + static_cast(response.size())); stdout_stream.flush(); } - // Restore original cout buffer std::cout.rdbuf(original_cout_buf); return 0; } -#if !defined(__wasm__) && !defined(_WIN32) -// Set up platform-specific parent death monitoring -// This ensures the bb process exits when the parent (Node.js) dies -static void setup_parent_death_monitoring() -{ -#ifdef __linux__ - // Linux: Use prctl to request SIGTERM when parent dies - // This is kernel-level and very reliable - if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { - std::cerr << "Warning: Could not set parent death signal" << '\n'; - } -#elif defined(__APPLE__) - // macOS: Use kqueue to monitor parent process - // Spawn a dedicated thread that blocks waiting for parent to exit - pid_t parent_pid = getppid(); - std::thread([parent_pid]() { - int kq = kqueue(); - if (kq == -1) { - std::cerr << "Warning: Could not create kqueue for parent monitoring" << '\n'; - return; - } - - struct kevent change; - EV_SET(&change, parent_pid, EVFILT_PROC, EV_ADD | EV_ENABLE, NOTE_EXIT, 0, nullptr); - if (kevent(kq, &change, 1, nullptr, 0, nullptr) == -1) { - std::cerr << "Warning: Could not monitor parent process" << '\n'; - close(kq); - return; - } - - // Block until parent exits - struct kevent event; - kevent(kq, nullptr, 0, &event, 1, nullptr); - - std::cerr << "Parent process exited, shutting down..." << '\n'; - close(kq); - std::exit(0); - }).detach(); -#endif -} - -int execute_msgpack_ipc_server(std::unique_ptr server) -{ - // Store server pointer for signal handler cleanup (works for both socket and shared memory) - // MUST be set before listen() since SIGBUS can occur during listen() - static ipc::IpcServer* global_server = server.get(); - - // Register signal handlers for graceful cleanup - // MUST be registered before listen() since SIGBUS can occur during initialization - // SIGTERM: Sent by processes/test frameworks on shutdown - // SIGINT: Sent by Ctrl+C - auto graceful_shutdown_handler = [](int signal) { - std::cerr << "\nReceived signal " << signal << ", shutting down gracefully..." << '\n'; - if (global_server) { - global_server->request_shutdown(); - } - }; - - // Register handlers for fatal memory errors (SIGBUS, SIGSEGV) - // These occur when shared memory exhaustion happens during initialization - auto fatal_error_handler = [](int signal) { - const char* signal_name = "UNKNOWN"; - if (signal == SIGBUS) { - signal_name = "SIGBUS"; - } else if (signal == SIGSEGV) { - signal_name = "SIGSEGV"; - } - std::cerr << "\nFatal error: received " << signal_name << " during initialization" << '\n'; - std::cerr << "This likely means shared memory exhaustion (try reducing --max-clients)" << '\n'; - - // Clean up IPC resources before exiting - if (global_server) { - global_server->close(); - } - - std::exit(1); - }; - - (void)std::signal(SIGTERM, graceful_shutdown_handler); - (void)std::signal(SIGINT, graceful_shutdown_handler); - (void)std::signal(SIGBUS, fatal_error_handler); - (void)std::signal(SIGSEGV, fatal_error_handler); - - // Set up parent death monitoring (kills this process when parent dies) - setup_parent_death_monitoring(); - - if (!server->listen()) { - std::cerr << "Error: Could not start IPC server" << '\n'; - return 1; - } - - std::cerr << "IPC server ready" << '\n'; - - // Run server with msgpack handler - server->run([](int client_id, std::span request) -> std::vector { - try { - // Deserialize msgpack command - // The buffer should contain a tuple of arguments (array) matching the bbapi function signature. - // Since bbapi(Command) takes one argument, we expect a 1-element array containing the Command. - auto unpacked = msgpack::unpack(reinterpret_cast(request.data()), request.size()); - auto obj = unpacked.get(); - - // First, expect an array (the tuple of arguments) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { - std::cerr << "Error: Expected an array of size 1 (tuple of arguments) from client " << client_id - << '\n'; - return {}; // Return empty to skip response - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - auto& tuple_arr = obj.via.array; - auto& command_obj = tuple_arr.ptr[0]; - - // Now access the Command itself, which should be an array of size 2 [command-name, payload] - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - if (command_obj.type != msgpack::type::ARRAY || command_obj.via.array.size != 2) { - std::cerr << "Error: Expected Command to be an array of size 2 [command-name, payload] from client " - << client_id << '\n'; - return {}; - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - auto& command_arr = command_obj.via.array; - if (command_arr.ptr[0].type != msgpack::type::STR) { - std::cerr << "Error: Expected first element of Command to be a string (type name) from client " - << client_id << '\n'; - return {}; - } - - // Check if this is a Shutdown command - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) - std::string command_name(command_arr.ptr[0].via.str.ptr, command_arr.ptr[0].via.str.size); - bool is_shutdown = (command_name == "Shutdown"); - - // Convert to Command and execute - bb::bbapi::Command command; - command_obj.convert(command); - auto response = bbapi::bbapi(std::move(command)); - - // Serialize response - msgpack::sbuffer response_buffer; - msgpack::pack(response_buffer, response); - std::vector result(response_buffer.data(), response_buffer.data() + response_buffer.size()); - - // If this was a shutdown command, throw exception with response - // This signals the server to send the response and then exit gracefully - if (is_shutdown) { - throw ipc::ShutdownRequested(std::move(result)); - } - - return result; - } catch (const ipc::ShutdownRequested&) { - // Re-throw shutdown request - throw; - } catch (const std::exception& e) { - // Log error to stderr for debugging (goes to log file if logger enabled) - std::cerr << "Error processing request from client " << client_id << ": " << e.what() << '\n'; - std::cerr.flush(); - - // Create error response with exception message - bb::bbapi::ErrorResponse error_response{ .message = std::string(e.what()) }; - bb::bbapi::CommandResponse response = error_response; - - // Serialize and return error response to client - msgpack::sbuffer response_buffer; - msgpack::pack(response_buffer, response); - return std::vector(response_buffer.data(), response_buffer.data() + response_buffer.size()); - } - }); - - server->close(); - return 0; -} -#endif - int execute_msgpack_run(const std::string& msgpack_input_file, [[maybe_unused]] int max_clients, [[maybe_unused]] size_t request_ring_size, [[maybe_unused]] size_t response_ring_size) { #if !defined(__wasm__) && !defined(_WIN32) - // Check if this is a shared memory path (ends with .shm) - if (!msgpack_input_file.empty() && msgpack_input_file.size() >= 4 && - msgpack_input_file.substr(msgpack_input_file.size() - 4) == ".shm") { - // Strip .shm suffix to get base name - std::string base_name = msgpack_input_file.substr(0, msgpack_input_file.size() - 4); - auto server = ipc::IpcServer::create_shm(base_name, request_ring_size, response_ring_size); - std::cerr << "Shared memory server at " << base_name << '\n'; - return execute_msgpack_ipc_server(std::move(server)); - } - - // Check if this is a Unix domain socket path (ends with .sock) - if (!msgpack_input_file.empty() && msgpack_input_file.size() >= 5 && - msgpack_input_file.substr(msgpack_input_file.size() - 5) == ".sock") { - // Socket server still supports max_clients (multiple clients via MPSC) - auto server = ipc::IpcServer::create_socket(msgpack_input_file, max_clients); - std::cerr << "Socket server at " << msgpack_input_file << '\n'; - return execute_msgpack_ipc_server(std::move(server)); - } -#endif - - // Process msgpack API commands from stdin or file - std::istream* input_stream = &std::cin; - std::ifstream file_stream; - - if (!msgpack_input_file.empty()) { - file_stream.open(msgpack_input_file, std::ios::binary); - if (!file_stream.is_open()) { - std::cerr << "Error: Could not open input file: " << msgpack_input_file << '\n'; + // Live transports: stdio pipe ("" / "-"), UDS (*.sock), MPSC-SHM (*.shm), + // all served by the shared ipc-runtime server with envelope framing. + const std::string input_path = msgpack_input_file.empty() ? "-" : msgpack_input_file; + ipc::ServerOptions opts{ + .max_shm_clients = static_cast(max_clients), + .shm_request_ring_size = request_ring_size, + .shm_response_ring_size = response_ring_size, + .socket_backlog = max_clients, + }; + if (auto server = ipc::make_server(input_path, opts)) { + // Install runtime lifecycle handlers (SIGTERM/SIGINT → request_shutdown, + // SIGBUS/SIGSEGV → close+exit, parent-death watch, SIGPIPE → EPIPE) + // before listen(): SIGBUS can occur during init when SHM is exhausted. + ipc::install_default_signal_handlers(*server); + if (!server->listen()) { + std::cerr << "Error: Could not start IPC server at " << input_path << '\n'; return 1; } - input_stream = &file_stream; + std::cerr << "bb msgpack serving " << input_path << '\n'; + + // One request context for the whole serve so stateful command + // sequences (ChonkStart/Load/Accumulate/Prove) share IVC state. + bb::bbapi::BBApiRequest request; + auto handler = bb::bbapi::make_bb_handler(request); + // Every bb handler responds before it returns, so serve with the serial run() loop rather than run_reactor: + // the request stays a zero-copy span into the ring and the response is sent straight back, with none of the + // reactor's per-request copy, completion queue and cross-thread wake, which only a deferring handler needs. + server->run([&handler](int /*client_id*/, std::span raw) { + std::optional> response; + handler(raw, [&response](std::vector r) { response = std::move(r); }); + BB_ASSERT(response.has_value(), "bb handlers must respond before returning"); + return std::move(*response); + }); + + server->close(); + return 0; } +#endif - return process_msgpack_commands(*input_stream); + // Offline replay: bare length-prefixed commands from a file. + std::ifstream file_stream(msgpack_input_file, std::ios::binary); + if (!file_stream.is_open()) { + std::cerr << "Error: Could not open input file: " << msgpack_input_file << '\n'; + return 1; + } + return process_msgpack_commands(file_stream); } } // namespace bb diff --git a/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp b/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp index 8f4a22cffa7..aa4d0fde9a7 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp +++ b/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp @@ -2,57 +2,40 @@ #include #include -#include #include -#ifndef __wasm__ -#include "barretenberg/ipc/ipc_server.hpp" -#endif - namespace bb { /** - * @brief Process msgpack API commands from an input stream - * - * This function reads length-encoded msgpack buffers from the provided input stream, - * deserializes them into Command objects, executes them via the bbapi interface, - * and writes length-encoded responses back to stdout. + * @brief Process msgpack API commands from an input stream (offline replay / wasm). * - * The format for each message is: - * - 4-byte length prefix (little-endian) - * - msgpack buffer of the specified length + * Reads bare length-prefixed msgpack buffers ([4-byte LE length][payload]) from + * the stream, executes them via the generated bbapi dispatch, and writes + * length-prefixed responses to stdout. This is the offline-file format; live + * transports (stdio pipe, socket, shared memory) run over ipc-runtime with its + * request-id envelope framing instead. * - * @param input_stream The input stream to read msgpack commands from (stdin or file) + * @param input_stream The input stream to read msgpack commands from * @return int Status code: 0 for success, non-zero for errors */ int process_msgpack_commands(std::istream& input_stream); -#ifndef __wasm__ /** - * @brief Execute msgpack commands over IPC + * @brief Execute the `bb msgpack run` subcommand. * - * Runs an IPC server that accepts concurrent clients. - * Clients can send msgpack commands independently, and responses are automatically - * routed back to the correct client. - * - * @param server IPC server instance (socket or shared memory) - * @return int Status code: 0 for success, non-zero for errors - */ -int execute_msgpack_ipc_server(std::unique_ptr server); -#endif - -/** - * @brief Execute msgpack run command + * Input selection: + * - "" or "-" → serve the process's own stdin/stdout (ipc-runtime pipe transport) + * - "*.sock" → serve a Unix domain socket + * - "*.shm" → serve MPSC shared memory + * - existing file → offline replay of bare length-prefixed commands * - * This function handles the msgpack run subcommand, reading commands from either - * stdin, a specified file, a Unix domain socket (if path ends in .sock), or - * shared memory IPC (if path ends in .shm). + * All live transports use the shared ipc-runtime server (request-id envelope + * framing), served by its serial run() loop: every bb handler responds inline. * - * @param msgpack_input_file Path to input file (empty string means use stdin, - * .sock suffix means Unix socket, .shm suffix means shared memory) - * @param max_clients Maximum number of concurrent clients for IPC servers (default: 1) - * @param request_ring_size Request ring buffer size for shared memory (default: 1MB) - * @param response_ring_size Response ring buffer size for shared memory (default: 1MB) + * @param msgpack_input_file Input path as above + * @param max_clients Maximum concurrent clients for IPC servers + * @param request_ring_size Request ring size for shared memory + * @param response_ring_size Response ring size for shared memory * @return int Status code: 0 for success, non-zero for errors */ int execute_msgpack_run(const std::string& msgpack_input_file, diff --git a/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt index fa6b7858a1e..e0f3d89f9dd 100644 --- a/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt @@ -23,7 +23,7 @@ if (NOT(FUZZING)) target_link_libraries(bb PRIVATE avm_transpiler) endif() if(NOT WASM AND NOT BB_LITE) - target_link_libraries(bb PRIVATE ipc) + target_link_libraries(bb PRIVATE ipc_runtime) endif() if(ENABLE_STACKTRACES) target_link_libraries( @@ -63,7 +63,7 @@ if (NOT(FUZZING)) target_link_libraries(bb-avm PRIVATE avm_transpiler) endif() if(NOT WASM AND NOT BB_LITE) - target_link_libraries(bb-avm PRIVATE ipc) + target_link_libraries(bb-avm PRIVATE ipc_runtime) endif() if(ENABLE_STACKTRACES) target_link_libraries( diff --git a/barretenberg/cpp/src/barretenberg/bb/cli.cpp b/barretenberg/cpp/src/barretenberg/bb/cli.cpp index afe3529ad65..eb7fb3e1699 100644 --- a/barretenberg/cpp/src/barretenberg/bb/cli.cpp +++ b/barretenberg/cpp/src/barretenberg/bb/cli.cpp @@ -24,6 +24,7 @@ #include "barretenberg/bb/cli11_formatter.hpp" #include "barretenberg/bb/curve_constants.hpp" #include "barretenberg/bbapi/bbapi.hpp" +#include "barretenberg/bbapi/bbapi_schema.hpp" #include "barretenberg/bbapi/bbapi_ultra_honk.hpp" #include "barretenberg/bbapi/c_bind.hpp" #include "barretenberg/common/bb_bench.hpp" @@ -913,7 +914,7 @@ int parse_and_run_cli_command(int argc, char* argv[]) // MSGPACK if (msgpack_schema_command->parsed()) { - std::cout << bbapi::get_msgpack_schema_as_json() << std::endl; + std::cout << bbapi::get_bb_schema_as_json() << std::endl; return 0; } if (msgpack_curve_constants_command->parsed()) { diff --git a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt index 8baaef5b027..e7a90defb53 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt @@ -1,5 +1,55 @@ +# Generate BB IPC wire types and server dispatch from bb_schema.json (the +# checked-in wire contract). bb is the server; the clients are bb.js and +# barretenberg-rs, generated from the same file, so no C++ client is emitted. +# WASM builds consume the generated dispatch header, so codegen must run +# outside native-only blocks. +set(BB_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/bb_schema.json) +set(BB_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) + +if(NOT FUZZING) + set(BB_GEN_OUTPUTS + ${BB_GEN_DIR}/bb_dispatch.hpp + ${BB_GEN_DIR}/bb_ipc_server.hpp + ${BB_GEN_DIR}/bb_types.hpp + ${BB_GEN_DIR}/ipc_codegen/msgpack_adaptor.hpp + ${BB_GEN_DIR}/ipc_codegen/msgpack_include.hpp + ${BB_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 ${BB_GEN_OUTPUTS} + COMMAND node --experimental-strip-types --experimental-transform-types --no-warnings + ${IPC_CODEGEN_DIR}/src/generate.ts + --schema ${BB_SCHEMA} + --lang cpp + --out ${BB_GEN_DIR} + --server + --cpp-namespace bb::bbapi + --cpp-include-dir barretenberg/bbapi/generated + --strip-method-prefix + DEPENDS ${BB_SCHEMA} ${IPC_CODEGEN_SRC} + COMMENT "Generating BB IPC wire types + server dispatch from bb_schema.json" + VERBATIM + ) + add_custom_target(bb_ipc_generated DEPENDS ${BB_GEN_OUTPUTS}) +endif() + +# Embed the schema so `bb msgpack schema` reports the exact contract this +# binary was built against. Configure-time: re-runs when the schema changes. +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${BB_SCHEMA}) +file(READ ${BB_SCHEMA} BB_SCHEMA_CONTENT) +configure_file(bb_schema_embed.hpp.in ${BB_GEN_DIR}/bb_schema_embed.hpp @ONLY) + barretenberg_module(bbapi common chonk dsl crypto_poseidon2 crypto_pedersen_commitment crypto_pedersen_hash crypto_blake2s crypto_aes128 crypto_schnorr crypto_ecdsa ecc srs) +if(NOT FUZZING) + add_dependencies(bbapi_objects bb_ipc_generated) +endif() + # bbapi_tests needs vm2_stub to resolve dsl's AVM recursion constraint references if(NOT WASM AND NOT FUZZING) target_link_libraries(bbapi_tests PRIVATE vm2_stub) diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json b/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json new file mode 100644 index 00000000000..20dab049c50 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json @@ -0,0 +1,36 @@ +{ + "bn254_fr_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + "bn254_fq_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", + "bn254_g1_generator": { + "x": "0000000000000000000000000000000000000000000000000000000000000001", + "y": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "bn254_g2_generator": { + "x": [ + "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed", + "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2" + ], + "y": [ + "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", + "090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b" + ] + }, + "grumpkin_fr_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", + "grumpkin_fq_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + "grumpkin_g1_generator": { + "x": "0000000000000000000000000000000000000000000000000000000000000001", + "y": "0000000000000002cf135e7506a45d632d270d45f1181294833fc48d823f272c" + }, + "secp256k1_fr_modulus": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "secp256k1_fq_modulus": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + "secp256k1_g1_generator": { + "x": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "y": "483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + }, + "secp256r1_fr_modulus": "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", + "secp256r1_fq_modulus": "ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", + "secp256r1_g1_generator": { + "x": "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + "y": "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5" + } +} \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json b/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json new file mode 100644 index 00000000000..0a305b611f8 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json @@ -0,0 +1,646 @@ +{ + "service": "Bb", + "error": { + "message": "string" + }, + "aliases": { + "fq": "bin32", + "fr": "bin32", + "secp256k1_fq": "bin32", + "secp256k1_fr": "bin32", + "secp256r1_fq": "bin32", + "secp256r1_fr": "bin32", + "uint256_t": "bin32" + }, + "types": { + "AvmStat": { + "name": "string", + "value_ms": "u64" + }, + "Bn254G1Point": { + "x": "fq", + "y": "fq" + }, + "Bn254G2Point": { + "x": "fq[2]", + "y": "fq[2]" + }, + "ChonkProof": { + "hiding_oink_proof": "fr[]", + "merge_proof": "fr[]", + "eccvm_proof": "fr[]", + "ipa_proof": "fr[]", + "joint_proof": "fr[]" + }, + "CircuitInput": { + "name": "string", + "bytecode": "bytes", + "verification_key": "bytes" + }, + "CircuitInputNoVK": { + "name": "string", + "bytecode": "bytes" + }, + "GrumpkinPoint": { + "x": "fr", + "y": "fr" + }, + "ProofSystemSettings": { + "ipa_accumulation": "bool", + "oracle_hash_type": "string", + "disable_zk": "bool", + "optimized_solidity_verifier": "bool" + }, + "Secp256k1Point": { + "x": "secp256k1_fq", + "y": "secp256k1_fq" + }, + "Secp256r1Point": { + "x": "secp256r1_fq", + "y": "secp256r1_fq" + }, + "VkData": { + "bytes": "bytes", + "fields": "uint256_t[]", + "hash": "bytes" + } + }, + "commands": { + "AvmProve": { + "request": { + "inputs": "bytes" + }, + "response": { + "proof": "fr[]", + "stats": "AvmStat[]" + } + }, + "AvmVerify": { + "request": { + "proof": "fr[]", + "public_inputs": "bytes" + }, + "response": { + "verified": "bool" + } + }, + "AvmCheckCircuit": { + "request": { + "inputs": "bytes" + }, + "response": { + "passed": "bool", + "stats": "AvmStat[]" + } + }, + "CircuitProve": { + "request": { + "circuit": "CircuitInput", + "witness": "bytes", + "settings": "ProofSystemSettings" + }, + "response": { + "public_inputs": "uint256_t[]", + "proof": "uint256_t[]", + "vk": "VkData" + } + }, + "CircuitComputeVk": { + "request": { + "circuit": "CircuitInputNoVK", + "settings": "ProofSystemSettings" + }, + "response": { + "bytes": "bytes", + "fields": "uint256_t[]", + "hash": "bytes" + } + }, + "CircuitStats": { + "request": { + "circuit": "CircuitInput", + "include_gates_per_opcode": "bool", + "settings": "ProofSystemSettings" + }, + "response": { + "num_gates": "u32", + "num_gates_dyadic": "u32", + "num_acir_opcodes": "u32", + "gates_per_opcode": "u32[]" + } + }, + "CircuitVerify": { + "request": { + "verification_key": "bytes", + "public_inputs": "uint256_t[]", + "proof": "uint256_t[]", + "settings": "ProofSystemSettings" + }, + "response": { + "verified": "bool" + } + }, + "ChonkComputeVk": { + "request": { + "circuit": "CircuitInputNoVK", + "kind": "u8" + }, + "response": { + "bytes": "bytes", + "fields": "fr[]" + } + }, + "ChonkStart": { + "request": { + "kinds": "u8[]" + }, + "response": {} + }, + "ChonkLoad": { + "request": { + "circuit": "CircuitInput", + "kind": "u8" + }, + "response": {} + }, + "ChonkAccumulate": { + "request": { + "witness": "bytes" + }, + "response": {} + }, + "ChonkProve": { + "request": {}, + "response": { + "proof": "ChonkProof" + } + }, + "ChonkVerify": { + "request": { + "proof": "ChonkProof", + "vk": "bytes" + }, + "response": { + "valid": "bool" + } + }, + "ChonkVerifyFromFields": { + "request": { + "proof": "fr[]", + "vk": "bytes" + }, + "response": { + "valid": "bool" + } + }, + "ChonkBatchVerify": { + "request": { + "proofs": "ChonkProof[]", + "vks": "bytes[]" + }, + "response": { + "valid": "bool" + } + }, + "VkAsFields": { + "request": { + "verification_key": "bytes" + }, + "response": { + "fields": "fr[]" + } + }, + "MegaVkAsFields": { + "request": { + "verification_key": "bytes" + }, + "response": { + "fields": "fr[]" + } + }, + "MegaAppVkAsFields": { + "request": { + "verification_key": "bytes" + }, + "response": { + "fields": "fr[]" + } + }, + "MegaKernelVkAsFields": { + "request": { + "verification_key": "bytes" + }, + "response": { + "fields": "fr[]" + } + }, + "MegaZKVkAsFields": { + "request": { + "verification_key": "bytes" + }, + "response": { + "fields": "fr[]" + } + }, + "CircuitWriteSolidityVerifier": { + "request": { + "verification_key": "bytes", + "settings": "ProofSystemSettings" + }, + "response": { + "solidity_code": "string" + } + }, + "ChonkCheckPrecomputedVk": { + "request": { + "circuit": "CircuitInput", + "kind": "u8" + }, + "response": { + "valid": "bool", + "actual_vk": "bytes" + } + }, + "ChonkStats": { + "request": { + "circuit": "CircuitInputNoVK", + "include_gates_per_opcode": "bool" + }, + "response": { + "acir_opcodes": "u32", + "circuit_size": "u32", + "gates_per_opcode": "u32[]" + } + }, + "ChonkCompressProof": { + "request": { + "proof": "ChonkProof" + }, + "response": { + "compressed_proof": "bytes" + } + }, + "ChonkDecompressProof": { + "request": { + "compressed_proof": "bytes" + }, + "response": { + "proof": "ChonkProof" + } + }, + "Poseidon2Hash": { + "request": { + "inputs": "fr[]" + }, + "response": { + "hash": "fr" + } + }, + "Poseidon2Permutation": { + "request": { + "inputs": "fr[4]" + }, + "response": { + "outputs": "fr[4]" + } + }, + "Poseidon2AbsorbChain": { + "request": { + "state": "fr[4]", + "inputs": "bytes" + }, + "response": { + "state": "fr[4]" + } + }, + "PedersenCommit": { + "request": { + "inputs": "fr[]", + "hash_index": "u32" + }, + "response": { + "point": "GrumpkinPoint" + } + }, + "PedersenHash": { + "request": { + "inputs": "fr[]", + "hash_index": "u32" + }, + "response": { + "hash": "fr" + } + }, + "Blake2s": { + "request": { + "data": "bytes" + }, + "response": { + "hash": "u8[32]" + } + }, + "Blake2sToField": { + "request": { + "data": "bytes" + }, + "response": { + "field": "fr" + } + }, + "AesEncrypt": { + "request": { + "plaintext": "bytes", + "iv": "u8[16]", + "key": "u8[16]", + "length": "u32" + }, + "response": { + "ciphertext": "bytes" + } + }, + "AesDecrypt": { + "request": { + "ciphertext": "bytes", + "iv": "u8[16]", + "key": "u8[16]", + "length": "u32" + }, + "response": { + "plaintext": "bytes" + } + }, + "GrumpkinMul": { + "request": { + "point": "GrumpkinPoint", + "scalar": "fq" + }, + "response": { + "point": "GrumpkinPoint" + } + }, + "GrumpkinAdd": { + "request": { + "point_a": "GrumpkinPoint", + "point_b": "GrumpkinPoint" + }, + "response": { + "point": "GrumpkinPoint" + } + }, + "GrumpkinBatchMul": { + "request": { + "points": "GrumpkinPoint[]", + "scalar": "fq" + }, + "response": { + "points": "GrumpkinPoint[]" + } + }, + "GrumpkinGetRandomFr": { + "request": { + "dummy": "u8" + }, + "response": { + "value": "fr" + } + }, + "GrumpkinReduce512": { + "request": { + "input": "u8[64]" + }, + "response": { + "value": "fr" + } + }, + "Secp256k1Mul": { + "request": { + "point": "Secp256k1Point", + "scalar": "secp256k1_fr" + }, + "response": { + "point": "Secp256k1Point" + } + }, + "Secp256k1GetRandomFr": { + "request": { + "dummy": "u8" + }, + "response": { + "value": "secp256k1_fr" + } + }, + "Secp256k1Reduce512": { + "request": { + "input": "u8[64]" + }, + "response": { + "value": "secp256k1_fr" + } + }, + "Bn254FrSqrt": { + "request": { + "input": "fr" + }, + "response": { + "is_square_root": "bool", + "value": "fr" + } + }, + "Bn254FqSqrt": { + "request": { + "input": "fq" + }, + "response": { + "is_square_root": "bool", + "value": "fq" + } + }, + "Bn254G1Mul": { + "request": { + "point": "Bn254G1Point", + "scalar": "fr" + }, + "response": { + "point": "Bn254G1Point" + } + }, + "Bn254G2Mul": { + "request": { + "point": "Bn254G2Point", + "scalar": "fr" + }, + "response": { + "point": "Bn254G2Point" + } + }, + "Bn254G1IsOnCurve": { + "request": { + "point": "Bn254G1Point" + }, + "response": { + "is_on_curve": "bool" + } + }, + "Bn254G1FromCompressed": { + "request": { + "compressed": "u8[32]" + }, + "response": { + "point": "Bn254G1Point" + } + }, + "SchnorrComputePublicKey": { + "request": { + "private_key": "fq" + }, + "response": { + "public_key": "GrumpkinPoint" + } + }, + "SchnorrConstructSignature": { + "request": { + "message_field": "fr", + "private_key": "fq" + }, + "response": { + "s": "fq", + "e": "fq" + } + }, + "SchnorrVerifySignature": { + "request": { + "message_field": "fr", + "public_key": "GrumpkinPoint", + "s": "fq", + "e": "fq" + }, + "response": { + "verified": "bool" + } + }, + "EcdsaSecp256k1ComputePublicKey": { + "request": { + "private_key": "secp256k1_fr" + }, + "response": { + "public_key": "Secp256k1Point" + } + }, + "EcdsaSecp256r1ComputePublicKey": { + "request": { + "private_key": "secp256r1_fr" + }, + "response": { + "public_key": "Secp256r1Point" + } + }, + "EcdsaSecp256k1ConstructSignature": { + "request": { + "message": "bytes", + "private_key": "secp256k1_fr" + }, + "response": { + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + } + }, + "EcdsaSecp256r1ConstructSignature": { + "request": { + "message": "bytes", + "private_key": "secp256r1_fr" + }, + "response": { + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + } + }, + "EcdsaSecp256k1RecoverPublicKey": { + "request": { + "message": "bytes", + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + }, + "response": { + "public_key": "Secp256k1Point" + } + }, + "EcdsaSecp256r1RecoverPublicKey": { + "request": { + "message": "bytes", + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + }, + "response": { + "public_key": "Secp256r1Point" + } + }, + "EcdsaSecp256k1VerifySignature": { + "request": { + "message": "bytes", + "public_key": "Secp256k1Point", + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + }, + "response": { + "verified": "bool" + } + }, + "EcdsaSecp256r1VerifySignature": { + "request": { + "message": "bytes", + "public_key": "Secp256r1Point", + "r": "u8[32]", + "s": "u8[32]", + "v": "u8" + }, + "response": { + "verified": "bool" + } + }, + "SrsInitSrs": { + "request": { + "points_buf": "bytes", + "num_points": "u32", + "g2_point": "bytes" + }, + "response": { + "points_buf": "bytes" + } + }, + "ChonkBatchVerifierStart": { + "request": { + "vks": "bytes[]", + "num_cores": "u32", + "batch_size": "u32", + "fifo_path": "string" + }, + "response": {} + }, + "ChonkBatchVerifierQueue": { + "request": { + "request_id": "u64", + "vk_index": "u32", + "proof_fields": "fr[]" + }, + "response": {} + }, + "ChonkBatchVerifierStop": { + "request": {}, + "response": {} + }, + "SrsInitGrumpkinSrs": { + "request": { + "points_buf": "bytes", + "num_points": "u32" + }, + "response": { + "dummy": "u8" + } + } + } +} diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bb_schema_embed.hpp.in b/barretenberg/cpp/src/barretenberg/bbapi/bb_schema_embed.hpp.in new file mode 100644 index 00000000000..f7eb958f81b --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bb_schema_embed.hpp.in @@ -0,0 +1,7 @@ +// Configure-time embed of bb_schema.json (see CMakeLists.txt). @ONLY substitution. +#pragma once +#include + +namespace bb::bbapi { +inline constexpr std::string_view k_bb_schema_json = R"bbschema(@BB_SCHEMA_CONTENT@)bbschema"; +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi.hpp index 74210e06aa0..0d9b39960dd 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi.hpp @@ -1,14 +1,16 @@ #pragma once /** - * @file bbapi_commands.hpp + * @file bbapi.hpp * @brief Central command definitions BB's outward-facing API. * - * This file includes and exports all command structures from specialized headers - * and provides unified Command and CommandResponse types for the API. + * This file includes and exports all command structures from specialized headers. */ +#include "barretenberg/bbapi/bbapi_avm.hpp" #include "barretenberg/bbapi/bbapi_chonk.hpp" #include "barretenberg/bbapi/bbapi_crypto.hpp" -#include "barretenberg/bbapi/bbapi_execute.hpp" +#include "barretenberg/bbapi/bbapi_ecc.hpp" +#include "barretenberg/bbapi/bbapi_ecdsa.hpp" +#include "barretenberg/bbapi/bbapi_schnorr.hpp" #include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/bbapi_srs.hpp" #include "barretenberg/bbapi/bbapi_ultra_honk.hpp" -#include "barretenberg/common/named_union.hpp" diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp index 65c74d59636..c2a298cdf19 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp @@ -46,7 +46,6 @@ TYPED_TEST(BBApiMsgpack, DefaultConstructorRoundtrip) typename TypeParam::Response response{}; auto [actual_response, expected_response] = msgpack_roundtrip(response); EXPECT_EQ(actual_response, expected_response); - std::cout << msgpack_schema_to_string(command) << " " << msgpack_schema_to_string(response) << std::endl; } // Regression tests for input validation at API boundaries. diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk_handlers.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk_handlers.cpp new file mode 100644 index 00000000000..d5f1c34ce8b --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk_handlers.cpp @@ -0,0 +1,170 @@ +/** + * @file bbapi_chonk_handlers.cpp + * @brief Wire adapters for the Chonk commands: convert generated wire structs + * to the domain command structs, run `execute()`, convert the response back. + */ +#include "barretenberg/bbapi/bbapi_chonk.hpp" +#include "barretenberg/bbapi/bbapi_handlers.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/bbapi_wire_convert.hpp" + +namespace bb::bbapi { + +namespace { + +CircuitInput circuit_input_from_wire(wire::CircuitInput&& w) +{ + return { .name = std::move(w.name), + .bytecode = std::move(w.bytecode), + .verification_key = std::move(w.verification_key) }; +} + +CircuitInputNoVK circuit_input_no_vk_from_wire(wire::CircuitInputNoVK&& w) +{ + return { .name = std::move(w.name), .bytecode = std::move(w.bytecode) }; +} + +CircuitKind circuit_kind_from_wire(uint8_t kind) +{ + return static_cast(kind); +} + +} // namespace + +void handle_chonk_start(BBApiRequest& ctx, wire::BbChonkStart&& cmd, Responder respond) +{ + std::vector kinds; + kinds.reserve(cmd.kinds.size()); + for (uint8_t k : cmd.kinds) { + kinds.push_back(circuit_kind_from_wire(k)); + } + ChonkStart{ .kinds = std::move(kinds) }.execute(ctx); + respond.ok({}); +} + +void handle_chonk_load(BBApiRequest& ctx, wire::BbChonkLoad&& cmd, Responder respond) +{ + ChonkLoad{ .circuit = circuit_input_from_wire(std::move(cmd.circuit)), .kind = circuit_kind_from_wire(cmd.kind) } + .execute(ctx); + respond.ok({}); +} + +void handle_chonk_accumulate(BBApiRequest& ctx, + wire::BbChonkAccumulate&& cmd, + Responder respond) +{ + ChonkAccumulate{ .witness = std::move(cmd.witness) }.execute(ctx); + respond.ok({}); +} + +void handle_chonk_prove(BBApiRequest& ctx, wire::BbChonkProve&& /*cmd*/, Responder respond) +{ + auto r = ChonkProve{}.execute(ctx); + respond.ok({ .proof = chonk_proof_to_wire(r.proof) }); +} + +void handle_chonk_verify(BBApiRequest& ctx, wire::BbChonkVerify&& cmd, Responder respond) +{ + auto r = ChonkVerify{ .proof = chonk_proof_from_wire(std::move(cmd.proof)), .vk = std::move(cmd.vk) }.execute(ctx); + respond.ok({ .valid = r.valid }); +} + +void handle_chonk_verify_from_fields(BBApiRequest& ctx, + wire::BbChonkVerifyFromFields&& cmd, + Responder respond) +{ + auto r = ChonkVerifyFromFields{ .proof = fr_vec_from_wire(cmd.proof), .vk = std::move(cmd.vk) }.execute(ctx); + respond.ok({ .valid = r.valid }); +} + +void handle_chonk_compute_vk(BBApiRequest& ctx, + wire::BbChonkComputeVk&& cmd, + Responder respond) +{ + auto r = ChonkComputeVk{ .circuit = circuit_input_no_vk_from_wire(std::move(cmd.circuit)), + .kind = circuit_kind_from_wire(cmd.kind) } + .execute(ctx); + respond.ok({ .bytes = std::move(r.bytes), .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_chonk_check_precomputed_vk(BBApiRequest& ctx, + wire::BbChonkCheckPrecomputedVk&& cmd, + Responder respond) +{ + auto r = ChonkCheckPrecomputedVk{ .circuit = circuit_input_from_wire(std::move(cmd.circuit)), + .kind = circuit_kind_from_wire(cmd.kind) } + .execute(ctx); + respond.ok({ .valid = r.valid, .actual_vk = std::move(r.actual_vk) }); +} + +void handle_chonk_stats(BBApiRequest& ctx, wire::BbChonkStats&& cmd, Responder respond) +{ + auto r = ChonkStats{ .circuit = circuit_input_no_vk_from_wire(std::move(cmd.circuit)), + .include_gates_per_opcode = cmd.include_gates_per_opcode } + .execute(ctx); + respond.ok({ .acir_opcodes = r.acir_opcodes, + .circuit_size = r.circuit_size, + .gates_per_opcode = std::move(r.gates_per_opcode) }); +} + +void handle_chonk_batch_verify(BBApiRequest& ctx, + wire::BbChonkBatchVerify&& cmd, + Responder respond) +{ + std::vector proofs; + proofs.reserve(cmd.proofs.size()); + for (auto& p : cmd.proofs) { + proofs.push_back(chonk_proof_from_wire(std::move(p))); + } + auto r = ChonkBatchVerify{ .proofs = std::move(proofs), .vks = std::move(cmd.vks) }.execute(ctx); + respond.ok({ .valid = r.valid }); +} + +void handle_chonk_compress_proof(BBApiRequest& ctx, + wire::BbChonkCompressProof&& cmd, + Responder respond) +{ + auto r = ChonkCompressProof{ .proof = chonk_proof_from_wire(std::move(cmd.proof)) }.execute(ctx); + respond.ok({ .compressed_proof = std::move(r.compressed_proof) }); +} + +void handle_chonk_decompress_proof(BBApiRequest& ctx, + wire::BbChonkDecompressProof&& cmd, + Responder respond) +{ + auto r = ChonkDecompressProof{ .compressed_proof = std::move(cmd.compressed_proof) }.execute(ctx); + respond.ok({ .proof = chonk_proof_to_wire(r.proof) }); +} + +void handle_chonk_batch_verifier_start(BBApiRequest& ctx, + wire::BbChonkBatchVerifierStart&& cmd, + Responder respond) +{ + ChonkBatchVerifierStart{ .vks = std::move(cmd.vks), + .num_cores = cmd.num_cores, + .batch_size = cmd.batch_size, + .fifo_path = std::move(cmd.fifo_path) } + .execute(ctx); + respond.ok({}); +} + +void handle_chonk_batch_verifier_queue(BBApiRequest& ctx, + wire::BbChonkBatchVerifierQueue&& cmd, + Responder respond) +{ + ChonkBatchVerifierQueue{ .request_id = cmd.request_id, + .vk_index = cmd.vk_index, + .proof_fields = fr_vec_from_wire(cmd.proof_fields) } + .execute(ctx); + respond.ok({}); +} + +void handle_chonk_batch_verifier_stop(BBApiRequest& ctx, + wire::BbChonkBatchVerifierStop&& /*cmd*/, + Responder respond) +{ + ChonkBatchVerifierStop{}.execute(ctx); + respond.ok({}); +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.cpp deleted file mode 100644 index 4a59f76cd33..00000000000 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "bbapi_execute.hpp" - -namespace bb::bbapi { -namespace { // anonymous -struct Api { - Command commands; - bb::bbapi::CommandResponse responses; - SERIALIZATION_FIELDS(commands, responses); -}; -} // namespace -std::string get_msgpack_schema_as_json() -{ - return msgpack_schema_to_string(Api{}); -} -} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp deleted file mode 100644 index 43b21d73838..00000000000 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp +++ /dev/null @@ -1,179 +0,0 @@ -#pragma once - -#include "barretenberg/bbapi/bbapi_avm.hpp" -#include "barretenberg/bbapi/bbapi_chonk.hpp" -#include "barretenberg/bbapi/bbapi_crypto.hpp" -#include "barretenberg/bbapi/bbapi_ecc.hpp" -#include "barretenberg/bbapi/bbapi_ecdsa.hpp" -#include "barretenberg/bbapi/bbapi_schnorr.hpp" -#include "barretenberg/bbapi/bbapi_shared.hpp" -#include "barretenberg/bbapi/bbapi_srs.hpp" -#include "barretenberg/bbapi/bbapi_ultra_honk.hpp" -#include "barretenberg/common/throw_or_abort.hpp" -#include - -namespace bb::bbapi { - -using Command = NamedUnion; - -using CommandResponse = NamedUnion; - -/** - * @brief Executes a command by visiting a variant of all possible commands. - * - * @param command The command to execute, consumed by this function. - * @param request The circuit registry (acting as the request context). - * @return A variant of all possible command responses. - */ -inline CommandResponse execute(BBApiRequest& request, Command&& command) -{ - // Reset error state before execution - request.error_message.clear(); - - CommandResponse response = std::move(command).visit([&request](auto&& cmd) -> CommandResponse { - using CmdType = std::decay_t; - return std::forward(cmd).execute(request); - }); - - // Check if an error occurred during execution - if (!request.error_message.empty()) { - return ErrorResponse{ .message = std::move(request.error_message) }; - } - - return response; -} - -// The msgpack scheme is an ad-hoc format that allows for cbind/compiler.ts to -// generate TypeScript bindings for the API. -std::string get_msgpack_schema_as_json(); - -} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp new file mode 100644 index 00000000000..5ffa23edd91 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp @@ -0,0 +1,579 @@ +/** + * @file bbapi_handlers.cpp + * @brief Per-command handlers consumed by the codegen-emitted server dispatch. + * + * Each handler matches the signature declared by generated/bb_dispatch.hpp + * but as a non-template overload for `BBApiRequest` so + * `make_bb_handler` resolves to these via overload resolution. + * + * Every handler converts wire fields to domain fields, calls + * `Cmd::execute()`, and converts the domain response back to wire fields — + * all explicit, all field-by-field. The shared converters live in + * `bbapi_wire_convert.hpp`. + */ +#include "barretenberg/bbapi/bbapi_handlers.hpp" +#include "barretenberg/api/api_avm.hpp" +#include "barretenberg/bbapi/bbapi_chonk.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/bbapi_wire_convert.hpp" +#include "barretenberg/bbapi/generated/bb_dispatch.hpp" +#include "barretenberg/common/assert.hpp" +#include "barretenberg/common/serialize.hpp" +#include "barretenberg/common/thread.hpp" +#include "barretenberg/common/throw_or_abort.hpp" +#include "barretenberg/crypto/aes128/aes128.hpp" +#include "barretenberg/crypto/blake2s/blake2s.hpp" +#include "barretenberg/crypto/ecdsa/ecdsa.hpp" +#include "barretenberg/crypto/pedersen_commitment/pedersen.hpp" +#include "barretenberg/crypto/pedersen_hash/pedersen.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2.hpp" +#include "barretenberg/crypto/poseidon2/poseidon2_permutation.hpp" +#include "barretenberg/crypto/schnorr/schnorr.hpp" +#include "barretenberg/crypto/sha256/sha256.hpp" +#include "barretenberg/srs/factories/bn254_crs_data.hpp" +#include "barretenberg/srs/factories/bn254_g1_chunk_hashes.hpp" +#include "barretenberg/srs/global_crs.hpp" +#include "barretenberg/vm2/tooling/stats.hpp" + +namespace bb::bbapi { + +namespace { + +// Reset the AVM per-stage timings registry so the snapshot we return reflects only this call. +void reset_avm_stats() +{ + ::bb::avm2::Stats::get().reset(); +} + +// Take a snapshot of the AVM per-stage timings registry as wire-typed stats entries. +std::vector snapshot_avm_stats_wire() +{ + auto snapshot = ::bb::avm2::Stats::get().snapshot(); + std::vector result; + result.reserve(snapshot.size()); + for (auto& [name, value] : snapshot) { + result.push_back(wire::AvmStat{ .name = std::move(name), .value_ms = value }); + } + return result; +} + +} // namespace + +// =========================================================================== +// AVM +// =========================================================================== + +void handle_avm_prove(BBApiRequest& /*ctx*/, wire::BbAvmProve&& cmd, Responder respond) +{ + reset_avm_stats(); + auto result = avm_prove_from_bytes(std::move(cmd.inputs)); + respond.ok({ .proof = fr_vec_to_wire(result.proof), .stats = snapshot_avm_stats_wire() }); +} +void handle_avm_verify(BBApiRequest& /*ctx*/, wire::BbAvmVerify&& cmd, Responder respond) +{ + bool verified = avm_verify_from_bytes(fr_vec_from_wire(cmd.proof), std::move(cmd.public_inputs)); + respond.ok({ .verified = verified }); +} +void handle_avm_check_circuit(BBApiRequest& /*ctx*/, + wire::BbAvmCheckCircuit&& cmd, + Responder respond) +{ + reset_avm_stats(); + bool passed = avm_check_circuit_from_bytes(std::move(cmd.inputs)); + respond.ok({ .passed = passed, .stats = snapshot_avm_stats_wire() }); +} + +// =========================================================================== +// Circuit + Chonk + UltraHonk +// =========================================================================== + +// UltraHonk handlers live in bbapi_ultra_honk.cpp. +// Chonk handlers live in bbapi_chonk.cpp. + +// =========================================================================== +// Hashing primitives +// =========================================================================== + +void handle_poseidon2_hash(BBApiRequest& /*ctx*/, + wire::BbPoseidon2Hash&& cmd, + Responder respond) +{ + auto inputs = fr_vec_from_wire(cmd.inputs); + auto hash = crypto::Poseidon2::hash(inputs); + respond.ok({ .hash = fr_to_wire(hash) }); +} +void handle_poseidon2_permutation(BBApiRequest& /*ctx*/, + wire::BbPoseidon2Permutation&& cmd, + Responder respond) +{ + using Permutation = crypto::Poseidon2Permutation; + auto inputs = fr_array_from_wire<4>(cmd.inputs); + auto outputs = Permutation::permutation(inputs); + respond.ok({ .outputs = fr_array_to_wire<4>(outputs) }); +} +void handle_poseidon2_absorb_chain(BBApiRequest& /*ctx*/, + wire::BbPoseidon2AbsorbChain&& cmd, + Responder respond) +{ + using Sponge = crypto::Poseidon2::Sponge; + constexpr size_t FIELD_BYTES = sizeof(fr::data); + constexpr size_t BLOCK_BYTES = std::tuple_size_v * FIELD_BYTES; + static_assert(sizeof(Sponge::Block) == BLOCK_BYTES, + "Blocks must have no padding: many_from_buffer strides by sizeof(Block), while serialization writes " + "every field limb"); + + BB_ASSERT(cmd.inputs.size() % BLOCK_BYTES == 0, + "Poseidon2AbsorbChain: input size must be a multiple of " << BLOCK_BYTES << " bytes"); + + auto state = fr_array_from_wire<4>(cmd.state); + auto absorbed = Sponge::absorb_blocks(state, many_from_buffer(cmd.inputs)); + respond.ok({ .state = fr_array_to_wire<4>(absorbed) }); +} +void handle_pedersen_commit(BBApiRequest& /*ctx*/, + wire::BbPedersenCommit&& cmd, + Responder respond) +{ + crypto::GeneratorContext gctx; + gctx.offset = static_cast(cmd.hash_index); + auto inputs = fr_vec_from_wire(cmd.inputs); + auto point = crypto::pedersen_commitment::commit_native(inputs, gctx); + respond.ok({ .point = grumpkin_point_to_wire(point) }); +} +void handle_pedersen_hash(BBApiRequest& /*ctx*/, + wire::BbPedersenHash&& cmd, + Responder respond) +{ + crypto::GeneratorContext gctx; + gctx.offset = static_cast(cmd.hash_index); + auto inputs = fr_vec_from_wire(cmd.inputs); + auto hash = crypto::pedersen_hash::hash(inputs, gctx); + respond.ok({ .hash = fr_to_wire(hash) }); +} +void handle_blake2s(BBApiRequest& /*ctx*/, wire::BbBlake2s&& cmd, Responder respond) +{ + respond.ok({ .hash = crypto::blake2s(cmd.data) }); +} +void handle_blake2s_to_field(BBApiRequest& /*ctx*/, + wire::BbBlake2sToField&& cmd, + Responder respond) +{ + auto hash_result = crypto::blake2s(cmd.data); + respond.ok({ .field = fr_to_wire(fr::serialize_from_buffer(hash_result.data())) }); +} +void handle_aes_encrypt(BBApiRequest& /*ctx*/, wire::BbAesEncrypt&& cmd, Responder respond) +{ + BB_ASSERT(cmd.length == cmd.plaintext.size(), "AesEncrypt: length must equal plaintext.size()"); + BB_ASSERT(cmd.length % 16 == 0, "AesEncrypt: length must be a multiple of 16"); + + std::vector result = std::move(cmd.plaintext); + result.resize(cmd.length); + crypto::aes128_encrypt_buffer_cbc(result.data(), cmd.iv.data(), cmd.key.data(), cmd.length); + respond.ok({ .ciphertext = std::move(result) }); +} +void handle_aes_decrypt(BBApiRequest& /*ctx*/, wire::BbAesDecrypt&& cmd, Responder respond) +{ + BB_ASSERT(cmd.length == cmd.ciphertext.size(), "AesDecrypt: length must equal ciphertext.size()"); + BB_ASSERT(cmd.length % 16 == 0, "AesDecrypt: length must be a multiple of 16"); + + std::vector result = std::move(cmd.ciphertext); + result.resize(cmd.length); + crypto::aes128_decrypt_buffer_cbc(result.data(), cmd.iv.data(), cmd.key.data(), cmd.length); + respond.ok({ .plaintext = std::move(result) }); +} + +void handle_grumpkin_get_random_fr(BBApiRequest& /*ctx*/, + wire::BbGrumpkinGetRandomFr&& /*cmd*/, + Responder respond) +{ + respond.ok({ .value = field_to_wire(grumpkin::fr::random_element()) }); +} +void handle_secp256k1_get_random_fr(BBApiRequest& /*ctx*/, + wire::BbSecp256k1GetRandomFr&& /*cmd*/, + Responder respond) +{ + respond.ok({ .value = field_to_wire_as(secp256k1::fr::random_element()) }); +} + +// =========================================================================== +// Grumpkin curve +// =========================================================================== + +void handle_grumpkin_mul(BBApiRequest& /*ctx*/, + wire::BbGrumpkinMul&& cmd, + Responder respond) +{ + auto point = grumpkin_point_from_wire(cmd.point); + auto scalar = field_from_wire(cmd.scalar); + if (!point.on_curve()) { + respond.error("Input point must be on the curve"); + return; + } + respond.ok({ .point = grumpkin_point_to_wire(point * scalar) }); +} +void handle_grumpkin_add(BBApiRequest& /*ctx*/, + wire::BbGrumpkinAdd&& cmd, + Responder respond) +{ + auto a = grumpkin_point_from_wire(cmd.point_a); + auto b = grumpkin_point_from_wire(cmd.point_b); + if (!a.on_curve()) { + respond.error("Input point_a must be on the curve"); + return; + } + if (!b.on_curve()) { + respond.error("Input point_b must be on the curve"); + return; + } + respond.ok({ .point = grumpkin_point_to_wire(a + b) }); +} +void handle_grumpkin_batch_mul(BBApiRequest& /*ctx*/, + wire::BbGrumpkinBatchMul&& cmd, + Responder respond) +{ + auto points = grumpkin_point_vec_from_wire(cmd.points); + auto scalar = field_from_wire(cmd.scalar); + for (const auto& p : points) { + if (!p.on_curve()) { + respond.error("Input point must be on the curve"); + return; + } + } + auto output = grumpkin::g1::element::batch_mul_with_endomorphism(points, scalar); + respond.ok({ .points = grumpkin_point_vec_to_wire(output) }); +} +wire::BbGrumpkinGetRandomFrResponse handle_grumpkin_get_random_fr(BBApiRequest& /*ctx*/, + wire::BbGrumpkinGetRandomFr&& /*cmd*/) +{ + return { .value = fr_to_wire(bb::fr::random_element()) }; +} +void handle_grumpkin_reduce512(BBApiRequest& /*ctx*/, + wire::BbGrumpkinReduce512&& cmd, + Responder respond) +{ + auto bigint_input = from_buffer(cmd.input.data()); + uint512_t barretenberg_modulus(bb::fr::modulus); + uint512_t target_output = bigint_input % barretenberg_modulus; + respond.ok({ .value = fr_to_wire(bb::fr(target_output.lo)) }); +} + +// =========================================================================== +// Secp256k1 curve +// =========================================================================== + +void handle_secp256k1_mul(BBApiRequest& /*ctx*/, + wire::BbSecp256k1Mul&& cmd, + Responder respond) +{ + auto point = secp256k1_point_from_wire(cmd.point); + auto scalar = field_from_wire(cmd.scalar); + if (!point.on_curve()) { + respond.error("Input point must be on the curve"); + return; + } + respond.ok({ .point = secp256k1_point_to_wire(point * scalar) }); +} +wire::BbSecp256k1GetRandomFrResponse handle_secp256k1_get_random_fr(BBApiRequest& /*ctx*/, + wire::BbSecp256k1GetRandomFr&& /*cmd*/) +{ + return { .value = field_to_wire_as(secp256k1::fr::random_element()) }; +} +void handle_secp256k1_reduce512(BBApiRequest& /*ctx*/, + wire::BbSecp256k1Reduce512&& cmd, + Responder respond) +{ + auto bigint_input = from_buffer(cmd.input.data()); + uint512_t secp256k1_modulus(secp256k1::fr::modulus); + uint512_t target_output = bigint_input % secp256k1_modulus; + respond.ok({ .value = field_to_wire_as(secp256k1::fr(target_output.lo)) }); +} + +// =========================================================================== +// Bn254 curve +// =========================================================================== + +void handle_bn254_fr_sqrt(BBApiRequest& /*ctx*/, + wire::BbBn254FrSqrt&& cmd, + Responder respond) +{ + auto [is_sqr, root] = fr_from_wire(cmd.input).sqrt(); + respond.ok({ .is_square_root = is_sqr, .value = fr_to_wire(root) }); +} +void handle_bn254_fq_sqrt(BBApiRequest& /*ctx*/, + wire::BbBn254FqSqrt&& cmd, + Responder respond) +{ + auto [is_sqr, root] = field_from_wire(cmd.input).sqrt(); + respond.ok({ .is_square_root = is_sqr, .value = field_to_wire_as(root) }); +} +void handle_bn254_g1_mul(BBApiRequest& /*ctx*/, wire::BbBn254G1Mul&& cmd, Responder respond) +{ + auto point = bn254_g1_point_from_wire(cmd.point); + auto scalar = fr_from_wire(cmd.scalar); + if (!point.on_curve()) { + respond.error("Input point must be on the curve"); + return; + } + auto result = point * scalar; + if (!result.on_curve()) { + respond.error("Output point must be on the curve"); + return; + } + respond.ok({ .point = bn254_g1_point_to_wire(result) }); +} +void handle_bn254_g2_mul(BBApiRequest& /*ctx*/, wire::BbBn254G2Mul&& cmd, Responder respond) +{ + auto point = bn254_g2_point_from_wire(cmd.point); + auto scalar = fr_from_wire(cmd.scalar); + if (!point.on_curve()) { + respond.error("Input point must be on the curve"); + return; + } + // BN254 G2 has cofactor h2 ≈ 2^254. An on-curve point may lie in a cofactor subgroup of order + // dividing h2 rather than the prime-order subgroup; we do not want to allow such points + // as inputs to bbapi. + if (!point.is_in_prime_subgroup()) { + respond.error("Input point must lie in the prime-order subgroup"); + return; + } + auto result = point * scalar; + if (!result.on_curve()) { + respond.error("Output point must be on the curve"); + return; + } + respond.ok({ .point = bn254_g2_point_to_wire(result) }); +} +void handle_bn254_g1_is_on_curve(BBApiRequest& /*ctx*/, + wire::BbBn254G1IsOnCurve&& cmd, + Responder respond) +{ + respond.ok({ .is_on_curve = bn254_g1_point_from_wire(cmd.point).on_curve() }); +} +void handle_bn254_g1_from_compressed(BBApiRequest& /*ctx*/, + wire::BbBn254G1FromCompressed&& cmd, + Responder respond) +{ + uint256_t compressed_value = from_buffer(cmd.compressed.data()); + auto point = bb::g1::affine_element::from_compressed(compressed_value); + if (!point.on_curve()) { + respond.error("Decompressed point is not on the curve"); + return; + } + respond.ok({ .point = bn254_g1_point_to_wire(point) }); +} + +// =========================================================================== +// Schnorr +// =========================================================================== + +void handle_schnorr_compute_public_key(BBApiRequest& /*ctx*/, + wire::BbSchnorrComputePublicKey&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + respond.ok({ .public_key = grumpkin_point_to_wire(grumpkin::g1::one * private_key) }); +} +// Schnorr signing takes a pre-derived field element message. +void handle_schnorr_construct_signature(BBApiRequest& /*ctx*/, + wire::BbSchnorrConstructSignature&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + grumpkin::g1::affine_element pub_key = grumpkin::g1::one * private_key; + crypto::schnorr_key_pair key_pair = { private_key, pub_key }; + + auto message_field = field_from_wire(cmd.message_field); + auto sig = crypto::schnorr_construct_signature(message_field, key_pair); + crypto::secure_erase_bytes(&key_pair.private_key, sizeof(key_pair.private_key)); + + respond.ok({ .s = field_to_wire(sig.s), .e = field_to_wire(sig.e) }); +} +void handle_schnorr_verify_signature(BBApiRequest& /*ctx*/, + wire::BbSchnorrVerifySignature&& cmd, + Responder respond) +{ + auto message_field = field_from_wire(cmd.message_field); + crypto::schnorr_signature sig = { field_from_wire(cmd.s), field_from_wire(cmd.e) }; + auto public_key = grumpkin_point_from_wire(cmd.public_key); + + bool result = crypto::schnorr_verify_signature(message_field, public_key, sig); + respond.ok({ .verified = result }); +} + +// =========================================================================== +// ECDSA +// =========================================================================== + +void handle_ecdsa_secp256k1_compute_public_key(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256k1ComputePublicKey&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + respond.ok({ .public_key = secp256k1_point_to_wire(secp256k1::g1::one * private_key) }); +} +void handle_ecdsa_secp256r1_compute_public_key(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256r1ComputePublicKey&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + respond.ok({ .public_key = secp256r1_point_to_wire(secp256r1::g1::one * private_key) }); +} +void handle_ecdsa_secp256k1_construct_signature(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256k1ConstructSignature&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + auto pub_key = secp256k1::g1::one * private_key; + crypto::ecdsa_key_pair key_pair = { private_key, pub_key }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto sig = crypto::ecdsa_construct_signature( + message_str, key_pair); + respond.ok({ .r = sig.r, .s = sig.s, .v = sig.v }); +} +void handle_ecdsa_secp256r1_construct_signature(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256r1ConstructSignature&& cmd, + Responder respond) +{ + auto private_key = field_from_wire(cmd.private_key); + auto pub_key = secp256r1::g1::one * private_key; + crypto::ecdsa_key_pair key_pair = { private_key, pub_key }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto sig = crypto::ecdsa_construct_signature( + message_str, key_pair); + respond.ok({ .r = sig.r, .s = sig.s, .v = sig.v }); +} +void handle_ecdsa_secp256k1_recover_public_key(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256k1RecoverPublicKey&& cmd, + Responder respond) +{ + crypto::ecdsa_signature sig = { cmd.r, cmd.s, cmd.v }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto pubkey = crypto::ecdsa_recover_public_key( + message_str, sig); + respond.ok({ .public_key = secp256k1_point_to_wire(pubkey) }); +} +void handle_ecdsa_secp256r1_recover_public_key(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256r1RecoverPublicKey&& cmd, + Responder respond) +{ + crypto::ecdsa_signature sig = { cmd.r, cmd.s, cmd.v }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto pubkey = crypto::ecdsa_recover_public_key( + message_str, sig); + respond.ok({ .public_key = secp256r1_point_to_wire(pubkey) }); +} +void handle_ecdsa_secp256k1_verify_signature(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256k1VerifySignature&& cmd, + Responder respond) +{ + crypto::ecdsa_signature sig = { cmd.r, cmd.s, cmd.v }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto pubkey = secp256k1_point_from_wire(cmd.public_key); + bool verified = crypto::ecdsa_verify_signature( + message_str, pubkey, sig); + respond.ok({ .verified = verified }); +} +void handle_ecdsa_secp256r1_verify_signature(BBApiRequest& /*ctx*/, + wire::BbEcdsaSecp256r1VerifySignature&& cmd, + Responder respond) +{ + crypto::ecdsa_signature sig = { cmd.r, cmd.s, cmd.v }; + std::string message_str(reinterpret_cast(cmd.message.data()), cmd.message.size()); + auto pubkey = secp256r1_point_from_wire(cmd.public_key); + bool verified = crypto::ecdsa_verify_signature( + message_str, pubkey, sig); + respond.ok({ .verified = verified }); +} + +// =========================================================================== +// SRS init +// =========================================================================== + +void handle_srs_init_srs(BBApiRequest& /*ctx*/, wire::BbSrsInitSrs&& cmd, Responder respond) +{ + constexpr size_t COMPRESSED_POINT_SIZE = 32; + constexpr size_t UNCOMPRESSED_POINT_SIZE = sizeof(g1::affine_element); // 64 + + auto& points_buf = cmd.points_buf; + auto num_points = cmd.num_points; + size_t bytes_per_point = num_points > 0 ? points_buf.size() / num_points : 0; + std::vector g1_points(num_points); + std::vector uncompressed_out; + + if (bytes_per_point == UNCOMPRESSED_POINT_SIZE) { + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + g1_points[i] = from_buffer(points_buf.data(), i * UNCOMPRESSED_POINT_SIZE); + } + }); + } else if (bytes_per_point == COMPRESSED_POINT_SIZE) { + if (points_buf.size() == 0 || points_buf.size() % bb::srs::SRS_CHUNK_SIZE_BYTES != 0) { + throw_or_abort("SrsInitSrs: compressed points_buf size " + std::to_string(points_buf.size()) + + " must be a positive multiple of " + std::to_string(bb::srs::SRS_CHUNK_SIZE_BYTES)); + } + size_t num_full_chunks = points_buf.size() / bb::srs::SRS_CHUNK_SIZE_BYTES; + size_t chunks_to_verify = std::min(num_full_chunks, static_cast(bb::srs::SRS_NUM_FULL_CHUNKS)); + for (size_t i = 0; i < chunks_to_verify; ++i) { + auto chunk = std::span(points_buf.data() + i * bb::srs::SRS_CHUNK_SIZE_BYTES, + bb::srs::SRS_CHUNK_SIZE_BYTES); + auto hash = bb::crypto::sha256(chunk); + if (hash != bb::srs::BN254_G1_CHUNK_HASHES[i]) { + throw_or_abort("SrsInitSrs: g1 compressed chunk " + std::to_string(i) + " SHA-256 mismatch"); + } + } + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + uint256_t c = from_buffer(points_buf.data(), i * COMPRESSED_POINT_SIZE); + g1_points[i] = g1::affine_element::from_compressed(c); + } + }); + uncompressed_out.resize(static_cast(num_points) * UNCOMPRESSED_POINT_SIZE); + parallel_for([&](ThreadChunk chunk) { + for (auto i : chunk.range(static_cast(num_points))) { + auto buf = to_buffer(g1_points[i]); + std::copy(buf.begin(), buf.end(), &uncompressed_out[i * UNCOMPRESSED_POINT_SIZE]); + } + }); + } else { + throw_or_abort("SrsInitSrs: invalid points_buf size. Expected 32 or 64 bytes per point, got " + + std::to_string(bytes_per_point)); + } + + if (num_points >= 1 && g1_points[0] != bb::srs::BN254_G1_FIRST_ELEMENT) { + throw_or_abort("SrsInitSrs: g1_points[0] is not the canonical BN254 generator"); + } + if (num_points >= 2 && g1_points[1] != bb::srs::get_bn254_g1_second_element()) { + throw_or_abort("SrsInitSrs: g1_points[1] does not match the canonical trusted-setup tau·G"); + } + + auto g2_hash = bb::crypto::sha256(std::span(cmd.g2_point.data(), cmd.g2_point.size())); + if (g2_hash != bb::srs::BN254_G2_ELEMENT_SHA256) { + throw_or_abort("SrsInitSrs: g2_point bytes do not match the canonical Aztec [x]_2 SHA-256"); + } + auto g2_point_elem = from_buffer(cmd.g2_point.data()); + if (!g2_point_elem.is_in_prime_subgroup()) { + throw_or_abort("SrsInitSrs: g2_point is not in the BN254 G2 prime-order subgroup"); + } + + bb::srs::init_bn254_mem_crs_factory(g1_points, g2_point_elem); + respond.ok({ .points_buf = std::move(uncompressed_out) }); +} +void handle_srs_init_grumpkin_srs(BBApiRequest& /*ctx*/, + wire::BbSrsInitGrumpkinSrs&& cmd, + Responder respond) +{ + const size_t required_size = static_cast(cmd.num_points) * sizeof(curve::Grumpkin::AffineElement); + if (cmd.points_buf.size() < required_size) { + throw_or_abort("SrsInitGrumpkinSrs: points_buf too small (" + std::to_string(cmd.points_buf.size()) + + " bytes) for num_points=" + std::to_string(cmd.num_points) + " (need " + + std::to_string(required_size) + ")"); + } + std::vector points(cmd.num_points); + for (uint32_t i = 0; i < cmd.num_points; ++i) { + points[i] = from_buffer(cmd.points_buf.data(), + i * sizeof(curve::Grumpkin::AffineElement)); + } + bb::srs::init_grumpkin_mem_crs_factory(points); + respond.ok({}); +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp new file mode 100644 index 00000000000..58b6d1d6e26 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp @@ -0,0 +1,173 @@ +#pragma once +/** + * @file bbapi_handlers.hpp + * @brief Non-template handler declarations for the bb service. + * + * The codegen-emitted dispatch header (generated/bb_dispatch.hpp) declares + * `template handle_(Ctx&, wire::Cmd&&)`. These free-function + * overloads provide concrete definitions for `Ctx = BBApiRequest`; overload + * resolution prefers them at the template instantiation point inside + * make_bb_handler(...). + */ +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/generated/bb_dispatch.hpp" + +namespace bb::bbapi { + +void handle_avm_prove(BBApiRequest& ctx, wire::BbAvmProve&& cmd, Responder respond); +void handle_avm_verify(BBApiRequest& ctx, wire::BbAvmVerify&& cmd, Responder respond); +void handle_avm_check_circuit(BBApiRequest& ctx, + wire::BbAvmCheckCircuit&& cmd, + Responder respond); +void handle_circuit_prove(BBApiRequest& ctx, + wire::BbCircuitProve&& cmd, + Responder respond); +void handle_circuit_compute_vk(BBApiRequest& ctx, + wire::BbCircuitComputeVk&& cmd, + Responder respond); +void handle_circuit_stats(BBApiRequest& ctx, + wire::BbCircuitStats&& cmd, + Responder respond); +void handle_circuit_verify(BBApiRequest& ctx, + wire::BbCircuitVerify&& cmd, + Responder respond); +void handle_chonk_compute_vk(BBApiRequest& ctx, + wire::BbChonkComputeVk&& cmd, + Responder respond); +void handle_chonk_start(BBApiRequest& ctx, wire::BbChonkStart&& cmd, Responder respond); +void handle_chonk_load(BBApiRequest& ctx, wire::BbChonkLoad&& cmd, Responder respond); +void handle_chonk_accumulate(BBApiRequest& ctx, + wire::BbChonkAccumulate&& cmd, + Responder respond); +void handle_chonk_prove(BBApiRequest& ctx, wire::BbChonkProve&& cmd, Responder respond); +void handle_chonk_verify(BBApiRequest& ctx, wire::BbChonkVerify&& cmd, Responder respond); +void handle_chonk_verify_from_fields(BBApiRequest& ctx, + wire::BbChonkVerifyFromFields&& cmd, + Responder respond); +void handle_chonk_batch_verify(BBApiRequest& ctx, + wire::BbChonkBatchVerify&& cmd, + Responder respond); +void handle_vk_as_fields(BBApiRequest& ctx, wire::BbVkAsFields&& cmd, Responder respond); +void handle_mega_app_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaAppVkAsFields&& cmd, + Responder respond); +void handle_mega_kernel_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaKernelVkAsFields&& cmd, + Responder respond); +void handle_mega_z_k_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaZKVkAsFields&& cmd, + Responder respond); +void handle_mega_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaVkAsFields&& cmd, + Responder respond); +void handle_circuit_write_solidity_verifier(BBApiRequest& ctx, + wire::BbCircuitWriteSolidityVerifier&& cmd, + Responder respond); +void handle_chonk_check_precomputed_vk(BBApiRequest& ctx, + wire::BbChonkCheckPrecomputedVk&& cmd, + Responder respond); +void handle_chonk_stats(BBApiRequest& ctx, wire::BbChonkStats&& cmd, Responder respond); +void handle_chonk_compress_proof(BBApiRequest& ctx, + wire::BbChonkCompressProof&& cmd, + Responder respond); +void handle_chonk_decompress_proof(BBApiRequest& ctx, + wire::BbChonkDecompressProof&& cmd, + Responder respond); +void handle_poseidon2_hash(BBApiRequest& ctx, + wire::BbPoseidon2Hash&& cmd, + Responder respond); +void handle_poseidon2_permutation(BBApiRequest& ctx, + wire::BbPoseidon2Permutation&& cmd, + Responder respond); +void handle_poseidon2_absorb_chain(BBApiRequest& ctx, + wire::BbPoseidon2AbsorbChain&& cmd, + Responder respond); +void handle_pedersen_commit(BBApiRequest& ctx, + wire::BbPedersenCommit&& cmd, + Responder respond); +void handle_pedersen_hash(BBApiRequest& ctx, + wire::BbPedersenHash&& cmd, + Responder respond); +void handle_blake2s(BBApiRequest& ctx, wire::BbBlake2s&& cmd, Responder respond); +void handle_blake2s_to_field(BBApiRequest& ctx, + wire::BbBlake2sToField&& cmd, + Responder respond); +void handle_aes_encrypt(BBApiRequest& ctx, wire::BbAesEncrypt&& cmd, Responder respond); +void handle_aes_decrypt(BBApiRequest& ctx, wire::BbAesDecrypt&& cmd, Responder respond); +void handle_grumpkin_mul(BBApiRequest& ctx, wire::BbGrumpkinMul&& cmd, Responder respond); +void handle_grumpkin_add(BBApiRequest& ctx, wire::BbGrumpkinAdd&& cmd, Responder respond); +void handle_grumpkin_batch_mul(BBApiRequest& ctx, + wire::BbGrumpkinBatchMul&& cmd, + Responder respond); +void handle_grumpkin_get_random_fr(BBApiRequest& ctx, + wire::BbGrumpkinGetRandomFr&& cmd, + Responder respond); +void handle_grumpkin_reduce512(BBApiRequest& ctx, + wire::BbGrumpkinReduce512&& cmd, + Responder respond); +void handle_secp256k1_mul(BBApiRequest& ctx, + wire::BbSecp256k1Mul&& cmd, + Responder respond); +void handle_secp256k1_get_random_fr(BBApiRequest& ctx, + wire::BbSecp256k1GetRandomFr&& cmd, + Responder respond); +void handle_secp256k1_reduce512(BBApiRequest& ctx, + wire::BbSecp256k1Reduce512&& cmd, + Responder respond); +void handle_bn254_fr_sqrt(BBApiRequest& ctx, wire::BbBn254FrSqrt&& cmd, Responder respond); +void handle_bn254_fq_sqrt(BBApiRequest& ctx, wire::BbBn254FqSqrt&& cmd, Responder respond); +void handle_bn254_g1_mul(BBApiRequest& ctx, wire::BbBn254G1Mul&& cmd, Responder respond); +void handle_bn254_g2_mul(BBApiRequest& ctx, wire::BbBn254G2Mul&& cmd, Responder respond); +void handle_bn254_g1_is_on_curve(BBApiRequest& ctx, + wire::BbBn254G1IsOnCurve&& cmd, + Responder respond); +void handle_bn254_g1_from_compressed(BBApiRequest& ctx, + wire::BbBn254G1FromCompressed&& cmd, + Responder respond); +void handle_schnorr_compute_public_key(BBApiRequest& ctx, + wire::BbSchnorrComputePublicKey&& cmd, + Responder respond); +void handle_schnorr_construct_signature(BBApiRequest& ctx, + wire::BbSchnorrConstructSignature&& cmd, + Responder respond); +void handle_schnorr_verify_signature(BBApiRequest& ctx, + wire::BbSchnorrVerifySignature&& cmd, + Responder respond); +void handle_ecdsa_secp256k1_compute_public_key(BBApiRequest& ctx, + wire::BbEcdsaSecp256k1ComputePublicKey&& cmd, + Responder respond); +void handle_ecdsa_secp256r1_compute_public_key(BBApiRequest& ctx, + wire::BbEcdsaSecp256r1ComputePublicKey&& cmd, + Responder respond); +void handle_ecdsa_secp256k1_construct_signature(BBApiRequest& ctx, + wire::BbEcdsaSecp256k1ConstructSignature&& cmd, + Responder respond); +void handle_ecdsa_secp256r1_construct_signature(BBApiRequest& ctx, + wire::BbEcdsaSecp256r1ConstructSignature&& cmd, + Responder respond); +void handle_ecdsa_secp256k1_recover_public_key(BBApiRequest& ctx, + wire::BbEcdsaSecp256k1RecoverPublicKey&& cmd, + Responder respond); +void handle_ecdsa_secp256r1_recover_public_key(BBApiRequest& ctx, + wire::BbEcdsaSecp256r1RecoverPublicKey&& cmd, + Responder respond); +void handle_ecdsa_secp256k1_verify_signature(BBApiRequest& ctx, + wire::BbEcdsaSecp256k1VerifySignature&& cmd, + Responder respond); +void handle_ecdsa_secp256r1_verify_signature(BBApiRequest& ctx, + wire::BbEcdsaSecp256r1VerifySignature&& cmd, + Responder respond); +void handle_srs_init_srs(BBApiRequest& ctx, wire::BbSrsInitSrs&& cmd, Responder respond); +void handle_chonk_batch_verifier_start(BBApiRequest& ctx, + wire::BbChonkBatchVerifierStart&& cmd, + Responder respond); +void handle_chonk_batch_verifier_queue(BBApiRequest& ctx, + wire::BbChonkBatchVerifierQueue&& cmd, + Responder respond); +void handle_chonk_batch_verifier_stop(BBApiRequest& ctx, + wire::BbChonkBatchVerifierStop&& cmd, + Responder respond); +void handle_srs_init_grumpkin_srs(BBApiRequest& ctx, + wire::BbSrsInitGrumpkinSrs&& cmd, + Responder respond); +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.cpp new file mode 100644 index 00000000000..f40bae2e2d7 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.cpp @@ -0,0 +1,16 @@ +#include "barretenberg/bbapi/bbapi_schema.hpp" + +#include "barretenberg/bbapi/generated/bb_schema_embed.hpp" + +namespace bb::bbapi { + +// `bb msgpack schema` output: the checked-in bb_schema.json verbatim. The +// schema file is the wire contract every consumer (this server's dispatch, +// bb.js, barretenberg-rs) generates from, so the binary reports exactly the +// contract it was built against. +std::string get_bb_schema_as_json() +{ + return std::string(k_bb_schema_json); +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.hpp new file mode 100644 index 00000000000..08f4df83499 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace bb::bbapi { + +std::string get_bb_schema_as_json(); + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_ultra_honk_handlers.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_ultra_honk_handlers.cpp new file mode 100644 index 00000000000..5ddaa3c3dfb --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_ultra_honk_handlers.cpp @@ -0,0 +1,139 @@ +/** + * @file bbapi_ultra_honk_handlers.cpp + * @brief Wire adapters for the UltraHonk circuit commands: convert generated + * wire structs to the domain command structs, run `execute()`, convert back. + */ +#include "barretenberg/bbapi/bbapi_handlers.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/bbapi_ultra_honk.hpp" +#include "barretenberg/bbapi/bbapi_wire_convert.hpp" + +namespace bb::bbapi { + +namespace { + +CircuitInput circuit_input_from_wire(wire::CircuitInput&& w) +{ + return { .name = std::move(w.name), + .bytecode = std::move(w.bytecode), + .verification_key = std::move(w.verification_key) }; +} + +CircuitInputNoVK circuit_input_no_vk_from_wire(wire::CircuitInputNoVK&& w) +{ + return { .name = std::move(w.name), .bytecode = std::move(w.bytecode) }; +} + +ProofSystemSettings settings_from_wire(wire::ProofSystemSettings&& w) +{ + return { .ipa_accumulation = w.ipa_accumulation, + .oracle_hash_type = std::move(w.oracle_hash_type), + .disable_zk = w.disable_zk, + .optimized_solidity_verifier = w.optimized_solidity_verifier }; +} + +wire::VkData vk_data_to_wire(CircuitComputeVk::Response&& r) +{ + return { .bytes = std::move(r.bytes), .fields = uint256_vec_to_wire(r.fields), .hash = std::move(r.hash) }; +} + +} // namespace + +void handle_circuit_prove(BBApiRequest& ctx, + wire::BbCircuitProve&& cmd, + Responder respond) +{ + auto r = CircuitProve{ .circuit = circuit_input_from_wire(std::move(cmd.circuit)), + .witness = std::move(cmd.witness), + .settings = settings_from_wire(std::move(cmd.settings)) } + .execute(ctx); + respond.ok({ .public_inputs = uint256_vec_to_wire(r.public_inputs), + .proof = uint256_vec_to_wire(r.proof), + .vk = vk_data_to_wire(std::move(r.vk)) }); +} + +void handle_circuit_compute_vk(BBApiRequest& ctx, + wire::BbCircuitComputeVk&& cmd, + Responder respond) +{ + auto r = CircuitComputeVk{ .circuit = circuit_input_no_vk_from_wire(std::move(cmd.circuit)), + .settings = settings_from_wire(std::move(cmd.settings)) } + .execute(ctx); + respond.ok({ .bytes = std::move(r.bytes), .fields = uint256_vec_to_wire(r.fields), .hash = std::move(r.hash) }); +} + +void handle_circuit_stats(BBApiRequest& ctx, + wire::BbCircuitStats&& cmd, + Responder respond) +{ + auto r = CircuitStats{ .circuit = circuit_input_from_wire(std::move(cmd.circuit)), + .include_gates_per_opcode = cmd.include_gates_per_opcode, + .settings = settings_from_wire(std::move(cmd.settings)) } + .execute(ctx); + respond.ok({ .num_gates = r.num_gates, + .num_gates_dyadic = r.num_gates_dyadic, + .num_acir_opcodes = r.num_acir_opcodes, + .gates_per_opcode = std::move(r.gates_per_opcode) }); +} + +void handle_circuit_verify(BBApiRequest& ctx, + wire::BbCircuitVerify&& cmd, + Responder respond) +{ + auto r = CircuitVerify{ .verification_key = std::move(cmd.verification_key), + .public_inputs = uint256_vec_from_wire(cmd.public_inputs), + .proof = uint256_vec_from_wire(cmd.proof), + .settings = settings_from_wire(std::move(cmd.settings)) } + .execute(ctx); + respond.ok({ .verified = r.verified }); +} + +void handle_vk_as_fields(BBApiRequest& ctx, wire::BbVkAsFields&& cmd, Responder respond) +{ + auto r = VkAsFields{ .verification_key = std::move(cmd.verification_key) }.execute(ctx); + respond.ok({ .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_mega_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaVkAsFields&& cmd, + Responder respond) +{ + auto r = MegaVkAsFields{ .verification_key = std::move(cmd.verification_key) }.execute(ctx); + respond.ok({ .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_mega_app_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaAppVkAsFields&& cmd, + Responder respond) +{ + auto r = MegaAppVkAsFields{ .verification_key = std::move(cmd.verification_key) }.execute(ctx); + respond.ok({ .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_mega_kernel_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaKernelVkAsFields&& cmd, + Responder respond) +{ + auto r = MegaKernelVkAsFields{ .verification_key = std::move(cmd.verification_key) }.execute(ctx); + respond.ok({ .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_mega_z_k_vk_as_fields(BBApiRequest& ctx, + wire::BbMegaZKVkAsFields&& cmd, + Responder respond) +{ + auto r = MegaZKVkAsFields{ .verification_key = std::move(cmd.verification_key) }.execute(ctx); + respond.ok({ .fields = fr_vec_to_wire(r.fields) }); +} + +void handle_circuit_write_solidity_verifier(BBApiRequest& ctx, + wire::BbCircuitWriteSolidityVerifier&& cmd, + Responder respond) +{ + auto r = CircuitWriteSolidityVerifier{ .verification_key = std::move(cmd.verification_key), + .settings = settings_from_wire(std::move(cmd.settings)) } + .execute(ctx); + respond.ok({ .solidity_code = std::move(r.solidity_code) }); +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_wire_convert.hpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_wire_convert.hpp new file mode 100644 index 00000000000..9f6d4e11c68 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_wire_convert.hpp @@ -0,0 +1,296 @@ +#pragma once +/** + * @file bbapi_wire_convert.hpp + * @brief Wire <-> domain conversion helpers for the bbapi handlers. + * + * All conversions are field-by-field: each handler in bbapi_handlers.cpp + * builds the domain command struct from the wire fields, calls execute(), + * and builds the wire response from the domain response fields. + * + * Wire field types (Fr / Fq / Uint256 / … — nominal bin32 aliases) and + * domain field types (`bb::fr`, `bb::fq`, `uint256_t`, …) share a 32-byte + * msgpack `bin32` encoding, so the byte-level conversion is a + * `serialize_to_buffer` / `serialize_from_buffer` call. + */ +#include "barretenberg/bbapi/bbapi_chonk.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" +#include "barretenberg/bbapi/generated/bb_types.hpp" +#include "barretenberg/ecc/curves/bn254/bn254.hpp" +#include "barretenberg/ecc/curves/bn254/fq.hpp" +#include "barretenberg/ecc/curves/bn254/fq2.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" +#include "barretenberg/ecc/curves/grumpkin/grumpkin.hpp" +#include "barretenberg/ecc/curves/secp256k1/secp256k1.hpp" +#include "barretenberg/ecc/curves/secp256r1/secp256r1.hpp" +#include "barretenberg/numeric/uint256/uint256.hpp" +#include "barretenberg/serialize/msgpack.hpp" + +#include +#include +#include + +namespace bb::bbapi { + +// The generated field aliases live in the wire sub-namespace. Named one by one rather than pulled +// in wholesale, so a wire type can never shadow a domain type of the same name. +using wire::Fq; +using wire::Fr; +using wire::Secp256k1Fq; +using wire::Secp256k1Fr; +using wire::Secp256r1Fq; +using wire::Secp256r1Fr; +using wire::Uint256; + +// --------------------------------------------------------------------------- +// Field element conversions. All field types (bb::fr, bb::fq, grumpkin::fr, +// grumpkin::fq, secp256k1::*, secp256r1::*) pack as msgpack bin32. The wire +// aliases are nominal C++ wrappers over the same 32 bytes, so conversions are +// just serialize_to_buffer / serialize_from_buffer at the boundary. +// --------------------------------------------------------------------------- + +inline const std::array& wire_bytes(const std::array& w) +{ + return w; +} + +template inline const std::array& wire_bytes(const Wire& w) +{ + return static_cast&>(w); +} + +template inline std::array field_to_bytes(const Field& d) +{ + std::array r{}; + Field::serialize_to_buffer(d, r.data()); + return r; +} + +template inline std::array field_to_wire(const Field& d) +{ + return field_to_bytes(d); +} + +template inline Wire field_to_wire_as(const Field& d) +{ + return Wire{ field_to_bytes(d) }; +} + +template inline Field field_from_wire(const Wire& w) +{ + return Field::serialize_from_buffer(wire_bytes(w).data()); +} + +inline Fr fr_to_wire(const bb::fr& d) +{ + return field_to_wire_as(d); +} +inline bb::fr fr_from_wire(const Fr& w) +{ + return field_from_wire(w); +} + +inline std::vector fr_vec_to_wire(const std::vector& d) +{ + std::vector r; + r.reserve(d.size()); + for (const auto& x : d) { + r.push_back(fr_to_wire(x)); + } + return r; +} + +inline std::vector fr_vec_from_wire(const std::vector& w) +{ + std::vector r; + r.reserve(w.size()); + for (const auto& x : w) { + r.push_back(fr_from_wire(x)); + } + return r; +} + +template inline std::array fr_array_to_wire(const std::array& d) +{ + std::array r{}; + for (std::size_t i = 0; i < N; ++i) { + r[i] = fr_to_wire(d[i]); + } + return r; +} + +template inline std::array fr_array_from_wire(const std::array& w) +{ + std::array r{}; + for (std::size_t i = 0; i < N; ++i) { + r[i] = fr_from_wire(w[i]); + } + return r; +} + +// --------------------------------------------------------------------------- +// Curve point conversions. Wire types follow a uniform {Fr x, Fr y} shape. +// Domain types use the curve-specific affine_element. The default +// affine_element msgpack adapter packs as a 2-field map {x: bin32, y: bin32}, +// matching the wire encoding, so field-by-field conversion is safe. +// --------------------------------------------------------------------------- + +inline wire::GrumpkinPoint grumpkin_point_to_wire(const grumpkin::g1::affine_element& d) +{ + return { .x = field_to_wire_as(d.x), .y = field_to_wire_as(d.y) }; +} + +inline grumpkin::g1::affine_element grumpkin_point_from_wire(const wire::GrumpkinPoint& w) +{ + return { field_from_wire(w.x), field_from_wire(w.y) }; +} + +inline std::vector grumpkin_point_vec_to_wire(const std::vector& d) +{ + std::vector r; + r.reserve(d.size()); + for (const auto& p : d) { + r.push_back(grumpkin_point_to_wire(p)); + } + return r; +} + +inline std::vector grumpkin_point_vec_from_wire(const std::vector& w) +{ + std::vector r; + r.reserve(w.size()); + for (const auto& p : w) { + r.push_back(grumpkin_point_from_wire(p)); + } + return r; +} + +inline wire::Bn254G1Point bn254_g1_point_to_wire(const bb::g1::affine_element& d) +{ + return { .x = field_to_wire_as(d.x), .y = field_to_wire_as(d.y) }; +} + +inline bb::g1::affine_element bn254_g1_point_from_wire(const wire::Bn254G1Point& w) +{ + return { field_from_wire(w.x), field_from_wire(w.y) }; +} + +// Fq2 = { c0: bb::fq, c1: bb::fq }; wire Fq2 is two fq bin32 aliases. +inline std::array fq2_to_wire(const bb::fq2& d) +{ + return { field_to_wire_as(d.c0), field_to_wire_as(d.c1) }; +} + +inline bb::fq2 fq2_from_wire(const std::array& w) +{ + return { field_from_wire(w[0]), field_from_wire(w[1]) }; +} + +inline wire::Bn254G2Point bn254_g2_point_to_wire(const bb::g2::affine_element& d) +{ + return { .x = fq2_to_wire(d.x), .y = fq2_to_wire(d.y) }; +} + +inline bb::g2::affine_element bn254_g2_point_from_wire(const wire::Bn254G2Point& w) +{ + return { fq2_from_wire(w.x), fq2_from_wire(w.y) }; +} + +inline wire::Secp256k1Point secp256k1_point_to_wire(const secp256k1::g1::affine_element& d) +{ + return { .x = field_to_wire_as(d.x), .y = field_to_wire_as(d.y) }; +} + +inline secp256k1::g1::affine_element secp256k1_point_from_wire(const wire::Secp256k1Point& w) +{ + return { field_from_wire(w.x), field_from_wire(w.y) }; +} + +inline wire::Secp256r1Point secp256r1_point_to_wire(const secp256r1::g1::affine_element& d) +{ + return { .x = field_to_wire_as(d.x), .y = field_to_wire_as(d.y) }; +} + +inline secp256r1::g1::affine_element secp256r1_point_from_wire(const wire::Secp256r1Point& w) +{ + return { field_from_wire(w.x), field_from_wire(w.y) }; +} + +// --------------------------------------------------------------------------- +// uint256_t ↔ Uint256 (= std::array). +// Wire format is 32 bytes big-endian (matches uint256_t::msgpack_pack). +// --------------------------------------------------------------------------- + +inline Uint256 uint256_to_wire(const bb::numeric::uint256_t& d) +{ + Uint256 r{}; + for (std::size_t i = 0; i < 4; ++i) { + const uint64_t v = d.data[3 - i]; + for (std::size_t j = 0; j < 8; ++j) { + r[i * 8 + j] = static_cast(v >> (56 - j * 8)); + } + } + return r; +} + +inline bb::numeric::uint256_t uint256_from_wire(const Uint256& w) +{ + uint64_t parts[4]{}; + for (std::size_t i = 0; i < 4; ++i) { + uint64_t v = 0; + for (std::size_t j = 0; j < 8; ++j) { + v = (v << 8) | w[i * 8 + j]; + } + parts[i] = v; + } + return bb::numeric::uint256_t(parts[3], parts[2], parts[1], parts[0]); +} + +inline std::vector uint256_vec_to_wire(const std::vector& d) +{ + std::vector r; + r.reserve(d.size()); + for (const auto& x : d) { + r.push_back(uint256_to_wire(x)); + } + return r; +} + +inline std::vector uint256_vec_from_wire(const std::vector& w) +{ + std::vector r; + r.reserve(w.size()); + for (const auto& x : w) { + r.push_back(uint256_from_wire(x)); + } + return r; +} + +inline ChonkProof chonk_proof_from_wire(wire::ChonkProof&& w) +{ + return ChonkProof(fr_vec_from_wire(w.hiding_oink_proof), + fr_vec_from_wire(w.merge_proof), + fr_vec_from_wire(w.eccvm_proof), + fr_vec_from_wire(w.ipa_proof), + fr_vec_from_wire(w.joint_proof)); +} + +inline wire::ChonkProof chonk_proof_to_wire(const ChonkProof& d) +{ + return { .hiding_oink_proof = fr_vec_to_wire(d.hiding_oink_proof), + .merge_proof = fr_vec_to_wire(d.merge_proof), + .eccvm_proof = fr_vec_to_wire(d.eccvm_proof), + .ipa_proof = fr_vec_to_wire(d.ipa_proof), + .joint_proof = fr_vec_to_wire(d.joint_proof) }; +} + +inline std::vector chonk_proof_vec_from_wire(std::vector&& w) +{ + std::vector r; + r.reserve(w.size()); + for (auto& p : w) { + r.push_back(chonk_proof_from_wire(std::move(p))); + } + return r; +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp index 7c365dbcd1e..056d3504c50 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp @@ -1,61 +1,54 @@ #include "c_bind.hpp" -#include "barretenberg/bbapi/bbapi_execute.hpp" +#include "barretenberg/bbapi/bbapi_handlers.hpp" #include "barretenberg/bbapi/bbapi_shared.hpp" -#include "barretenberg/common/throw_or_abort.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" +#include "barretenberg/bbapi/generated/bb_dispatch.hpp" +#include +#include #include +#include +#include +#include -namespace bb::bbapi { - -// Global BBApiRequest object in anonymous namespace namespace { +// One request context for the process so stateful command sequences +// (ChonkStart/Load/Accumulate/Prove) share IVC state, mirroring a serve loop's +// single connection context. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -BBApiRequest global_request; - -#ifndef BB_NO_EXCEPTIONS -void write_error_response(const char* message, uint8_t** output_out, size_t* output_len_out) -{ - CommandResponse response = ErrorResponse{ .message = message }; - auto [output, output_len] = msgpack_encode_buffer(response, *output_out, *output_len_out); - *output_out = output; - *output_len_out = output_len; -} -#endif +bb::bbapi::BBApiRequest global_request; } // namespace /** - * @brief Main API function that processes commands and returns responses + * @brief In-process FFI/wasm entrypoint: the ipc-codegen FFI backend contract. * - * @param command The command to execute - * @return CommandResponse The response from executing the command + * Takes exactly the msgpack command payload a transport client would put inside + * a frame (no length/id envelope — framing is transport-level and an in-process + * call has none) and answers through the same generated dispatch the pipe / + * socket / shared-memory servers use. The output buffer is aligned_alloc'd and + * owned by the caller (free()-compatible), matching the cbind buffer contract. */ -CommandResponse bbapi(Command&& command) -{ -#ifndef BB_NO_EXCEPTIONS - try { -#endif - // Execute the command using the global request and return the response - return execute(global_request, std::move(command)); -#ifndef BB_NO_EXCEPTIONS - } catch (const std::exception& e) { - return ErrorResponse{ .message = e.what() }; - } -#endif -} - -} // namespace bb::bbapi - -WASM_EXPORT void bbapi(const uint8_t* input_in, size_t input_len_in, uint8_t** output_out, size_t* output_len_out) +WASM_EXPORT void ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len) { + static auto handler = bb::bbapi::make_bb_handler(global_request); + std::vector response; + // The dispatch answers a malformed envelope, an unknown command and a + // throwing handler with an error frame, but an unpack or convert failure + // still escapes it. There is no frame to carry an exception across an FFI + // boundary, so anything that gets this far has to become a response. #ifndef BB_NO_EXCEPTIONS try { #endif - msgpack_cbind_impl(bb::bbapi::bbapi, input_in, input_len_in, output_out, output_len_out); + handler(std::span(input, input_len), + [&response](std::vector r) { response = std::move(r); }); #ifndef BB_NO_EXCEPTIONS } catch (const std::exception& e) { - bb::bbapi::write_error_response(e.what(), output_out, output_len_out); + response = bb::bbapi::detail::make_error(e.what()); } catch (...) { - bb::bbapi::write_error_response("bbapi: unknown exception", output_out, output_len_out); + response = bb::bbapi::detail::make_error("bbapi: unknown exception"); } #endif + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) + auto* out = static_cast(aligned_alloc(64, response.size())); + std::memcpy(out, response.data(), response.size()); + *output = out; + *output_len = response.size(); } diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp index 7b7878d4412..1c9794db74a 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp @@ -1,12 +1,13 @@ #pragma once -#include "barretenberg/bbapi/bbapi_execute.hpp" -#include "barretenberg/serialize/cbind_fwd.hpp" -#include +#include "barretenberg/common/wasm_export.hpp" +#include +#include -namespace bb::bbapi { -// Function declaration for CLI usage -CommandResponse bbapi(Command&& command); -} // namespace bb::bbapi - -// Forward declaration for CBIND -CBIND_DECL(bbapi) +/** + * @brief In-process FFI/wasm entrypoint (the ipc-codegen FFI backend symbol). + * + * Same msgpack command/response payload as every transport, without the + * transport-level length/id envelope. Output is aligned_alloc'd; the caller + * frees it with free(). + */ +WASM_EXPORT void ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len); diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp index b0a7e3fd8a4..3ed45750e9d 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp @@ -1,72 +1,89 @@ -#include "barretenberg/bbapi/bbapi_execute.hpp" -#include "barretenberg/bbapi/bbapi_srs.hpp" #include "barretenberg/bbapi/c_bind.hpp" +#include "barretenberg/bbapi/generated/bb_types.hpp" +#include "barretenberg/bbapi/generated/ipc_codegen/msgpack_adaptor.hpp" +#include "barretenberg/bbapi/generated/ipc_codegen/msgpack_include.hpp" +#include #include -#include -#include +#include +#include using namespace bb::bbapi; -#ifndef BB_NO_EXCEPTIONS +namespace { -TEST(CBind, Poseidon2AbsorbChainMalformedLengthReturnsErrorResponse) +// Call the FFI entrypoint with a wire command and return the response's +// named-union tag ([type_name, payload]). +template std::string ffi_response_type(const char* name, const Cmd& cmd) { - for (size_t length : { 1U, 32U, 95U, 97U }) { - SCOPED_TRACE(length); - auto response = bbapi(Poseidon2AbsorbChain{ .state = {}, .inputs = std::vector(length) }); - ASSERT_TRUE(std::holds_alternative(response.get())); - EXPECT_NE(std::get(response.get()).message.find("Poseidon2AbsorbChain"), std::string::npos); + // Request framing: [[CommandName, payload]] — the named-union pair inside + // a one-element argument array. + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(name)); + pk.pack(cmd); + + uint8_t* out = nullptr; + size_t out_len = 0; + ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &out, &out_len); + + auto oh = msgpack::unpack(reinterpret_cast(out), out_len); + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) + free(out); + auto arr = oh.get().via.array; + EXPECT_EQ(arr.size, 2U); + auto type = arr.ptr[0].as(); + if (type == "BbErrorResponse") { + std::cout << "error payload: " << arr.ptr[1] << '\n'; } + return type; } -// Test that exceptions thrown during command execution are caught and converted to ErrorResponse -TEST(CBind, CatchesExceptionAndReturnsErrorResponse) -{ - // Create an SrsInitSrs command with invalid data that will cause an exception - // The from_buffer calls in bbapi_srs.cpp will read past buffer boundaries - SrsInitSrs cmd; - cmd.num_points = 100; // Request 100 points (6400 bytes needed) - cmd.points_buf = std::vector(10, 0); // Only provide 10 bytes - will cause out of bounds access - cmd.g2_point = std::vector(10, 0); // Also too small (needs 128 bytes) +} // namespace - Command command = std::move(cmd); - - // Call bbapi - exception should be caught and converted to ErrorResponse - CommandResponse response = bbapi(std::move(command)); +#ifndef BB_NO_EXCEPTIONS - // Check that we got an ErrorResponse using get_type_name() - std::string_view type_name = response.get_type_name(); - EXPECT_EQ(type_name, "ErrorResponse") << "Expected ErrorResponse but got: " << type_name; +// An exception thrown during command execution must come back as the error +// response, not kill the process: SrsInitSrs with truncated buffers throws in +// from_buffer. +TEST(CBind, CatchesExceptionAndReturnsErrorResponse) +{ + wire::BbSrsInitSrs cmd; + cmd.num_points = 100; // needs 6400 bytes of points + cmd.points_buf = std::vector(10, 0); // far too small + cmd.g2_point = std::vector(10, 0); // needs 128 bytes - // Also verify using std::holds_alternative on the underlying variant - bool is_error = std::holds_alternative(response.get()); - EXPECT_TRUE(is_error) << "Expected ErrorResponse variant"; + EXPECT_EQ(ffi_response_type("BbSrsInitSrs", cmd), "BbErrorResponse"); +} - if (is_error) { - const auto& error = std::get(response.get()); - EXPECT_FALSE(error.message.empty()) << "Error message should not be empty"; - std::cout << "Successfully caught exception with message: " << error.message << '\n'; +// A block-aligned input is the handler's precondition; a ragged one must come +// back as an error frame rather than trip the assert inside the sponge. +TEST(CBind, Poseidon2AbsorbChainMalformedLengthReturnsErrorResponse) +{ + for (size_t length : { 1U, 32U, 95U, 97U }) { + SCOPED_TRACE(length); + wire::BbPoseidon2AbsorbChain cmd; + cmd.inputs = std::vector(length); + EXPECT_EQ(ffi_response_type("BbPoseidon2AbsorbChain", cmd), "BbErrorResponse"); } } -// Test that valid operations still work correctly (no false positives) +// A valid command answers with its own response type (no false-positive errors). TEST(CBind, ValidOperationReturnsSuccess) { - // Create a Shutdown command which should succeed without throwing - Shutdown shutdown_cmd; - Command command = shutdown_cmd; - - // Call bbapi - should return success response - CommandResponse response = bbapi(std::move(command)); + wire::BbPoseidon2Hash cmd; + cmd.inputs = { std::array{} }; // hash of a single zero field - // Check that we got a ShutdownResponse, not an ErrorResponse - std::string_view type_name = response.get_type_name(); - EXPECT_NE(type_name, "ErrorResponse") << "Valid command should not return ErrorResponse"; - EXPECT_EQ(type_name, "ShutdownResponse") << "Expected ShutdownResponse"; + EXPECT_EQ(ffi_response_type("BbPoseidon2Hash", cmd), "BbPoseidon2HashResponse"); +} - // Also verify using std::holds_alternative on the underlying variant - bool is_shutdown = std::holds_alternative(response.get()); - EXPECT_TRUE(is_shutdown) << "Expected Shutdown::Response variant"; +// An unknown command tag must produce the error response rather than a decode +// failure or silence. +TEST(CBind, UnknownCommandReturnsErrorResponse) +{ + wire::BbPoseidon2Hash cmd; + EXPECT_EQ(ffi_response_type("NoSuchCommand", cmd), "BbErrorResponse"); } #else diff --git a/barretenberg/cpp/src/barretenberg/benchmark/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/CMakeLists.txt index 2916043712d..e09ac7ad46e 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/benchmark/CMakeLists.txt @@ -2,7 +2,6 @@ add_subdirectory(basics_bench) add_subdirectory(decrypt_bench) add_subdirectory(goblin_bench) add_subdirectory(ipa_bench) -add_subdirectory(ipc_bench) add_subdirectory(pippenger_bench) add_subdirectory(relations_bench) add_subdirectory(sumcheck_bench) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/CMakeLists.txt deleted file mode 100644 index 47ece59c46a..00000000000 --- a/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -barretenberg_module(ipc_bench crypto_poseidon2 ipc) diff --git a/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/ipc.bench.cpp b/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/ipc.bench.cpp deleted file mode 100644 index 6a9b8afcf01..00000000000 --- a/barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/ipc.bench.cpp +++ /dev/null @@ -1,327 +0,0 @@ -#include "barretenberg/bbapi/bbapi.hpp" -#include "barretenberg/crypto/poseidon2/poseidon2.hpp" -#include "barretenberg/ecc/curves/bn254/fr.hpp" -#include "barretenberg/ipc/ipc_client.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace benchmark; -using namespace bb; - -namespace { - -void poseidon_hash_direct(State& state) noexcept -{ - fr x = fr::random_element(); - fr y = fr::random_element(); - for (auto _ : state) { - std::vector to_hash{ x, y }; - auto hash = bb::crypto::Poseidon2::hash(to_hash); - DoNotOptimize(hash); - } -} -BENCHMARK(poseidon_hash_direct)->Unit(benchmark::kMicrosecond)->Iterations(10000); - -// Helper: Spawn bb binary for msgpack benchmarks -static pid_t spawn_bb_msgpack_server(const std::string& path) -{ - pid_t bb_pid = fork(); - if (bb_pid == 0) { - // Child process - redirect stdout/stderr to /dev/null - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { - dup2(devnull, STDOUT_FILENO); - dup2(devnull, STDERR_FILENO); - close(devnull); - } - - // Try multiple bb binary paths - const std::array bb_paths = { "./bb", // Same directory - "./build/bin/bb", // From cpp/ - "./bin/bb", // From cpp/build - "../bin/bb", // From subdirectory - "bb" }; // From PATH - for (const char* bb_path : bb_paths) { - execl(bb_path, bb_path, "msgpack", "run", "--input", path.c_str(), nullptr); - } - _exit(1); - } - return bb_pid; -} - -// Transport type enum for template specialization -enum class TransportType { Socket, Shm }; - -// BB Binary Msgpack Benchmark: Full stack test with actual bb binary -// Template parameters: -// - Transport: Socket or Shm -// - NumClients: Number of concurrent clients (1 for SPSC, >1 for MPSC) -template class Poseidon2BBMsgpack : public Fixture { - public: - static_assert(NumClients >= 1, "Must have at least 1 client"); - - std::array, NumClients> clients{}; - pid_t bb_pid{ 0 }; - std::array 1 ? NumClients - 1 : 1)> background_threads{}; - std::atomic stop_background{ false }; - fr x{}; - fr y{}; - - std::string ipc_path; - - Poseidon2BBMsgpack() - { - if constexpr (Transport == TransportType::Socket) { - ipc_path = "/tmp/poseidon_bb_msgpack_bench.sock"; - } else { - // Use short name for macOS shm_open 31-char limit - ipc_path = "/p2_bench.shm"; - } - } - - // Helper to check if socket file exists (only for socket transport) - bool socket_exists(const char* path, int max_attempts = 20) - { - for (int i = 0; i < max_attempts; i++) { - struct stat st; - if (stat(path, &st) == 0 && S_ISSOCK(st.st_mode)) { - return true; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - return false; - } - - void SetUp(const ::benchmark::State& /*unused*/) override - { - stop_background.store(false, std::memory_order_relaxed); - - // Spawn bb binary in IPC server mode - bb_pid = spawn_bb_msgpack_server(ipc_path); - if (bb_pid < 0) { - throw std::runtime_error("Failed to fork bb process"); - } - - // Wait for server to be ready - if constexpr (Transport == TransportType::Socket) { - // Wait for socket file to be created - if (!socket_exists(ipc_path.c_str())) { - kill(bb_pid, SIGKILL); - waitpid(bb_pid, nullptr, 0); - throw std::runtime_error("BB binary failed to create socket within timeout"); - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } else { - // Shared memory needs more time to initialize - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - - // Create and connect all clients - for (size_t i = 0; i < NumClients; i++) { - if constexpr (Transport == TransportType::Socket) { - clients[i] = ipc::IpcClient::create_socket(ipc_path); - } else { - // Strip .shm suffix for base name - std::string base_name = ipc_path.substr(0, ipc_path.size() - 4); - clients[i] = ipc::IpcClient::create_shm(base_name); - } - - bool connected = false; - for (int retry_count = 0; retry_count < 5; retry_count++) { - if (clients[i]->connect()) { - connected = true; - break; - } - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - if (!connected) { - kill(bb_pid, SIGKILL); - waitpid(bb_pid, nullptr, 0); - throw std::runtime_error("Failed to connect to BB IPC server after retries"); - } - } - - // Spawn background threads for MPSC scenarios (NumClients > 1) - if constexpr (NumClients > 1) { - for (size_t i = 1; i < NumClients; i++) { - background_threads[i - 1] = std::thread([this, i]() { - fr bx = fr::random_element(); - fr by = fr::random_element(); - - while (!stop_background.load(std::memory_order_relaxed)) { - // Create Poseidon2Hash command - bb::bbapi::Poseidon2Hash hash_cmd; - hash_cmd.inputs = { uint256_t(bx), uint256_t(by) }; - bb::bbapi::Command command{ std::move(hash_cmd) }; - - // Serialize command with tuple wrapping for CBIND compatibility - msgpack::sbuffer cmd_buffer; - msgpack::pack(cmd_buffer, std::make_tuple(command)); - - // Send with retry on backpressure (100ms timeout) - constexpr uint64_t TIMEOUT_NS = 100000000; // 100ms - while (!clients[i]->send(cmd_buffer.data(), cmd_buffer.size(), TIMEOUT_NS)) { - // Ring buffer full, retry - if (stop_background.load(std::memory_order_relaxed)) { - return; // Exit if shutting down - } - } - - // Receive with retry (100ms timeout) - std::span response; - while ((response = clients[i]->receive(TIMEOUT_NS)).empty()) { - // Response not ready, retry - if (stop_background.load(std::memory_order_relaxed)) { - return; // Exit if shutting down - } - } - - // Release the message - clients[i]->release(response.size()); - } - }); - } - } - - // Pre-generate test inputs for benchmark thread (client 0) - x = fr::random_element(); - y = fr::random_element(); - } - - void TearDown(const ::benchmark::State& /*unused*/) override - { - // Stop background threads if any - if constexpr (NumClients > 1) { - stop_background.store(true, std::memory_order_relaxed); - for (size_t i = 0; i < NumClients - 1; i++) { - if (background_threads[i].joinable()) { - background_threads[i].join(); - } - } - } - - // Send Shutdown command to bb so it exits gracefully (use client 0) - if (clients[0]) { - // Create Shutdown command - bb::bbapi::Shutdown shutdown_cmd; - bb::bbapi::Command command{ std::move(shutdown_cmd) }; - - // Serialize command with tuple wrapping for CBIND compatibility - msgpack::sbuffer cmd_buffer; - msgpack::pack(cmd_buffer, std::make_tuple(command)); - - // Send shutdown command with retry (1s timeout) - constexpr uint64_t TIMEOUT_NS = 1000000000; // 1 second - while (!clients[0]->send(cmd_buffer.data(), cmd_buffer.size(), TIMEOUT_NS)) { - // Retry on backpressure - } - - std::span response; - while ((response = clients[0]->receive(TIMEOUT_NS)).empty()) { - // Retry until response ready - } - - clients[0]->release(response.size()); - } - - // Close all clients - for (auto& client : clients) { - if (client) { - client->close(); - } - } - - // Wait for bb to exit gracefully (destructors will clean up resources) - if (bb_pid > 0) { - int status = 0; - pid_t result = waitpid(bb_pid, &status, 0); // Blocking wait - if (result <= 0) { - // If wait failed, force kill - kill(bb_pid, SIGKILL); - waitpid(bb_pid, nullptr, 0); - } - } - } - - // Benchmark implementation shared across all variants - void run_benchmark(benchmark::State& state) - { - constexpr uint64_t TIMEOUT_NS = 1000000000; // 1 second - - for (auto _ : state) { - // Create Poseidon2Hash command - bb::bbapi::Poseidon2Hash hash_cmd; - hash_cmd.inputs = { uint256_t(x), uint256_t(y) }; - bb::bbapi::Command command{ std::move(hash_cmd) }; - - // Serialize command with tuple wrapping for CBIND compatibility - msgpack::sbuffer cmd_buffer; - msgpack::pack(cmd_buffer, std::make_tuple(command)); - - // Send command with retry on backpressure - while (!clients[0]->send(cmd_buffer.data(), cmd_buffer.size(), TIMEOUT_NS)) { - // Ring buffer full, retry (shouldn't happen often in benchmarks) - } - - // Receive response with retry - std::span resp; - while ((resp = clients[0]->receive(TIMEOUT_NS)).empty()) { - // Response not ready, retry - } - - // Deserialize response - auto unpacked = msgpack::unpack(reinterpret_cast(resp.data()), resp.size()); - bb::bbapi::CommandResponse response; - unpacked.get().convert(response); - - // Release the message - clients[0]->release(resp.size()); - - // Extract hash from response - const auto& response_variant = static_cast(response); - const auto* hash_response = std::get_if(&response_variant); - if (hash_response == nullptr) { - state.SkipWithError("Invalid response type"); - break; - } - - auto hash = hash_response->hash; - DoNotOptimize(hash); - } - } -}; - -// Type aliases for specific test cases -// SPSC: Single client -using Poseidon2BBSocketSPSC = Poseidon2BBMsgpack; -using Poseidon2BBShmSPSC = Poseidon2BBMsgpack; - -// MPSC: Multiple clients (socket only - SHM is SPSC-only now) -using Poseidon2BBSocketMPSC = Poseidon2BBMsgpack; - -// Macro to register benchmark variants -#define REGISTER_BB_BENCHMARK(fixture_name) \ - BENCHMARK_DEFINE_F(fixture_name, poseidon_hash_roundtrip)(benchmark::State & state) \ - { \ - run_benchmark(state); \ - } \ - BENCHMARK_REGISTER_F(fixture_name, poseidon_hash_roundtrip)->Unit(benchmark::kMicrosecond)->Iterations(10000) - -REGISTER_BB_BENCHMARK(Poseidon2BBSocketSPSC); -REGISTER_BB_BENCHMARK(Poseidon2BBSocketMPSC); -REGISTER_BB_BENCHMARK(Poseidon2BBShmSPSC); - -} // namespace - -BENCHMARK_MAIN(); diff --git a/barretenberg/cpp/src/barretenberg/common/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/common/CMakeLists.txt index 43d026405de..4f0bd33e488 100644 --- a/barretenberg/cpp/src/barretenberg/common/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/common/CMakeLists.txt @@ -1,5 +1,4 @@ add_subdirectory(tracy_mem) barretenberg_module(common env libdeflate::libdeflate_static nlohmann_json::nlohmann_json) -# Need -fPIC because it's linked into the nodejs_module shared library set_target_properties(common_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/cpp/src/barretenberg/crypto/keccak/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/keccak/CMakeLists.txt index 1ba8c36e331..fb700842960 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/keccak/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/keccak/CMakeLists.txt @@ -1,3 +1,2 @@ barretenberg_module(crypto_keccak) -# Need -fPIC because it's linked into the nodejs_module shared library set_target_properties(crypto_keccak_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/CMakeLists.txt index ed6b426643f..635ebdacd5b 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/CMakeLists.txt @@ -1,3 +1,2 @@ barretenberg_module(crypto_poseidon2 ecc) -# Need -fPIC because it's linked into the nodejs_module shared library set_target_properties(crypto_poseidon2_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/cpp/src/barretenberg/crypto/sha256/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/sha256/CMakeLists.txt index 32363538e1f..4babf790755 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/sha256/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/sha256/CMakeLists.txt @@ -1,3 +1,2 @@ barretenberg_module(crypto_sha256 common) -# Need -fPIC because it's linked into the nodejs_module shared library set_target_properties(crypto_sha256_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/cpp/src/barretenberg/ipc/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/ipc/CMakeLists.txt deleted file mode 100644 index 99e29c91e89..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -barretenberg_module(ipc common) -set_target_properties(ipc_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/cpp/src/barretenberg/ipc/README.md b/barretenberg/cpp/src/barretenberg/ipc/README.md deleted file mode 100644 index 387f0efa27c..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/README.md +++ /dev/null @@ -1,389 +0,0 @@ -# Barretenberg IPC Module - -Modern C++ inter-process communication (IPC) library providing unified abstractions over multiple transport mechanisms. Designed for high-performance request/response patterns between processes. - -## Overview - -The IPC module provides: -- **Abstract interfaces** (`IpcClient`, `IpcServer`) for transport-independent code -- **Unix domain sockets** transport for simplicity and broad compatibility -- **Shared memory** transport for ultra-low latency (sub-microsecond) -- **Factory pattern** for easy instantiation -- **Multi-client support** with dynamic capacity (sockets) or fixed capacity (shared memory) -- **Built-in server loop** with graceful shutdown support - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ Abstract Interfaces │ -│ IpcClient IpcServer │ -│ - connect() - listen() │ -│ - send() - wait_for_data() │ -│ - recv() - receive() / release() │ -│ - close() - send() / close() │ -│ - run(handler) │ -└──────────────┬─────────────────┬────────────────┘ - │ │ - ┌───────┴────────┐ ┌──────┴──────────┐ - │ Socket │ │ Shared Memory │ - │ Implementation │ │ Implementation │ - └────────────────┘ └─────────────────┘ -``` - -### Transport Implementations - -#### 1. Unix Domain Sockets (`SocketClient` / `SocketServer`) - -**Architecture:** -- Standard POSIX socket API with epoll for multi-client handling -- Direct implementation (no wrapper layers) -- Dynamic client capacity with O(1) client lookup -- Requires system calls for each message - -**Use when:** -- Simplicity and compatibility are priorities -- Latency requirements are moderate (5-15 µs) -- Need unlimited dynamic client capacity -- Running in environments without shared memory support - -**Example:** -```cpp -#include "barretenberg/ipc/ipc_server.hpp" -#include "barretenberg/ipc/ipc_client.hpp" - -// Server process -auto server = IpcServer::create_socket("/tmp/my.sock", 10); // max 10 initial clients -server->listen(); - -server->run([](int client_id, std::span request) { - // Process request, return response - std::vector response = process(request); - return response; -}); - -// Client process -auto client = IpcClient::create_socket("/tmp/my.sock"); -client->connect(); - -std::vector request = {...}; -client->send(request.data(), request.size()); - -std::vector response(1024); -ssize_t n = client->recv(response.data(), response.size()); -``` - -#### 2. Shared Memory (`ShmClient` / `ShmServer`) - -**Architecture:** -- Lock-free SPSC/MPSC ring buffers (see `shm/README.md` for details) -- Requests: MPSC (multi-producer single-consumer) ring -- Responses: Dedicated SPSC ring per client -- Adaptive spin + futex for efficient blocking -- Fixed client capacity (set at server creation) - -**Use when:** -- Ultra-low latency is critical (0.3-1 µs hot, 3-6 µs cold) -- Number of clients is known and fixed -- Linux/POSIX shared memory available -- Zero-copy message passing desired - -**Example:** -```cpp -#include "barretenberg/ipc/ipc_server.hpp" -#include "barretenberg/ipc/ipc_client.hpp" - -// Server process -auto server = IpcServer::create_shm("my_shm", 4); // exactly 4 clients -server->listen(); - -server->run([](int client_id, std::span request) { - std::vector response = process(request); - return response; -}); - -// Client process (run 4 instances) -auto client = IpcClient::create_shm("my_shm", 4); -client->connect(); // Atomically claims client_id 0, 1, 2, or 3 - -std::vector request = {...}; -client->send(request.data(), request.size()); - -std::vector response(1024); -ssize_t n = client->recv(response.data(), response.size()); -``` - -## API Reference - -### IpcClient Interface - -```cpp -class IpcClient { -public: - virtual ~IpcClient() = default; - - // Connect to server - virtual bool connect() = 0; - - // Send request to server - virtual bool send(const void* data, size_t len, uint64_t timeout_ns = 0) = 0; - - // Receive response from server - virtual ssize_t recv(void* buffer, size_t max_len, uint64_t timeout_ns = 0) = 0; - - // Close connection - virtual void close() = 0; - - // Factory methods - static std::unique_ptr create_socket(const std::string& socket_path); - static std::unique_ptr create_shm(const std::string& base_name, size_t max_clients); -}; -``` - -### IpcServer Interface - -```cpp -class IpcServer { -public: - using Handler = std::function(int client_id, std::span request)>; - - virtual ~IpcServer() = default; - - // Start listening for connections - virtual bool listen() = 0; - - // Wait for data from any client (spins then blocks, returns client_id) - virtual int wait_for_data(uint64_t spin_ns) = 0; - - // Receive next message (blocks until complete, zero-copy for SHM) - virtual std::span receive(int client_id) = 0; - - // Release/consume the received message - virtual void release(int client_id, size_t message_size) = 0; - - // Send to specific client - virtual bool send(int client_id, const void* data, size_t len) = 0; - - // Close server - virtual void close() = 0; - - // High-level event loop with handler (uses peek/release internally) - virtual void run(Handler handler); - - // Factory methods - static std::unique_ptr create_socket(const std::string& socket_path, int max_clients); - static std::unique_ptr create_shm(const std::string& base_name, size_t max_clients, - size_t request_ring_size = 1MB, size_t response_ring_size = 1MB); -}; -``` - -The receive/release pattern ensures: -- **Zero-copy for SHM**: `receive()` returns a span pointing directly into the ring buffer -- **No message loss**: Both implementations guarantee complete messages; incomplete = corruption -- **Explicit lifecycle**: Messages are only consumed when `release()` is explicitly called with size -- **Semantic equivalence**: Both socket and SHM implementations block until complete message available - -### Graceful Shutdown - -The server's `run()` method supports graceful shutdown via exception: - -```cpp -#include "barretenberg/ipc/ipc_server.hpp" - -server->run([](int client_id, std::span request) { - if (is_shutdown_request(request)) { - std::vector goodbye = encode_goodbye(); - throw ShutdownRequested(goodbye); // Sends response, then exits cleanly - } - return process_normal_request(request); -}); -// Destructors run here, cleaning up all resources -``` - -## Performance Comparison - -### Latency (Round-trip time) - -| Transport | Hot Path | Cold Path | Notes | -|----------------|----------------|--------------|--------------------------------| -| Sockets | 6-15 µs | 10-20 µs | Requires syscalls per message | -| Shared Memory | 0.3-1 µs | 3-6 µs | Zero-copy, adaptive spin+futex | - -### Throughput - -| Transport | Single Client | Multi-Client (3) | Notes | -|----------------|----------------|------------------|--------------------------| -| Sockets | ~150K msgs/s | ~120K msgs/s | Epoll scales well | -| Shared Memory | ~1M msgs/s | ~700K msgs/s | Lock-free, per-client queues | - -*Benchmarks measured on AMD Ryzen 9 5950X, small messages (<1KB)* - -## Implementation Details - -### Socket Transport - -**SocketClient:** -- Direct socket file descriptor management -- RAII cleanup (close on destruction) -- Blocking I/O with optional timeout -- Length-prefixed messages (4-byte header) - -**SocketServer:** -- Epoll for efficient multi-client event handling -- Dynamic client table (grows on demand) -- O(1) fd→client_id lookup via `std::unordered_map` -- Automatic cleanup on client disconnect - -### Shared Memory Transport - -**ShmClient:** -- Atomically claims client ID from shared counter -- Connects to MPSC ring (producer role) for requests -- Connects to dedicated SPSC ring (consumer role) for responses -- Length-prefixed messages (4-byte header) matching socket behavior - -**ShmServer:** -- Creates MPSC consumer for receiving requests from all clients -- Pre-creates SPSC rings for each client's responses -- Round-robin polling across client rings -- Shared doorbell futex for efficient wakeup - -For deep dive into shared memory ring buffer architecture, see [`shm/README.md`](shm/README.md). - -## Build Integration - -The IPC module is included in Barretenberg's main CMake build: - -```cmake -# CMakeLists.txt -add_library(ipc - ipc_client.cpp - ipc_server.cpp - socket_client.cpp - socket_server.cpp - # shm implementations are header-only -) - -target_link_libraries(ipc - PRIVATE pthread # For futex operations - PRIVATE rt # For shm_open/shm_unlink -) -``` - -## Testing - -Run IPC tests with: - -```bash -# From barretenberg/cpp -cd build-no-avm -ninja ipc_bench # Performance benchmarks -./bin/ipc_bench -``` - -The benchmark compares socket vs shared memory performance with single and multiple clients. - -## Best Practices - -### Choosing a Transport - -**Use sockets when:** -- You need dynamic client capacity -- Compatibility and simplicity are priorities -- Moderate latency (10-15 µs) is acceptable -- You're crossing network boundaries (with TCP sockets) - -**Use shared memory when:** -- Ultra-low latency (<1 µs) is critical -- Number of clients is known and fixed -- Running on same machine with POSIX shared memory -- You want zero-copy message passing - -### Error Handling - -All operations return bool (success) or -1 (error) for easy checking: - -```cpp -if (!client->connect()) { - // Handle connection failure -} - -if (!client->send(data, len)) { - // Handle send failure -} - -ssize_t n = client->recv(buffer, size); -if (n < 0) { - // Handle receive failure/timeout -} -``` - -### Resource Management - -Both transports use RAII for automatic cleanup: - -```cpp -{ - auto server = IpcServer::create_socket("/tmp/my.sock", 10); - server->listen(); - // Use server... -} // Destructor closes connections and cleans up resources -``` - -For shared memory, the server should explicitly unlink on clean shutdown: - -```cpp -auto server = IpcServer::create_shm("my_shm", 4); -server->listen(); -server->run(handler); -// Destructor automatically calls unlink to remove shared memory objects -``` - -### Message Framing - -Both transports use 4-byte length prefixes for messages: - -``` -┌────────────┬──────────────────┐ -│ Length │ Payload │ -│ (4 bytes) │ (Length bytes) │ -└────────────┴──────────────────┘ -``` - -This is handled automatically by the implementations. - -### Threading Model - -- **Socket server:** Single-threaded event loop with epoll -- **Socket client:** Thread-safe (each client is independent) -- **Shared memory server:** Single-threaded consumer (by design) -- **Shared memory client:** Lock-free producer (multiple clients can send concurrently) - -## Limitations - -### Sockets -- System call overhead (cannot eliminate) -- Buffer copying on send (recv is internal buffer, minimal copy) -- File descriptor limits (ulimit) - -### Shared Memory -- Fixed client capacity (must specify at creation) -- Linux-specific (uses futex, though portable to other POSIX systems) -- Requires cleanup of /dev/shm objects (automatic on destruction) -- No security boundaries (all clients can access all memory) - -## Future Enhancements - -Potential improvements for future versions: - -- [ ] Zero-copy socket option (SCM_RIGHTS / vmsplice) -- [ ] Configurable ring buffer sizes for shared memory -- [ ] Metrics/telemetry API (latency histograms, throughput) -- [ ] Windows named pipes support -- [ ] Cross-platform shared memory abstraction (Windows shared memory objects) - -## See Also - -- [`shm/README.md`](shm/README.md) - Deep dive into lock-free ring buffer implementation -- [`shm.test.cpp`](shm.test.cpp) - Comprehensive test suite -- Benchmarks in `barretenberg/cpp/build-no-avm/bin/ipc_bench` diff --git a/barretenberg/cpp/src/barretenberg/ipc/grind_ipc.sh b/barretenberg/cpp/src/barretenberg/ipc/grind_ipc.sh deleted file mode 100755 index 26be91bde7c..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/grind_ipc.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -source $(git rev-parse --show-toplevel)/ci3/source - -trap 'clean' EXIT - -function clean { - rm -f /dev/shm/shm_wrap_* -} - -jobs=${1:-128} -shift - -clean -cp ../../../build/bin/ipc_tests ../../../build/bin/ipc_tests_live -while true; do - echo "dump_fail '$@ timeout 30s ../../../build/bin/ipc_tests_live --gtest_filter=ShmTest.SingleClientSmallRingHighVolume &> >(add_timestamps && date)' >/dev/null" -done | parallel -j$jobs --halt now,fail=1 diff --git a/barretenberg/cpp/src/barretenberg/ipc/ipc_client.cpp b/barretenberg/cpp/src/barretenberg/ipc/ipc_client.cpp deleted file mode 100644 index 49d6c5da4df..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/ipc_client.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "barretenberg/ipc/ipc_client.hpp" -#include "barretenberg/ipc/mpsc_shm_client.hpp" -#include "barretenberg/ipc/shm_client.hpp" -#include "barretenberg/ipc/socket_client.hpp" -#include -#include -#include - -namespace bb::ipc { - -std::unique_ptr IpcClient::create_socket(const std::string& socket_path) -{ - return std::make_unique(socket_path); -} - -std::unique_ptr IpcClient::create_shm(const std::string& base_name) -{ - return std::make_unique(base_name); -} - -std::unique_ptr IpcClient::create_mpsc_shm(const std::string& base_name, size_t client_id) -{ - return std::make_unique(base_name, client_id); -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/ipc_client.hpp b/barretenberg/cpp/src/barretenberg/ipc/ipc_client.hpp deleted file mode 100644 index a14879efe4b..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/ipc_client.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief Abstract interface for IPC client - * - * Provides a unified interface for connecting to IPC servers and exchanging messages. - * Implementations handle transport-specific details (Unix domain sockets, shared memory, etc). - */ -class IpcClient { - public: - IpcClient() = default; - virtual ~IpcClient() = default; - - // Abstract interface - no copy or move - IpcClient(const IpcClient&) = delete; - IpcClient& operator=(const IpcClient&) = delete; - IpcClient(IpcClient&&) = delete; - IpcClient& operator=(IpcClient&&) = delete; - - /** - * @brief Connect to the server - * @return true if connection successful, false otherwise - */ - virtual bool connect() = 0; - - /** - * @brief Send a message to the server - * @param data Pointer to message data - * @param len Length of message in bytes - * @param timeout_ns Timeout in nanoseconds (0 = infinite) - * @return true if sent successfully, false on error or timeout - */ - virtual bool send(const void* data, size_t len, uint64_t timeout_ns) = 0; - - /** - * @brief Receive a message from the server (zero-copy for shared memory) - * @param timeout_ns Timeout in nanoseconds - * @return Span of message data (empty on error/timeout) - * - * The span remains valid until release() is called or the next recv(). - * For shared memory: direct view into ring buffer (true zero-copy) - * For sockets: view into internal buffer (eliminates one copy) - * - * Must be followed by release() to consume the message. - */ - virtual std::span receive(uint64_t timeout_ns) = 0; - - /** - * @brief Release the previously received message - * @param message_size Size of the message being released (from span.size()) - * - * Must be called after recv() to consume the message and free resources. - * For shared memory: releases space in the ring buffer - * For sockets: no-op (message already consumed during recv) - */ - virtual void release(size_t message_size) = 0; - - /** - * @brief Close the connection - */ - virtual void close() = 0; - - // Factory methods - static std::unique_ptr create_socket(const std::string& socket_path); - static std::unique_ptr create_shm(const std::string& base_name); - static std::unique_ptr create_mpsc_shm(const std::string& base_name, size_t client_id); -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/ipc_server.cpp b/barretenberg/cpp/src/barretenberg/ipc/ipc_server.cpp deleted file mode 100644 index 4833d7029d4..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/ipc_server.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "barretenberg/ipc/ipc_server.hpp" -#include "barretenberg/ipc/mpsc_shm_server.hpp" -#include "barretenberg/ipc/shm_server.hpp" -#include "barretenberg/ipc/socket_server.hpp" -#include -#include -#include - -namespace bb::ipc { - -std::unique_ptr IpcServer::create_socket(const std::string& socket_path, int max_clients) -{ - return std::make_unique(socket_path, max_clients); -} - -std::unique_ptr IpcServer::create_shm(const std::string& base_name, - size_t request_ring_size, - size_t response_ring_size) -{ - return std::make_unique(base_name, request_ring_size, response_ring_size); -} - -std::unique_ptr IpcServer::create_mpsc_shm(const std::string& base_name, - size_t max_clients, - size_t request_ring_size, - size_t response_ring_size) -{ - return std::make_unique(base_name, max_clients, request_ring_size, response_ring_size); -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/ipc_server.hpp b/barretenberg/cpp/src/barretenberg/ipc/ipc_server.hpp deleted file mode 100644 index 0684ebeea89..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/ipc_server.hpp +++ /dev/null @@ -1,214 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief Exception thrown by handler to signal graceful shutdown - * - * Carries the response data to be sent before shutting down. - */ -class ShutdownRequested : public std::exception { - std::vector response_; - - public: - explicit ShutdownRequested(std::vector response) - : response_(std::move(response)) - {} - const std::vector& response() const { return response_; } - const char* what() const noexcept override { return "Server shutdown requested"; } -}; - -/** - * @brief Abstract interface for IPC server - * - * Provides a unified interface for accepting client connections and exchanging messages. - * Implementations handle transport-specific details (Unix domain sockets, shared memory, etc). - */ -class IpcServer { - public: - IpcServer() = default; - virtual ~IpcServer() = default; - - // Abstract interface - no copy or move - IpcServer(const IpcServer&) = delete; - IpcServer& operator=(const IpcServer&) = delete; - IpcServer(IpcServer&&) = delete; - IpcServer& operator=(IpcServer&&) = delete; - - /** - * @brief Start listening for client connections - * @return true if successful, false otherwise - */ - virtual bool listen() = 0; - - /** - * @brief Wait for data from any connected client - * - * @param timeout_ns Maximum time to wait in nanoseconds (0 = non-blocking poll) - * @return Client ID that has data available, or -1 on timeout/error - */ - virtual int wait_for_data(uint64_t timeout_ns) = 0; - - /** - * @brief Receive next message from a specific client - * - * Blocks until a complete message is available. Returns a span pointing to the message data. - * For shared memory, this is a zero-copy view directly into the ring buffer. - * For sockets, this is a view into an internal buffer. - * - * The message remains valid until release() is called with the message size. - * - * @param client_id Client to receive from - * @return Span of message data (empty only on error/disconnect) - */ - virtual std::span receive(int client_id) = 0; - - /** - * @brief Release/consume the previously received message - * - * Must be called after receive() to advance to the next message. - * For shared memory, this releases space in the ring buffer. - * For sockets, this is a no-op (message already consumed during receive). - * - * @param client_id Client whose message to release - * @param message_size Size of the message being released (from span.size()) - */ - virtual void release(int client_id, size_t message_size) = 0; - - /** - * @brief Send a message to a specific client - * @param client_id Client to send to - * @param data Pointer to message data - * @param len Length of message in bytes - * @return true if sent successfully, false on error - */ - virtual bool send(int client_id, const void* data, size_t len) = 0; - - /** - * @brief Close the server and all client connections - */ - virtual void close() = 0; - - /** - * @brief Request graceful shutdown. - * - * Sets shutdown flag and wakes all blocked threads. Safe to call from signal handlers. - * After this returns, the run() loop will exit on its next iteration. - * Call close() afterward to clean up resources. - */ - virtual void request_shutdown() - { - shutdown_requested_.store(true, std::memory_order_release); - wakeup_all(); - } - - /** - * @brief High-level request handler function type - * - * Takes client_id and request data, returns response data. - * Return empty vector to skip sending a response. - */ - using Handler = std::function(int client_id, std::span request)>; - - /** - * @brief Accept a new client connection (optional for some transports) - * @param timeout_ns Timeout in nanoseconds (0 = non-blocking, <0 = infinite) - * @return Client ID if successful, -1 if no pending connection or error - * - * Note: Some transports (like shared memory) may not need explicit accept calls. - */ - virtual int accept() { return -1; } - - /** - * @brief Run server event loop with handler - * - * Continuously waits for client requests and invokes handler. - * Handler is responsible for deserializing request, processing, and serializing response. - * This is a convenience method that encapsulates the typical server loop. - * - * Uses peek/release pattern: - * - peek() returns a span (zero-copy for SHM, internal buffer for sockets) - * - handler processes the request - * - release() explicitly consumes the message - * - * This design ensures no messages are lost and enables zero-copy for shared memory. - * - * Server exits gracefully when handler throws ShutdownRequested exception. - * - * @param handler Function to process requests and generate responses - */ - virtual void run(const Handler& handler) - { - while (!shutdown_requested_.load(std::memory_order_acquire)) { - // Try to accept new clients (non-blocking for socket servers) - accept(); - - int client_id = wait_for_data(100000000); // 100ms timeout - if (client_id < 0) { - // Timeout or error - check shutdown flag on next iteration - continue; - } - - // Receive message (blocks until complete message available, zero-copy for SHM) - auto request = receive(client_id); - if (request.empty()) { - continue; - } - - try { - auto response = handler(client_id, request); - if (!response.empty()) { - send(client_id, response.data(), response.size()); - } - - // Explicitly release/consume the message - release(client_id, request.size()); - } catch (const ShutdownRequested& shutdown) { - // Release message before shutting down - release(client_id, request.size()); - - // Send final response before shutting down - if (!shutdown.response().empty()) { - send(client_id, shutdown.response().data(), shutdown.response().size()); - } - // Graceful shutdown - exit loop and let destructors run - return; - } - } - } - - // Factory methods - static std::unique_ptr create_socket(const std::string& socket_path, int max_clients); - static std::unique_ptr create_shm(const std::string& base_name, - size_t request_ring_size = static_cast(1024 * 1024), - size_t response_ring_size = static_cast(1024 * 1024)); - static std::unique_ptr create_mpsc_shm(const std::string& base_name, - size_t max_clients, - size_t request_ring_size = static_cast(1024 * 1024), - size_t response_ring_size = static_cast(1024 * 1024)); - - protected: - std::atomic shutdown_requested_{ false }; - - /** - * @brief Wake all blocked threads (for graceful shutdown) - * - * Wakes any threads blocked in wait_for_data() or other blocking operations. - * Used by signal handlers to trigger graceful shutdown without waiting for timeouts. - */ - virtual void wakeup_all() {}; -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_client.hpp b/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_client.hpp deleted file mode 100644 index 51922b14f9a..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_client.hpp +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include "ipc_client.hpp" -#include "shm/mpsc_shm.hpp" -#include "shm/spsc_shm.hpp" -#include "shm_common.hpp" -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC client for multi-client shared memory server - * - * Uses MpscProducer for sending requests and a dedicated SPSC ring for - * receiving responses. Each client is assigned a unique client_id. - */ -class MpscShmClient : public IpcClient { - public: - MpscShmClient(std::string base_name, size_t client_id) - : base_name_(std::move(base_name)) - , client_id_(client_id) - {} - - ~MpscShmClient() override = default; - - // Non-copyable, non-movable - MpscShmClient(const MpscShmClient&) = delete; - MpscShmClient& operator=(const MpscShmClient&) = delete; - MpscShmClient(MpscShmClient&&) = delete; - MpscShmClient& operator=(MpscShmClient&&) = delete; - - bool connect() override - { - if (producer_.has_value()) { - return true; // Already connected - } - - try { - // Connect as producer to the MPSC request system - producer_ = MpscProducer::connect(base_name_ + "_req", client_id_); - - // Connect to our dedicated SPSC response ring - std::string resp_name = base_name_ + "_resp_" + std::to_string(client_id_); - response_ring_ = SpscShm::connect(resp_name); - - return true; - } catch (...) { - producer_.reset(); - response_ring_.reset(); - return false; - } - } - - bool send(const void* data, size_t len, uint64_t timeout_ns) override - { - if (!producer_.has_value()) { - return false; - } - - // Claim space for length prefix + data - size_t total_size = sizeof(uint32_t) + len; - void* buf = producer_->claim(total_size, static_cast(timeout_ns)); - if (buf == nullptr) { - return false; - } - - // Write length prefix + data - auto len_u32 = static_cast(len); - std::memcpy(buf, &len_u32, sizeof(uint32_t)); - std::memcpy(static_cast(buf) + sizeof(uint32_t), data, len); - - // Publish (rings doorbell to wake server) - producer_->publish(total_size); - return true; - } - - std::span receive(uint64_t timeout_ns) override - { - if (!response_ring_.has_value()) { - return {}; - } - return ring_receive_msg(response_ring_.value(), timeout_ns); - } - - void release(size_t message_size) override - { - if (!response_ring_.has_value()) { - return; - } - response_ring_->release(sizeof(uint32_t) + message_size); - } - - void close() override - { - producer_.reset(); - response_ring_.reset(); - } - - private: - std::string base_name_; - size_t client_id_; - std::optional producer_; - std::optional response_ring_; -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_server.hpp b/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_server.hpp deleted file mode 100644 index f6b52b9c552..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_server.hpp +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once - -#include "ipc_server.hpp" -#include "shm/mpsc_shm.hpp" -#include "shm/spsc_shm.hpp" -#include "shm_common.hpp" -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC server implementation using shared memory with multi-client support - * - * Uses MPSC (multi-producer single-consumer) for requests and per-client SPSC - * rings for responses. Supports up to max_clients concurrent clients. - * - * Shared memory layout: - * - Request: MPSC consumer with one SPSC ring per client (client writes, server reads) - * - Response: Separate SPSC ring per client (server writes, client reads) - */ -class MpscShmServer : public IpcServer { - public: - static constexpr size_t DEFAULT_RING_SIZE = 1 << 20; // 1MB - - MpscShmServer(std::string base_name, - size_t max_clients, - size_t request_ring_size = DEFAULT_RING_SIZE, - size_t response_ring_size = DEFAULT_RING_SIZE) - : base_name_(std::move(base_name)) - , max_clients_(max_clients) - , request_ring_size_(request_ring_size) - , response_ring_size_(response_ring_size) - {} - - ~MpscShmServer() override { close(); } - - // Non-copyable, non-movable - MpscShmServer(const MpscShmServer&) = delete; - MpscShmServer& operator=(const MpscShmServer&) = delete; - MpscShmServer(MpscShmServer&&) = delete; - MpscShmServer& operator=(MpscShmServer&&) = delete; - - bool listen() override - { - if (request_consumer_.has_value()) { - return true; // Already listening - } - - // Clean up any leftover shared memory - MpscConsumer::unlink(base_name_ + "_req", max_clients_); - for (size_t i = 0; i < max_clients_; i++) { - SpscShm::unlink(base_name_ + "_resp_" + std::to_string(i)); - } - - try { - // Create MPSC consumer for requests (one ring per client) - request_consumer_ = MpscConsumer::create(base_name_ + "_req", max_clients_, request_ring_size_); - - // Create per-client SPSC response rings - response_rings_.reserve(max_clients_); - for (size_t i = 0; i < max_clients_; i++) { - std::string resp_name = base_name_ + "_resp_" + std::to_string(i); - response_rings_.push_back(SpscShm::create(resp_name, response_ring_size_)); - } - - return true; - } catch (...) { - close(); - return false; - } - } - - int wait_for_data(uint64_t timeout_ns) override - { - if (!request_consumer_.has_value()) { - return -1; - } - // MpscConsumer::wait_for_data returns ring index = client_id - return request_consumer_->wait_for_data(static_cast(timeout_ns)); - } - - std::span receive(int client_id) override - { - if (!request_consumer_.has_value() || client_id < 0 || static_cast(client_id) >= max_clients_) { - return {}; - } - // Peek on the specific client's request ring via MpscConsumer - void* len_ptr = request_consumer_->peek(static_cast(client_id), sizeof(uint32_t), 100000000); - if (len_ptr == nullptr) { - return {}; - } - uint32_t msg_len = 0; - std::memcpy(&msg_len, len_ptr, sizeof(uint32_t)); - - void* msg_ptr = request_consumer_->peek(static_cast(client_id), sizeof(uint32_t) + msg_len, 100000000); - if (msg_ptr == nullptr) { - return {}; - } - return std::span(static_cast(msg_ptr) + sizeof(uint32_t), msg_len); - } - - void release(int client_id, size_t message_size) override - { - if (!request_consumer_.has_value() || client_id < 0 || static_cast(client_id) >= max_clients_) { - return; - } - request_consumer_->release(static_cast(client_id), sizeof(uint32_t) + message_size); - } - - bool send(int client_id, const void* data, size_t len) override - { - if (client_id < 0 || static_cast(client_id) >= response_rings_.size()) { - return false; - } - return ring_send_msg(response_rings_[static_cast(client_id)], data, len, 100000000); - } - - void close() override - { - request_consumer_.reset(); - response_rings_.clear(); - - // Clean up shared memory - MpscConsumer::unlink(base_name_ + "_req", max_clients_); - for (size_t i = 0; i < max_clients_; i++) { - SpscShm::unlink(base_name_ + "_resp_" + std::to_string(i)); - } - } - - void wakeup_all() override - { - if (request_consumer_.has_value()) { - request_consumer_->wakeup_all(); - } - for (auto& ring : response_rings_) { - ring.wakeup_all(); - } - } - - private: - std::string base_name_; - size_t max_clients_; - size_t request_ring_size_; - size_t response_ring_size_; - std::optional request_consumer_; - std::vector response_rings_; -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm.test.cpp b/barretenberg/cpp/src/barretenberg/ipc/shm.test.cpp deleted file mode 100644 index 22208e5a61c..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm.test.cpp +++ /dev/null @@ -1,308 +0,0 @@ -#include "barretenberg/ipc/ipc_client.hpp" -#include "barretenberg/ipc/ipc_server.hpp" -#include "barretenberg/ipc/shm/spsc_shm.hpp" -#include "barretenberg/ipc/shm_client.hpp" -#include "barretenberg/ipc/shm_server.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace bb::ipc; - -namespace { - -/** - * You can really stress test this with grind_ipc.sh - */ -TEST(ShmTest, SingleClientSmallRingHighVolume) -{ - constexpr size_t RING_SIZE = 2UL * 1024; - constexpr size_t NUM_ITERATIONS = 10000000; - // Sizing ensures that no matter that state of the internal ring buffer, we can't deadlock. - constexpr size_t MAX_MSG_SIZE = (RING_SIZE / 2) - 4; - - // Use short name for macOS compatibility (31-char limit) - std::string wrap_test_shm = "shm_wrap_" + std::to_string(getpid()); - auto server = IpcServer::create_shm(wrap_test_shm, RING_SIZE, RING_SIZE); - ASSERT_TRUE(server->listen()) << "Wrap test server failed to listen"; - - std::atomic server_running{ true }; - std::atomic corruptions{ 0 }; - - // Echo server with validation - std::thread server_thread([&]() { - size_t iter = 0; - while (server_running.load(std::memory_order_acquire)) { - server->accept(); - - int client_id = server->wait_for_data(10000000); // 10ms - if (client_id < 0) { - continue; - } - - auto request_buf = server->receive(client_id); - // std::cerr << "Server received " << request.size() << " bytes" << '\n'; - - if (request_buf.empty()) { - continue; - } - - // Take a copy of the request so we can release. - std::vector request(request_buf.begin(), request_buf.end()); - server->release(client_id, request.size()); - - // Validate pattern: first byte should be XOR with offsets - // Check a few bytes to detect corruption without slowing down too much - if (request.size() > 0) { - uint8_t first = request[0]; - for (size_t i = 0; i < std::min(request.size(), size_t(16)); i++) { - uint8_t expected = static_cast((first ^ i) & 0xFF); - if (request[i] != expected) { - corruptions.fetch_add(1); - std::cerr << "Pattern mismatch at offset " << i << ": expected=" << (int)expected - << " actual=" << (int)request[i] << '\n'; - break; - } - } - } - - // Retry send until success. - while (!server->send(client_id, request.data(), request.size())) { - // Timeout - retry (response ring might be full) - std::cerr << iter << " Server send size " << request.size() << " timeout, retrying..." << '\n'; - dynamic_cast(server.get())->debug_dump(); - } - // std::cerr << "Server sent response of " << request.size() << " bytes" << '\n'; - iter++; - } - }); - - std::this_thread::sleep_for(std::chrono::milliseconds(300)); - - auto client = IpcClient::create_shm(wrap_test_shm); - ASSERT_TRUE(client->connect()); - - // Random message sizes. - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution size_dist(1, MAX_MSG_SIZE); - - // Store sizes for each iteration so receiver knows what to expect - std::vector iteration_sizes(NUM_ITERATIONS); - for (size_t i = 0; i < NUM_ITERATIONS; i++) { - iteration_sizes[i] = size_dist(gen); - // iteration_sizes[i] = MAX_MSG_SIZE - 1; - } - - // Sender thread: continuously send requests - std::thread sender_thread([&]() { - std::vector send_buffer(MAX_MSG_SIZE); - - for (size_t iter = 0; iter < NUM_ITERATIONS; iter++) { - size_t size = iteration_sizes[iter]; - // std::cerr << "Client: Iteration " << iter << ": sending " << size << " bytes" << '\n'; - - // Fill buffer with iteration-specific pattern - // First byte is iteration number (mod 256), rest is XOR pattern with offset - uint8_t iter_byte = static_cast(iter & 0xFF); - for (size_t i = 0; i < size; i++) { - send_buffer[i] = static_cast((iter_byte ^ i) & 0xFF); - } - - // Retry send until success - timeouts are expected under extreme load - while (!client->send(send_buffer.data(), size, 100000000)) { - // Timeout - retry (ring might be full, server might be slow) - std::cerr << iter << " Client send size " << size << " timeout, retrying..." << '\n'; - dynamic_cast(client.get())->debug_dump(); - } - } - }); - - // Receiver thread: continuously receive and validate responses - std::thread receiver_thread([&]() { - for (size_t iter = 0; iter < NUM_ITERATIONS; iter++) { - size_t expected_size = iteration_sizes[iter]; - - // Retry recv until success - timeouts are expected under extreme load - std::span response; - while ((response = client->receive(100000000)).empty()) { - std::cerr << iter << " Client receive timeout, retrying..." << '\n'; - // Timeout - retry - } - // std::cerr << "Client received response of " << response.size() << " bytes" << '\n'; - - ASSERT_EQ(response.size(), expected_size) << "Size mismatch at iteration " << iter; - - // Validate entire response - check iteration byte and pattern - uint8_t iter_byte = static_cast(iter & 0xFF); - if (response.size() > 0) { - ASSERT_EQ(response[0], iter_byte) << "Iteration byte mismatch at iteration " << iter; - for (size_t i = 0; i < response.size(); i++) { - uint8_t expected = static_cast((iter_byte ^ i) & 0xFF); - if (response[i] != expected) { - FAIL() << "Data corruption at iteration " << iter << " offset " << i - << ": expected=" << (int)expected << " actual=" << (int)response[i]; - } - } - } - - client->release(response.size()); - } - }); - - sender_thread.join(); - receiver_thread.join(); - - client->close(); - - server_running.store(false); - server->request_shutdown(); - server_thread.join(); - server->close(); - - EXPECT_EQ(corruptions.load(), 0) << "Corruptions detected in single-threaded wrap test"; -} - -/** - * Test to reproduce deadlock with specific message size sequence - * This test uses a single-threaded, deterministic approach to control - * the exact ordering of client and server operations. - */ -// TEST(ShmTest, DeadlockReproduction) -// { -// constexpr size_t RING_SIZE = 8UL * 1024; // 8KB rings -// // Max message size is half capacity minus 4 bytes (length prefix) -// constexpr size_t MAX_MSG_SIZE = RING_SIZE / 2 - 4; - -// std::string test_shm = "shm_deadlock_" + std::to_string(getpid()); -// auto server = IpcServer::create_shm(test_shm, RING_SIZE, RING_SIZE); -// ASSERT_TRUE(server->listen()) << "Deadlock test server failed to listen"; - -// auto client = IpcClient::create_shm(test_shm); -// ASSERT_TRUE(client->connect()); - -// #define snd(s) -// { -// ASSERT_TRUE(client->send(std::vector(s, 0).data(), s, 0)); -// dynamic_cast(client.get())->debug_dump(); -// } -// #define rcv() -// { -// auto request = server->receive(0); -// ASSERT_FALSE(request.empty()); -// server->release(0, request.size()); -// dynamic_cast(server.get())->debug_dump(); -// } - -// snd(MAX_MSG_SIZE - 1); -// snd(MAX_MSG_SIZE); -// rcv(); -// rcv(); -// snd(MAX_MSG_SIZE); - -// client->close(); -// server->close(); -// } // namespace - -/** - * Sanity check for the MPSC (multi-producer single-consumer) SHM transport: two clients - * concurrently send distinct payloads and each receives back its own echoed response. - * This is the load-bearing property MPSC adds over SPSC — multiple producers must not - * mix up responses or block each other. - */ -TEST(ShmTest, MpscEchoTwoClients) -{ - constexpr size_t NUM_CLIENTS = 2; - constexpr size_t NUM_MESSAGES = 200; - constexpr size_t MSG_SIZE = 64; - constexpr size_t RING_SIZE = 4UL * 1024; - - std::string base_name = "shm_mpsc_" + std::to_string(getpid()); - auto server = IpcServer::create_mpsc_shm(base_name, NUM_CLIENTS, RING_SIZE, RING_SIZE); - ASSERT_TRUE(server->listen()) << "MPSC server failed to listen"; - - std::atomic server_running{ true }; - - // Echo server: poll for any client with data, echo it back to that client. - std::thread server_thread([&]() { - while (server_running.load(std::memory_order_acquire)) { - server->accept(); - int client_id = server->wait_for_data(1000000); // 1ms - if (client_id < 0) { - continue; - } - auto request_buf = server->receive(client_id); - if (request_buf.empty()) { - continue; - } - std::vector request(request_buf.begin(), request_buf.end()); - server->release(client_id, request.size()); - while (!server->send(client_id, request.data(), request.size())) { - // Retry if the client's response ring is full. - } - } - }); - - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - auto run_client = [&](size_t client_id) { - auto client = IpcClient::create_mpsc_shm(base_name, client_id); - ASSERT_TRUE(client->connect()) << "Client " << client_id << " failed to connect"; - - for (size_t iter = 0; iter < NUM_MESSAGES; iter++) { - std::vector payload(MSG_SIZE); - // First byte tags the client; remaining bytes encode (client_id, iter, offset). - payload[0] = static_cast(client_id); - for (size_t i = 1; i < MSG_SIZE; i++) { - payload[i] = static_cast((client_id ^ iter ^ i) & 0xFF); - } - - while (!client->send(payload.data(), payload.size(), 100000000)) { - // Retry on send timeout. - } - - std::span response; - while ((response = client->receive(100000000)).empty()) { - // Retry on receive timeout. - } - - ASSERT_EQ(response.size(), MSG_SIZE) << "client " << client_id << " iter " << iter; - // The crucial MPSC invariant: client sees its own payload back, not another client's. - ASSERT_EQ(response[0], static_cast(client_id)) - << "client " << client_id << " got cross-client response at iter " << iter; - for (size_t i = 1; i < MSG_SIZE; i++) { - uint8_t expected = static_cast((client_id ^ iter ^ i) & 0xFF); - ASSERT_EQ(response[i], expected) << "client " << client_id << " iter " << iter << " offset " << i; - } - client->release(response.size()); - } - client->close(); - }; - - std::vector client_threads; - client_threads.reserve(NUM_CLIENTS); - for (size_t id = 0; id < NUM_CLIENTS; id++) { - client_threads.emplace_back(run_client, id); - } - for (auto& t : client_threads) { - t.join(); - } - - server_running.store(false); - server->request_shutdown(); - server_thread.join(); - server->close(); -} - -} // namespace diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/README.md b/barretenberg/cpp/src/barretenberg/ipc/shm/README.md deleted file mode 100644 index e3d2f403116..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/README.md +++ /dev/null @@ -1,439 +0,0 @@ -# Lock-Free Shared Memory Ring Buffers (C++) - -Ultra-low-latency shared-memory ring buffers for inter-process communication using modern C++. Built on Linux `shm_open` + `mmap` with lock-free atomics and efficient futex-based blocking. - -## Features - -- **Zero-copy IPC** between processes via MAP_SHARED -- **Lock-free**: No mutexes, no syscalls in hot path -- **Adaptive blocking**: Brief spin, then futex sleep for power efficiency -- **Single-Producer Single-Consumer (SPSC)**: Lock-free ring buffer building block -- **Multi-Producer Single-Consumer (MPSC)**: Compositional layer using SPSC + doorbell -- **Modern C++**: RAII, move semantics, factory methods -- **Cache-optimized**: Careful alignment to avoid false sharing - -## Performance - -| Operation | Latency | Notes | -|------------------------------------|------------------|--------------------------------| -| SPSC roundtrip (hot) | 0.3–1 µs | No contention, busy loop | -| SPSC roundtrip (cold) | 3–6 µs | After futex wakeup | -| MPSC roundtrip (3 producers, hot) | ~40 µs | 3-way contention | -| Pipe/socket (for comparison) | 6–15 µs | Requires syscalls | - -*Measured on AMD Ryzen 9 5950X, Ubuntu 24.04, small messages (<1KB)* - -## Architecture - -### SPSC (Single-Producer Single-Consumer) - -``` -┌──────────────────────────────────────────────────┐ -│ SpscCtrl (control block) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ head (producer-owned, cacheline-aligned) │ │ -│ │ tail (consumer-owned, cacheline-aligned) │ │ -│ │ data_seq, space_seq (futex sequencers) │ │ -│ │ capacity, mask (immutable) │ │ -│ └────────────────────────────────────────────┘ │ -│ │ -│ Data buffer (power-of-2 size) │ -│ ┌────────────────────────────────────────────┐ │ -│ │ [producer writes here] [consumer reads] │ │ -│ └────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────┘ -``` - -**Key characteristics:** -- **Lock-free**: Producer and consumer never block each other -- **Cache-friendly**: head/tail separated by cache line to avoid false sharing -- **Variable-length messages**: Automatic padding when wrapping around ring -- **Efficient blocking**: Spin briefly, then futex sleep/wake - -### MPSC (Multi-Producer Single-Consumer) - -``` -┌─────────────────────────────────────────────────┐ -│ MPSC System (N producers) │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Producer │ │ Producer │ │ Producer │ │ -│ │ 0 │ │ 1 │ │ 2 │ │ -│ └─────┬────┘ └─────┬────┘ └─────┬────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ SPSC │ │ SPSC │ │ SPSC │ │ -│ │ Ring 0 │ │ Ring 1 │ │ Ring 2 │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ -│ └─────────────┼─────────────┘ │ -│ │ │ -│ ┌─────▼──────┐ │ -│ │ Doorbell │◄─────────────────┤ -│ │ Futex │ (wake on data) │ -│ └─────┬──────┘ │ -│ │ │ -│ ┌─────▼──────┐ │ -│ │ Consumer │ │ -│ │ (polls all │ │ -│ │ rings) │ │ -│ └────────────┘ │ -└─────────────────────────────────────────────────┘ -``` - -**Key characteristics:** -- Each producer gets dedicated SPSC ring (no contention between producers) -- Consumer polls all rings in round-robin fashion -- Shared doorbell futex: producers ring on empty→non-empty transition -- Per-producer backpressure (full ring blocks only that producer) - -## API Overview - -### SpscShm Class - -```cpp -namespace bb::ipc { - -class SpscShm { -public: - // Factory methods - static SpscShm create(const std::string& name, size_t min_capacity); - static SpscShm connect(const std::string& name); - static bool unlink(const std::string& name); - - // Move-only (RAII) - SpscShm(SpscShm&& other) noexcept; - SpscShm& operator=(SpscShm&& other) noexcept; - ~SpscShm(); - - // Introspection - uint64_t available() const; // bytes ready to read - uint64_t free_space() const; // bytes free to write - - // Producer API - void* claim(size_t want, size_t* granted); // Claim write space - void publish(size_t n); // Commit n bytes - - // Consumer API - void* peek(size_t* n); // Peek read space (auto-skips padding) - void release(size_t n); // Release n bytes - - // Blocking wait (spin, then futex) - bool wait_for_data(uint32_t spin_ns); - bool wait_for_space(size_t need, uint32_t spin_ns); -}; - -} // namespace bb::ipc -``` - -### MpscConsumer / MpscProducer Classes - -```cpp -namespace bb::ipc { - -class MpscConsumer { -public: - // Factory - static MpscConsumer create(const std::string& name, - size_t num_producers, - size_t ring_capacity); - static bool unlink(const std::string& name, size_t num_producers); - - // Move-only (RAII) - MpscConsumer(MpscConsumer&& other) noexcept; - ~MpscConsumer(); - - // Consumer API - int wait_for_data(uint32_t spin_ns); // Returns ring index with data - void* peek(size_t ring_idx, size_t* n); // Peek specific ring - void release(size_t ring_idx, size_t n); // Release from specific ring -}; - -class MpscProducer { -public: - // Factory - static MpscProducer connect(const std::string& name, size_t producer_id); - - // Move-only (RAII) - MpscProducer(MpscProducer&& other) noexcept; - ~MpscProducer(); - - // Producer API - void* claim(size_t want, size_t* granted); - void publish(size_t n); // Rings doorbell if needed - bool wait_for_space(size_t need, uint32_t spin_ns); -}; - -} // namespace bb::ipc -``` - -## Usage Examples - -### SPSC: Simple Message Passing - -**Producer process:** -```cpp -#include "barretenberg/ipc/shm/spsc_shm.hpp" -#include - -using namespace bb::ipc; - -int main() { - // Create ring buffer (1 MB capacity) - auto tx = SpscShm::create("/demo_ring", 1 << 20); - - std::string msg = "hello from producer"; - - while (true) { - // Wait for space (spin 20 µs, then futex) - if (!tx.wait_for_space(msg.size(), 20000)) { - continue; - } - - // Claim write space - size_t granted; - void* buf = tx.claim(msg.size(), &granted); - - // Write message - std::memcpy(buf, msg.data(), msg.size()); - tx.publish(msg.size()); - - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } -} -``` - -**Consumer process:** -```cpp -#include "barretenberg/ipc/shm/spsc_shm.hpp" -#include - -using namespace bb::ipc; - -int main() { - // Connect to existing ring - auto rx = SpscShm::connect("/demo_ring"); - - while (true) { - // Wait for data (spin 20 µs, then futex) - if (!rx.wait_for_data(20000)) { - continue; - } - - // Peek data - size_t n; - void* data = rx.peek(&n); - - if (n > 0) { - std::cout << "Received: " << std::string((char*)data, n) << "\n"; - rx.release(n); - } - } -} -``` - -**Cleanup:** -```cpp -// When done (from either process) -SpscShm::unlink("/demo_ring"); -``` - -### MPSC: Multiple Producers, Single Consumer - -**Consumer process:** -```cpp -#include "barretenberg/ipc/shm/mpsc_shm.hpp" -#include - -using namespace bb::ipc; - -int main() { - // Create MPSC with 3 producers, 1 MB rings - auto consumer = MpscConsumer::create("my_mpsc", 3, 1 << 20); - - while (true) { - // Wait for data from any producer - int ring_idx = consumer.wait_for_data(20000); // spin 20 µs, then futex - if (ring_idx < 0) continue; - - // Process data from that producer - size_t n; - void* data = consumer.peek(ring_idx, &n); - - if (n > 0) { - std::cout << "Received " << n << " bytes from producer " - << ring_idx << "\n"; - // Process data... - consumer.release(ring_idx, n); - } - } -} -``` - -**Producer processes (3 separate processes):** -```cpp -#include "barretenberg/ipc/shm/mpsc_shm.hpp" -#include - -using namespace bb::ipc; - -int main(int argc, char** argv) { - int producer_id = std::stoi(argv[1]); // 0, 1, or 2 - - // Connect as producer - auto producer = MpscProducer::connect("my_mpsc", producer_id); - - std::string msg = "hello from producer " + std::to_string(producer_id); - - while (true) { - // Wait for space in our ring - if (!producer.wait_for_space(msg.size(), 20000)) { - continue; - } - - // Claim space and write - size_t granted; - void* buf = producer.claim(msg.size(), &granted); - - if (granted >= msg.size()) { - std::memcpy(buf, msg.data(), msg.size()); - producer.publish(msg.size()); // Rings doorbell - } - - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } -} -``` - -**Cleanup:** -```cpp -MpscConsumer::unlink("my_mpsc", 3); // Removes doorbell + 3 rings -``` - -## Implementation Details - -### Memory Layout - -The shared memory region contains: -1. **SpscCtrl** (control block, 256 bytes) - - Atomic head/tail counters (cache-line aligned) - - Futex sequencers for sleep/wake - - Capacity and mask (immutable) -2. **Data buffer** (power-of-2 size, follows control block) - -Total size: `sizeof(SpscCtrl) + capacity` - -### Padding and Wrapping - -When a message would wrap around the ring boundary, automatic padding is inserted: - -``` -┌────────────────────────────────────────────────┐ -│ [msg1] [msg2] [...............] [padding] │ -│ ^ │ -│ └─ wrap point │ -└────────────────────────────────────────────────┘ - ^ - └─ next message starts at beginning -``` - -The consumer's `peek()` automatically skips padding, so callers never see it. - -### Futex-Based Blocking - -Instead of busy-waiting forever: -1. **Producer**: Spins briefly checking for space, then sleeps on `space_seq` futex -2. **Consumer**: Spins briefly checking for data, then sleeps on `data_seq` futex -3. **Wakeup**: Incrementing sequencer + `futex_wake` wakes sleeping side - -This provides: -- Low latency when active (spin catches transitions) -- Low power when idle (futex sleep) -- No thundering herd (one waker, one sleeper) - -### MPSC Doorbell - -The doorbell is a simple futex counter in shared memory: - -```cpp -struct alignas(64) MpscDoorbell { - std::atomic seq; - uint8_t _pad[60]; // Cache line padding -}; -``` - -**Protocol:** -1. Producer publishes data to its SPSC ring -2. If ring was empty (first message), increment doorbell seq and call `futex_wake` -3. Consumer wakes up, polls all rings in round-robin -4. Consumer sleeps on doorbell only when all rings are empty - -This ensures the consumer wakes promptly when any producer has data, while minimizing futex overhead when rings stay populated. - -## Performance Tuning - -### Spin Time - -The `spin_ns` parameter controls busy-wait duration before sleeping: - -- **Low latency**: Use longer spin (e.g., 100 µs) to avoid futex overhead -- **Power efficiency**: Use shorter spin (e.g., 1 µs) to sleep sooner -- **Recommended**: 10-20 µs balances latency and power - -### Ring Size - -- Must be **power of two** -- Larger rings reduce wrapping overhead but use more memory -- Recommended: 1 MB (1 << 20) for most use cases -- Small messages (<1 KB): Can use smaller rings (256 KB) -- Large messages (>100 KB): Use larger rings (4-16 MB) - -### Number of Producers (MPSC) - -- More producers → more ring poll overhead for consumer -- Recommended: ≤8 producers for best performance -- Beyond that, consider multiple MPSC systems or alternative architecture - -## Thread Safety - -### SPSC -- **One producer thread**, **one consumer thread** -- No internal synchronization needed (lock-free by design) -- Cannot share producer or consumer role across threads - -### MPSC -- **Multiple producer threads** (one per producer instance) -- **One consumer thread** -- Each producer is independent (no contention) -- Consumer must be single-threaded - -## Limitations - -1. **Platform**: Linux-only (uses futex, though portable to other POSIX with modifications) -2. **Capacity**: Must be power of two -3. **Fixed size**: Cannot resize after creation -4. **No security**: All processes with access can read/write shared memory -5. **Manual cleanup**: Must call `unlink()` to remove `/dev/shm` objects - -## Comparison with Other IPC Mechanisms - -| Mechanism | Latency | Throughput | Complexity | Use Case | -|--------------------|------------|------------|------------|-------------------------| -| Pipe | 6-15 µs | 150K/s | Low | Simple IPC | -| Unix Socket | 6-15 µs | 150K/s | Low | Network-like API | -| SPSC Ring | 0.3-1 µs | 1M/s | Medium | Ultra-low latency | -| MPSC Ring | ~3 µs | 700K/s | Medium | Multiple producers | -| POSIX MQ | 10-20 µs | 100K/s | Medium | Message queue semantics | - -## See Also - -- Parent IPC module: [`../README.md`](../README.md) -- Tests: [`../shm.test.cpp`](../shm.test.cpp) -- Benchmarks: `barretenberg/cpp/build-no-avm/bin/ipc_bench` -- Higher-level wrappers: `ShmClient` / `ShmServer` in [`../shm_client.hpp`](../shm_client.hpp) - -## License - -Part of Barretenberg cryptographic library. -See repository root for license details. diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/futex.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm/futex.hpp deleted file mode 100644 index e8c307134b4..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/futex.hpp +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file futex.hpp - * @brief Cross-platform futex-like synchronization primitives - * - * Provides unified wait/wake operations for cross-process synchronization: - * - macOS: Uses os_sync_wait_on_address / os_sync_wake_by_address_any - * - Linux: Uses futex syscalls - */ -#pragma once - -#include -#include - -#ifdef __APPLE__ -// Darwin's os_sync API (available since macOS 10.12 / iOS 10) -// Forward declarations to avoid header dependency -extern "C" { -int os_sync_wait_on_address(void* addr, uint64_t value, size_t size, uint32_t flags); -int os_sync_wait_on_address_with_timeout( - void* addr, uint64_t value, size_t size, uint32_t flags, uint32_t clockid, uint64_t timeout_ns); -int os_sync_wake_by_address_any(void* addr, size_t size, uint32_t flags); -} -#define OS_SYNC_WAIT_ON_ADDRESS_SHARED 1u -#define OS_SYNC_WAKE_BY_ADDRESS_SHARED 1u -#define OS_CLOCK_MACH_ABSOLUTE_TIME 32u -#else -// Linux futex -#include -#include -#include -#include -#endif - -namespace bb::ipc { - -/** - * @brief Atomic compare-and-wait operation - * - * Blocks if the value at addr equals expect. Works across process boundaries. - * - * @param addr Pointer to 32-bit value to wait on - * @param expect Expected value - blocks if *addr == expect - * @return 0 on wake, -1 on error - */ -inline int futex_wait(volatile uint32_t* addr, uint32_t expect) -{ -#ifdef __APPLE__ - // macOS: Use os_sync_wait_on_address with SHARED flag for cross-process - return os_sync_wait_on_address( - const_cast(addr), static_cast(expect), sizeof(uint32_t), OS_SYNC_WAIT_ON_ADDRESS_SHARED); -#else - // Linux futex - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - return static_cast(syscall(SYS_futex, addr, FUTEX_WAIT, expect, nullptr, nullptr, 0)); -#endif -} - -/** - * @brief Atomic compare-and-wait operation with timeout - * - * Blocks if the value at addr equals expect, but only for up to timeout_ns nanoseconds. - * Works across process boundaries. - * - * @param addr Pointer to 32-bit value to wait on - * @param expect Expected value - blocks if *addr == expect - * @param timeout_ns Maximum time to wait in nanoseconds (0 = return immediately if value matches) - * @return 0 on wake, -1 on error (check errno for ETIMEDOUT on timeout) - */ -inline int futex_wait_timeout(volatile uint32_t* addr, uint32_t expect, uint64_t timeout_ns) -{ -#ifdef __APPLE__ - // macOS: Use os_sync_wait_on_address_with_timeout with SHARED flag for cross-process - // Uses MACH_ABSOLUTE_TIME clock (monotonic, measures time since boot) - return os_sync_wait_on_address_with_timeout(const_cast(addr), - static_cast(expect), - sizeof(uint32_t), - OS_SYNC_WAIT_ON_ADDRESS_SHARED, - OS_CLOCK_MACH_ABSOLUTE_TIME, - timeout_ns); -#else - // Linux futex with timeout - struct timespec timeout = { .tv_sec = static_cast(timeout_ns / 1000000000ULL), - .tv_nsec = static_cast(timeout_ns % 1000000000ULL) }; - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - return static_cast(syscall(SYS_futex, addr, FUTEX_WAIT, expect, &timeout, nullptr, 0)); -#endif -} - -/** - * @brief Wake waiters blocked on an address - * - * Wakes up to n waiters blocked on addr. Works across process boundaries. - * - * @param addr Pointer to 32-bit value to wake on - * @param n Number of waiters to wake (1 for single, INT_MAX for all) - * @return Number of waiters woken, or -1 on error - */ -inline int futex_wake(volatile uint32_t* addr, int n) -{ -#ifdef __APPLE__ - // macOS: Use os_sync_wake_by_address with SHARED flag for cross-process - (void)n; - return os_sync_wake_by_address_any(const_cast(addr), sizeof(uint32_t), OS_SYNC_WAKE_BY_ADDRESS_SHARED); -#else - // Linux futex - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - return static_cast(syscall(SYS_futex, addr, FUTEX_WAKE, n, nullptr, nullptr, 0)); -#endif -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.cpp b/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.cpp deleted file mode 100644 index 384f7ea0dc1..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.cpp +++ /dev/null @@ -1,384 +0,0 @@ -#include "mpsc_shm.hpp" -#include "futex.hpp" -#include "utilities.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -// ----- MpscConsumer Implementation ----- - -MpscConsumer::MpscConsumer(std::vector&& rings, int doorbell_fd, size_t doorbell_len, MpscDoorbell* doorbell) - : rings_(std::move(rings)) - , doorbell_fd_(doorbell_fd) - , doorbell_len_(doorbell_len) - , doorbell_(doorbell) -{} - -MpscConsumer::MpscConsumer(MpscConsumer&& other) noexcept - : rings_(std::move(other.rings_)) - , doorbell_fd_(other.doorbell_fd_) - , doorbell_len_(other.doorbell_len_) - , doorbell_(other.doorbell_) - , last_served_(other.last_served_) -{ - other.doorbell_fd_ = -1; - other.doorbell_len_ = 0; - other.doorbell_ = nullptr; - other.last_served_ = 0; -} - -MpscConsumer& MpscConsumer::operator=(MpscConsumer&& other) noexcept -{ - if (this != &other) { - // Clean up current resources - if (doorbell_ != nullptr) { - munmap(doorbell_, doorbell_len_); - } - if (doorbell_fd_ >= 0) { - ::close(doorbell_fd_); - } - - // Move from other - rings_ = std::move(other.rings_); - doorbell_fd_ = other.doorbell_fd_; - doorbell_len_ = other.doorbell_len_; - doorbell_ = other.doorbell_; - last_served_ = other.last_served_; - - // Clear other - other.doorbell_fd_ = -1; - other.doorbell_len_ = 0; - other.doorbell_ = nullptr; - other.last_served_ = 0; - } - return *this; -} - -MpscConsumer::~MpscConsumer() -{ - if (doorbell_ != nullptr) { - munmap(doorbell_, doorbell_len_); - } - if (doorbell_fd_ >= 0) { - ::close(doorbell_fd_); - } -} - -MpscConsumer MpscConsumer::create(const std::string& name, size_t num_producers, size_t ring_capacity) -{ - if (name.empty() || num_producers == 0) { - throw std::runtime_error("MpscConsumer::create: invalid arguments"); - } - - // Create doorbell shared memory - std::string doorbell_name = name + "_doorbell"; - size_t doorbell_len = sizeof(MpscDoorbell); - - int doorbell_fd = shm_open(doorbell_name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); - if (doorbell_fd < 0) { - throw std::runtime_error("MpscConsumer::create: shm_open doorbell failed: " + - std::string(std::strerror(errno))); - } - - if (ftruncate(doorbell_fd, static_cast(doorbell_len)) != 0) { - int e = errno; - ::close(doorbell_fd); - shm_unlink(doorbell_name.c_str()); - throw std::runtime_error("MpscConsumer::create: ftruncate doorbell failed: " + std::string(std::strerror(e))); - } - - auto* doorbell = - static_cast(mmap(nullptr, doorbell_len, PROT_READ | PROT_WRITE, MAP_SHARED, doorbell_fd, 0)); - if (doorbell == MAP_FAILED) { - int e = errno; - ::close(doorbell_fd); - shm_unlink(doorbell_name.c_str()); - throw std::runtime_error("MpscConsumer::create: mmap doorbell failed: " + std::string(std::strerror(e))); - } - - // Initialize doorbell (use placement new to avoid memset on non-trivial type) - new (doorbell) MpscDoorbell{}; - doorbell->consumer_blocked.store(false, std::memory_order_release); - - // Create all SPSC rings - std::vector rings; - rings.reserve(num_producers); - - try { - for (size_t i = 0; i < num_producers; i++) { - std::string ring_name = name + "_ring_" + std::to_string(i); - rings.push_back(SpscShm::create(ring_name, ring_capacity)); - } - } catch (...) { - // Cleanup on failure - for (size_t i = 0; i < rings.size(); i++) { - std::string ring_name = name + "_ring_" + std::to_string(i); - SpscShm::unlink(ring_name); - } - munmap(doorbell, doorbell_len); - ::close(doorbell_fd); - shm_unlink(doorbell_name.c_str()); - throw; - } - - return MpscConsumer(std::move(rings), doorbell_fd, doorbell_len, doorbell); -} - -bool MpscConsumer::unlink(const std::string& name, size_t num_producers) -{ - std::string doorbell_name = name + "_doorbell"; - shm_unlink(doorbell_name.c_str()); - - for (size_t i = 0; i < num_producers; i++) { - std::string ring_name = name + "_ring_" + std::to_string(i); - SpscShm::unlink(ring_name); - } - - return true; -} - -int MpscConsumer::wait_for_data(uint32_t timeout_ns) -{ - size_t num_rings = rings_.size(); - - // Phase 1: Quick poll - check if data already available - for (size_t i = 0; i < num_rings; i++) { - size_t idx = (last_served_ + 1 + i) % num_rings; - if (rings_[idx].available() > 0) { - last_served_ = idx; - previous_had_data_ = true; // Found data - enable spinning on next call - return static_cast(idx); - } - } - - // Adaptive spinning: only spin if previous call found data - constexpr uint64_t SPIN_NS = 100000; // 100us - uint64_t spin_duration; - uint64_t remaining_timeout; - - if (previous_had_data_) { - // Previous call found data - do full spin (optimistic) - spin_duration = (timeout_ns < SPIN_NS) ? timeout_ns : SPIN_NS; - remaining_timeout = (timeout_ns > SPIN_NS) ? (timeout_ns - SPIN_NS) : 0; - } else { - // Previous call timed out - skip spinning (idle channel) - spin_duration = 0; - remaining_timeout = timeout_ns; - } - - // Phase 2: Spin phase (only if previous call found data) - if (spin_duration > 0) { - uint64_t start = mono_ns_now(); - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - do { - for (size_t i = 0; i < num_rings; i++) { - size_t idx = (last_served_ + 1 + i) % num_rings; - if (rings_[idx].available() > 0) { - last_served_ = idx; - previous_had_data_ = true; // Found data during spin - return static_cast(idx); - } - } - IPC_PAUSE(); - } while ((mono_ns_now() - start) < spin_duration); - - // Check after spin - for (size_t i = 0; i < num_rings; i++) { - size_t idx = (last_served_ + 1 + i) % num_rings; - if (rings_[idx].available() > 0) { - last_served_ = idx; - previous_had_data_ = true; // Found data after spin - return static_cast(idx); - } - } - } - - // No more time or didn't spin - check if we can block - if (remaining_timeout == 0) { - previous_had_data_ = false; // Timeout - disable spinning on next call - return -1; - } - - // About to block - load seq, final check, then block - uint32_t seq = doorbell_->seq.load(std::memory_order_acquire); - - // Final check before blocking - for (size_t i = 0; i < num_rings; i++) { - size_t idx = (last_served_ + 1 + i) % num_rings; - if (rings_[idx].available() > 0) { - last_served_ = idx; - previous_had_data_ = true; // Found data before blocking - return static_cast(idx); - } - } - - // Set blocked flag RIGHT BEFORE futex_wait - doorbell_->consumer_blocked.store(true, std::memory_order_release); - futex_wait_timeout(reinterpret_cast(&doorbell_->seq), seq, remaining_timeout); - // Clear blocked flag RIGHT AFTER futex_wait returns - doorbell_->consumer_blocked.store(false, std::memory_order_relaxed); - - // After waking, poll again - for (size_t i = 0; i < num_rings; i++) { - size_t idx = (last_served_ + 1 + i) % num_rings; - if (rings_[idx].available() > 0) { - last_served_ = idx; - previous_had_data_ = true; // Found data after waking - return static_cast(idx); - } - } - - previous_had_data_ = false; // Timeout or spurious wakeup - disable spinning on next call - return -1; // No data available (timeout or spurious wakeup) -} - -void* MpscConsumer::peek(size_t ring_idx, size_t want, uint32_t timeout_ns) -{ - if (ring_idx >= rings_.size()) { - return nullptr; - } - return rings_[ring_idx].peek(want, timeout_ns); -} - -void MpscConsumer::release(size_t ring_idx, size_t n) -{ - if (ring_idx < rings_.size()) { - rings_[ring_idx].release(n); - } -} - -void MpscConsumer::wakeup_all() -{ - // Wake consumer blocked on doorbell - futex_wake(reinterpret_cast(&doorbell_->seq), INT_MAX); - - // Wake all producers blocked on their rings - for (auto& ring : rings_) { - ring.wakeup_all(); - } -} - -// ----- MpscProducer Implementation ----- - -MpscProducer::MpscProducer( - SpscShm&& ring, int doorbell_fd, size_t doorbell_len, MpscDoorbell* doorbell, size_t producer_id) - : ring_(std::move(ring)) - , doorbell_fd_(doorbell_fd) - , doorbell_len_(doorbell_len) - , doorbell_(doorbell) - , producer_id_(producer_id) -{} - -MpscProducer::MpscProducer(MpscProducer&& other) noexcept - : ring_(std::move(other.ring_)) - , doorbell_fd_(other.doorbell_fd_) - , doorbell_len_(other.doorbell_len_) - , doorbell_(other.doorbell_) - , producer_id_(other.producer_id_) -{ - other.doorbell_fd_ = -1; - other.doorbell_len_ = 0; - other.doorbell_ = nullptr; - other.producer_id_ = 0; -} - -MpscProducer& MpscProducer::operator=(MpscProducer&& other) noexcept -{ - if (this != &other) { - // Clean up current resources - if (doorbell_ != nullptr) { - munmap(doorbell_, doorbell_len_); - } - if (doorbell_fd_ >= 0) { - ::close(doorbell_fd_); - } - - // Move from other - ring_ = std::move(other.ring_); - doorbell_fd_ = other.doorbell_fd_; - doorbell_len_ = other.doorbell_len_; - doorbell_ = other.doorbell_; - producer_id_ = other.producer_id_; - - // Clear other - other.doorbell_fd_ = -1; - other.doorbell_len_ = 0; - other.doorbell_ = nullptr; - other.producer_id_ = 0; - } - return *this; -} - -MpscProducer::~MpscProducer() -{ - if (doorbell_ != nullptr) { - munmap(doorbell_, doorbell_len_); - } - if (doorbell_fd_ >= 0) { - ::close(doorbell_fd_); - } -} - -MpscProducer MpscProducer::connect(const std::string& name, size_t producer_id) -{ - if (name.empty()) { - throw std::runtime_error("MpscProducer::connect: empty name"); - } - - // Connect to doorbell - std::string doorbell_name = name + "_doorbell"; - size_t doorbell_len = sizeof(MpscDoorbell); - - int doorbell_fd = shm_open(doorbell_name.c_str(), O_RDWR, 0600); - if (doorbell_fd < 0) { - throw std::runtime_error("MpscProducer::connect: shm_open doorbell failed: " + - std::string(std::strerror(errno))); - } - - auto* doorbell = - static_cast(mmap(nullptr, doorbell_len, PROT_READ | PROT_WRITE, MAP_SHARED, doorbell_fd, 0)); - if (doorbell == MAP_FAILED) { - int e = errno; - ::close(doorbell_fd); - throw std::runtime_error("MpscProducer::connect: mmap doorbell failed: " + std::string(std::strerror(e))); - } - - // Connect to assigned ring - std::string ring_name = name + "_ring_" + std::to_string(producer_id); - SpscShm ring = SpscShm::connect(ring_name); - - return MpscProducer(std::move(ring), doorbell_fd, doorbell_len, doorbell, producer_id); -} - -void* MpscProducer::claim(size_t want, uint32_t timeout_ns) -{ - return ring_.claim(want, timeout_ns); -} - -void MpscProducer::publish(size_t n) -{ - // Publish to ring first - ring_.publish(n); - - // Ring doorbell to wake consumer - // Always increment seq (for futex synchronization) - doorbell_->seq.fetch_add(1, std::memory_order_release); - - // Conditional wake: Only wake if consumer is blocked on futex - if (doorbell_->consumer_blocked.load(std::memory_order_acquire)) { - futex_wake(reinterpret_cast(&doorbell_->seq), 1); - } -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.hpp deleted file mode 100644 index df1d6257ed8..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.hpp +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @file mpsc_shm.hpp - * @brief Multi-Producer Single-Consumer via SPSC rings + doorbell futex - * - * Coordinates multiple producers using individual SPSC rings and a shared doorbell. - */ - -#pragma once - -#include "spsc_shm.hpp" -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief Shared doorbell for waking consumer - * - * Producers ring this when publishing data to wake the sleeping consumer. - * Carefully aligned to avoid false sharing between producer and consumer. - */ -struct alignas(64) MpscDoorbell { - // Producer-written (written by producers in publish()) - alignas(64) std::atomic seq; - std::array _pad0; - - // Consumer-written (written by consumer in wait_for_data()) - alignas(64) std::atomic consumer_blocked; // Set RIGHT BEFORE futex_wait, cleared RIGHT AFTER - std::array _pad1; -}; - -/** - * @brief Multi-producer single-consumer - consumer side - * - * Manages multiple SPSC rings (one per producer) and waits on a shared doorbell. - */ -class MpscConsumer { - public: - /** - * @brief Create MPSC consumer - * @param name Base name for shared memory objects - * @param num_producers Number of producer rings to create - * @param ring_capacity Capacity for each SPSC ring - * @throws std::runtime_error if creation fails - */ - static MpscConsumer create(const std::string& name, size_t num_producers, size_t ring_capacity); - - /** - * @brief Unlink all shared memory for this MPSC system - * @param name Base name - * @param num_producers Number of producers - * @return true if all unlinks successful - */ - static bool unlink(const std::string& name, size_t num_producers); - - // Move-only - MpscConsumer(MpscConsumer&& other) noexcept; - MpscConsumer& operator=(MpscConsumer&& other) noexcept; - MpscConsumer(const MpscConsumer&) = delete; - MpscConsumer& operator=(const MpscConsumer&) = delete; - - ~MpscConsumer(); - - /** - * @brief Wait for data on any ring - * @param timeout_ns Total timeout in nanoseconds (spins 10ms, then futex waits for remainder) - * @return Ring index with data, or -1 on timeout - */ - int wait_for_data(uint32_t timeout_ns); - - /** - * @brief Peek data from specific ring - * @param ring_idx Ring index - * @param want Minimum bytes required - * @param timeout_ns Timeout in nanoseconds - * @return Pointer to data, or nullptr on timeout - */ - void* peek(size_t ring_idx, size_t want, uint32_t timeout_ns); - - /** - * @brief Release data from specific ring - * @param ring_idx Ring index - * @param n Bytes to release - */ - void release(size_t ring_idx, size_t n); - - /** - * @brief Wake all blocked threads (for graceful shutdown) - * Wakes consumer blocked on doorbell and all producers blocked on their rings - */ - void wakeup_all(); - - private: - MpscConsumer(std::vector&& rings, int doorbell_fd, size_t doorbell_len, MpscDoorbell* doorbell); - - std::vector rings_; - int doorbell_fd_ = -1; - size_t doorbell_len_ = 0; - MpscDoorbell* doorbell_ = nullptr; - size_t last_served_ = 0; // Round-robin fairness - bool previous_had_data_ = false; // Adaptive spinning: only spin if previous call found data -}; - -/** - * @brief Multi-producer single-consumer - producer side - * - * Connects to one SPSC ring and rings the shared doorbell when publishing. - */ -class MpscProducer { - public: - /** - * @brief Connect to MPSC system as a producer - * @param name Base name for shared memory objects - * @param producer_id Producer ID (determines which ring to use) - * @throws std::runtime_error if connection fails - */ - static MpscProducer connect(const std::string& name, size_t producer_id); - - // Move-only - MpscProducer(MpscProducer&& other) noexcept; - MpscProducer& operator=(MpscProducer&& other) noexcept; - MpscProducer(const MpscProducer&) = delete; - MpscProducer& operator=(const MpscProducer&) = delete; - - ~MpscProducer(); - - /** - * @brief Claim space in producer's ring - * @param want Bytes wanted - * @param timeout_ns Timeout in nanoseconds - * @return Pointer to buffer, or nullptr on timeout - */ - void* claim(size_t want, uint32_t timeout_ns); - - /** - * @brief Publish data to producer's ring (rings doorbell) - * @param n Bytes to publish - */ - void publish(size_t n); - - private: - MpscProducer(SpscShm&& ring, int doorbell_fd, size_t doorbell_len, MpscDoorbell* doorbell, size_t producer_id); - - SpscShm ring_; - int doorbell_fd_ = -1; - size_t doorbell_len_ = 0; - MpscDoorbell* doorbell_ = nullptr; - size_t producer_id_ = 0; -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.cpp b/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.cpp deleted file mode 100644 index 0587ab1f3ee..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.cpp +++ /dev/null @@ -1,545 +0,0 @@ -#include "spsc_shm.hpp" -#include "futex.hpp" -#include "utilities.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -namespace { - -inline uint64_t pow2_ceil_u64(uint64_t x) -{ - if (x < 2) { - return 2; - } - x--; - x |= x >> 1; - x |= x >> 2; - x |= x >> 4; - x |= x >> 8; - x |= x >> 16; - x |= x >> 32; - return x + 1; -} - -} // anonymous namespace - -// ----- SpscShm Implementation ----- - -SpscShm::SpscShm(int fd, size_t map_len, SpscCtrl* ctrl, uint8_t* buf) - : fd_(fd) - , map_len_(map_len) - , ctrl_(ctrl) - , buf_(buf) -{} - -SpscShm::SpscShm(SpscShm&& other) noexcept - : fd_(other.fd_) - , map_len_(other.map_len_) - , ctrl_(other.ctrl_) - , buf_(other.buf_) -{ - other.fd_ = -1; - other.map_len_ = 0; - other.ctrl_ = nullptr; - other.buf_ = nullptr; -} - -SpscShm& SpscShm::operator=(SpscShm&& other) noexcept -{ - if (this != &other) { - // Clean up current resources - if (ctrl_ != nullptr) { - munmap(ctrl_, map_len_); - } - if (fd_ >= 0) { - ::close(fd_); - } - - // Move from other - fd_ = other.fd_; - map_len_ = other.map_len_; - ctrl_ = other.ctrl_; - buf_ = other.buf_; - - // Clear other - other.fd_ = -1; - other.map_len_ = 0; - other.ctrl_ = nullptr; - other.buf_ = nullptr; - } - return *this; -} - -SpscShm::~SpscShm() -{ - if (ctrl_ != nullptr) { - munmap(ctrl_, map_len_); - } - if (fd_ >= 0) { - ::close(fd_); - } -} - -SpscShm SpscShm::create(const std::string& name, size_t min_capacity) -{ - if (name.empty()) { - throw std::runtime_error("SpscShm::create: empty name"); - } - - size_t cap = pow2_ceil_u64(min_capacity); - size_t map_len = sizeof(SpscCtrl) + cap; - - int fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); - if (fd < 0) { - std::string error_msg = "SpscShm::create: shm_open failed for '" + name + "': " + std::strerror(errno); - if (errno == ENOSPC || errno == ENOMEM) { - error_msg += " (likely /dev/shm is full - check df -h /dev/shm)"; - } - throw std::runtime_error(error_msg); - } - - if (ftruncate(fd, static_cast(map_len)) != 0) { - int e = errno; - std::string error_msg = "SpscShm::create: ftruncate failed for '" + name + - "' (size=" + std::to_string(map_len) + "): " + std::strerror(e); - if (e == ENOSPC || e == ENOMEM) { - error_msg += " (likely /dev/shm is full - check df -h /dev/shm)"; - } - ::close(fd); - shm_unlink(name.c_str()); - throw std::runtime_error(error_msg); - } - - void* mem = mmap(nullptr, map_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) { - int e = errno; - std::string error_msg = "SpscShm::create: mmap failed for '" + name + "' (size=" + std::to_string(map_len) + - "): " + std::strerror(e); - if (e == ENOSPC || e == ENOMEM) { - error_msg += " (likely /dev/shm is full - check df -h /dev/shm)"; - } - ::close(fd); - shm_unlink(name.c_str()); - throw std::runtime_error(error_msg); - } - - std::memset(mem, 0, map_len); - auto* ctrl = static_cast(mem); - - // Initialize non-atomic fields first - ctrl->capacity = cap; - ctrl->mask = cap - 1; - ctrl->wrap_head = UINT64_MAX; - - // Initialize atomics with release ordering to ensure capacity/mask/wrap_head are visible - ctrl->head.store(0ULL, std::memory_order_release); - ctrl->tail.store(0ULL, std::memory_order_release); - ctrl->consumer_blocked.store(false, std::memory_order_release); - ctrl->producer_blocked.store(false, std::memory_order_release); - - auto* buf = reinterpret_cast(ctrl + 1); - return SpscShm(fd, map_len, ctrl, buf); -} - -SpscShm SpscShm::connect(const std::string& name) -{ - if (name.empty()) { - throw std::runtime_error("SpscShm::connect: empty name"); - } - - int fd = shm_open(name.c_str(), O_RDWR, 0600); - if (fd < 0) { - throw std::runtime_error("SpscShm::connect: shm_open failed: " + std::string(std::strerror(errno))); - } - - struct stat st; - if (fstat(fd, &st) != 0) { - int e = errno; - ::close(fd); - throw std::runtime_error("SpscShm::connect: fstat failed: " + std::string(std::strerror(e))); - } - size_t map_len = static_cast(st.st_size); - - void* mem = mmap(nullptr, map_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) { - int e = errno; - ::close(fd); - throw std::runtime_error("SpscShm::connect: mmap failed: " + std::string(std::strerror(e))); - } - - auto* ctrl = static_cast(mem); - auto* buf = reinterpret_cast(ctrl + 1); - - // Ensure initialization is visible before use (pairs with release in create) - (void)ctrl->head.load(std::memory_order_acquire); - - return SpscShm(fd, map_len, ctrl, buf); -} - -bool SpscShm::unlink(const std::string& name) -{ - return shm_unlink(name.c_str()) == 0; -} - -uint64_t SpscShm::available() const -{ - uint64_t head = ctrl_->head.load(std::memory_order_acquire); - uint64_t tail = ctrl_->tail.load(std::memory_order_acquire); - return head - tail; -} - -void* SpscShm::claim(size_t want, uint32_t timeout_ns) -{ - // Wait for contiguous space to be available - if (!wait_for_space(want, timeout_ns)) { - return nullptr; // Timeout - } - - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - uint64_t head = ctrl_->head.load(std::memory_order_relaxed); - uint64_t pos = head & mask; - uint64_t till_end = cap - pos; - - // Check if it fits contiguously without wrapping - if (want <= till_end) { - // Fits contiguously - no wrap - return buf_ + pos; - } - - // Needs to wrap - return buf_; // Return pointer to beginning of ring -} - -void SpscShm::publish(size_t n) -{ - uint64_t head = ctrl_->head.load(std::memory_order_relaxed); - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - uint64_t pos = head & mask; - uint64_t till_end = cap - pos; - - // Detect if we published wrapped data - // If at current head position we can't fit n bytes, it must have wrapped - uint64_t total_advance = n; - if (n > till_end) { - // We wrote at the beginning after wrapping - skip padding and our data - total_advance += till_end; - ctrl_->wrap_head = head; - } - - // Advance head atomically with release - synchronizes wrap_head write - ctrl_->head.store(head + total_advance, std::memory_order_release); - - if (ctrl_->consumer_blocked.load(std::memory_order_acquire)) { - // Ensure that head update is visible before waking consumer. - std::atomic_thread_fence(std::memory_order_release); - futex_wake(reinterpret_cast(&ctrl_->head), 1); - } -} - -void* SpscShm::peek(size_t want, uint32_t timeout_ns) -{ - // Wait for contiguous data to be available - if (!wait_for_data(want, timeout_ns)) { - return nullptr; // Timeout - } - - // Read head with acquire to synchronize wrap_head - ctrl_->head.load(std::memory_order_acquire); - - uint64_t tail = ctrl_->tail.load(std::memory_order_relaxed); - - // Check if we're at the position where a message wrapped - // If tail == wrap_head, the message starts at position 0 - if (tail == ctrl_->wrap_head) { - return buf_; - } - - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - uint64_t pos = tail & mask; - [[maybe_unused]] uint64_t till_end = cap - pos; - - // At this point wait_for_data() has guaranteed contiguity from tail - // (or we would have wrapped via wrap_head), so want must fit here. - assert(want <= till_end); - - // Data fits contiguously at current position - return buf_ + pos; -} - -void SpscShm::release(size_t n) -{ - uint64_t tail = ctrl_->tail.load(std::memory_order_relaxed); - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - uint64_t pos = tail & mask; - uint64_t till_end = cap - pos; - - uint64_t total_release = 0; - if (tail == ctrl_->wrap_head) { - // We're releasing data from a wrapped message - skip padding - total_release = till_end + n; - } else { - assert(n <= till_end); - // Normal case: data was contiguous - total_release = n; - } - - uint64_t new_tail = tail + total_release; - ctrl_->tail.store(new_tail, std::memory_order_release); - - if (ctrl_->producer_blocked.load(std::memory_order_acquire)) { - // Ensure that tail update is visible before waking producer. - std::atomic_thread_fence(std::memory_order_release); - futex_wake(reinterpret_cast(&ctrl_->tail), 1); - } -} - -bool SpscShm::wait_for_data(size_t need, uint32_t timeout_ns) -{ - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - - // Check if we need contiguous data that would wrap - auto check_available = [this, cap, mask, need]() -> bool { - uint64_t head = ctrl_->head.load(std::memory_order_acquire); - uint64_t tail = ctrl_->tail.load(std::memory_order_relaxed); - uint64_t avail = head - tail; - - if (avail < need) { - return false; // Not enough total data - } - - // Check if data is contiguous - uint64_t pos = tail & mask; - uint64_t till_end = cap - pos; - - if (need <= till_end) { - return true; // Fits contiguously - } - - // Would wrap - need padding + actual data available - return avail >= (till_end + need); - }; - - if (check_available()) { - previous_had_data_ = true; // Found data - enable spinning on next call - return true; - } - - // Adaptive spinning: only spin if previous call found data - constexpr uint64_t SPIN_NS = 100000; // 100us - uint64_t spin_duration; - uint64_t remaining_timeout; - - if (previous_had_data_) { - // Previous call found data - do full spin (optimistic) - spin_duration = (timeout_ns < SPIN_NS) ? timeout_ns : SPIN_NS; - remaining_timeout = (timeout_ns > SPIN_NS) ? (timeout_ns - SPIN_NS) : 0; - } else { - // Previous call timed out - skip spinning (idle channel) - spin_duration = 0; - remaining_timeout = timeout_ns; - } - - // Spin phase (only if previous call found data) - if (spin_duration > 0) { - uint64_t start = mono_ns_now(); - constexpr uint32_t TIME_CHECK_INTERVAL = 256; // Check time every 256 iterations - uint32_t iterations = 0; - - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - do { - if (check_available()) { - previous_had_data_ = true; // Found data during spin - return true; - } - IPC_PAUSE(); - - // Only check time periodically to avoid syscall overhead - iterations++; - if (iterations >= TIME_CHECK_INTERVAL) { - if ((mono_ns_now() - start) >= spin_duration) { - break; - } - iterations = 0; - } - } while (true); - - // Check after spin - if (check_available()) { - previous_had_data_ = true; // Found data after spin - return true; - } - } - - // No more time or didn't spin - check if we can block - if (remaining_timeout == 0) { - previous_had_data_ = false; // Timeout - disable spinning on next call - return false; - } - - // About to block - load seq, final check, then block - uint32_t head_now = static_cast(ctrl_->head.load(std::memory_order_acquire)); - - ctrl_->consumer_blocked.store(true, std::memory_order_release); - - if (check_available()) { - ctrl_->consumer_blocked.store(false, std::memory_order_relaxed); - previous_had_data_ = true; // Found data before blocking - return true; - } - - // Wait on futex for producer to signal new data - futex_wait_timeout(reinterpret_cast(&ctrl_->head), head_now, remaining_timeout); - ctrl_->consumer_blocked.store(false, std::memory_order_relaxed); - - bool result = check_available(); - previous_had_data_ = result; // Update flag based on final result - return result; -} - -bool SpscShm::wait_for_space(size_t need, uint32_t timeout_ns) -{ - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - - // Check if we need contiguous space that would wrap - auto check_space = [this, cap, mask, need]() -> bool { - uint64_t head = ctrl_->head.load(std::memory_order_relaxed); - uint64_t tail = ctrl_->tail.load(std::memory_order_acquire); - uint64_t freeb = cap - (head - tail); - - // std::cerr << "Checking space: head=" << head << " tail=" << tail << " free=" << freeb << " need=" << need - // << "\n"; - if (freeb < need) { - return false; // Not enough total free space - } - - // Check if space is contiguous - uint64_t pos = head & mask; - uint64_t till_end = cap - pos; - - if (need <= till_end) { - return true; // Fits contiguously - } - - // Would wrap - just check if we have enough total space - // If we have till_end + need bytes free, the ring buffer invariant - // guarantees the beginning is available for writing - return freeb >= (till_end + need); - }; - - if (check_space()) { - previous_had_space_ = true; // Found space - enable spinning on next call - return true; - } - - // Adaptive spinning: only spin if previous call found space - constexpr uint64_t SPIN_NS = 100000; // 100us - uint64_t spin_duration = 0; - uint64_t remaining_timeout = timeout_ns; - - if (previous_had_space_) { - // Previous call found space - do full spin (optimistic) - spin_duration = (timeout_ns < SPIN_NS) ? timeout_ns : SPIN_NS; - remaining_timeout = (timeout_ns > SPIN_NS) ? (timeout_ns - SPIN_NS) : 0; - } - - // Spin phase (only if previous call found space) - if (spin_duration > 0) { - uint64_t start = mono_ns_now(); - constexpr uint32_t TIME_CHECK_INTERVAL = 256; // Check time every 256 iterations - uint32_t iterations = 0; - - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - do { - if (check_space()) { - previous_had_space_ = true; // Found space during spin - return true; - } - IPC_PAUSE(); - - // Only check time periodically to avoid syscall overhead - iterations++; - if (iterations >= TIME_CHECK_INTERVAL) { - if ((mono_ns_now() - start) >= spin_duration) { - break; - } - iterations = 0; - } - } while (true); - - // Check after spin - if (check_space()) { - previous_had_space_ = true; // Found space after spin - return true; - } - } - - // No more time or didn't spin - check if we can block - if (remaining_timeout == 0) { - previous_had_space_ = false; // Timeout - disable spinning on next call - return false; - } - - // About to block - load seq, final check, then block - uint32_t tail_now = static_cast(ctrl_->tail.load(std::memory_order_acquire)); - - // Wait on futex for consumer to signal freed space - ctrl_->producer_blocked.store(true, std::memory_order_release); - - if (check_space()) { - ctrl_->producer_blocked.store(false, std::memory_order_relaxed); - previous_had_space_ = true; // Found space before blocking - return true; - } - - futex_wait_timeout(reinterpret_cast(&ctrl_->tail), tail_now, remaining_timeout); - ctrl_->producer_blocked.store(false, std::memory_order_relaxed); - - bool result = check_space(); - previous_had_space_ = result; // Update flag based on final result - return result; -} - -void SpscShm::wakeup_all() -{ - futex_wake(reinterpret_cast(&ctrl_->head), INT_MAX); - futex_wake(reinterpret_cast(&ctrl_->tail), INT_MAX); -} - -void SpscShm::debug_dump(const char* prefix) const -{ - uint64_t head = ctrl_->head.load(std::memory_order_acquire); - uint64_t tail = ctrl_->tail.load(std::memory_order_acquire); - uint64_t cap = ctrl_->capacity; - uint64_t mask = ctrl_->mask; - uint64_t wrap_head = ctrl_->wrap_head; - - uint64_t head_pos = head & mask; - uint64_t tail_pos = tail & mask; - uint64_t used = head - tail; - uint64_t free = cap - used; - - std::cerr << "[" << prefix << "] head=" << head << " tail=" << tail << " | head_pos=" << head_pos - << " tail_pos=" << tail_pos << " | used=" << used << " free=" << free << " cap=" << cap - << " | wrap_head=" << (wrap_head == UINT64_MAX ? "NONE" : std::to_string(wrap_head)) << '\n'; -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.hpp deleted file mode 100644 index 33a2479ea48..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.hpp +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @file spsc_shm.hpp - * @brief Single-producer/single-consumer shared-memory ring buffer (Linux, x86-64 optimized) - * - * - Zero-copy between processes via MAP_SHARED - * - One producer, one consumer. No locks. Hot path has no syscalls - * - Adaptive spin, then futex sleep/wake on empty/full transitions - * - Variable-length message framing - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace bb::ipc { - -constexpr size_t SPSC_CACHELINE = 64; - -/** - * @brief Control structure for SPSC ring buffer - * - * Carefully aligned to avoid false sharing between producer and consumer. - */ -struct alignas(SPSC_CACHELINE) SpscCtrl { - // Producer-owned (written by producer, read by consumer) - alignas(SPSC_CACHELINE) std::atomic head; // bytes written - uint64_t wrap_head; // Head value when last message wrapped (UINT64_MAX = no wrap), synchronized by head - std::atomic producer_blocked; // Written by producer in wait_for_space() - std::array _pad0; - - // Consumer-owned (written by consumer, read by producer) - alignas(SPSC_CACHELINE) std::atomic tail; // bytes consumed - std::atomic consumer_blocked; // Written by consumer in wait_for_data() - std::array _pad1; - - // Immutable capacity information - alignas(SPSC_CACHELINE) uint64_t capacity; // power of two - alignas(SPSC_CACHELINE) uint64_t mask; // capacity - 1 - - // uint8_t buffer[capacity] follows in memory... -}; - -static_assert(alignof(SpscCtrl) == SPSC_CACHELINE, "SpscCtrl alignment"); -static_assert(sizeof(SpscCtrl) % SPSC_CACHELINE == 0, "SpscCtrl size multiple of cache line"); - -/** - * @brief Lock-free single-producer single-consumer shared memory ring buffer - * - * Provides zero-copy message passing between processes using shared memory. - * Uses futex for efficient blocking when empty/full. - * - * CRITICAL USAGE REQUIREMENT: - * Each claim(n)/publish(n) pair by the producer MUST be perfectly matched by a corresponding - * peek(n)/release(n) pair by the consumer, with the EXACT same sizes. - * - * This is because the wrapping logic is completely stateless - it decides whether to wrap based - * solely on whether the requested size fits in the remaining space before the end of the buffer. - * If the producer and consumer use different sizes, they will make inconsistent wrap decisions - * and data corruption will occur. - * - * CORRECT usage example (framed messages): - * Producer: Consumer: - * claim(4), publish(4) <---> peek(4), release(4) // length prefix - * claim(msg_len), publish(msg_len) <---> peek(msg_len), release(msg_len) // message data - */ -class SpscShm { - public: - /** - * @brief Create a new SPSC ring buffer - * @param name Shared memory object name (without /dev/shm prefix) - * @param min_capacity Minimum capacity (rounded up to power of 2) - * @throws std::runtime_error if creation fails - */ - static SpscShm create(const std::string& name, size_t min_capacity); - - /** - * @brief Connect to existing SPSC ring buffer - * @param name Shared memory object name - * @throws std::runtime_error if connection fails - */ - static SpscShm connect(const std::string& name); - - /** - * @brief Unlink shared memory object (cleanup after close) - * @param name Shared memory object name - * @return true if successful, false otherwise - */ - static bool unlink(const std::string& name); - - // Move-only (no copy) - SpscShm(SpscShm&& other) noexcept; - SpscShm& operator=(SpscShm&& other) noexcept; - SpscShm(const SpscShm&) = delete; - SpscShm& operator=(const SpscShm&) = delete; - - ~SpscShm(); - - // Introspection - uint64_t available() const; // bytes ready to read - - uint64_t capacity() const { return ctrl_->capacity; } - - /** - * Producer API: claim() and publish() must be used in pairs - * - * @brief Claim contiguous space in the ring buffer (blocks until available) - * @param want Number of bytes to claim - * @param timeout_ns Timeout in nanoseconds - * @return Pointer to claimed space, or nullptr on timeout - * - * IMPORTANT: The size passed to claim(want) must exactly match the size passed to the - * corresponding peek(want) call by the consumer. Otherwise wrap decisions will be inconsistent. - */ - void* claim(size_t want, uint32_t timeout_ns); - - /** - * @brief Publish n bytes previously claimed - * @param n Number of bytes to publish (must match what was claimed) - * - * IMPORTANT: The size passed to publish(n) must exactly match the size passed to the - * corresponding release(n) call by the consumer. Otherwise wrap decisions will be inconsistent. - */ - void publish(size_t n); - - /** - * Consumer API: peek() and release() must be used in pairs - * - * @brief Peek contiguous readable region (blocks until available) - * @param want Number of bytes to peek - * @param timeout_ns Timeout in nanoseconds - * @return Pointer to readable data, or nullptr on timeout - * - * IMPORTANT: The size passed to peek(want) must exactly match the size passed to the - * corresponding claim(want) call by the producer. Otherwise wrap decisions will be inconsistent. - */ - void* peek(size_t want, uint32_t timeout_ns); - - /** - * @brief Release n bytes previously peeked - * @param n Number of bytes to release (must match what was peeked) - * - * IMPORTANT: The size passed to release(n) must exactly match the size passed to the - * corresponding publish(n) call by the producer. Otherwise wrap decisions will be inconsistent. - */ - void release(size_t n); - - /** - * @brief Wake all blocked threads (for graceful shutdown) - * - * Wakes both producers blocked on space and consumers blocked on data. - * Used for graceful shutdown of the communication channel. - */ - void wakeup_all(); - - bool wait_for_data(size_t need, uint32_t spin_ns); - bool wait_for_space(size_t need, uint32_t spin_ns); - - /** - * @brief Dump internal ring buffer state for debugging - * @param prefix Prefix string for the debug output (e.g., "Client REQ" or "Server RESP") - */ - void debug_dump(const char* prefix) const; - - private: - // Private constructor for create/connect factories - SpscShm(int fd, size_t map_len, SpscCtrl* ctrl, uint8_t* buf); - - int fd_ = -1; - size_t map_len_ = 0; - SpscCtrl* ctrl_ = nullptr; - uint8_t* buf_ = nullptr; - bool previous_had_data_ = false; // Adaptive spinning: consumer only spins if previous call found data - bool previous_had_space_ = false; // Adaptive spinning: producer only spins if previous call found space -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm/utilities.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm/utilities.hpp deleted file mode 100644 index 709deca3159..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm/utilities.hpp +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @file utilities.hpp - * @brief Common utilities for IPC shared memory implementation - * - * Provides timing and CPU pause utilities for spin-wait loops. - */ -#pragma once - -#include -#include // NOLINT(modernize-deprecated-headers) - need POSIX clock_gettime/CLOCK_MONOTONIC - -#if defined(__x86_64__) || defined(_M_X64) -#include -#define IPC_PAUSE() _mm_pause() -#else -#define IPC_PAUSE() \ - do { \ - } while (0) -#endif - -namespace bb::ipc { - -/** - * @brief Get current monotonic time in nanoseconds - * - * Uses CLOCK_MONOTONIC which is suitable for measuring elapsed time - * and not affected by system clock adjustments. - * - * @return Current monotonic time in nanoseconds, or 0 on error - */ -inline uint64_t mono_ns_now() -{ - struct timespec ts; - if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { - return 0; - } - return (static_cast(ts.tv_sec) * 1000000000ULL) + static_cast(ts.tv_nsec); -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm_client.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm_client.hpp deleted file mode 100644 index 0fc8140a0e1..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm_client.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include "ipc_client.hpp" -#include "shm/spsc_shm.hpp" -#include "shm_common.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC client implementation using shared memory - * - * Uses SPSC (single-producer single-consumer) for both requests and responses. - * Simple 1:1 client-server communication. - */ -class ShmClient : public IpcClient { - public: - explicit ShmClient(std::string base_name) - : base_name_(std::move(base_name)) - {} - - ~ShmClient() override = default; - - // Non-copyable, non-movable (owns shared memory resources) - ShmClient(const ShmClient&) = delete; - ShmClient& operator=(const ShmClient&) = delete; - ShmClient(ShmClient&&) = delete; - ShmClient& operator=(ShmClient&&) = delete; - - bool connect() override - { - if (request_ring_.has_value()) { - return true; // Already connected - } - - try { - // Connect to request ring (client writes, server reads) - std::string req_name = base_name_ + "_request"; - request_ring_ = SpscShm::connect(req_name); - - // Connect to response ring (server writes, client reads) - std::string resp_name = base_name_ + "_response"; - response_ring_ = SpscShm::connect(resp_name); - - return true; - } catch (...) { - request_ring_.reset(); - response_ring_.reset(); - return false; - } - } - - bool send(const void* data, size_t len, uint64_t timeout_ns) override - { - if (!request_ring_.has_value()) { - return false; - } - return ring_send_msg(request_ring_.value(), data, len, timeout_ns); - } - - std::span receive(uint64_t timeout_ns) override - { - if (!response_ring_.has_value()) { - return {}; - } - return ring_receive_msg(response_ring_.value(), timeout_ns); - } - - void release(size_t message_size) override - { - if (!response_ring_.has_value()) { - return; - } - response_ring_->release(sizeof(uint32_t) + message_size); - } - - void close() override - { - request_ring_.reset(); - response_ring_.reset(); - } - - void debug_dump() const - { - if (request_ring_.has_value()) { - request_ring_->debug_dump("Client REQ"); - } - if (response_ring_.has_value()) { - response_ring_->debug_dump("Client RESP"); - } - } - - private: - std::string base_name_; - std::optional request_ring_; // Client writes to this - std::optional response_ring_; // Client reads from this -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm_common.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm_common.hpp deleted file mode 100644 index 57c1a01e3d3..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm_common.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "barretenberg/ipc/shm/spsc_shm.hpp" -#include -#include -#include -#include -#include - -namespace bb::ipc { - -inline bool ring_send_msg(SpscShm& ring, const void* data, size_t len, uint64_t timeout_ns) -{ - // Prevent sending messages larger than half the ring buffer capacity. - // This simplifies wrap-around logic. - if (len > ring.capacity() / 2 - 4) { - throw std::runtime_error( - "ring_send_msg: message too large for ring buffer, must be <= half capacity minus 4 bytes"); - } - - // Atomic send: claim space for entire message (length + data) - size_t total_size = 4 + len; - void* buf = ring.claim(total_size, static_cast(timeout_ns)); - if (buf == nullptr) { - return false; // Timeout or no space - nothing published yet (atomic failure) - } - - // Write length prefix and message data together - auto len_u32 = static_cast(len); - std::memcpy(buf, &len_u32, 4); - std::memcpy(static_cast(buf) + 4, data, len); - - // Publish entire message atomically - ring.publish(total_size); - - return true; -} - -inline std::span ring_receive_msg(SpscShm& ring, uint64_t timeout_ns) -{ - // Peek the length prefix (4 bytes) - void* len_ptr = ring.peek(4, static_cast(timeout_ns)); - if (len_ptr == nullptr) { - return {}; // Timeout - } - - // Read message length - uint32_t msg_len = 0; - std::memcpy(&msg_len, len_ptr, 4); - - // Now peek the message data - void* msg_ptr = ring.peek(4 + msg_len, static_cast(timeout_ns)); - if (msg_ptr == nullptr) { - return {}; // Timeout - } - - // Return span directly into ring buffer (zero-copy!) - return std::span(static_cast(msg_ptr) + 4, msg_len); -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/shm_server.hpp b/barretenberg/cpp/src/barretenberg/ipc/shm_server.hpp deleted file mode 100644 index 3fe15b6d43a..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/shm_server.hpp +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#include "barretenberg/common/throw_or_abort.hpp" -#include "ipc_server.hpp" -#include "shm/spsc_shm.hpp" -#include "shm_common.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC server implementation using shared memory - * - * Uses SPSC (single-producer single-consumer) for both requests and responses. - * Simple 1:1 client-server communication. - */ -class ShmServer : public IpcServer { - public: - static constexpr size_t DEFAULT_RING_SIZE = 1 << 20; // 1MB - - ShmServer(std::string base_name, - size_t request_ring_size = DEFAULT_RING_SIZE, - size_t response_ring_size = DEFAULT_RING_SIZE) - : base_name_(std::move(base_name)) - , request_ring_size_(request_ring_size) - , response_ring_size_(response_ring_size) - {} - - ~ShmServer() override { close(); } - - // Non-copyable, non-movable (owns shared memory resources) - ShmServer(const ShmServer&) = delete; - ShmServer& operator=(const ShmServer&) = delete; - ShmServer(ShmServer&&) = delete; - ShmServer& operator=(ShmServer&&) = delete; - - bool listen() override - { - if (request_ring_.has_value()) { - return true; // Already listening - } - - // Clean up any leftover shared memory - std::string req_name = base_name_ + "_request"; - std::string resp_name = base_name_ + "_response"; - SpscShm::unlink(req_name); - SpscShm::unlink(resp_name); - - try { - // Create SPSC ring for requests (client writes, server reads) - request_ring_ = SpscShm::create(req_name, request_ring_size_); - - // Create SPSC ring for responses (server writes, client reads) - response_ring_ = SpscShm::create(resp_name, response_ring_size_); - - return true; - } catch (...) { - close(); // Cleanup on failure - return false; - } - } - - int wait_for_data(uint64_t timeout_ns) override - { - assert(request_ring_); - if (!request_ring_.has_value()) { - return -1; - } - - // Wait for data in request ring, return client ID 0 (always single client) - if (request_ring_->wait_for_data(sizeof(uint32_t), static_cast(timeout_ns))) { - return 0; // Single client, always ID 0 - } - return -1; // Timeout - } - - std::span receive([[maybe_unused]] int client_id) override - { - if (!request_ring_.has_value()) { - return {}; - } - // TODO: Plumb timeout. - return ring_receive_msg(request_ring_.value(), 100000000); // 100ms timeout - } - - void release([[maybe_unused]] int client_id, size_t message_size) override - { - if (!request_ring_.has_value()) { - return; - } - request_ring_->release(sizeof(uint32_t) + message_size); - } - - bool send([[maybe_unused]] int client_id, const void* data, size_t len) override - { - if (!response_ring_.has_value()) { - return false; - } - return ring_send_msg(response_ring_.value(), data, len, 100000000); - } - - void close() override - { - // Close rings - request_ring_.reset(); - response_ring_.reset(); - - // Clean up shared memory - std::string req_name = base_name_ + "_request"; - std::string resp_name = base_name_ + "_response"; - SpscShm::unlink(req_name); - SpscShm::unlink(resp_name); - } - - void wakeup_all() override - { - // Wake any threads blocked in wait/peek/claim - if (request_ring_.has_value()) { - request_ring_->wakeup_all(); - } - if (response_ring_.has_value()) { - response_ring_->wakeup_all(); - } - } - - void debug_dump() const - { - if (request_ring_.has_value()) { - request_ring_->debug_dump("Server REQ"); - } - if (response_ring_.has_value()) { - response_ring_->debug_dump("Server RESP"); - } - } - - private: - std::string base_name_; - size_t request_ring_size_; - size_t response_ring_size_; - std::optional request_ring_; // Server reads from this - std::optional response_ring_; // Server writes to this -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/socket_client.cpp b/barretenberg/cpp/src/barretenberg/ipc/socket_client.cpp deleted file mode 100644 index 42e60b54030..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/socket_client.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "barretenberg/ipc/socket_client.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -SocketClient::SocketClient(std::string socket_path) - : socket_path_(std::move(socket_path)) -{} - -SocketClient::~SocketClient() -{ - close_internal(); -} - -bool SocketClient::connect() -{ - if (fd_ >= 0) { - return true; // Already connected - } - - // Create socket - fd_ = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd_ < 0) { - return false; - } - - // Connect to server - struct sockaddr_un addr; - std::memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - std::strncpy(addr.sun_path, socket_path_.c_str(), sizeof(addr.sun_path) - 1); - - if (::connect(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { - ::close(fd_); - fd_ = -1; - return false; - } - - return true; -} - -bool SocketClient::send(const void* data, size_t len, uint64_t /*timeout_ns*/) -{ - if (fd_ < 0) { - errno = EINVAL; - return false; - } - - // Send length prefix (4 bytes, little-endian) - auto msg_len = static_cast(len); - ssize_t n = ::send(fd_, &msg_len, sizeof(msg_len), 0); - if (n < 0 || static_cast(n) != sizeof(msg_len)) { - return false; - } - - // Send message data - n = ::send(fd_, data, len, 0); - if (n < 0) { - return false; - } - const auto bytes_sent = static_cast(n); - return bytes_sent == len; -} - -std::span SocketClient::receive(uint64_t /*timeout_ns*/) -{ - if (fd_ < 0) { - return {}; - } - - // Read length prefix (4 bytes) - uint32_t msg_len = 0; - ssize_t n = ::recv(fd_, &msg_len, sizeof(msg_len), MSG_WAITALL); - if (n < 0 || static_cast(n) != sizeof(msg_len)) { - return {}; - } - - // Ensure buffer is large enough - if (recv_buffer_.size() < msg_len) { - recv_buffer_.resize(msg_len); - } - - // Read message data into internal buffer - n = ::recv(fd_, recv_buffer_.data(), msg_len, MSG_WAITALL); - if (n < 0 || static_cast(n) != msg_len) { - return {}; - } - - // Return span into internal buffer - return std::span(recv_buffer_.data(), msg_len); -} - -void SocketClient::release(size_t /*message_size*/) -{ - // No-op for sockets - data is already consumed from kernel buffer during recv() -} - -void SocketClient::close() -{ - close_internal(); -} - -void SocketClient::close_internal() -{ - if (fd_ >= 0) { - ::close(fd_); - fd_ = -1; - } -} - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/socket_client.hpp b/barretenberg/cpp/src/barretenberg/ipc/socket_client.hpp deleted file mode 100644 index 42ee599d9d1..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/socket_client.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "barretenberg/ipc/ipc_client.hpp" -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC client implementation using Unix domain sockets - * - * Direct implementation with no wrapper layer - manages socket connection directly. - */ -class SocketClient : public IpcClient { - public: - explicit SocketClient(std::string socket_path); - ~SocketClient() override; - - // Non-copyable, non-movable (owns file descriptor) - SocketClient(const SocketClient&) = delete; - SocketClient& operator=(const SocketClient&) = delete; - SocketClient(SocketClient&&) = delete; - SocketClient& operator=(SocketClient&&) = delete; - - bool connect() override; - bool send(const void* data, size_t len, uint64_t timeout_ns) override; - std::span receive(uint64_t timeout_ns) override; - void release(size_t message_size) override; - void close() override; - - private: - void close_internal(); - - std::string socket_path_; - int fd_ = -1; - std::vector recv_buffer_; // Internal buffer for socket recv -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/socket_server.cpp b/barretenberg/cpp/src/barretenberg/ipc/socket_server.cpp deleted file mode 100644 index 62c8bf69cbd..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/socket_server.cpp +++ /dev/null @@ -1,573 +0,0 @@ -#include "barretenberg/ipc/socket_server.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Platform-specific event notification includes -#ifdef __APPLE__ -#include // kqueue on macOS/BSD -#else -#include // epoll on Linux -#endif - -namespace bb::ipc { - -SocketServer::SocketServer(std::string socket_path, int initial_max_clients) - : socket_path_(std::move(socket_path)) - , initial_max_clients_(initial_max_clients) -{ - const size_t reserve_size = initial_max_clients > 0 ? static_cast(initial_max_clients) : 10; - client_fds_.reserve(reserve_size); - recv_buffers_.reserve(reserve_size); -} - -SocketServer::~SocketServer() -{ - close_internal(); -} - -void SocketServer::close() -{ - close_internal(); -} - -void SocketServer::close_internal() -{ - // Close all client connections - for (int fd : client_fds_) { - if (fd >= 0) { - ::close(fd); - } - } - client_fds_.clear(); - fd_to_client_id_.clear(); - num_clients_ = 0; - - if (fd_ >= 0) { - ::close(fd_); - fd_ = -1; - } - - if (listen_fd_ >= 0) { - ::close(listen_fd_); - listen_fd_ = -1; - } - - // Clean up socket file - ::unlink(socket_path_.c_str()); -} - -int SocketServer::find_free_slot() -{ - // Look for existing free slot - for (size_t i = 0; i < client_fds_.size(); i++) { - if (client_fds_[i] == -1) { - return static_cast(i); - } - } - - // No free slot found, allocate new one at end - return static_cast(client_fds_.size()); -} - -bool SocketServer::send(int client_id, const void* data, size_t len) -{ - if (client_id < 0 || static_cast(client_id) >= client_fds_.size() || - client_fds_[static_cast(client_id)] < 0) { - errno = EINVAL; - return false; - } - - int fd = client_fds_[static_cast(client_id)]; - - // Send length prefix (4 bytes) - auto msg_len = static_cast(len); - ssize_t n = ::send(fd, &msg_len, sizeof(msg_len), 0); - if (n < 0 || static_cast(n) != sizeof(msg_len)) { - return false; - } - - // Send message data - n = ::send(fd, data, len, 0); - if (n < 0) { - return false; - } - const auto bytes_sent = static_cast(n); - return bytes_sent == len; -} - -void SocketServer::release(int client_id, size_t message_size) -{ - // No-op for sockets - message already consumed from kernel buffer during receive() - (void)client_id; - (void)message_size; -} - -std::span SocketServer::receive(int client_id) -{ - if (client_id < 0 || static_cast(client_id) >= client_fds_.size() || - client_fds_[static_cast(client_id)] < 0) { - return {}; - } - - int fd = client_fds_[static_cast(client_id)]; - const auto client_idx = static_cast(client_id); - - // Ensure buffers are sized for this client - if (client_idx >= recv_buffers_.size()) { - recv_buffers_.resize(client_idx + 1); - } - - // Read length prefix (4 bytes) - must loop until all bytes received (MSG_WAITALL unreliable on macOS) - uint32_t msg_len = 0; - size_t total_read = 0; - while (total_read < sizeof(msg_len)) { - ssize_t n = ::recv(fd, reinterpret_cast(&msg_len) + total_read, sizeof(msg_len) - total_read, 0); - if (n < 0) { - if (errno == EINTR) { - continue; // Interrupted, retry - } - return {}; - } - if (n == 0) { - // Client disconnected - disconnect_client(client_id); - return {}; - } - total_read += static_cast(n); - } - - // Resize buffer if needed to fit length prefix + message - size_t total_size = sizeof(uint32_t) + msg_len; - if (recv_buffers_[client_idx].size() < total_size) { - recv_buffers_[client_idx].resize(total_size); - } - - // Store length prefix in buffer - std::memcpy(recv_buffers_[client_idx].data(), &msg_len, sizeof(uint32_t)); - - // Read message data - must loop until all bytes received (MSG_WAITALL unreliable on macOS) - total_read = 0; - while (total_read < msg_len) { - ssize_t n = - ::recv(fd, recv_buffers_[client_idx].data() + sizeof(uint32_t) + total_read, msg_len - total_read, 0); - if (n < 0) { - if (errno == EINTR) { - continue; // Interrupted, retry - } - disconnect_client(client_id); - return {}; - } - if (n == 0) { - // Client disconnected mid-message - disconnect_client(client_id); - return {}; - } - total_read += static_cast(n); - } - - return std::span(recv_buffers_[client_idx].data() + sizeof(uint32_t), msg_len); -} - -#ifdef __APPLE__ -// ============================================================================ -// macOS Implementation (kqueue, blocking sockets, simple accept) -// ============================================================================ - -bool SocketServer::listen() -{ - if (listen_fd_ >= 0) { - return true; // Already listening - } - - // Remove any existing socket file - ::unlink(socket_path_.c_str()); - - // Create socket - listen_fd_ = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd_ < 0) { - return false; - } - - // Set non-blocking mode (required for accept-until-EAGAIN pattern) - int flags = fcntl(listen_fd_, F_GETFL, 0); - if (flags < 0 || fcntl(listen_fd_, F_SETFL, flags | O_NONBLOCK) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - return false; - } - - // Bind to path - struct sockaddr_un addr; - std::memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - std::strncpy(addr.sun_path, socket_path_.c_str(), sizeof(addr.sun_path) - 1); - - if (bind(listen_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - return false; - } - - // Restrict socket to owner only, matching the 0600 mode used for SHM transport - ::chmod(socket_path_.c_str(), 0600); - - // Listen with backlog - int backlog = initial_max_clients_ > 0 ? initial_max_clients_ : 10; - if (::listen(listen_fd_, backlog) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - // Create kqueue instance - fd_ = kqueue(); - if (fd_ < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - // Add listen socket to kqueue - struct kevent ev; - EV_SET(&ev, listen_fd_, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, nullptr); - if (kevent(fd_, &ev, 1, nullptr, 0, nullptr) < 0) { - ::close(fd_); - fd_ = -1; - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - return true; -} - -int SocketServer::accept() -{ - if (listen_fd_ < 0) { - errno = EINVAL; - return -1; - } - - // Accept all pending connections (loop until EAGAIN) - // Non-blocking socket ensures this returns immediately - int last_client_id = -1; - - while (true) { - int client_fd = ::accept(listen_fd_, nullptr, nullptr); - - if (client_fd < 0) { - // Check if this is expected (no more connections) or a real error - if (errno == EAGAIN || errno == EWOULDBLOCK) { - // No more pending connections - expected, break - break; - } - // Real error - but if we already accepted some, return success - if (last_client_id >= 0) { - break; - } - // No connections accepted and got real error - return -1; - } - - // Set client socket to BLOCKING mode (inherited non-blocking from listen socket) - // This avoids busy-waiting in recv() - we only recv after kqueue signals data ready - int flags = fcntl(client_fd, F_GETFL, 0); - if (flags >= 0) { - fcntl(client_fd, F_SETFL, flags & ~O_NONBLOCK); - } - - // Find free slot (or allocate new one) - int client_id = find_free_slot(); - - // Store client fd - const auto client_id_unsigned = static_cast(client_id); - if (client_id_unsigned >= client_fds_.size()) { - client_fds_.resize(client_id_unsigned + 1, -1); - } - client_fds_[static_cast(client_id)] = client_fd; - fd_to_client_id_[client_fd] = client_id; - num_clients_++; - - // Add client to kqueue - struct kevent kev; - EV_SET(&kev, client_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, nullptr); - if (kevent(fd_, &kev, 1, nullptr, 0, nullptr) < 0) { - disconnect_client(client_id); - // Continue trying to accept other pending connections - continue; - } - - last_client_id = client_id; - } - - return last_client_id; -} - -int SocketServer::wait_for_data(uint64_t timeout_ns) -{ - if (fd_ < 0) { - errno = EINVAL; - return -1; - } - - struct kevent ev; - struct timespec timeout; - struct timespec* timeout_ptr = nullptr; - - if (timeout_ns > 0) { - timeout.tv_sec = static_cast(timeout_ns / 1000000000ULL); - timeout.tv_nsec = static_cast(timeout_ns % 1000000000ULL); - timeout_ptr = &timeout; - } else if (timeout_ns == 0) { - timeout.tv_sec = 0; - timeout.tv_nsec = 0; - timeout_ptr = &timeout; - } - - int n = kevent(fd_, nullptr, 0, &ev, 1, timeout_ptr); - if (n <= 0) { - return -1; - } - - int ready_fd = static_cast(ev.ident); - - // Check if it's listen socket (new connection) or client data - if (ready_fd == listen_fd_) { - errno = EAGAIN; // Signal caller to call accept - return -1; - } - - // Find which client - auto it = fd_to_client_id_.find(ready_fd); - if (it == fd_to_client_id_.end()) { - errno = ENOENT; - return -1; - } - - return it->second; -} - -void SocketServer::disconnect_client(int client_id) -{ - if (client_id < 0 || static_cast(client_id) >= client_fds_.size()) { - return; - } - - int fd = client_fds_[static_cast(client_id)]; - if (fd >= 0) { - // For kqueue, we don't need explicit deletion - closing the fd removes it automatically - // But we can explicitly remove it for clarity - struct kevent ev; - EV_SET(&ev, fd, EVFILT_READ, EV_DELETE, 0, 0, nullptr); - kevent(fd_, &ev, 1, nullptr, 0, nullptr); - - ::close(fd); - fd_to_client_id_.erase(fd); - client_fds_[static_cast(client_id)] = -1; - num_clients_--; - } -} - -#else - -// ============================================================================ -// Linux Implementation (epoll, non-blocking sockets, accept-until-EAGAIN) -// ============================================================================ - -bool SocketServer::listen() -{ - if (listen_fd_ >= 0) { - return true; // Already listening - } - - // Remove any existing socket file - ::unlink(socket_path_.c_str()); - - // Create socket - listen_fd_ = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd_ < 0) { - return false; - } - - // Set non-blocking mode (required for accept-until-EAGAIN pattern) - int flags = fcntl(listen_fd_, F_GETFL, 0); - if (flags < 0 || fcntl(listen_fd_, F_SETFL, flags | O_NONBLOCK) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - return false; - } - - // Bind to path - struct sockaddr_un addr; - std::memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - std::strncpy(addr.sun_path, socket_path_.c_str(), sizeof(addr.sun_path) - 1); - - if (bind(listen_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - return false; - } - - // Restrict socket to owner only, matching the 0600 mode used for SHM transport - ::chmod(socket_path_.c_str(), 0600); - - // Listen with backlog - int backlog = initial_max_clients_ > 0 ? initial_max_clients_ : 10; - if (::listen(listen_fd_, backlog) < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - // Create epoll instance - fd_ = epoll_create1(0); - if (fd_ < 0) { - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - // Add listen socket to epoll - struct epoll_event ev; - ev.events = EPOLLIN; - ev.data.fd = listen_fd_; - if (epoll_ctl(fd_, EPOLL_CTL_ADD, listen_fd_, &ev) < 0) { - ::close(fd_); - fd_ = -1; - ::close(listen_fd_); - listen_fd_ = -1; - ::unlink(socket_path_.c_str()); - return false; - } - - return true; -} - -int SocketServer::accept() -{ - if (listen_fd_ < 0) { - errno = EINVAL; - return -1; - } - - // Accept all pending connections (loop until EAGAIN) - // Non-blocking socket ensures this returns immediately - int last_client_id = -1; - - while (true) { - int client_fd = ::accept(listen_fd_, nullptr, nullptr); - - if (client_fd < 0) { - // Check if this is expected (no more connections) or a real error - if (errno == EAGAIN || errno == EWOULDBLOCK) { - // No more pending connections - expected, break - break; - } - // Real error - but if we already accepted some, return success - if (last_client_id >= 0) { - break; - } - // No connections accepted and got real error - return -1; - } - - // Set client socket to BLOCKING mode (inherited non-blocking from listen socket) - // This avoids busy-waiting in recv() - we only recv after epoll signals data ready - int flags = fcntl(client_fd, F_GETFL, 0); - if (flags >= 0) { - fcntl(client_fd, F_SETFL, flags & ~O_NONBLOCK); - } - - // Find free slot (or allocate new one) - int client_id = find_free_slot(); - - // Store client fd - const auto client_id_unsigned = static_cast(client_id); - if (client_id_unsigned >= client_fds_.size()) { - client_fds_.resize(client_id_unsigned + 1, -1); - } - client_fds_[static_cast(client_id)] = client_fd; - fd_to_client_id_[client_fd] = client_id; - num_clients_++; - - // Add client to epoll - struct epoll_event client_ev; - client_ev.events = EPOLLIN; - client_ev.data.fd = client_fd; - if (epoll_ctl(fd_, EPOLL_CTL_ADD, client_fd, &client_ev) < 0) { - disconnect_client(client_id); - // Continue trying to accept other pending connections - continue; - } - - last_client_id = client_id; - } - - return last_client_id; -} - -int SocketServer::wait_for_data(uint64_t timeout_ns) -{ - if (fd_ < 0) { - errno = EINVAL; - return -1; - } - - struct epoll_event ev; - int timeout_ms = timeout_ns > 0 ? static_cast(timeout_ns / 1000000) : -1; - int n = epoll_wait(fd_, &ev, 1, timeout_ms); - if (n <= 0) { - return -1; - } - - // Check if it's listen socket (new connection) or client data - if (ev.data.fd == listen_fd_) { - errno = EAGAIN; // Signal caller to call accept - return -1; - } - - // Find which client - auto it = fd_to_client_id_.find(ev.data.fd); - if (it == fd_to_client_id_.end()) { - errno = ENOENT; - return -1; - } - - return it->second; -} - -void SocketServer::disconnect_client(int client_id) -{ - if (client_id < 0 || static_cast(client_id) >= client_fds_.size()) { - return; - } - - int fd = client_fds_[static_cast(client_id)]; - if (fd >= 0) { - epoll_ctl(fd_, EPOLL_CTL_DEL, fd, nullptr); - ::close(fd); - fd_to_client_id_.erase(fd); - client_fds_[static_cast(client_id)] = -1; - num_clients_--; - } -} - -#endif - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/ipc/socket_server.hpp b/barretenberg/cpp/src/barretenberg/ipc/socket_server.hpp deleted file mode 100644 index 1d1dd941a56..00000000000 --- a/barretenberg/cpp/src/barretenberg/ipc/socket_server.hpp +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include "barretenberg/ipc/ipc_server.hpp" -#include -#include -#include -#include -#include -#include - -namespace bb::ipc { - -/** - * @brief IPC server implementation using Unix domain sockets - * - * Platform-specific implementation: - * - Linux: uses epoll for efficient multi-client handling - * - macOS: uses kqueue for efficient multi-client handling - * Dynamic client capacity with no artificial limits. - */ -class SocketServer : public IpcServer { - public: - SocketServer(std::string socket_path, int initial_max_clients); - ~SocketServer() override; - - // Non-copyable, non-movable (owns file descriptors) - SocketServer(const SocketServer&) = delete; - SocketServer& operator=(const SocketServer&) = delete; - SocketServer(SocketServer&&) = delete; - SocketServer& operator=(SocketServer&&) = delete; - - bool listen() override; - int accept() override; - int wait_for_data(uint64_t timeout_ns) override; - std::span receive(int client_id) override; - void release(int client_id, size_t message_size) override; - bool send(int client_id, const void* data, size_t len) override; - void close() override; - - private: - void close_internal(); - void disconnect_client(int client_id); - int find_free_slot(); - - std::string socket_path_; - int initial_max_clients_; - int listen_fd_ = -1; - int fd_ = -1; // kqueue or epoll fd - std::vector client_fds_; // client_id -> fd - std::unordered_map fd_to_client_id_; // fd -> client_id (for fast lookup) - std::vector> recv_buffers_; // client_id -> recv buffer - int num_clients_ = 0; -}; - -} // namespace bb::ipc diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/.gitignore b/barretenberg/cpp/src/barretenberg/nodejs_module/.gitignore deleted file mode 100644 index 61c3bc75a05..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.yarn diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/.yarnrc.yml b/barretenberg/cpp/src/barretenberg/nodejs_module/.yarnrc.yml deleted file mode 100644 index 1d8b263ddcf..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -nodeLinker: node-modules - -npmMinimalAgeGate: 7d - -npmPreapprovedPackages: [] diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt deleted file mode 100644 index 77d07c8119e..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -# see https://nodejs.org/dist/latest/docs/api/n-api.html#node-api-version-matrix -add_definitions(-DNAPI_VERSION=9) - -file(GLOB_RECURSE SOURCE_FILES *.cpp) -file(GLOB_RECURSE HEADER_FILES *.hpp *.tcc) - -execute_process( - COMMAND yarn --immutable - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) - -execute_process( - COMMAND node -p "require('node-addon-api').include" - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE NODE_ADDON_API_DIR -) - -execute_process( - COMMAND node -p "require('node-api-headers').include_dir" - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_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_library(nodejs_module SHARED ${SOURCE_FILES}) -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 ipc ipc_runtime) - -# On macOS, Node.js N-API symbols are provided by the runtime, not at link time -if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - target_link_options(nodejs_module PRIVATE "-undefined" "dynamic_lookup") -endif() diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp deleted file mode 100644 index 5dcc6eade9a..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp" -#include "barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp" -#include "napi.h" - -Napi::Object Init(Napi::Env env, Napi::Object exports) -{ - exports.Set(Napi::String::New(env, "MsgpackClient"), - bb::nodejs::msgpack_client::MsgpackClientWrapper::get_class(env)); - exports.Set(Napi::String::New(env, "MsgpackClientAsync"), - bb::nodejs::msgpack_client::MsgpackClientAsync::get_class(env)); - return exports; -} - -// NOLINTNEXTLINE -NODE_API_MODULE(addon, Init) diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.cpp deleted file mode 100644 index 5b46c505f5e..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp" -#include "barretenberg/ipc/ipc_client.hpp" -#include "napi.h" -#include -#include - -using namespace bb::nodejs::msgpack_client; - -MsgpackClientAsync::MsgpackClientAsync(const Napi::CallbackInfo& info) - : ObjectWrap(info) -{ - Napi::Env env = info.Env(); - - // Arg 0: shared memory base name (string) - if (info.Length() < 1 || !info[0].IsString()) { - throw Napi::TypeError::New(env, "First argument must be a string (shared memory name)"); - } - std::string shm_name = info[0].As(); - - // Create shared memory client (SPSC-only, no max_clients needed) - client_ = bb::ipc::IpcClient::create_shm(shm_name); - - // Connect to bb server - if (!client_->connect()) { - throw Napi::Error::New(env, "Failed to connect to shared memory server"); - } -} - -Napi::Value MsgpackClientAsync::setResponseCallback(const Napi::CallbackInfo& info) -{ - Napi::Env env = info.Env(); - - // Arg 0: JavaScript callback function - if (info.Length() < 1 || !info[0].IsFunction()) { - throw Napi::TypeError::New(env, "First argument must be a function"); - } - - // Store the callback for lazy TSFN creation - // Don't create TSFN yet - it will be created on first acquire() - js_callback_ = Napi::Persistent(info[0].As()); - - // Start background polling thread now that callback is registered - poll_thread_ = std::thread(&MsgpackClientAsync::poll_responses, this); - - // Detach the thread - it will run until process exits - // No need for explicit shutdown or join - poll_thread_.detach(); - - return env.Undefined(); -} - -void MsgpackClientAsync::poll_responses() -{ - constexpr uint64_t TIMEOUT_NS = 1000000000; // 1s - - while (true) { // Run forever until process exits - // Poll for response (blocks with timeout using futex) - std::span response = client_->receive(TIMEOUT_NS); - - if (response.empty()) { - // Timeout - just continue polling - continue; - } - - // Copy response data before releasing (span is invalidated by release()) - auto* response_data = new std::vector(response.begin(), response.end()); - - // Release the message in ring buffer to free space - client_->release(response.size()); - - // Lock mutex to safely access TSFN - { - std::lock_guard lock(tsfn_mutex_); - - // TSFN is active - invoke JavaScript callback - // The callback will handle matching this response to the correct promise - auto status = tsfn_.NonBlockingCall( - response_data, [](Napi::Env env, Napi::Function js_callback, std::vector* data) { - // This lambda runs on the JavaScript main thread! - // Safe to create JS objects and call functions here - - // Create Buffer with response data - auto js_buffer = Napi::Buffer::Copy(env, data->data(), data->size()); - - // Call the registered JavaScript callback with the response - // TypeScript will pop its queue and resolve the appropriate promise - js_callback.Call({ js_buffer }); - - // Clean up response data - delete data; - }); - - if (status != napi_ok) { - // Failed to queue callback - likely process is exiting - // Just clean up and continue (process will exit soon anyway) - delete response_data; - } - } - } -} - -Napi::Value MsgpackClientAsync::call(const Napi::CallbackInfo& info) -{ - Napi::Env env = info.Env(); - - // Arg 0: msgpack buffer to send - if (info.Length() < 1 || !info[0].IsBuffer()) { - throw Napi::TypeError::New(env, "First argument must be a Buffer"); - } - - auto input_buffer = info[0].As>(); - const uint8_t* input_data = input_buffer.Data(); - size_t input_len = input_buffer.Length(); - - // Send request (non-blocking write to ring buffer with no timeout) - // TypeScript will handle promise creation and queueing - if (!client_->send(input_data, input_len, 0)) { - throw Napi::Error::New(env, "Failed to send request, ring buffer full. Make it bigger?"); - } - - // Return undefined - TypeScript manages promises - return env.Undefined(); -} - -Napi::Value MsgpackClientAsync::acquire(const Napi::CallbackInfo& info) -{ - Napi::Env env = info.Env(); - - std::lock_guard lock(tsfn_mutex_); - - if (ref_count_ == 0) { - // Lazily create TSFN when first needed (0 → 1) - tsfn_ = Napi::ThreadSafeFunction::New(env, - js_callback_.Value(), // The actual JS function to call - "ShmResponseCallback", // Resource name for debugging - 0, // Unlimited queue size - 1 // Initial thread count (must be >= 1) - ); - } - - ref_count_++; - return env.Undefined(); -} - -Napi::Value MsgpackClientAsync::release(const Napi::CallbackInfo& info) -{ - std::lock_guard lock(tsfn_mutex_); - - ref_count_--; - - if (ref_count_ == 0) { - // Destroy TSFN when no longer needed (1 → 0) - // This releases the initial reference, bringing ref count to 0 - tsfn_.Release(); - } - - return info.Env().Undefined(); -} - -Napi::Function MsgpackClientAsync::get_class(Napi::Env env) -{ - return DefineClass( - env, - "MsgpackClientAsync", - { - MsgpackClientAsync::InstanceMethod("setResponseCallback", &MsgpackClientAsync::setResponseCallback), - MsgpackClientAsync::InstanceMethod("call", &MsgpackClientAsync::call), - MsgpackClientAsync::InstanceMethod("acquire", &MsgpackClientAsync::acquire), - MsgpackClientAsync::InstanceMethod("release", &MsgpackClientAsync::release), - }); -} diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp deleted file mode 100644 index 580bde93413..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include "barretenberg/ipc/ipc_client.hpp" -#include "napi.h" -#include -#include -#include - -namespace bb::nodejs::msgpack_client { - -/** - * @brief Asynchronous NAPI wrapper for msgpack calls via shared memory IPC - * - * Provides an asynchronous interface with request pipelining for sending msgpack - * buffers to the bb binary via shared memory. Multiple requests can be in flight - * simultaneously, with responses matched to requests in FIFO order by TypeScript. - * - * Architecture (matches socket backend pattern): - * - TypeScript: Creates promises, manages queue, handles request/response matching - * - C++ Main thread: Sends requests to shared memory ring buffer - * - C++ Background thread: Polls response ring buffer, invokes JS callback via ThreadSafeFunction - * - ThreadSafeFunction: Safely bridges C++ background thread to JavaScript main thread - * - * This design eliminates the need for C++ mutex/queue by leveraging JavaScript's - * single-threaded nature for queue management. - */ -class MsgpackClientAsync : public Napi::ObjectWrap { - public: - MsgpackClientAsync(const Napi::CallbackInfo& info); - - /** - * @brief Set the JavaScript callback to be invoked when responses arrive - * @param info[0] - JavaScript function to call with response buffer - * - * The callback will be invoked from the background thread via ThreadSafeFunction. - * TypeScript code should use this to resolve promises from its queue. - */ - Napi::Value setResponseCallback(const Napi::CallbackInfo& info); - - /** - * @brief Send a msgpack buffer asynchronously - * @param info[0] - Buffer containing msgpack data - * @returns undefined (promise management handled in TypeScript) - * - * Writes request to shared memory. TypeScript should create and manage promises. - */ - Napi::Value call(const Napi::CallbackInfo& info); - - /** - * @brief Acquire a reference to keep the event loop alive - * Called by TypeScript when there are pending callbacks - */ - Napi::Value acquire(const Napi::CallbackInfo& info); - - /** - * @brief Release a reference to allow the event loop to exit - * Called by TypeScript when there are no pending callbacks - */ - Napi::Value release(const Napi::CallbackInfo& info); - - static Napi::Function get_class(Napi::Env env); - - private: - /** - * @brief Background thread function that polls for responses - * - * Continuously polls the response ring buffer using recv() with timeout. - * When a response arrives, invokes the registered JavaScript callback via ThreadSafeFunction. - * Runs until process exits (thread is detached, no explicit shutdown needed). - */ - void poll_responses(); - - // IPC client for shared memory communication - std::unique_ptr client_; - - // Background polling thread (detached - will be cleaned up by OS on process exit) - std::thread poll_thread_; - - // Mutex protecting TSFN access from multiple threads - std::mutex tsfn_mutex_; - - // JavaScript callback stored for lazy TSFN creation - Napi::FunctionReference js_callback_; - - // ThreadSafeFunction for invoking JavaScript callback from background thread - // Created lazily when first needed, destroyed when no longer needed - Napi::ThreadSafeFunction tsfn_; - - // Reference count for TSFN lifecycle management - // When 0→1: create TSFN, when 1→0: destroy TSFN - int ref_count_ = 0; -}; - -} // namespace bb::nodejs::msgpack_client diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.cpp deleted file mode 100644 index 39606d309d8..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp" -#include "barretenberg/ipc/ipc_client.hpp" -#include "napi.h" -#include -#include - -using namespace bb::nodejs::msgpack_client; - -MsgpackClientWrapper::MsgpackClientWrapper(const Napi::CallbackInfo& info) - : ObjectWrap(info) -{ - Napi::Env env = info.Env(); - - // Arg 0: shared memory base name (string) - if (info.Length() < 1 || !info[0].IsString()) { - throw Napi::TypeError::New(env, "First argument must be a string (shared memory name)"); - } - std::string shm_name = info[0].As(); - - // Create shared memory client (SPSC-only, no max_clients needed) - client_ = bb::ipc::IpcClient::create_shm(shm_name); - - // Connect to bb server - if (!client_->connect()) { - throw Napi::Error::New(env, "Failed to connect to shared memory server"); - } - - connected_ = true; -} - -MsgpackClientWrapper::~MsgpackClientWrapper() -{ - if (client_ && connected_) { - client_->close(); - } -} - -Napi::Value MsgpackClientWrapper::call(const Napi::CallbackInfo& info) -{ - Napi::Env env = info.Env(); - - if (!connected_) { - throw Napi::Error::New(env, "Client is not connected"); - } - - // Arg 0: msgpack buffer to send - if (info.Length() < 1 || !info[0].IsBuffer()) { - throw Napi::TypeError::New(env, "First argument must be a Buffer"); - } - - auto input_buffer = info[0].As>(); - const uint8_t* input_data = input_buffer.Data(); - size_t input_len = input_buffer.Length(); - - // Send request with retry on backpressure (1s timeout per attempt) - // NOTE: timeout_ns=0 means IMMEDIATE timeout (not infinite wait!) - // Loop until send succeeds - handles case where consumer is temporarily behind - constexpr uint64_t TIMEOUT_NS = 1000000000; // 1 second - while (!client_->send(input_data, input_len, TIMEOUT_NS)) { - // Ring buffer full, consumer is behind - retry - } - - // Receive response with retry (1s timeout per attempt) - // Loop until response is ready - handles case where server is processing. - // - // This waits forever by design, and there is no deadline to tune: a slow bb and a dead one look - // identical from here, so any deadline would eventually fail a legitimately slow call. The cost - // is that a bb process dying mid-call wedges the caller rather than failing it. This call blocks - // the JS thread, so on the main thread the event loop stops, the child's exit is never observed - // (it stays an unreaped zombie, which still answers kill(pid, 0)), and nothing recovers. - // - // Deliberately not addressed. Detecting it needs a liveness signal that survives the zombie - // window — an inherited pipe whose read end sees EOF is the usual answer, since descriptors - // close on death before anything reaps — which means plumbing a descriptor through this client - // and bb. The process on the other end of a synchronous client runs one thread doing hashes and - // signatures, so it is the smallest and least likely thing on the machine to be killed, and the - // fix costs more than the case is worth. The socket backend, which carries the workloads that do - // get killed, detects death and can replace the process; see bb.js BackendOptions.respawn. - std::span response; - while ((response = client_->receive(TIMEOUT_NS)).empty()) { - // Response not ready yet, server is processing - retry - } - - // Create JavaScript Buffer with the response (copy to JS land) - auto js_buffer = Napi::Buffer::Copy(env, response.data(), response.size()); - - // Release the message (for shared memory this frees space in ring buffer) - client_->release(response.size()); - - return js_buffer; -} - -Napi::Value MsgpackClientWrapper::close(const Napi::CallbackInfo& info) -{ - Napi::Env env = info.Env(); - - if (client_ && connected_) { - client_->close(); - connected_ = false; - } - - return env.Undefined(); -} - -Napi::Function MsgpackClientWrapper::get_class(Napi::Env env) -{ - return DefineClass(env, - "MsgpackClient", - { - MsgpackClientWrapper::InstanceMethod("call", &MsgpackClientWrapper::call), - MsgpackClientWrapper::InstanceMethod("close", &MsgpackClientWrapper::close), - }); -} diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp deleted file mode 100644 index e426376d963..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include "barretenberg/ipc/ipc_client.hpp" -#include "napi.h" -#include - -namespace bb::nodejs::msgpack_client { - -/** - * @brief NAPI wrapper for msgpack calls via shared memory IPC - * - * Provides a simple synchronous interface to send msgpack buffers - * to the bb binary via shared memory and receive responses. - */ -class MsgpackClientWrapper : public Napi::ObjectWrap { - public: - MsgpackClientWrapper(const Napi::CallbackInfo& info); - ~MsgpackClientWrapper(); - - /** - * @brief Send a msgpack buffer and receive response - * @param info[0] - Buffer containing msgpack data - * @returns Buffer containing msgpack response - */ - Napi::Value call(const Napi::CallbackInfo& info); - - /** - * @brief Close the shared memory connection - */ - Napi::Value close(const Napi::CallbackInfo& info); - - static Napi::Function get_class(Napi::Env env); - - private: - std::unique_ptr client_; - bool connected_ = false; -}; - -} // namespace bb::nodejs::msgpack_client diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/package.json b/barretenberg/cpp/src/barretenberg/nodejs_module/package.json deleted file mode 100644 index b30468a65e8..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "nodejs_module", - "private": true, - "version": "0.0.0", - "packageManager": "yarn@4.13.0", - "dependencies": { - "node-addon-api": "^8.0.0", - "node-api-headers": "^1.1.0" - }, - "binary": { - "napi_versions": [ - 9 - ] - } -} diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/yarn.lock b/barretenberg/cpp/src/barretenberg/nodejs_module/yarn.lock deleted file mode 100644 index ed311173828..00000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/yarn.lock +++ /dev/null @@ -1,579 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 - languageName: node - linkType: hard - -"@npmcli/agent@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/agent@npm:4.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^11.2.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/fs@npm:5.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10c0/26e376d780f60ff16e874a0ac9bc3399186846baae0b6e1352286385ac134d900cc5dafaded77f38d77f86898fc923ae1cee9d7399f0275b1aa24878915d722b - languageName: node - linkType: hard - -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 - languageName: node - linkType: hard - -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": - version: 7.1.4 - resolution: "agent-base@npm:7.1.4" - checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe - languageName: node - linkType: hard - -"balanced-match@npm:^4.0.2": - version: 4.0.4 - resolution: "balanced-match@npm:4.0.4" - checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b - languageName: node - linkType: hard - -"brace-expansion@npm:^5.0.2": - version: 5.0.4 - resolution: "brace-expansion@npm:5.0.4" - dependencies: - balanced-match: "npm:^4.0.2" - checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a - languageName: node - linkType: hard - -"cacache@npm:^20.0.1": - version: 20.0.3 - resolution: "cacache@npm:20.0.3" - dependencies: - "@npmcli/fs": "npm:^5.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^13.0.0" - lru-cache: "npm:^11.1.0" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^13.0.0" - unique-filename: "npm:^5.0.0" - checksum: 10c0/c7da1ca694d20e8f8aedabd21dc11518f809a7d2b59aa76a1fc655db5a9e62379e465c157ddd2afe34b19230808882288effa6911b2de26a088a6d5645123462 - languageName: node - linkType: hard - -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.3.4": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - -"encoding@npm:^0.1.13": - version: 0.1.13 - resolution: "encoding@npm:0.1.13" - dependencies: - iconv-lite: "npm:^0.6.2" - checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 - languageName: node - linkType: hard - -"err-code@npm:^2.0.2": - version: 2.0.3 - resolution: "err-code@npm:2.0.3" - checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 - languageName: node - linkType: hard - -"exponential-backoff@npm:^3.1.1": - version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" - checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 - languageName: node - linkType: hard - -"fdir@npm:^6.5.0": - version: 6.5.0 - resolution: "fdir@npm:6.5.0" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f - languageName: node - linkType: hard - -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 - languageName: node - linkType: hard - -"glob@npm:^13.0.0": - version: 13.0.2 - resolution: "glob@npm:13.0.2" - dependencies: - minimatch: "npm:^10.1.2" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10c0/3d4b09efa922c4cba9be6d5b9efae14384e7422aeb886eb35fba8a94820b8281474b8d3f16927127fb1a0c8580e18fc00e3fda03c8dc31fa0af3ba918edeeb04 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.1": - version: 4.2.0 - resolution: "http-cache-semantics@npm:4.2.0" - checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^7.0.0": - version: 7.0.2 - resolution: "http-proxy-agent@npm:7.0.2" - dependencies: - agent-base: "npm:^7.1.0" - debug: "npm:^4.3.4" - checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^7.0.1": - version: 7.0.6 - resolution: "https-proxy-agent@npm:7.0.6" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:4" - checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac - languageName: node - linkType: hard - -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 - languageName: node - linkType: hard - -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 - languageName: node - linkType: hard - -"ip-address@npm:^10.0.1": - version: 10.1.0 - resolution: "ip-address@npm:10.1.0" - checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 - languageName: node - linkType: hard - -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce - languageName: node - linkType: hard - -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.2.6 - resolution: "lru-cache@npm:11.2.6" - checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd - languageName: node - linkType: hard - -"make-fetch-happen@npm:^15.0.0": - version: 15.0.3 - resolution: "make-fetch-happen@npm:15.0.3" - dependencies: - "@npmcli/agent": "npm:^4.0.0" - cacache: "npm:^20.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^5.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^6.0.0" - promise-retry: "npm:^2.0.1" - ssri: "npm:^13.0.0" - checksum: 10c0/525f74915660be60b616bcbd267c4a5b59481b073ba125e45c9c3a041bb1a47a2bd0ae79d028eb6f5f95bf9851a4158423f5068539c3093621abb64027e8e461 - languageName: node - linkType: hard - -"minimatch@npm:^10.1.2": - version: 10.2.4 - resolution: "minimatch@npm:10.2.4" - dependencies: - brace-expansion: "npm:^5.0.2" - checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 - languageName: node - linkType: hard - -"minipass-collect@npm:^2.0.1": - version: 2.0.1 - resolution: "minipass-collect@npm:2.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e - languageName: node - linkType: hard - -"minipass-fetch@npm:^5.0.0": - version: 5.0.1 - resolution: "minipass-fetch@npm:5.0.1" - dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^7.0.3" - minipass-sized: "npm:^2.0.0" - minizlib: "npm:^3.0.1" - dependenciesMeta: - encoding: - optional: true - checksum: 10c0/50bcf48c9841ebb25e29a2817468595219c72cfffc7c175a1d7327843c8bef9b72cb01778f46df7eca695dfe47ab98e6167af4cb026ddd80f660842919a5193c - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 - languageName: node - linkType: hard - -"minipass-sized@npm:^2.0.0": - version: 2.0.0 - resolution: "minipass-sized@npm:2.0.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/f9201696a6f6d68610d04c9c83e3d2e5cb9c026aae1c8cbf7e17f386105cb79c1bb088dbc21bf0b1eb4f3fb5df384fd1e7aa3bf1f33868c416ae8c8a92679db8 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0": - version: 3.3.6 - resolution: "minipass@npm:3.3.6" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c - languageName: node - linkType: hard - -"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 - languageName: node - linkType: hard - -"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec - languageName: node - linkType: hard - -"ms@npm:^2.1.3": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 - languageName: node - linkType: hard - -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b - languageName: node - linkType: hard - -"node-addon-api@npm:^8.0.0": - version: 8.0.0 - resolution: "node-addon-api@npm:8.0.0" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/20eb231362cc07c62d9839164473744d985be5d82685214f3750d990d9f61ef366e0ba112a766c925d640ed29b2a500b83568e895dc2444dcd5db01e615aac2b - languageName: node - linkType: hard - -"node-api-headers@npm:^1.1.0": - version: 1.1.0 - resolution: "node-api-headers@npm:1.1.0" - checksum: 10c0/7806d71077348ea199034e8c90a9147038d37fcccc1b85717e48c095fe31783a4f909f5daced4506e6cbce93fba91220bb3fc8626ee0640d26de9860f6500174 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 12.2.0 - resolution: "node-gyp@npm:12.2.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^15.0.0" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.5.4" - tinyglobby: "npm:^0.2.12" - which: "npm:^6.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10c0/3ed046746a5a7d90950cd8b0547332b06598443f31fe213ef4332a7174c7b7d259e1704835feda79b87d3f02e59d7791842aac60642ede4396ab25fdf0f8f759 - languageName: node - linkType: hard - -"nodejs_module@workspace:.": - version: 0.0.0-use.local - resolution: "nodejs_module@workspace:." - dependencies: - node-addon-api: "npm:^8.0.0" - node-api-headers: "npm:^1.1.0" - languageName: unknown - linkType: soft - -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" - dependencies: - abbrev: "npm:^4.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd - languageName: node - linkType: hard - -"p-map@npm:^7.0.2": - version: 7.0.4 - resolution: "p-map@npm:7.0.4" - checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd - languageName: node - linkType: hard - -"path-scurry@npm:^2.0.0": - version: 2.0.1 - resolution: "path-scurry@npm:2.0.1" - dependencies: - lru-cache: "npm:^11.0.0" - minipass: "npm:^7.1.2" - checksum: 10c0/2a16ed0e81fbc43513e245aa5763354e25e787dab0d539581a6c3f0f967461a159ed6236b2559de23aa5b88e7dc32b469b6c47568833dd142a4b24b4f5cd2620 - languageName: node - linkType: hard - -"picomatch@npm:^4.0.3": - version: 4.0.3 - resolution: "picomatch@npm:4.0.3" - checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 - languageName: node - linkType: hard - -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 - languageName: node - linkType: hard - -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" - dependencies: - err-code: "npm:^2.0.2" - retry: "npm:^0.12.0" - checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 - languageName: node - linkType: hard - -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.7.4 - resolution: "semver@npm:7.7.4" - bin: - semver: bin/semver.js - checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.5 - resolution: "socks-proxy-agent@npm:8.0.5" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:^4.3.4" - socks: "npm:^2.8.3" - checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 - languageName: node - linkType: hard - -"socks@npm:^2.8.3": - version: 2.8.7 - resolution: "socks@npm:2.8.7" - dependencies: - ip-address: "npm:^10.0.1" - smart-buffer: "npm:^4.2.0" - checksum: 10c0/2805a43a1c4bcf9ebf6e018268d87b32b32b06fbbc1f9282573583acc155860dc361500f89c73bfbb157caa1b4ac78059eac0ef15d1811eb0ca75e0bdadbc9d2 - languageName: node - linkType: hard - -"ssri@npm:^13.0.0": - version: 13.0.1 - resolution: "ssri@npm:13.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/cf6408a18676c57ff2ed06b8a20dc64bb3e748e5c7e095332e6aecaa2b8422b1e94a739a8453bf65156a8a47afe23757ba4ab52d3ea3b62322dc40875763e17a - languageName: node - linkType: hard - -"tar@npm:^7.5.4": - version: 7.5.11 - resolution: "tar@npm:7.5.11" - dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" - yallist: "npm:^5.0.0" - checksum: 10c0/b6bb420550ef50ef23356018155e956cd83282c97b6128d8d5cfe5740c57582d806a244b2ef0bf686a74ce526babe8b8b9061527623e935e850008d86d838929 - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.12": - version: 0.2.15 - resolution: "tinyglobby@npm:0.2.15" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.3" - checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 - languageName: node - linkType: hard - -"unique-filename@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-filename@npm:5.0.0" - dependencies: - unique-slug: "npm:^6.0.0" - checksum: 10c0/afb897e9cf4c2fb622ea716f7c2bb462001928fc5f437972213afdf1cc32101a230c0f1e9d96fc91ee5185eca0f2feb34127145874975f347be52eb91d6ccc2c - languageName: node - linkType: hard - -"unique-slug@npm:^6.0.0": - version: 6.0.0 - resolution: "unique-slug@npm:6.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10c0/da7ade4cb04eb33ad0499861f82fe95ce9c7c878b7139dc54d140ecfb6a6541c18a5c8dac16188b8b379fe62c0c1f1b710814baac910cde5f4fec06212126c6a - languageName: node - linkType: hard - -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a - languageName: node - linkType: hard - -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 - languageName: node - linkType: hard diff --git a/barretenberg/cpp/src/barretenberg/serialize/msgpack_schema.test.cpp b/barretenberg/cpp/src/barretenberg/serialize/msgpack_check.test.cpp similarity index 65% rename from barretenberg/cpp/src/barretenberg/serialize/msgpack_schema.test.cpp rename to barretenberg/cpp/src/barretenberg/serialize/msgpack_check.test.cpp index 709713beeed..025ac2915bc 100644 --- a/barretenberg/cpp/src/barretenberg/serialize/msgpack_schema.test.cpp +++ b/barretenberg/cpp/src/barretenberg/serialize/msgpack_check.test.cpp @@ -5,7 +5,9 @@ using namespace bb; -// Sanity checking for msgpack +// Sanity checking for the SERIALIZATION_FIELDS completeness/overlap checks. +// The schema-reflection tests that used to live here went with the reflection +// itself; wire schemas are now declared in JSON and generated by ipc-codegen. struct GoodExample { fr a; @@ -42,7 +44,6 @@ struct BadExampleOutOfObject { } } bad_example_out_of_object; -// TODO eventually move to barretenberg TEST(msgpack_tests, msgpack_sanity_sanity) { EXPECT_EQ(msgpack::check_msgpack_method(good_example), ""); @@ -65,23 +66,3 @@ TEST(msgpack_tests, msgpack_sanity_sanity) EXPECT_EQ(msgpack::check_msgpack_method(bad_example_out_of_object), "Some BadExampleOutOfObject SERIALIZATION_FIELDS() params don't exist in object!"); } - -struct ComplicatedSchema { - std::vector> array; - std::optional good_or_not; - fr bare; - std::variant huh; - SERIALIZATION_FIELDS(array, good_or_not, bare, huh); -} complicated_schema; - -TEST(msgpack_tests, msgpack_schema_sanity) -{ - EXPECT_EQ( - msgpack_schema_to_string(good_example), - "{\"__typename\":\"GoodExample\",\"a\":[\"alias\",[\"fr\",\"bin32\"]],\"b\":[\"alias\",[\"fr\",\"bin32\"]]}\n"); - EXPECT_EQ(msgpack_schema_to_string(complicated_schema), - "{\"__typename\":\"ComplicatedSchema\",\"array\":[\"vector\",[[\"array\",[[\"alias\",[\"fr\",\"bin32\"]]," - "20]]]],\"good_or_not\":[\"optional\",[{\"__typename\":\"GoodExample\",\"a\":[\"alias\",[\"fr\"," - "\"bin32\"]],\"b\":[\"alias\",[\"fr\",\"bin32\"]]}]],\"bare\":[\"alias\",[\"fr\",\"bin32\"]],\"huh\":[" - "\"variant\",[[\"alias\",[\"fr\",\"bin32\"]],\"GoodExample\"]]}\n"); -} diff --git a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl.hpp b/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl.hpp index 7c86a5d588b..c0b49c1de8f 100644 --- a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl.hpp @@ -1,9 +1,6 @@ #pragma once // Meant to be the main header included by *.cpp files* that use msgpack. // Note: heavy header due to serialization logic, don't include if msgpack.hpp will do -// CBinding helpers that take a function or a lambda and -// - bind the input as a coded msgpack array of all the arguments (using template metamagic) -// - bind the return value to an out buffer, where the caller must free the memory #include #include @@ -13,10 +10,8 @@ #include "barretenberg/common/try_catch_shim.hpp" #include "msgpack_impl/check_memory_span.hpp" #include "msgpack_impl/concepts.hpp" -#include "msgpack_impl/func_traits.hpp" #include "msgpack_impl/msgpack_impl.hpp" #include "msgpack_impl/name_value_pair_macro.hpp" -#include "msgpack_impl/schema_impl.hpp" #include "msgpack_impl/schema_name.hpp" #include "msgpack_impl/struct_map_impl.hpp" @@ -46,70 +41,3 @@ inline std::pair msgpack_encode_buffer(auto&& obj, memcpy(output, buffer.data(), buffer.size()); return { output, buffer.size() }; } - -// This function is intended to bind a function to a MessagePack-formatted input data, -// perform the function with the unpacked data, then pack the result back into MessagePack format. -// Note: output_out and output_len_out are IN-OUT parameters: -// IN: Caller provides scratch buffer pointer and size -// OUT: Returns actual result buffer (may be scratch or newly allocated) and size -inline void msgpack_cbind_impl(const auto& func, // The function to be applied - const uint8_t* input_in, // The input data in MessagePack format - size_t input_len_in, // The length of the input data - uint8_t** output_out, // IN-OUT: scratch buffer ptr / result buffer ptr - size_t* output_len_out) // IN-OUT: scratch buffer size / result size -{ - using FuncTraits = decltype(get_func_traits()); - // Args: the parameter types of the function as a tuple. - typename FuncTraits::Args params; - - // Unpack the input data into the parameter tuple. - msgpack::unpack(reinterpret_cast(input_in), input_len_in).get().convert(params); - - // Read IN values: caller-provided scratch buffer - uint8_t* scratch_buf = *output_out; - size_t scratch_size = *output_len_out; - - // Apply the function to the parameters, then encode the result into a MessagePack buffer. - // Try to use scratch buffer; allocate if result doesn't fit. - auto [output, output_len] = msgpack_encode_buffer(FuncTraits::apply(func, params), scratch_buf, scratch_size); - - // Write OUT values: actual result buffer and size - // If result fit in scratch, output == scratch_buf (pointer unchanged) - // If result didn't fit, output is newly allocated buffer (pointer changed) - *output_out = output; - *output_len_out = output_len; -} - -// returns a C-style string json of the schema -inline void msgpack_cbind_schema_impl(auto func, uint8_t** output_out, size_t* output_len_out) -{ - (void)func; // unused except for type - // Object representation of the cbind - auto cbind_obj = get_func_traits(); - std::string schema = msgpack_schema_to_string(cbind_obj); - *output_out = static_cast(aligned_alloc(64, schema.size() + 1)); - memcpy(*output_out, schema.c_str(), schema.size() + 1); - *output_len_out = schema.size(); -} - -// The CBIND_NOSCHEMA macro generates a function named 'cname' that decodes the input arguments from msgpack format, -// calls the target function, and then encodes the return value back into msgpack format. It should be used over CBIND -// in cases where we do not want schema generation, such as meta-functions that themselves give information to control -// how the schema is interpreted. -#define CBIND_NOSCHEMA(cname, func) \ - WASM_EXPORT void cname(const uint8_t* input_in, size_t input_len_in, uint8_t** output_out, size_t* output_len_out) \ - { \ - msgpack_cbind_impl(func, input_in, input_len_in, output_out, output_len_out); \ - } - -// The CBIND macro is a convenient utility that abstracts away several steps in binding C functions with msgpack -// serialization. It creates two separate functions: -// 1. cname function: This decodes the input arguments from msgpack format, calls the target function, -// and then encodes the return value back into msgpack format. -// 2. cname##__schema function: This creates a JSON schema of the function's input arguments and return type. -#define CBIND(cname, func) \ - CBIND_NOSCHEMA(cname, func) \ - WASM_EXPORT void cname##__schema(uint8_t** output_out, size_t* output_len_out) \ - { \ - msgpack_cbind_schema_impl(func, output_out, output_len_out); \ - } diff --git a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/func_traits.hpp b/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/func_traits.hpp deleted file mode 100644 index 256d87f64d3..00000000000 --- a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/func_traits.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once -#include "../msgpack.hpp" -#include -#include - -// Base template for function traits -template struct func_traits; - -// Common implementation for all function types -template struct func_traits_base { - using Args = std::tuple::type...>; - Args args; - R ret; - SERIALIZATION_FIELDS(args, ret); - - template static R apply(Func&& f, Tuple&& t) - { - return std::apply([&f](auto&&... args) { return f(std::forward(std::forward(args))...); }, - std::forward(t)); - } -}; - -// Specializations inherit from common base -template struct func_traits : func_traits_base {}; - -template struct func_traits : func_traits_base {}; - -template -struct func_traits : func_traits_base {}; - -// Simplified trait getter -template constexpr auto get_func_traits() -{ - if constexpr (requires { &T::operator(); }) { - return func_traits {}; - } else { - return func_traits{}; - } -} diff --git a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp b/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp deleted file mode 100644 index 35a286abc10..00000000000 --- a/barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp +++ /dev/null @@ -1,220 +0,0 @@ -#pragma once - -#include "schema_name.hpp" -#include -#include -#include -#include - -struct MsgpackSchemaPacker; - -// Forward declare for MsgpackSchemaPacker -template inline void _msgpack_schema_pack(MsgpackSchemaPacker& packer, const T& obj); - -/** - * Define a serialization schema based on compile-time information about a type being serialized. - * This is then consumed by typescript to make bindings. - */ -struct MsgpackSchemaPacker : msgpack::packer { - MsgpackSchemaPacker(msgpack::sbuffer& stream) - : packer(stream) - {} - // For tracking emitted types - std::set emitted_types; - // Returns if already was emitted - bool set_emitted(const std::string& type) { return !emitted_types.insert(type).second; } - - /** - * Pack a type indicating it is an alias of a certain msgpack type - * Packs in the form ["alias", [schema_name, msgpack_name]] - * @param schema_name The CPP type. - * @param msgpack_name The msgpack type. - */ - void pack_alias(const std::string& schema_name, const std::string& msgpack_name) - { - // We will pack a size 2 tuple - pack_array(2); - pack("alias"); - // That has a size 2 tuple as its 2nd arg - pack_array(2); - pack(schema_name); - pack(msgpack_name); - } - - /** - * Pack the schema of a given object. - * @tparam T the object's type. - * @param obj the object. - */ - template void pack_schema(const T& obj) { _msgpack_schema_pack(*this, obj); } - - // Recurse over any templated containers - // Outputs e.g. ['vector', ['sub-type']] - template void pack_template_type(const std::string& schema_name) - { - // We will pack a size 2 tuple - pack_array(2); - pack(schema_name); - pack_array(sizeof...(Args)); - - // Note: if this fails to compile, check first in list of template Arg's - // it may need a msgpack_schema_pack specialization (particularly if it doesn't define SERIALIZATION_FIELDS). - (_msgpack_schema_pack(*this, *std::make_unique()), ...); /* pack schemas of all template Args */ - } - /** - * @brief Encode a type that defines msgpack based on its key value pairs. - * - * @tparam T the msgpack()'able type - * @param packer Our special packer. - * @param object The object in question. - */ - template void pack_with_name(const std::string& type, T const& object) - { - if (set_emitted(type)) { - pack(type); - return; // already emitted - } - msgpack::check_msgpack_usage(object); - // Encode as map - const_cast(object).msgpack([&](auto&... args) { - size_t kv_size = sizeof...(args); - // Calculate the number of entries in our map (half the size of keys + values, plus the typename) - pack_map(uint32_t(1 + kv_size / 2)); - pack("__typename"); - pack(type); - // Pack the map content based on the args to msgpack - _schema_pack_map_content(*this, args...); - }); - } -}; - -// Helper for packing (key, value, key, value, ...) arguments -inline void _schema_pack_map_content(MsgpackSchemaPacker&) -{ - // base case -} - -namespace msgpack_concepts { -template -concept SchemaPackable = requires(T value, MsgpackSchemaPacker packer) { msgpack_schema_pack(packer, value); }; -} // namespace msgpack_concepts - -// Helper for packing (key, value, key, value, ...) arguments -template -inline void _schema_pack_map_content(MsgpackSchemaPacker& packer, - const std::string& key, - const Value& value, - const Rest&... rest) -{ - static_assert( - msgpack_concepts::SchemaPackable, - "see the first type argument in the error trace, it might require a specialization of msgpack_schema_pack"); - packer.pack(key); - msgpack_schema_pack(packer, value); - _schema_pack_map_content(packer, rest...); -} - -template - requires(!msgpack_concepts::HasMsgPackSchema && !msgpack_concepts::HasMsgPack) -inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, T const& obj) -{ - packer.pack(msgpack_schema_name(obj)); -} - -/** - * Schema pack base case for types with no special msgpack method. - * @tparam T the type. - * @param packer the schema packer. - */ -template -inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, T const& obj) -{ - obj.msgpack_schema(packer); -} - -/** - * @brief Encode a type that defines msgpack based on its key value pairs. - * - * @tparam T the msgpack()'able type - * @param packer Our special packer. - * @param object The object in question. - */ -template - requires(!msgpack_concepts::HasMsgPackSchema) -inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, T const& object) -{ - std::string type = msgpack_schema_name(object); - packer.pack_with_name(type, object); -} - -/** - * @brief Helper method for better error reporting. Clang does not give the best errors for argument lists. - */ -template inline void _msgpack_schema_pack(MsgpackSchemaPacker& packer, const T& obj) -{ - static_assert(msgpack_concepts::SchemaPackable, - "see the first type argument in the error trace, it might need a msgpack_schema method!"); - msgpack_schema_pack(packer, obj); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::tuple const&) -{ - packer.pack_template_type("tuple"); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::map const&) -{ - packer.pack_template_type("map"); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::optional const&) -{ - packer.pack_template_type("optional"); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::vector const&) -{ - packer.pack_template_type("vector"); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::variant const&) -{ - packer.pack_template_type("variant"); -} - -template inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::shared_ptr const&) -{ - packer.pack_template_type("shared_ptr"); -} - -// Outputs e.g. ['array', ['array-type', 'N']] -template -inline void msgpack_schema_pack(MsgpackSchemaPacker& packer, std::array const&) -{ - // We will pack a size 2 tuple - packer.pack_array(2); - packer.pack("array"); - // That has a size 2 tuple as its 2nd arg - packer.pack_array(2); /* param list format for consistency*/ - // To avoid WASM problems with large stack objects, we use a heap allocation. - // Small note: This works because make_unique goes of scope only when the whole line is done. - _msgpack_schema_pack(packer, *std::make_unique()); - packer.pack(N); -} - -/** - * @brief Print's an object's derived msgpack schema as a string. - * - * @param obj The object to print schema of. - * @return std::string The schema as a string. - */ -inline std::string msgpack_schema_to_string(const auto& obj) -{ - msgpack::sbuffer output; - MsgpackSchemaPacker printer{ output }; - _msgpack_schema_pack(printer, obj); - msgpack::object_handle oh = msgpack::unpack(output.data(), output.size()); - std::stringstream pretty_output; - pretty_output << oh.get() << std::endl; - return pretty_output.str(); -} diff --git a/barretenberg/cpp/src/barretenberg/serialize/test_helper.hpp b/barretenberg/cpp/src/barretenberg/serialize/test_helper.hpp index 456e703e016..742d9fc8170 100644 --- a/barretenberg/cpp/src/barretenberg/serialize/test_helper.hpp +++ b/barretenberg/cpp/src/barretenberg/serialize/test_helper.hpp @@ -15,24 +15,3 @@ template std::pair msgpack_roundtrip(const T& object) msgpack::unpack(buffer.data(), buffer.size()).get().convert(result); return { object, result }; } - -template inline T call_msgpack_cbind(auto cbind_func, auto... test_args) -{ - auto [input, input_len] = msgpack_encode_buffer(std::make_tuple(test_args...)); - uint8_t* output; - size_t output_len; - cbind_func(input, input_len, &output, &output_len); - T actual_ret; - msgpack::unpack((const char*)output, output_len).get().convert(actual_ret); - aligned_free(output); - return actual_ret; -} - -// Running the end-to-end tests that msgpack bind creates -// This should suffice in testing the binding interface, function tests can be separate -inline auto call_func_and_wrapper(auto func, auto cbind_func, auto... test_args) -{ - auto expected_ret = func(test_args...); - auto actual_ret = call_msgpack_cbind(cbind_func, test_args...); - return std::make_pair(actual_ret, expected_ret); -} diff --git a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md index b768a9f5a93..f6cf8749745 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md +++ b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md @@ -58,8 +58,8 @@ Configure once: `cmake --preset clang20-assert`. All commands below are from `ba - Binary: `barretenberg/cpp/build/bin/bb-avm`. 2. **`vm2_tests`** — AVM unit tests. - Build: `cmake --build --preset clang20-assert --target vm2_tests`. Run from `barretenberg/cpp/build`: `./bin/vm2_tests --gtest_filter="*test_name*"`. -3. **`nodejs_module`** — Fast simulation only; builds quickly. - - Build: `cmake --build --preset clang20-assert --target nodejs_module`. For TS: from `barretenberg/cpp/`, run `(cd ../../barretenberg/ts/; ./bootstrap.sh build_bb_js)` then bootstrap `yarn-project` (see yarn-project/CLAUDE.md). +3. **`bb-avm-sim`** — Simulation-only IPC server that TS drives. + - Build: `cmake --build --preset clang20-assert --target bb-avm-sim`. For TS: from `barretenberg/cpp/`, run `(cd ../ts && ./bootstrap.sh build_bb_avm_sim)` then bootstrap `yarn-project` (see yarn-project/CLAUDE.md). ## AVM and TypeScript diff --git a/barretenberg/cpp/src/barretenberg/vm2/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2/CMakeLists.txt index b83d54d01dc..928e9be4553 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/vm2/CMakeLists.txt @@ -21,7 +21,6 @@ barretenberg_module_with_sources( DEPENDENCIES crypto_merkle_tree world_state_reference ) -# vm2_sim needs -fPIC because it's linked into the nodejs_module shared library set_target_properties(vm2_sim_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) # Build vm2 module with sources mutually exclusive from vm2_sim diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/world_state_reference/CMakeLists.txt index 45c1965b414..5a481ae7851 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/CMakeLists.txt @@ -1,5 +1,4 @@ # Standalone in-memory reference world state + world-state vocabulary. # vm2-free and single-threaded (no thread_pool) so it also builds under the fuzzing preset. barretenberg_module(world_state_reference crypto_merkle_tree aztec crypto_poseidon2) -# -fPIC: reaches the nodejs_module shared library via vm2_sim. set_target_properties(world_state_reference_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/barretenberg/docs/bootstrap.sh b/barretenberg/docs/bootstrap.sh index d958b5835dd..5806453a499 100755 --- a/barretenberg/docs/bootstrap.sh +++ b/barretenberg/docs/bootstrap.sh @@ -32,10 +32,12 @@ function build { } function test_cmds { - # The recursive example proves a circuit embedding a full in-circuit Honk verifier in WASM; on the - # default 2-CPU budget that prove+verify lands ~140-340s and intermittently trips the jest timeout. - # A 4-CPU budget roughly halves it, back well under the per-test cap. - echo "$hash:CPUS=4 barretenberg/docs/bootstrap.sh test" + # The recursive example proves a circuit embedding a full in-circuit Honk verifier in WASM. It takes + # ~35s on an idle box, but this lane runs alongside the bb build, and when that rebuilds everything + # the test slows by an order of magnitude (>400s seen on 4 CPUs). The published bb.js it uses is + # not built from this tree, so that slowdown says nothing about the change under test: budget for + # it here, and in the test's own jest timeout, rather than fail on it. + echo "$hash:CPUS=4:TIMEOUT=1500s barretenberg/docs/bootstrap.sh test" } function test { diff --git a/barretenberg/docs/examples/recursive.test.ts b/barretenberg/docs/examples/recursive.test.ts index 0fe99b09d02..11cfa493a4e 100644 --- a/barretenberg/docs/examples/recursive.test.ts +++ b/barretenberg/docs/examples/recursive.test.ts @@ -117,6 +117,7 @@ describe('Recursive Aggregation Example', () => { expect(recursiveInputs.public_inputs).toEqual([2]); }); + // Generous because CI runs this alongside a full bb build; see test_cmds in ../bootstrap.sh. it('should generate recursive proof', async () => { // docs:start:recursive_proof // Generate witness for recursive circuit @@ -134,5 +135,5 @@ describe('Recursive Aggregation Example', () => { // Verify the recursive proof const isValid = await recursiveBackend.verifyProof(recursiveProofData); expect(isValid).toBe(true); - }, 360000); + }, 1200000); }); diff --git a/barretenberg/rust/.rebuild_patterns b/barretenberg/rust/.rebuild_patterns index fe1aab37fbf..5ec39b9b5a3 100644 --- a/barretenberg/rust/.rebuild_patterns +++ b/barretenberg/rust/.rebuild_patterns @@ -1,2 +1,6 @@ ^barretenberg/rust/.*\.(rs|toml)$ ^barretenberg/rust/bootstrap.sh +^ipc-codegen/src/.*\.ts$ +^ipc-codegen/templates/rust/ +^ipc-runtime/(rust|cpp)/ +^barretenberg/cpp/src/barretenberg/bbapi/bb_schema\.json$ diff --git a/barretenberg/rust/Cargo.toml b/barretenberg/rust/Cargo.toml index 4ba3a07b711..9e121e0d12f 100644 --- a/barretenberg/rust/Cargo.toml +++ b/barretenberg/rust/Cargo.toml @@ -22,12 +22,10 @@ tokio = { version = "1.35", features = ["full"] } # IPC and system libc = "0.2" -nix = { version = "0.27", features = ["socket", "uio"] } # Testing criterion = "0.5" # Utilities thiserror = "1.0" -tracing = "0.1" hex = "0.4" diff --git a/barretenberg/rust/README.md b/barretenberg/rust/README.md index 177c295090f..53b6958301d 100644 --- a/barretenberg/rust/README.md +++ b/barretenberg/rust/README.md @@ -4,16 +4,18 @@ Rust bindings for the Barretenberg cryptographic library using msgpack protocol. ## Quick Start -### Using PipeBackend (default) +### Using an IPC transport (no linking required) -Communicates with BB via stdin/stdout - no linking required: +Talk to a `bb msgpack run` process over ipc-runtime. `IpcClient` implements the +`Backend` trait, so this crate carries no transport code of its own: use +`from_fds` for a child's stdin/stdout pipe, or `from_path` for a `.sock` (UDS) +or `.shm` (shared memory) endpoint. ```rust -use barretenberg_rs::{BarretenbergApi, backends::PipeBackend}; +use barretenberg_rs::{ipc_runtime::IpcClient, BbApi}; -// Create a pipe backend (requires BB binary) -let backend = PipeBackend::new("/path/to/bb", Some(4))?; -let mut api = BarretenbergApi::new(backend); +let client = IpcClient::from_path("/tmp/bb.sock")?; +let mut api = BbApi::new(client); // Hash some data let response = api.blake2s(b"hello world")?; @@ -28,13 +30,13 @@ api.destroy()?; Calls Barretenberg directly via FFI - maximum performance, enabled by default: ```rust -use barretenberg_rs::{BarretenbergApi, backends::FfiBackend}; +use barretenberg_rs::{BbApi, FfiBackend}; // Create FFI backend (links to libbarretenberg automatically) let backend = FfiBackend::new()?; -let mut api = BarretenbergApi::new(backend); +let mut api = BbApi::new(backend); -// Same API as PipeBackend +// Same API as the IPC transports let response = api.blake2s(b"hello world")?; println!("Hash: {:?}", response.hash); ``` @@ -46,13 +48,14 @@ The library path is automatically configured via `build.rs`. The crate provides a pluggable backend system: -- **PipeBackend**: Spawns BB process, communicates via stdin/stdout pipes +- **ipc_runtime::IpcClient**: Talks to a `bb msgpack run` process over a pipe, + UDS or shared memory; the transport lives in ipc-runtime, not here - **FfiBackend**: Direct C FFI calls to libbarretenberg (no process overhead) - **Custom Backend**: Implement the `Backend` trait for WASM, JSI, or other IPC ``` ┌─────────────────────────────────────────┐ -│ BarretenbergApi │ +│ BbApi │ │ (generated, type-safe Rust API) │ └─────────────────────────────────────────┘ │ @@ -64,7 +67,7 @@ The crate provides a pluggable backend system: │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ -│ PipeBackend │ │ FfiBackend │ +│ IpcClient │ │ FfiBackend │ │ (bb process) │ │ (libbarretenberg)│ └─────────────────┘ └─────────────────┘ ``` @@ -76,19 +79,41 @@ The crate provides a pluggable backend system: cargo test --release # Run tests without FFI (pipe backend only) -cargo test --release --no-default-features --features native +cargo test --release --no-default-features --features ipc-runtime ``` ## Generated Code -The API is auto-generated from the BB msgpack schema. To regenerate: +The API is generated by ipc-codegen from bb's checked-in schema +(`barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json`). To regenerate: ```bash -cd ../ts && yarn generate +./bootstrap.sh generate ``` ## Features -- `native` (default): Enables `PipeBackend` and async runtime +- `ipc-runtime` (default): Enables the `ipc_runtime::IpcClient` transport backend - `ffi` (default): Enables `FfiBackend` for direct C FFI calls, auto-links to cpp/build/lib -- `async`: Enables async/await support + +## Migrating from 6.x + +7.0 replaces the hand-written API with the generated one, and the old names +are gone. Older versions stay on crates.io, so pin `barretenberg-rs = "6"` (or +earlier) until you migrate. + +- `BarretenbergApi` is now `BbApi`; `backends::FfiBackend` is `FfiBackend`. +- `PipeBackend::new(bb, threads)` is gone. Spawn `bb msgpack run` yourself and + pass its stdout/stdin to `ipc_runtime::IpcClient::from_fds`, or connect to a + socket or shared-memory endpoint with `IpcClient::from_path`. +- Scalars and field elements are typed (`Fr`, `Fq`, `Secp256k1Fr`, ...: all + 32-byte `Bin32` values) instead of `&[u8]` / `Vec`, both in arguments + and in response fields. Build them with `Fr::from_u64`, `Fr::from_be_bytes` + or `.into()` from a `[u8; 32]`; read them as slices through `Deref`. +- Fixed-size byte arguments (AES key/IV, ECDSA `r`/`s`, compressed points) take + arrays rather than slices. +- `Point`, the `types`, `backend`, `error`, `api` and `generated_types` module + paths, and `shutdown()` are removed. Generated types live in + `barretenberg_rs::generated::bb_types`; bb exits when its connection closes, + so `destroy()` (or drop) replaces `shutdown()`. +- The `native` feature is now `ipc-runtime`, and `async` is gone. diff --git a/barretenberg/rust/barretenberg-rs/Cargo.toml b/barretenberg/rust/barretenberg-rs/Cargo.toml index f637a2a82a5..3c495e9d212 100644 --- a/barretenberg/rust/barretenberg-rs/Cargo.toml +++ b/barretenberg/rust/barretenberg-rs/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" keywords = ["zero-knowledge", "snark", "plonk", "noir", "aztec"] categories = ["cryptography", "api-bindings"] # Include generated files in the published crate even though they're gitignored. -# They're generated by `yarn generate` in ts/ and must exist on disk before `cargo publish`. +# They're generated by `./bootstrap.sh generate` and must exist on disk before `cargo publish`. include = ["src/**/*.rs", "build.rs", "README.md"] [dependencies] @@ -21,21 +21,20 @@ rmp-serde.workspace = true rmpv.workspace = true serde.workspace = true -# Async runtime -tokio = { workspace = true, optional = true } - # IPC and system libc.workspace = true -nix = { workspace = true, optional = true } + +# UDS / MPSC-SHM transport. The generated Backend trait carries a bridge impl +# for ipc_runtime::IpcClient, so this crate ships no transport code of its own. +ipc-runtime = { path = "../../../ipc-runtime/rust", version = "0.1.0", optional = true } # Utilities thiserror.workspace = true -tracing = { workspace = true, optional = true } hex.workspace = true [features] -default = ["native", "ffi"] -native = ["tokio", "nix", "tracing"] -async = ["tokio"] +default = ["ipc-runtime", "ffi"] # FFI backend - links against libbarretenberg from cpp build ffi = [] +# Transport backends (pipe, UDS, shared memory), provided by ipc-runtime. +ipc-runtime = ["dep:ipc-runtime"] diff --git a/barretenberg/rust/barretenberg-rs/src/backend.rs b/barretenberg/rust/barretenberg-rs/src/backend.rs deleted file mode 100644 index 75eef08387c..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/backend.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Backend trait for msgpack communication -//! -//! This module defines a simple, pluggable interface for Barretenberg backends. -//! Users can easily implement custom backends (FFI, WASM, IPC, etc.). - -use crate::error::Result; - -/// Simple interface for msgpack backend implementations. -/// -/// Implement this trait to create a custom backend for Barretenberg. -/// The backend handles msgpack-encoded command/response communication. -/// -/// # Example -/// -/// ```ignore -/// struct MyCustomBackend { -/// // your FFI handle, connection, etc. -/// } -/// -/// impl Backend for MyCustomBackend { -/// fn call(&mut self, input: &[u8]) -> Result> { -/// // Send input to your backend -/// // Return the response -/// } -/// -/// fn destroy(&mut self) -> Result<()> { -/// // Clean up resources -/// Ok(()) -/// } -/// } -/// ``` -pub trait Backend { - /// Execute a msgpack command and return the msgpack response. - /// - /// # Arguments - /// * `input` - Msgpack-encoded command - /// - /// # Returns - /// Msgpack-encoded response - fn call(&mut self, input: &[u8]) -> Result>; - - /// Clean up resources and shutdown the backend. - fn destroy(&mut self) -> Result<()>; -} diff --git a/barretenberg/rust/barretenberg-rs/src/backends/ffi.rs b/barretenberg/rust/barretenberg-rs/src/backends/ffi.rs deleted file mode 100644 index 22a4243d92e..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/backends/ffi.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! FFI backend for Barretenberg -//! -//! This backend calls the Barretenberg C API directly via FFI, -//! eliminating process spawn overhead. Ideal for mobile and embedded use cases. -//! -//! # Requirements -//! -//! This backend requires linking against `libbarretenberg`. You must: -//! 1. Build Barretenberg as a static library (`libbarretenberg.a`) -//! 2. Configure the library search path, either via: -//! - `.cargo/config.toml`: `[build] rustflags = ["-L", "/path/to/lib"]` -//! - Environment: `RUSTFLAGS="-L /path/to/lib"` -//! -//! # Example -//! -//! ```ignore -//! use barretenberg_rs::{BarretenbergApi, backends::FfiBackend}; -//! -//! let backend = FfiBackend::new()?; -//! let mut api = BarretenbergApi::new(backend); -//! -//! let response = api.blake2s(b"hello world")?; -//! println!("Hash: {:?}", response.hash); -//! ``` - -use crate::backend::Backend; -use crate::error::{BarretenbergError, Result}; -use std::ptr; - -// C API exported by Barretenberg -// See: barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp -// Link directives are in build.rs to control link order (barretenberg depends on env) -extern "C" { - /// Execute a msgpack-encoded command and return msgpack-encoded response. - /// - /// # Safety - /// - `input_in` must point to valid memory of `input_len_in` bytes - /// - `output_out` and `output_len_out` must be valid pointers - /// - Caller must free `*output_out` using `libc::free` - fn bbapi( - input_in: *const u8, - input_len_in: usize, - output_out: *mut *mut u8, - output_len_out: *mut usize, - ); -} - -/// FFI backend that calls Barretenberg directly via C API. -/// -/// This is the most performant backend option as it avoids process spawning -/// and IPC overhead. However, it requires linking against `libbarretenberg`. -/// -/// # Thread Safety -/// -/// This backend is **not** thread-safe. Each thread should have its own -/// `FfiBackend` instance, or access should be synchronized externally. -pub struct FfiBackend { - _initialized: bool, -} - -impl FfiBackend { - /// Create a new FFI backend. - /// - /// # Errors - /// - /// Returns an error if Barretenberg initialization fails. - pub fn new() -> Result { - // Future: Could add SRS initialization here if needed - // For now, Barretenberg initializes lazily on first use - Ok(Self { _initialized: true }) - } -} - -impl Backend for FfiBackend { - fn call(&mut self, input: &[u8]) -> Result> { - let mut output_ptr: *mut u8 = ptr::null_mut(); - let mut output_len: usize = 0; - - // SAFETY: - // - input.as_ptr() is valid for input.len() bytes - // - output_ptr and output_len are valid stack pointers - // - bbapi allocates output using malloc, which we free below - unsafe { - bbapi( - input.as_ptr(), - input.len(), - &mut output_ptr, - &mut output_len, - ); - } - - if output_ptr.is_null() { - return Err(BarretenbergError::Backend( - "bbapi returned null pointer".to_string(), - )); - } - - if output_len == 0 { - // Free the pointer even if length is 0 - unsafe { - libc::free(output_ptr as *mut libc::c_void); - } - return Err(BarretenbergError::Backend( - "bbapi returned empty response".to_string(), - )); - } - - // SAFETY: output_ptr is valid for output_len bytes, allocated by malloc - let output = unsafe { std::slice::from_raw_parts(output_ptr, output_len).to_vec() }; - - // Free the C-allocated memory - // SAFETY: output_ptr was allocated by bbapi using malloc - unsafe { - libc::free(output_ptr as *mut libc::c_void); - } - - Ok(output) - } - - fn destroy(&mut self) -> Result<()> { - // No cleanup needed - Barretenberg manages its own state - // Future: Could send Shutdown command here if needed - self._initialized = false; - Ok(()) - } -} - -impl Drop for FfiBackend { - fn drop(&mut self) { - let _ = self.destroy(); - } -} - -impl Default for FfiBackend { - fn default() -> Self { - Self::new().expect("Failed to initialize FfiBackend") - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::api::BarretenbergApi; - - #[test] - fn test_ffi_backend_creation() { - let backend = FfiBackend::new(); - assert!(backend.is_ok()); - } - - #[test] - fn test_ffi_blake2s() { - let backend = FfiBackend::new().unwrap(); - let mut api = BarretenbergApi::new(backend); - - let response = api.blake2s(b"hello world").unwrap(); - assert_eq!(response.hash.len(), 32); - - // Verify deterministic output - let response2 = api.blake2s(b"hello world").unwrap(); - assert_eq!(response.hash, response2.hash); - } -} diff --git a/barretenberg/rust/barretenberg-rs/src/backends/pipe.rs b/barretenberg/rust/barretenberg-rs/src/backends/pipe.rs deleted file mode 100644 index 2e3cd248061..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/backends/pipe.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Pipe backend for Barretenberg -//! -//! This backend communicates with the BB binary via stdin/stdout pipes, -//! using a 4-byte little-endian length prefix protocol. - -use crate::backend::Backend; -use crate::error::{BarretenbergError, Result}; -use std::io::{Read, Write}; -use std::path::Path; -use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; - -/// Pipe backend implementation using stdin/stdout -pub struct PipeBackend { - stdin: ChildStdin, - stdout: ChildStdout, - process: Option, -} - -impl PipeBackend { - /// Create a new pipe backend by spawning the BB process - /// - /// # Arguments - /// * `bb_binary_path` - Path to the BB binary - /// * `threads` - Number of threads for BB to use - pub fn new(bb_binary_path: impl AsRef, threads: Option) -> Result { - // Build command - let mut cmd = Command::new(bb_binary_path.as_ref()); - cmd.arg("msgpack") - .arg("run") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); - - // Note: BB uses HARDWARE_CONCURRENCY env var for thread control - if let Some(t) = threads { - cmd.env("HARDWARE_CONCURRENCY", t.to_string()); - } - - // Spawn the process - let mut process = cmd.spawn() - .map_err(|e| BarretenbergError::Backend(format!("Failed to spawn BB process: {}", e)))?; - - // Take stdin and stdout handles - let stdin = process.stdin.take() - .ok_or_else(|| BarretenbergError::Backend("Failed to get stdin handle".to_string()))?; - let stdout = process.stdout.take() - .ok_or_else(|| BarretenbergError::Backend("Failed to get stdout handle".to_string()))?; - - // Check if process exited immediately (indicates startup failure) - if let Ok(Some(status)) = process.try_wait() { - return Err(BarretenbergError::Backend( - format!("BB process exited immediately with status: {}", status) - )); - } - - Ok(Self { - stdin, - stdout, - process: Some(process), - }) - } - - /// Send data with length prefix - fn send_with_prefix(&mut self, data: &[u8]) -> Result<()> { - let len = data.len() as u32; - self.stdin.write_all(&len.to_le_bytes()) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write length: {}", e)))?; - self.stdin.write_all(data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write data: {}", e)))?; - self.stdin.flush() - .map_err(|e| BarretenbergError::Ipc(format!("Failed to flush stdin: {}", e)))?; - Ok(()) - } - - /// Receive data with length prefix - fn receive_with_prefix(&mut self) -> Result> { - let mut len_buf = [0u8; 4]; - self.stdout.read_exact(&mut len_buf) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read length: {}", e)))?; - - let len = u32::from_le_bytes(len_buf) as usize; - - let mut data = vec![0u8; len]; - self.stdout.read_exact(&mut data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read data: {}", e)))?; - - Ok(data) - } -} - -impl Backend for PipeBackend { - fn call(&mut self, input: &[u8]) -> Result> { - self.send_with_prefix(input)?; - self.receive_with_prefix() - } - - fn destroy(&mut self) -> Result<()> { - // Kill the process if it's still running - if let Some(mut process) = self.process.take() { - let _ = process.kill(); - let _ = process.wait(); - } - - Ok(()) - } -} - -impl Drop for PipeBackend { - fn drop(&mut self) { - let _ = self.destroy(); - } -} diff --git a/barretenberg/rust/barretenberg-rs/src/error.rs b/barretenberg/rust/barretenberg-rs/src/error.rs deleted file mode 100644 index 726ac4a9ad2..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Error types for Barretenberg operations - -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum BarretenbergError { - #[error("Serialization error: {0}")] - Serialization(String), - - #[error("Deserialization error: {0}")] - Deserialization(String), - - #[error("Backend error: {0}")] - Backend(String), - - #[error("IPC error: {0}")] - Ipc(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Invalid response: {0}")] - InvalidResponse(String), - - #[error("Connection error: {0}")] - Connection(String), - - #[error("WASM error: {0}")] - Wasm(String), -} - -pub type Result = std::result::Result; diff --git a/barretenberg/rust/barretenberg-rs/src/fr_ext.rs b/barretenberg/rust/barretenberg-rs/src/fr_ext.rs new file mode 100644 index 00000000000..81914e4f862 --- /dev/null +++ b/barretenberg/rust/barretenberg-rs/src/fr_ext.rs @@ -0,0 +1,52 @@ +//! Constructors and accessors on the generated `Fr` type. Kept as a separate +//! impl block here rather than inside `bb_types.rs` so the generated file +//! stays a pure regen target. + +use crate::generated::bb_types::{Bin32, Fr}; + +impl From for [u8; 32] { + fn from(value: Bin32) -> Self { + value.0 + } +} + +impl Fr { + /// Create a field element from a u64 value (big-endian, matching the + /// C++ msgpack representation). + pub fn from_u64(value: u64) -> Self { + let mut bytes = [0u8; 32]; + bytes[24..32].copy_from_slice(&value.to_be_bytes()); + Self(bytes) + } + + /// Create a field element from 32 big-endian bytes. + pub fn from_be_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Create a field element from 32 little-endian bytes. + pub fn from_le_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Create a field element from a 32-byte buffer (no reduction). + /// Panics if the buffer is not exactly 32 bytes long. + pub fn from_buffer(buffer: &[u8]) -> Self { + let bytes: [u8; 32] = buffer.try_into().expect("Buffer must be exactly 32 bytes"); + Self(bytes) + } + + /// Create a field element from a byte slice, truncating or zero-padding + /// to 32 bytes as needed. + pub fn from_buffer_reduce(buffer: &[u8]) -> Self { + let mut bytes = [0u8; 32]; + let len = buffer.len().min(32); + bytes[..len].copy_from_slice(&buffer[..len]); + Self(bytes) + } + + /// Convert to a byte buffer (as used in msgpack). + pub fn to_buffer(&self) -> Vec { + self.0.to_vec() + } +} diff --git a/barretenberg/rust/barretenberg-rs/src/lib.rs b/barretenberg/rust/barretenberg-rs/src/lib.rs index ffccd08c514..9d479100122 100644 --- a/barretenberg/rust/barretenberg-rs/src/lib.rs +++ b/barretenberg/rust/barretenberg-rs/src/lib.rs @@ -3,21 +3,20 @@ //! High-performance Rust bindings to the Barretenberg cryptographic library //! using msgpack protocol over pluggable backends. //! -//! ## Usage with PipeBackend +//! ## Usage over an IPC transport +//! +//! Point `ipc_runtime::IpcClient` at a running `bb msgpack run --input ` +//! (a `.sock` for UDS, `.shm` for shared memory); it implements [`Backend`] +//! directly, so no transport code lives in this crate. //! //! ```ignore -//! use barretenberg_rs::{BarretenbergApi, backends::PipeBackend}; +//! use barretenberg_rs::{ipc_runtime::IpcClient, BbApi}; //! -//! // Create a pipe backend (requires BB binary) -//! let backend = PipeBackend::new("/path/to/bb", Some(4))?; -//! let mut api = BarretenbergApi::new(backend); +//! let client = IpcClient::from_path("/tmp/bb.sock")?; +//! let mut api = BbApi::new(client); //! -//! // Use the API //! let response = api.blake2s(b"hello world")?; //! println!("Hash: {:?}", response.hash); -//! -//! // Cleanup -//! api.destroy()?; //! ``` //! //! ## Custom Backend @@ -46,30 +45,30 @@ //! } //! ``` -pub mod backend; -pub mod types; -pub mod api; -pub mod error; +// Generated by ipc-codegen from barretenberg/cpp/src/barretenberg/bbapi/ +// bb_schema.json. Regenerate with `cd ../../ts/bb.js && yarn generate`. +// The Backend trait, error type and FFI backend are generated too, so the +// client never depends on hand-maintained copies of its own dependencies. +pub mod generated { + pub mod backend; + pub mod bb_client; + pub mod bb_types; + pub mod error; + + #[cfg(feature = "ffi")] + pub mod ffi_backend; +} -// Generated types from msgpack schema -// Run: cd ../ts && yarn generate -pub mod generated_types; +mod fr_ext; -pub use backend::Backend; -pub use types::{Fr, Point}; -pub use generated_types::{Command, Response, GrumpkinPoint}; -pub use api::BarretenbergApi; -pub use error::{BarretenbergError, Result}; +pub use generated::backend::Backend; +pub use generated::bb_client::BbApi; +pub use generated::bb_types::{Bin32, Command, Fq, Fr, Response}; +pub use generated::error::{IpcError as BarretenbergError, Result}; -/// Backend implementations -pub mod backends { - #[cfg(feature = "native")] - pub mod pipe; - #[cfg(feature = "native")] - pub use pipe::PipeBackend; +#[cfg(feature = "ffi")] +pub use generated::ffi_backend::FfiBackend; - #[cfg(feature = "ffi")] - pub mod ffi; - #[cfg(feature = "ffi")] - pub use ffi::FfiBackend; -} +// Re-exported so callers get a transport without taking a separate dependency. +#[cfg(feature = "ipc-runtime")] +pub use ipc_runtime; diff --git a/barretenberg/rust/barretenberg-rs/src/types.rs b/barretenberg/rust/barretenberg-rs/src/types.rs deleted file mode 100644 index 6ad04b26ecd..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/types.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Core utility types for Barretenberg operations - -use serde::{Deserialize, Serialize}; - -/// Field element (Fr) - 254-bit field element for BN254 -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Fr(pub [u8; 32]); - -impl Fr { - /// Create a new field element from a u64 value (big-endian encoding, matching C++ msgpack format) - pub fn from_u64(value: u64) -> Self { - let mut bytes = [0u8; 32]; - bytes[24..32].copy_from_slice(&value.to_be_bytes()); - Fr(bytes) - } - - /// Create a field element from bytes (big-endian) - pub fn from_be_bytes(bytes: [u8; 32]) -> Self { - Fr(bytes) - } - - /// Create a field element from bytes (little-endian) - pub fn from_le_bytes(bytes: [u8; 32]) -> Self { - Fr(bytes) - } - - /// Create a field element from a 32-byte buffer (no reduction) - /// Panics if buffer is not exactly 32 bytes - pub fn from_buffer(buffer: &[u8]) -> Self { - let bytes: [u8; 32] = buffer.try_into().expect("Buffer must be exactly 32 bytes"); - Fr(bytes) - } - - /// Create a field element from a byte slice, reducing if necessary - pub fn from_buffer_reduce(buffer: &[u8]) -> Self { - let mut bytes = [0u8; 32]; - let len = buffer.len().min(32); - bytes[..len].copy_from_slice(&buffer[..len]); - Fr(bytes) - } - - /// Convert to a byte buffer (as used in msgpack) - pub fn to_buffer(&self) -> Vec { - self.0.to_vec() - } -} - -/// Point on the elliptic curve (affine_element) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Point { - pub x: [u8; 32], - pub y: [u8; 32], -} diff --git a/barretenberg/rust/bootstrap.sh b/barretenberg/rust/bootstrap.sh index 8c05fe8e50b..b49097e57f4 100755 --- a/barretenberg/rust/bootstrap.sh +++ b/barretenberg/rust/bootstrap.sh @@ -2,15 +2,31 @@ # Use ci3 script base. source $(git rev-parse --show-toplevel)/ci3/source_bootstrap -# Hash depends on ts because ts generates the Rust bindings -hash=$(hash_str $(../ts/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) +# The crate generates its own bindings, so the hash covers the schema and +# ipc-codegen (see .rebuild_patterns) rather than the whole ts build. +hash=$(hash_str $(cache_content_hash .rebuild_patterns)) + +# Generate the Rust client from the checked-in bb schema via ipc-codegen. +# --ffi also emits the Backend trait, error type and FFI backend, so the +# generated client never depends on hand-maintained copies of them. +function generate { + local root=$(git rev-parse --show-toplevel) + (cd barretenberg-rs && node --experimental-strip-types --experimental-transform-types --no-warnings \ + "$root/ipc-codegen/src/generate.ts" \ + --schema "$root/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json" \ + --lang rust \ + --client \ + --ffi \ + --strip-method-prefix \ + --strip-type-prefix \ + --out src/generated) +} function build { echo_header "barretenberg-rs build" if ! cache_download barretenberg-rs-$hash.tar.gz; then - # Generate Rust bindings from msgpack schema (uses ts-node, no build needed) - (cd ../ts/bb.js && yarn generate) + generate # Build all targets # BB_LIB_DIR tells build.rs to use local lib instead of downloading (ffi feature is on by default) @@ -18,7 +34,7 @@ function build { BB_LIB_DIR="$(cd ../cpp/build/lib && pwd)" denoise "cargo build --release" # Upload build artifacts and generated source files to cache - cache_upload barretenberg-rs-$hash.tar.gz target/release barretenberg-rs/src/generated_types.rs barretenberg-rs/src/api.rs + cache_upload barretenberg-rs-$hash.tar.gz target/release barretenberg-rs/src/generated fi } @@ -36,9 +52,9 @@ function test { source "$HOME/.cargo/env" fi - # Run PipeBackend tests (spawns bb binary) - # Use --no-default-features to skip FFI (which requires libbb-external.a) - denoise "cargo test --release --no-default-features --features native" + # Transport tests (spawn bb and talk to it through ipc-runtime) + # Transport tests only: --no-default-features skips FFI, which needs libbb-external.a + denoise "cargo test --release --no-default-features --features ipc-runtime" # Run FFI backend tests (requires libbb-external.a from cpp build) # BB_LIB_DIR tells build.rs to use local lib instead of downloading @@ -46,6 +62,10 @@ function test { BB_LIB_DIR="$(cd ../cpp/build/lib && pwd)" RUSTFLAGS="-C link-arg=-Wl,--allow-multiple-definition" denoise "cargo test --release --features ffi" } +function crate_published { + curl -sf -H "User-Agent: aztec-packages-ci (tech@aztec-labs.com)" "https://crates.io/api/v1/crates/$1/$2" | jq -e '.version.num' &>/dev/null +} + function release { echo_header "barretenberg-rs release" @@ -58,13 +78,17 @@ function release { sed -i "s/^version = \".*\"/version = \"$version\"/" Cargo.toml # Generated files must exist (created during build step, or generate now) - if [ ! -f barretenberg-rs/src/api.rs ] || [ ! -f barretenberg-rs/src/generated_types.rs ]; then - echo "Generated files not found, running yarn generate..." - (cd ../ts/bb.js && yarn generate) + if [ ! -d barretenberg-rs/src/generated ]; then + echo "Generated files not found, generating..." + generate fi + # ipc-runtime's release (which runs first) publishes its crate at this + # version; pin that exact build. + sed -i -E "s|^(ipc-runtime = \{ path = \"[^\"]*\", version = \")[^\"]*|\1=$version|" barretenberg-rs/Cargo.toml + # Check if this version is already published on crates.io (idempotent re-runs). - if curl -sf -H "User-Agent: aztec-packages-ci (tech@aztec-labs.com)" "https://crates.io/api/v1/crates/barretenberg-rs/$version" | jq -e '.version.num' &>/dev/null; then + if crate_published barretenberg-rs $version; then echo "barretenberg-rs@$version already published on crates.io. Skipping." return 0 fi diff --git a/barretenberg/rust/scripts/run_test.sh b/barretenberg/rust/scripts/run_test.sh index 02cf98907bd..3936ae9d71b 100755 --- a/barretenberg/rust/scripts/run_test.sh +++ b/barretenberg/rust/scripts/run_test.sh @@ -8,9 +8,9 @@ if [ -f "$HOME/.cargo/env" ]; then source "$HOME/.cargo/env" fi -# Run PipeBackend tests (spawns bb binary) -# Use --no-default-features to skip FFI (which requires libbb-external.a) -denoise "cargo test --release --no-default-features --features native" +# Transport tests (spawn bb and talk to it through ipc-runtime) +# Transport tests only: --no-default-features skips FFI, which needs libbb-external.a +denoise "cargo test --release --no-default-features --features ipc-runtime" # Run FFI backend tests (requires libbb-external.a from cpp build) # BB_LIB_DIR tells build.rs to use local lib instead of downloading diff --git a/barretenberg/rust/tests/Cargo.toml b/barretenberg/rust/tests/Cargo.toml index c52ddf88621..83f6c37915e 100644 --- a/barretenberg/rust/tests/Cargo.toml +++ b/barretenberg/rust/tests/Cargo.toml @@ -9,7 +9,7 @@ publish = false ffi = ["barretenberg-rs/ffi"] [dependencies] -barretenberg-rs = { path = "../barretenberg-rs", default-features = false, features = ["native", "async"] } +barretenberg-rs = { path = "../barretenberg-rs", default-features = false, features = ["ipc-runtime"] } # Serialization serde.workspace = true diff --git a/barretenberg/rust/tests/src/blake2s.rs b/barretenberg/rust/tests/src/blake2s.rs index a2bc57d8965..7d22ddbf032 100644 --- a/barretenberg/rust/tests/src/blake2s.rs +++ b/barretenberg/rust/tests/src/blake2s.rs @@ -5,7 +5,9 @@ //! These tests require the BB binary to be built. They are skipped if the binary is not found. #[cfg(test)] -use barretenberg_rs::{backends::PipeBackend, BarretenbergApi, Fr}; +use barretenberg_rs::Fr; +#[cfg(test)] +use crate::utils::spawn_bb; #[cfg(test)] use crate::utils::get_bb_binary_path; #[cfg(test)] @@ -16,9 +18,7 @@ fn test_blake2s() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let input = b"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"; let expected: [u8; 32] = [ @@ -43,9 +43,7 @@ fn test_blake2s_to_field() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let input = b"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"; // Blake2sToField returns the hash reduced to a field element @@ -54,7 +52,7 @@ fn test_blake2s_to_field() { 20, 232, 66, 122, 55, 70, 227, 140, 193, 28, 146, 32, 181, 158, 18, 66, ]; - let expected = Fr(expected_field); + let expected = Fr::from_be_bytes(expected_field); let response = api.blake2s_to_field(input).expect("Blake2sToField failed"); let result = Fr::from_buffer_reduce(&response.field); diff --git a/barretenberg/rust/tests/src/debug_msgpack.rs b/barretenberg/rust/tests/src/debug_msgpack.rs index 5ab317a5d8b..88e80397e0a 100644 --- a/barretenberg/rust/tests/src/debug_msgpack.rs +++ b/barretenberg/rust/tests/src/debug_msgpack.rs @@ -1,7 +1,7 @@ //! Debug msgpack serialization format #[cfg(test)] -use barretenberg_rs::{generated_types::*, Fr}; +use barretenberg_rs::{generated::bb_types::*, Fr}; #[test] fn test_msgpack_format() { @@ -22,10 +22,7 @@ fn test_msgpack_format() { #[test] fn test_pedersen_msgpack_format() { - let inputs: Vec> = vec![ - Fr::from_u64(4).to_buffer().to_vec(), - Fr::from_u64(8).to_buffer().to_vec(), - ]; + let inputs = vec![Fr::from_u64(4).into(), Fr::from_u64(8).into()]; let cmd = Command::PedersenHash(PedersenHash::new(inputs, 7)); let bytes = rmp_serde::to_vec_named(&vec![cmd]).unwrap(); diff --git a/barretenberg/rust/tests/src/ffi/aes.rs b/barretenberg/rust/tests/src/ffi/aes.rs index c9fc8173140..9c455b8216f 100644 --- a/barretenberg/rust/tests/src/ffi/aes.rs +++ b/barretenberg/rust/tests/src/ffi/aes.rs @@ -3,7 +3,7 @@ //! Ported from zkpassport/aztec-packages bb_rs aes_tests.rs #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, BarretenbergApi}; +use barretenberg_rs::{FfiBackend, BbApi}; /// Apply PKCS#7 padding to input data (for testing purposes) #[cfg(test)] @@ -44,7 +44,7 @@ fn remove_pkcs7_padding(data: &[u8]) -> Result, &'static str> { #[test] fn test_aes_encrypt_decrypt_roundtrip() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let plaintext = b"Hello, AES world! This is a test message for encryption."; let key = [ @@ -61,7 +61,7 @@ fn test_aes_encrypt_decrypt_roundtrip() { let length = padded_plaintext.len() as u32; let encrypt_response = api - .aes_encrypt(&padded_plaintext, &iv, &key, length) + .aes_encrypt(&padded_plaintext, iv, key, length) .expect("aes_encrypt failed"); let ciphertext = &encrypt_response.ciphertext; @@ -69,7 +69,7 @@ fn test_aes_encrypt_decrypt_roundtrip() { assert!(!ciphertext.is_empty()); let decrypt_response = api - .aes_decrypt(ciphertext, &iv, &key, length) + .aes_decrypt(ciphertext, iv, key, length) .expect("aes_decrypt failed"); let decrypted_with_padding = &decrypt_response.plaintext; @@ -86,7 +86,7 @@ fn test_aes_encrypt_decrypt_roundtrip() { #[test] fn test_aes_buffer_encrypt_decrypt() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let plaintext = b"AES buffer test message"; let key = [ @@ -103,12 +103,12 @@ fn test_aes_buffer_encrypt_decrypt() { let length = padded_plaintext.len() as u32; let encrypt_response = api - .aes_encrypt(&padded_plaintext, &iv, &key, length) + .aes_encrypt(&padded_plaintext, iv, key, length) .expect("aes_encrypt failed"); assert!(!encrypt_response.ciphertext.is_empty()); let decrypt_response = api - .aes_decrypt(&encrypt_response.ciphertext, &iv, &key, length) + .aes_decrypt(&encrypt_response.ciphertext, iv, key, length) .expect("aes_decrypt failed"); let decrypted_with_padding = &decrypt_response.plaintext; @@ -124,7 +124,7 @@ fn test_aes_buffer_encrypt_decrypt() { #[test] fn test_aes_different_keys_produce_different_outputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let plaintext = b"Test message for key difference"; let key1 = [ @@ -145,10 +145,10 @@ fn test_aes_different_keys_produce_different_outputs() { let length = padded_plaintext.len() as u32; let encrypt_response1 = api - .aes_encrypt(&padded_plaintext, &iv, &key1, length) + .aes_encrypt(&padded_plaintext, iv, key1, length) .expect("aes_encrypt failed"); let encrypt_response2 = api - .aes_encrypt(&padded_plaintext, &iv, &key2, length) + .aes_encrypt(&padded_plaintext, iv, key2, length) .expect("aes_encrypt failed"); // Different keys should produce different ciphertexts @@ -160,7 +160,7 @@ fn test_aes_different_keys_produce_different_outputs() { #[test] fn test_aes_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let plaintext = b"Deterministic test message"; let key = [ @@ -176,10 +176,10 @@ fn test_aes_deterministic() { let length = padded_plaintext.len() as u32; let encrypt_response1 = api - .aes_encrypt(&padded_plaintext, &iv, &key, length) + .aes_encrypt(&padded_plaintext, iv, key, length) .expect("aes_encrypt failed"); let encrypt_response2 = api - .aes_encrypt(&padded_plaintext, &iv, &key, length) + .aes_encrypt(&padded_plaintext, iv, key, length) .expect("aes_encrypt failed"); // Encryption should be deterministic for the same inputs diff --git a/barretenberg/rust/tests/src/ffi/blake2s.rs b/barretenberg/rust/tests/src/ffi/blake2s.rs index 871f07c0e78..fa4ceacdda4 100644 --- a/barretenberg/rust/tests/src/ffi/blake2s.rs +++ b/barretenberg/rust/tests/src/ffi/blake2s.rs @@ -3,12 +3,12 @@ //! Parallels barretenberg/ts/bb.js/src/barretenberg/blake2s.test.ts #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, BarretenbergApi, Fr}; +use barretenberg_rs::{FfiBackend, BbApi, Fr}; #[test] fn test_blake2s() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let input = b"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"; let expected: [u8; 32] = [ @@ -31,7 +31,7 @@ fn test_blake2s() { #[test] fn test_blake2s_to_field() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let input = b"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"; // Blake2sToField returns the hash reduced to a field element @@ -40,7 +40,7 @@ fn test_blake2s_to_field() { 20, 232, 66, 122, 55, 70, 227, 140, 193, 28, 146, 32, 181, 158, 18, 66, ]; - let expected = Fr(expected_field); + let expected = Fr::from_be_bytes(expected_field); let response = api.blake2s_to_field(input).expect("Blake2sToField failed"); let result = Fr::from_buffer_reduce(&response.field); diff --git a/barretenberg/rust/tests/src/ffi/bn254.rs b/barretenberg/rust/tests/src/ffi/bn254.rs index 4e7e0aa70f5..251195e0fd8 100644 --- a/barretenberg/rust/tests/src/ffi/bn254.rs +++ b/barretenberg/rust/tests/src/ffi/bn254.rs @@ -4,25 +4,25 @@ #[cfg(test)] use barretenberg_rs::{ - backends::FfiBackend, - generated_types::{Bn254G1Point, Bn254G2Point}, - BarretenbergApi, + FfiBackend, + generated::bb_types::{Bn254G1Point, Bn254G2Point, Fq}, + BbApi, }; #[test] fn test_bn254_fr_sqrt_of_zero() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Square root of zero should be zero - let zero = vec![0u8; 32]; + let zero = [0u8; 32]; - let response = api.bn254_fr_sqrt(&zero).expect("bn254_fr_sqrt failed"); + let response = api.bn254_fr_sqrt(zero.into()).expect("bn254_fr_sqrt failed"); assert!(response.is_square_root, "Square root of zero should exist"); assert_eq!( response.value, - vec![0u8; 32], + [0u8; 32], "Square root of zero should be zero" ); @@ -32,13 +32,13 @@ fn test_bn254_fr_sqrt_of_zero() { #[test] fn test_bn254_fr_sqrt_of_one() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Square root of one should be one - let mut one = vec![0u8; 32]; + let mut one = [0u8; 32]; one[31] = 1; - let response = api.bn254_fr_sqrt(&one).expect("bn254_fr_sqrt failed"); + let response = api.bn254_fr_sqrt(one.into()).expect("bn254_fr_sqrt failed"); assert!(response.is_square_root, "Square root of one should exist"); assert_eq!( @@ -52,18 +52,18 @@ fn test_bn254_fr_sqrt_of_one() { #[test] fn test_bn254_fr_sqrt_of_four() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Square root of four should be two - let mut four = vec![0u8; 32]; + let mut four = [0u8; 32]; four[31] = 4; - let response = api.bn254_fr_sqrt(&four).expect("bn254_fr_sqrt failed"); + let response = api.bn254_fr_sqrt(four.into()).expect("bn254_fr_sqrt failed"); assert!(response.is_square_root, "Square root of four should exist"); // The square root should be 2 - let mut expected = vec![0u8; 32]; + let mut expected = [0u8; 32]; expected[31] = 2; assert_eq!( response.value, expected, @@ -76,13 +76,13 @@ fn test_bn254_fr_sqrt_of_four() { #[test] fn test_bn254_fr_sqrt_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let mut input = vec![0u8; 32]; + let mut input = [0u8; 32]; input[31] = 16; // Perfect square - let response1 = api.bn254_fr_sqrt(&input).expect("bn254_fr_sqrt failed"); - let response2 = api.bn254_fr_sqrt(&input).expect("bn254_fr_sqrt failed"); + let response1 = api.bn254_fr_sqrt(input.into()).expect("bn254_fr_sqrt failed"); + let response2 = api.bn254_fr_sqrt(input.into()).expect("bn254_fr_sqrt failed"); // Should be deterministic assert_eq!(response1.is_square_root, response2.is_square_root); @@ -94,33 +94,33 @@ fn test_bn254_fr_sqrt_deterministic() { #[test] fn test_bn254_g1_mul_consistency() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // BN254 G1 generator: (1, 2) - let mut x = vec![0u8; 32]; + let mut x = [0u8; 32]; x[31] = 1; - let mut y = vec![0u8; 32]; + let mut y = [0u8; 32]; y[31] = 2; - let g1 = Bn254G1Point { x, y }; + let g1 = Bn254G1Point { x: x.into(), y: y.into() }; - let mut two = vec![0u8; 32]; + let mut two = [0u8; 32]; two[31] = 2; - let mut three = vec![0u8; 32]; + let mut three = [0u8; 32]; three[31] = 3; // Verify 2*(3*G1) == 3*(2*G1) == 6*G1 let result_3g = api - .bn254_g1_mul(g1.clone(), &three) + .bn254_g1_mul(g1.clone(), three.into()) .expect("bn254_g1_mul(3) failed"); let result_2_of_3g = api - .bn254_g1_mul(result_3g.point.clone(), &two) + .bn254_g1_mul(result_3g.point.clone(), two.into()) .expect("bn254_g1_mul(2*3G) failed"); let result_2g = api - .bn254_g1_mul(g1.clone(), &two) + .bn254_g1_mul(g1.clone(), two.into()) .expect("bn254_g1_mul(2) failed"); let result_3_of_2g = api - .bn254_g1_mul(result_2g.point.clone(), &three) + .bn254_g1_mul(result_2g.point.clone(), three.into()) .expect("bn254_g1_mul(3*2G) failed"); assert_eq!(result_2_of_3g.point.x, result_3_of_2g.point.x); @@ -138,28 +138,28 @@ fn test_bn254_g1_mul_consistency() { fn bn254_g2_generator() -> Bn254G2Point { Bn254G2Point { x: [ - vec![ + Fq::from_bytes([ 0x18, 0x00, 0xde, 0xef, 0x12, 0x1f, 0x1e, 0x76, 0x42, 0x6a, 0x00, 0x66, 0x5e, 0x5c, 0x44, 0x79, 0x67, 0x43, 0x22, 0xd4, 0xf7, 0x5e, 0xda, 0xdd, 0x46, 0xde, 0xbd, 0x5c, 0xd9, 0x92, 0xf6, 0xed, - ], - vec![ + ]), + Fq::from_bytes([ 0x19, 0x8e, 0x93, 0x93, 0x92, 0x0d, 0x48, 0x3a, 0x72, 0x60, 0xbf, 0xb7, 0x31, 0xfb, 0x5d, 0x25, 0xf1, 0xaa, 0x49, 0x33, 0x35, 0xa9, 0xe7, 0x12, 0x97, 0xe4, 0x85, 0xb7, 0xae, 0xf3, 0x12, 0xc2, - ], + ]), ], y: [ - vec![ + Fq::from_bytes([ 0x12, 0xc8, 0x5e, 0xa5, 0xdb, 0x8c, 0x6d, 0xeb, 0x4a, 0xab, 0x71, 0x80, 0x8d, 0xcb, 0x40, 0x8f, 0xe3, 0xd1, 0xe7, 0x69, 0x0c, 0x43, 0xd3, 0x7b, 0x4c, 0xe6, 0xcc, 0x01, 0x66, 0xfa, 0x7d, 0xaa, - ], - vec![ + ]), + Fq::from_bytes([ 0x09, 0x06, 0x89, 0xd0, 0x58, 0x5f, 0xf0, 0x75, 0xec, 0x9e, 0x99, 0xad, 0x69, 0x0c, 0x33, 0x95, 0xbc, 0x4b, 0x31, 0x33, 0x70, 0xb3, 0x8e, 0xf3, 0x55, 0xac, 0xda, 0xdc, 0xd1, 0x22, 0x97, 0x5b, - ], + ]), ], } } @@ -167,30 +167,30 @@ fn bn254_g2_generator() -> Bn254G2Point { #[test] fn test_bn254_g2_mul_consistency() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let g2 = bn254_g2_generator(); // Compute 3*G2 directly - let mut three = vec![0u8; 32]; + let mut three = [0u8; 32]; three[31] = 3; let result_3g = api - .bn254_g2_mul(g2.clone(), &three) + .bn254_g2_mul(g2.clone(), three.into()) .expect("bn254_g2_mul(3) failed"); // Compute 3*G2 as 2*G2 then add G2 via scalar mul of the same point: // We can verify by computing 6*G2 two ways: 2*(3*G2) vs 3*(2*G2) - let mut two = vec![0u8; 32]; + let mut two = [0u8; 32]; two[31] = 2; let result_2_of_3g = api - .bn254_g2_mul(result_3g.point.clone(), &two) + .bn254_g2_mul(result_3g.point.clone(), two.into()) .expect("bn254_g2_mul(2*3G) failed"); let result_2g = api - .bn254_g2_mul(g2.clone(), &two) + .bn254_g2_mul(g2.clone(), two.into()) .expect("bn254_g2_mul(2) failed"); let result_3_of_2g = api - .bn254_g2_mul(result_2g.point.clone(), &three) + .bn254_g2_mul(result_2g.point.clone(), three.into()) .expect("bn254_g2_mul(3*2G) failed"); // 2*(3*G2) == 3*(2*G2) == 6*G2 @@ -206,17 +206,17 @@ fn test_bn254_g2_mul_consistency() { #[test] fn test_bn254_fq_sqrt() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Test Fq sqrt (base field) with a perfect square - let mut four = vec![0u8; 32]; + let mut four = [0u8; 32]; four[31] = 4; - let response = api.bn254_fq_sqrt(&four).expect("bn254_fq_sqrt failed"); + let response = api.bn254_fq_sqrt(four.into()).expect("bn254_fq_sqrt failed"); assert!(response.is_square_root, "Square root of four in Fq should exist"); - let mut expected = vec![0u8; 32]; + let mut expected = [0u8; 32]; expected[31] = 2; assert_eq!(response.value, expected); diff --git a/barretenberg/rust/tests/src/ffi/ecdsa.rs b/barretenberg/rust/tests/src/ffi/ecdsa.rs index 1a4ce845512..f4bd65f3ec5 100644 --- a/barretenberg/rust/tests/src/ffi/ecdsa.rs +++ b/barretenberg/rust/tests/src/ffi/ecdsa.rs @@ -3,12 +3,12 @@ //! Tests for ECDSA secp256k1 signatures. #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, generated_types::Secp256k1Point, BarretenbergApi}; +use barretenberg_rs::{FfiBackend, generated::bb_types::Secp256k1Point, BbApi}; #[test] fn test_ecdsa_compute_public_key() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // A valid secp256k1 private key (32 bytes) let private_key: [u8; 32] = [ @@ -18,14 +18,14 @@ fn test_ecdsa_compute_public_key() { ]; let response = api - .ecdsa_secp256k1_compute_public_key(&private_key) + .ecdsa_secp256k1_compute_public_key(private_key.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); // Should return a valid public key point assert_eq!(response.public_key.x.len(), 32); assert_eq!(response.public_key.y.len(), 32); // Should not be all zeros - assert_ne!(response.public_key.x, vec![0u8; 32]); + assert_ne!(response.public_key.x, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -33,7 +33,7 @@ fn test_ecdsa_compute_public_key() { #[test] fn test_ecdsa_compute_public_key_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key: [u8; 32] = [ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, @@ -42,10 +42,10 @@ fn test_ecdsa_compute_public_key_deterministic() { ]; let response1 = api - .ecdsa_secp256k1_compute_public_key(&private_key) + .ecdsa_secp256k1_compute_public_key(private_key.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); let response2 = api - .ecdsa_secp256k1_compute_public_key(&private_key) + .ecdsa_secp256k1_compute_public_key(private_key.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); // Same private key should produce same public key @@ -58,7 +58,7 @@ fn test_ecdsa_compute_public_key_deterministic() { #[test] fn test_ecdsa_different_private_keys() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key1: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -72,10 +72,10 @@ fn test_ecdsa_different_private_keys() { ]; let response1 = api - .ecdsa_secp256k1_compute_public_key(&private_key1) + .ecdsa_secp256k1_compute_public_key(private_key1.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); let response2 = api - .ecdsa_secp256k1_compute_public_key(&private_key2) + .ecdsa_secp256k1_compute_public_key(private_key2.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); // Different private keys should produce different public keys @@ -90,7 +90,7 @@ fn test_ecdsa_different_private_keys() { #[test] fn test_ecdsa_sign_and_verify() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Private key let private_key: [u8; 32] = [ @@ -101,7 +101,7 @@ fn test_ecdsa_sign_and_verify() { // Compute public key let pub_key_response = api - .ecdsa_secp256k1_compute_public_key(&private_key) + .ecdsa_secp256k1_compute_public_key(private_key.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); let public_key = Secp256k1Point { x: pub_key_response.public_key.x.clone(), @@ -117,7 +117,7 @@ fn test_ecdsa_sign_and_verify() { // Sign let sign_response = api - .ecdsa_secp256k1_construct_signature(&message_hash, &private_key) + .ecdsa_secp256k1_construct_signature(&message_hash, private_key.into()) .expect("ecdsa_secp256k1_construct_signature failed"); // Signature should have r, s, and v components @@ -126,7 +126,7 @@ fn test_ecdsa_sign_and_verify() { // Verify let verify_response = api - .ecdsa_secp256k1_verify_signature(&message_hash, public_key, &sign_response.r, &sign_response.s, sign_response.v) + .ecdsa_secp256k1_verify_signature(&message_hash, public_key, sign_response.r, sign_response.s, sign_response.v) .expect("ecdsa_secp256k1_verify_signature failed"); assert!(verify_response.verified, "Signature should be valid"); @@ -137,7 +137,7 @@ fn test_ecdsa_sign_and_verify() { #[test] fn test_ecdsa_verify_wrong_message() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -146,7 +146,7 @@ fn test_ecdsa_verify_wrong_message() { ]; let pub_key_response = api - .ecdsa_secp256k1_compute_public_key(&private_key) + .ecdsa_secp256k1_compute_public_key(private_key.into()) .expect("ecdsa_secp256k1_compute_public_key failed"); let public_key = Secp256k1Point { x: pub_key_response.public_key.x.clone(), @@ -158,12 +158,12 @@ fn test_ecdsa_verify_wrong_message() { // Sign with message_hash1 let sign_response = api - .ecdsa_secp256k1_construct_signature(&message_hash1, &private_key) + .ecdsa_secp256k1_construct_signature(&message_hash1, private_key.into()) .expect("ecdsa_secp256k1_construct_signature failed"); // Verify with message_hash2 - should fail let verify_response = api - .ecdsa_secp256k1_verify_signature(&message_hash2, public_key, &sign_response.r, &sign_response.s, sign_response.v) + .ecdsa_secp256k1_verify_signature(&message_hash2, public_key, sign_response.r, sign_response.s, sign_response.v) .expect("ecdsa_secp256k1_verify_signature failed"); assert!(!verify_response.verified, "Signature should be invalid for wrong message"); diff --git a/barretenberg/rust/tests/src/ffi/grumpkin.rs b/barretenberg/rust/tests/src/ffi/grumpkin.rs index 1482485000a..01747a069c0 100644 --- a/barretenberg/rust/tests/src/ffi/grumpkin.rs +++ b/barretenberg/rust/tests/src/ffi/grumpkin.rs @@ -3,7 +3,7 @@ //! Ported from zkpassport/aztec-packages bb_rs grumpkin_tests.rs #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, generated_types::GrumpkinPoint, BarretenbergApi}; +use barretenberg_rs::{FfiBackend, generated::bb_types::GrumpkinPoint, BbApi}; // Grumpkin generator point // x = 1 @@ -24,28 +24,28 @@ fn grumpkin_generator() -> GrumpkinPoint { 0x27, 0x2c, ]; GrumpkinPoint { - x: x.to_vec(), - y: y.to_vec(), + x: x.into(), + y: y.into(), } } #[test] fn test_grumpkin_scalar_multiplication() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = grumpkin_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 3; // scalar = 3 let response = api - .grumpkin_mul(point.clone(), &scalar) + .grumpkin_mul(point.clone(), scalar.into()) .expect("grumpkin_mul failed"); // Result should be different from input (3*G != G) assert_ne!(response.point.x, point.x); // Result should be a valid point (non-zero) - assert_ne!(response.point.x, vec![0u8; 32]); + assert_ne!(response.point.x, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -53,15 +53,15 @@ fn test_grumpkin_scalar_multiplication() { #[test] fn test_grumpkin_scalar_multiplication_by_one() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = grumpkin_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 1; // scalar = 1 let response = api - .grumpkin_mul(point.clone(), &scalar) + .grumpkin_mul(point.clone(), scalar.into()) .expect("grumpkin_mul failed"); // Multiplying by 1 should give the same point @@ -74,7 +74,7 @@ fn test_grumpkin_scalar_multiplication_by_one() { #[test] fn test_grumpkin_random_scalar_generation() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let response1 = api .grumpkin_get_random_fr(0) @@ -86,8 +86,8 @@ fn test_grumpkin_random_scalar_generation() { // Random scalars should be different (very high probability) assert_ne!(response1.value, response2.value); // Should not be zero - assert_ne!(response1.value, vec![0u8; 32]); - assert_ne!(response2.value, vec![0u8; 32]); + assert_ne!(response1.value, [0u8; 32]); + assert_ne!(response2.value, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -95,7 +95,7 @@ fn test_grumpkin_random_scalar_generation() { #[test] fn test_grumpkin_random_scalar_multiple_calls() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let mut scalars = Vec::new(); for _ in 0..10 { @@ -122,7 +122,7 @@ fn test_grumpkin_random_scalar_multiple_calls() { #[test] fn test_grumpkin_reduce512() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let large_input = [0xffu8; 64]; // Maximum 512-bit value @@ -131,7 +131,7 @@ fn test_grumpkin_reduce512() { .expect("grumpkin_reduce512 failed"); // Should produce a valid field element - assert_ne!(response.value, vec![0u8; 32]); + assert_ne!(response.value, [0u8; 32]); // Should be different from the first 32 bytes of input (since we're reducing) assert_ne!(response.value.as_slice(), &large_input[..32]); @@ -141,7 +141,7 @@ fn test_grumpkin_reduce512() { #[test] fn test_grumpkin_reduce512_small_value() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let mut small_input = [0u8; 64]; small_input[63] = 42; // A small value @@ -151,7 +151,7 @@ fn test_grumpkin_reduce512_small_value() { .expect("grumpkin_reduce512 failed"); // For a small value, the reduction should preserve it - let mut expected = vec![0u8; 32]; + let mut expected = [0u8; 32]; expected[31] = 42; assert_eq!(response.value, expected); @@ -161,7 +161,7 @@ fn test_grumpkin_reduce512_small_value() { #[test] fn test_grumpkin_reduce512_zero() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let zero_input = [0u8; 64]; @@ -170,7 +170,7 @@ fn test_grumpkin_reduce512_zero() { .expect("grumpkin_reduce512 failed"); // Zero should remain zero after reduction - assert_eq!(response.value, vec![0u8; 32]); + assert_eq!(response.value, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -178,18 +178,18 @@ fn test_grumpkin_reduce512_zero() { #[test] fn test_grumpkin_mul_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = grumpkin_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 5; let result1 = api - .grumpkin_mul(point.clone(), &scalar) + .grumpkin_mul(point.clone(), scalar.into()) .expect("grumpkin_mul failed"); let result2 = api - .grumpkin_mul(point, &scalar) + .grumpkin_mul(point, scalar.into()) .expect("grumpkin_mul failed"); // Should be deterministic @@ -202,7 +202,7 @@ fn test_grumpkin_mul_deterministic() { #[test] fn test_grumpkin_reduce512_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let large_scalar_512 = [0xffu8; 64]; diff --git a/barretenberg/rust/tests/src/ffi/mod.rs b/barretenberg/rust/tests/src/ffi/mod.rs index c41986242e2..69b4123eeea 100644 --- a/barretenberg/rust/tests/src/ffi/mod.rs +++ b/barretenberg/rust/tests/src/ffi/mod.rs @@ -1,7 +1,7 @@ //! FFI backend tests //! //! These tests use FfiBackend which links directly to libbarretenberg. -//! They parallel the PipeBackend tests but use direct FFI calls for better performance. +//! They parallel the pipe transport tests but use direct FFI calls for better performance. //! //! Enable with: cargo test --features ffi diff --git a/barretenberg/rust/tests/src/ffi/pedersen.rs b/barretenberg/rust/tests/src/ffi/pedersen.rs index e317b5bccd3..dbee47ad76d 100644 --- a/barretenberg/rust/tests/src/ffi/pedersen.rs +++ b/barretenberg/rust/tests/src/ffi/pedersen.rs @@ -3,17 +3,17 @@ //! Ported from zkpassport/aztec-packages bb_rs pedersen_tests.rs #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, BarretenbergApi, Fr}; +use barretenberg_rs::{FfiBackend, BbApi, Fr}; #[test] fn test_pedersen_hash_basic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Test with simple inputs let inputs = vec![ - Fr::from_u64(1).to_buffer(), - Fr::from_u64(2).to_buffer(), + Fr::from_u64(1), + Fr::from_u64(2), ]; let response = api.pedersen_hash(inputs, 0).expect("PedersenHash failed"); @@ -21,7 +21,7 @@ fn test_pedersen_hash_basic() { // Result should be a valid field element (32 bytes) assert_eq!(response.hash.len(), 32); // Should not be zero - assert_ne!(response.hash, vec![0u8; 32]); + assert_ne!(response.hash, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -29,11 +29,11 @@ fn test_pedersen_hash_basic() { #[test] fn test_pedersen_hash_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs = vec![ - Fr::from_u64(42).to_buffer(), - Fr::from_u64(123).to_buffer(), + Fr::from_u64(42), + Fr::from_u64(123), ]; let response1 = api.pedersen_hash(inputs.clone(), 0).expect("PedersenHash failed"); @@ -48,15 +48,15 @@ fn test_pedersen_hash_deterministic() { #[test] fn test_pedersen_hash_different_inputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs1 = vec![ - Fr::from_u64(1).to_buffer(), - Fr::from_u64(2).to_buffer(), + Fr::from_u64(1), + Fr::from_u64(2), ]; let inputs2 = vec![ - Fr::from_u64(3).to_buffer(), - Fr::from_u64(4).to_buffer(), + Fr::from_u64(3), + Fr::from_u64(4), ]; let response1 = api.pedersen_hash(inputs1, 0).expect("PedersenHash failed"); @@ -71,14 +71,14 @@ fn test_pedersen_hash_different_inputs() { #[test] fn test_pedersen_hash_single_input() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let inputs = vec![Fr::from_u64(42).to_buffer()]; + let inputs = vec![Fr::from_u64(42)]; let response = api.pedersen_hash(inputs, 0).expect("PedersenHash failed"); assert_eq!(response.hash.len(), 32); - assert_ne!(response.hash, vec![0u8; 32]); + assert_ne!(response.hash, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -86,14 +86,14 @@ fn test_pedersen_hash_single_input() { #[test] fn test_pedersen_hash_zero_input() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let inputs = vec![Fr::from_u64(0).to_buffer()]; + let inputs = vec![Fr::from_u64(0)]; let response = api.pedersen_hash(inputs.clone(), 0).expect("PedersenHash failed"); // Even zero input should produce non-zero output - assert_ne!(response.hash, vec![0u8; 32]); + assert_ne!(response.hash, [0u8; 32]); assert_ne!(response.hash, inputs[0]); api.destroy().expect("Failed to destroy backend"); @@ -102,15 +102,15 @@ fn test_pedersen_hash_zero_input() { #[test] fn test_pedersen_hash_many_inputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Test with many inputs - let inputs: Vec> = (0..10).map(|i| Fr::from_u64(i).to_buffer()).collect(); + let inputs: Vec = (0..10).map(Fr::from_u64).collect(); let response = api.pedersen_hash(inputs, 0).expect("PedersenHash failed"); assert_eq!(response.hash.len(), 32); - assert_ne!(response.hash, vec![0u8; 32]); + assert_ne!(response.hash, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -118,11 +118,11 @@ fn test_pedersen_hash_many_inputs() { #[test] fn test_pedersen_hash_different_hash_indices() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs = vec![ - Fr::from_u64(1).to_buffer(), - Fr::from_u64(2).to_buffer(), + Fr::from_u64(1), + Fr::from_u64(2), ]; let response1 = api.pedersen_hash(inputs.clone(), 0).expect("PedersenHash failed"); @@ -137,11 +137,11 @@ fn test_pedersen_hash_different_hash_indices() { #[test] fn test_pedersen_commit_basic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs = vec![ - Fr::from_u64(1).to_buffer(), - Fr::from_u64(2).to_buffer(), + Fr::from_u64(1), + Fr::from_u64(2), ]; let response = api.pedersen_commit(inputs, 0).expect("PedersenCommit failed"); @@ -150,7 +150,7 @@ fn test_pedersen_commit_basic() { assert_eq!(response.point.x.len(), 32); assert_eq!(response.point.y.len(), 32); // Should not be the point at infinity - assert_ne!(response.point.x, vec![0u8; 32]); + assert_ne!(response.point.x, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -158,11 +158,11 @@ fn test_pedersen_commit_basic() { #[test] fn test_pedersen_commit_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs = vec![ - Fr::from_u64(42).to_buffer(), - Fr::from_u64(123).to_buffer(), + Fr::from_u64(42), + Fr::from_u64(123), ]; let response1 = api.pedersen_commit(inputs.clone(), 0).expect("PedersenCommit failed"); @@ -178,15 +178,15 @@ fn test_pedersen_commit_deterministic() { #[test] fn test_pedersen_commit_different_inputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs1 = vec![ - Fr::from_u64(1).to_buffer(), - Fr::from_u64(2).to_buffer(), + Fr::from_u64(1), + Fr::from_u64(2), ]; let inputs2 = vec![ - Fr::from_u64(3).to_buffer(), - Fr::from_u64(4).to_buffer(), + Fr::from_u64(3), + Fr::from_u64(4), ]; let response1 = api.pedersen_commit(inputs1, 0).expect("PedersenCommit failed"); diff --git a/barretenberg/rust/tests/src/ffi/poseidon.rs b/barretenberg/rust/tests/src/ffi/poseidon.rs index c1aae2dd708..b079316e94b 100644 --- a/barretenberg/rust/tests/src/ffi/poseidon.rs +++ b/barretenberg/rust/tests/src/ffi/poseidon.rs @@ -3,16 +3,16 @@ //! Ported from zkpassport/aztec-packages bb_rs poseidon2_tests.rs #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, BarretenbergApi, Fr}; +use barretenberg_rs::{FfiBackend, BbApi, Fr}; #[test] fn test_poseidon2_hash() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let inputs = vec![ - Fr::from_u64(4).to_buffer(), - Fr::from_u64(8).to_buffer(), + Fr::from_u64(4), + Fr::from_u64(8), ]; let response = api.poseidon2_hash(inputs).expect("Poseidon2Hash failed"); @@ -27,12 +27,12 @@ fn test_poseidon2_hash() { #[test] fn test_poseidon2_hash_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let input = vec![42u8; 32]; + let input = [42u8; 32]; - let response1 = api.poseidon2_hash(vec![input.clone()]).expect("Poseidon2Hash failed"); - let response2 = api.poseidon2_hash(vec![input]).expect("Poseidon2Hash failed"); + let response1 = api.poseidon2_hash(vec![Fr::from_be_bytes(input)]).expect("Poseidon2Hash failed"); + let response2 = api.poseidon2_hash(vec![input.into()]).expect("Poseidon2Hash failed"); // Same input should produce same output assert_eq!(response1.hash, response2.hash); @@ -43,13 +43,13 @@ fn test_poseidon2_hash_deterministic() { #[test] fn test_poseidon2_hash_different_inputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let input1 = vec![1u8; 32]; - let input2 = vec![2u8; 32]; + let input1 = [1u8; 32]; + let input2 = [2u8; 32]; - let response1 = api.poseidon2_hash(vec![input1]).expect("Poseidon2Hash failed"); - let response2 = api.poseidon2_hash(vec![input2]).expect("Poseidon2Hash failed"); + let response1 = api.poseidon2_hash(vec![input1.into()]).expect("Poseidon2Hash failed"); + let response2 = api.poseidon2_hash(vec![input2.into()]).expect("Poseidon2Hash failed"); // Different inputs should produce different outputs assert_ne!(response1.hash, response2.hash); @@ -60,14 +60,14 @@ fn test_poseidon2_hash_different_inputs() { #[test] fn test_poseidon2_hash_zero_input() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); - let input = vec![0u8; 32]; + let input = [0u8; 32]; - let response = api.poseidon2_hash(vec![input.clone()]).expect("Poseidon2Hash failed"); + let response = api.poseidon2_hash(vec![Fr::from_be_bytes(input)]).expect("Poseidon2Hash failed"); // Even zero input should produce non-zero output - assert_ne!(response.hash, vec![0u8; 32]); + assert_ne!(response.hash, [0u8; 32]); assert_ne!(response.hash, input); api.destroy().expect("Failed to destroy backend"); @@ -76,17 +76,17 @@ fn test_poseidon2_hash_zero_input() { #[test] fn test_poseidon2_permutation_js_compatibility_cpp() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // JS test: poseidon2Permutation([0, 1, 2, 3]) // Expected results from the JS test - let mut inputs = [vec![0u8; 32], vec![0u8; 32], vec![0u8; 32], vec![0u8; 32]]; + let mut inputs = [[0u8; 32], [0u8; 32], [0u8; 32], [0u8; 32]]; // inputs[0] stays 0 inputs[1][31] = 1; inputs[2][31] = 2; inputs[3][31] = 3; - let response = api.poseidon2_permutation(inputs).expect("Poseidon2Permutation failed"); + let response = api.poseidon2_permutation(inputs.map(Fr::from_be_bytes)).expect("Poseidon2Permutation failed"); assert_eq!(response.outputs.len(), 4); @@ -123,10 +123,10 @@ fn test_poseidon2_permutation_js_compatibility_cpp() { #[test] fn test_poseidon2_permutation_js_compatibility_noir() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // JS test: poseidon2Permutation([1n, 2n, 3n, 0x0a0000000000000000n]) - let mut inputs = [vec![0u8; 32], vec![0u8; 32], vec![0u8; 32], vec![0u8; 32]]; + let mut inputs = [[0u8; 32], [0u8; 32], [0u8; 32], [0u8; 32]]; // Set the values in big-endian inputs[0][31] = 1; // 1n @@ -135,7 +135,7 @@ fn test_poseidon2_permutation_js_compatibility_noir() { // 0x0a0000000000000000n = 720575940379279360 inputs[3][23] = 0x0a; // Set the appropriate byte for this large number - let response = api.poseidon2_permutation(inputs).expect("Poseidon2Permutation failed"); + let response = api.poseidon2_permutation(inputs.map(Fr::from_be_bytes)).expect("Poseidon2Permutation failed"); assert_eq!(response.outputs.len(), 4); diff --git a/barretenberg/rust/tests/src/ffi/schnorr.rs b/barretenberg/rust/tests/src/ffi/schnorr.rs index b31f662339f..1673c1407bd 100644 --- a/barretenberg/rust/tests/src/ffi/schnorr.rs +++ b/barretenberg/rust/tests/src/ffi/schnorr.rs @@ -3,12 +3,12 @@ //! Tests for Schnorr signatures over the Grumpkin curve. #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, BarretenbergApi}; +use barretenberg_rs::{FfiBackend, BbApi}; #[test] fn test_schnorr_compute_public_key() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // A valid private key (32 bytes) let private_key: [u8; 32] = [ @@ -18,14 +18,14 @@ fn test_schnorr_compute_public_key() { ]; let response = api - .schnorr_compute_public_key(&private_key) + .schnorr_compute_public_key(private_key.into()) .expect("schnorr_compute_public_key failed"); // Should return a valid public key point assert_eq!(response.public_key.x.len(), 32); assert_eq!(response.public_key.y.len(), 32); // Should not be zero - assert_ne!(response.public_key.x, vec![0u8; 32]); + assert_ne!(response.public_key.x, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -33,7 +33,7 @@ fn test_schnorr_compute_public_key() { #[test] fn test_schnorr_compute_public_key_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key: [u8; 32] = [ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, @@ -42,10 +42,10 @@ fn test_schnorr_compute_public_key_deterministic() { ]; let response1 = api - .schnorr_compute_public_key(&private_key) + .schnorr_compute_public_key(private_key.into()) .expect("schnorr_compute_public_key failed"); let response2 = api - .schnorr_compute_public_key(&private_key) + .schnorr_compute_public_key(private_key.into()) .expect("schnorr_compute_public_key failed"); // Same private key should produce same public key @@ -58,7 +58,7 @@ fn test_schnorr_compute_public_key_deterministic() { #[test] fn test_schnorr_different_private_keys() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key1: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -72,10 +72,10 @@ fn test_schnorr_different_private_keys() { ]; let response1 = api - .schnorr_compute_public_key(&private_key1) + .schnorr_compute_public_key(private_key1.into()) .expect("schnorr_compute_public_key failed"); let response2 = api - .schnorr_compute_public_key(&private_key2) + .schnorr_compute_public_key(private_key2.into()) .expect("schnorr_compute_public_key failed"); // Different private keys should produce different public keys @@ -90,7 +90,7 @@ fn test_schnorr_different_private_keys() { #[test] fn test_schnorr_sign_and_verify() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Private key let private_key: [u8; 32] = [ @@ -101,7 +101,7 @@ fn test_schnorr_sign_and_verify() { // Compute public key let pub_key_response = api - .schnorr_compute_public_key(&private_key) + .schnorr_compute_public_key(private_key.into()) .expect("schnorr_compute_public_key failed"); // Message: a 32-byte big-endian serialized grumpkin base-field element (bbapi asserts size == 32). @@ -113,7 +113,7 @@ fn test_schnorr_sign_and_verify() { // Sign let sign_response = api - .schnorr_construct_signature(&message, &private_key) + .schnorr_construct_signature(message.into(), private_key.into()) .expect("schnorr_construct_signature failed"); // Signature should have s and e components (32 bytes each) @@ -123,10 +123,10 @@ fn test_schnorr_sign_and_verify() { // Verify let verify_response = api .schnorr_verify_signature( - &message, + message.into(), pub_key_response.public_key.clone(), - &sign_response.s, - &sign_response.e, + sign_response.s.clone(), + sign_response.e.clone(), ) .expect("schnorr_verify_signature failed"); @@ -138,7 +138,7 @@ fn test_schnorr_sign_and_verify() { #[test] fn test_schnorr_verify_wrong_message() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let private_key: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -147,7 +147,7 @@ fn test_schnorr_verify_wrong_message() { ]; let pub_key_response = api - .schnorr_compute_public_key(&private_key) + .schnorr_compute_public_key(private_key.into()) .expect("schnorr_compute_public_key failed"); // Two distinct 32-byte serialized field elements (bbapi asserts size == 32). @@ -164,16 +164,16 @@ fn test_schnorr_verify_wrong_message() { // Sign with message1 let sign_response = api - .schnorr_construct_signature(&message1, &private_key) + .schnorr_construct_signature(message1.into(), private_key.into()) .expect("schnorr_construct_signature failed"); // Verify with message2 - should fail let verify_response = api .schnorr_verify_signature( - &message2, + message2.into(), pub_key_response.public_key.clone(), - &sign_response.s, - &sign_response.e, + sign_response.s.clone(), + sign_response.e.clone(), ) .expect("schnorr_verify_signature failed"); diff --git a/barretenberg/rust/tests/src/ffi/secp256k1.rs b/barretenberg/rust/tests/src/ffi/secp256k1.rs index d65358663ef..97d5b07f6ed 100644 --- a/barretenberg/rust/tests/src/ffi/secp256k1.rs +++ b/barretenberg/rust/tests/src/ffi/secp256k1.rs @@ -3,7 +3,7 @@ //! Ported from zkpassport/aztec-packages bb_rs secp256k1_tests.rs #[cfg(test)] -use barretenberg_rs::{backends::FfiBackend, generated_types::Secp256k1Point, BarretenbergApi}; +use barretenberg_rs::{FfiBackend, generated::bb_types::Secp256k1Point, BbApi}; // secp256k1 generator point G // x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 @@ -23,28 +23,28 @@ fn secp256k1_generator() -> Secp256k1Point { 0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8, ]; Secp256k1Point { - x: generator_x.to_vec(), - y: generator_y.to_vec(), + x: generator_x.into(), + y: generator_y.into(), } } #[test] fn test_secp256k1_scalar_multiplication() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = secp256k1_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 3; // scalar = 3 let response = api - .secp256k1_mul(point.clone(), &scalar) + .secp256k1_mul(point.clone(), scalar.into()) .expect("secp256k1_mul failed"); // Result should be different from input (3*G != G) assert_ne!(response.point.x, point.x); // Result should be a valid point (non-zero) - assert_ne!(response.point.x, vec![0u8; 32]); + assert_ne!(response.point.x, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -52,15 +52,15 @@ fn test_secp256k1_scalar_multiplication() { #[test] fn test_secp256k1_scalar_multiplication_by_one() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = secp256k1_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 1; // scalar = 1 let response = api - .secp256k1_mul(point.clone(), &scalar) + .secp256k1_mul(point.clone(), scalar.into()) .expect("secp256k1_mul failed"); // Multiplying by 1 should give the same point @@ -73,7 +73,7 @@ fn test_secp256k1_scalar_multiplication_by_one() { #[test] fn test_secp256k1_random_scalar_generation() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let response1 = api .secp256k1_get_random_fr(0) @@ -85,8 +85,8 @@ fn test_secp256k1_random_scalar_generation() { // Random scalars should be different (very high probability) assert_ne!(response1.value, response2.value); // Should not be zero - assert_ne!(response1.value, vec![0u8; 32]); - assert_ne!(response2.value, vec![0u8; 32]); + assert_ne!(response1.value, [0u8; 32]); + assert_ne!(response2.value, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -94,7 +94,7 @@ fn test_secp256k1_random_scalar_generation() { #[test] fn test_secp256k1_random_scalar_multiple_calls() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let mut scalars = Vec::new(); for _ in 0..10 { @@ -121,7 +121,7 @@ fn test_secp256k1_random_scalar_multiple_calls() { #[test] fn test_secp256k1_reduce512() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let large_input = [0xffu8; 64]; // Maximum 512-bit value @@ -130,7 +130,7 @@ fn test_secp256k1_reduce512() { .expect("secp256k1_reduce512 failed"); // Should produce a valid field element - assert_ne!(response.value, vec![0u8; 32]); + assert_ne!(response.value, [0u8; 32]); // Should be different from the first 32 bytes of input (since we're reducing) assert_ne!(response.value.as_slice(), &large_input[..32]); @@ -140,7 +140,7 @@ fn test_secp256k1_reduce512() { #[test] fn test_secp256k1_reduce512_small_value() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let mut small_input = [0u8; 64]; small_input[63] = 42; // A small value @@ -150,7 +150,7 @@ fn test_secp256k1_reduce512_small_value() { .expect("secp256k1_reduce512 failed"); // For a small value, the reduction should preserve it - let mut expected = vec![0u8; 32]; + let mut expected = [0u8; 32]; expected[31] = 42; assert_eq!(response.value, expected); @@ -160,7 +160,7 @@ fn test_secp256k1_reduce512_small_value() { #[test] fn test_secp256k1_reduce512_zero() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let zero_input = [0u8; 64]; @@ -169,7 +169,7 @@ fn test_secp256k1_reduce512_zero() { .expect("secp256k1_reduce512 failed"); // Zero should remain zero after reduction - assert_eq!(response.value, vec![0u8; 32]); + assert_eq!(response.value, [0u8; 32]); api.destroy().expect("Failed to destroy backend"); } @@ -177,7 +177,7 @@ fn test_secp256k1_reduce512_zero() { #[test] fn test_secp256k1_reduce512_various_inputs() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); // Test with different patterns let mut input1 = [0u8; 64]; @@ -201,18 +201,18 @@ fn test_secp256k1_reduce512_various_inputs() { #[test] fn test_secp256k1_mul_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let point = secp256k1_generator(); - let mut scalar = vec![0u8; 32]; + let mut scalar = [0u8; 32]; scalar[31] = 5; let result1 = api - .secp256k1_mul(point.clone(), &scalar) + .secp256k1_mul(point.clone(), scalar.into()) .expect("secp256k1_mul failed"); let result2 = api - .secp256k1_mul(point, &scalar) + .secp256k1_mul(point, scalar.into()) .expect("secp256k1_mul failed"); // Should be deterministic @@ -225,7 +225,7 @@ fn test_secp256k1_mul_deterministic() { #[test] fn test_secp256k1_reduce512_deterministic() { let backend = FfiBackend::new().expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = BbApi::new(backend); let large_scalar_512 = [0xffu8; 64]; diff --git a/barretenberg/rust/tests/src/pedersen.rs b/barretenberg/rust/tests/src/pedersen.rs index 9fe71df5e77..bf1c9e39723 100644 --- a/barretenberg/rust/tests/src/pedersen.rs +++ b/barretenberg/rust/tests/src/pedersen.rs @@ -5,7 +5,9 @@ //! These tests require the BB binary to be built. They are skipped if the binary is not found. #[cfg(test)] -use barretenberg_rs::{backends::PipeBackend, BarretenbergApi, Fr}; +use barretenberg_rs::Fr; +#[cfg(test)] +use crate::utils::spawn_bb; #[cfg(test)] use crate::utils::{get_bb_binary_path, random_fr, Timer}; #[cfg(test)] @@ -16,13 +18,11 @@ fn test_pedersen_hash() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let inputs = vec![ - Fr::from_u64(4).to_buffer().try_into().unwrap(), - Fr::from_u64(8).to_buffer().try_into().unwrap(), + Fr::from_u64(4), + Fr::from_u64(8), ]; let response = api.pedersen_hash(inputs, 7).expect("PedersenHash failed"); @@ -39,14 +39,12 @@ fn test_pedersen_commit() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let inputs = vec![ - Fr::from_u64(4).to_buffer().try_into().unwrap(), - Fr::from_u64(8).to_buffer().try_into().unwrap(), - Fr::from_u64(12).to_buffer().try_into().unwrap(), + Fr::from_u64(4), + Fr::from_u64(8), + Fr::from_u64(12), ]; let response = api.pedersen_commit(inputs, 0).expect("PedersenCommit failed"); @@ -67,9 +65,7 @@ fn test_pedersen_hash_perf() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let loops = 1000; let mut fields = Vec::with_capacity(loops * 2); @@ -80,8 +76,8 @@ fn test_pedersen_hash_perf() { let timer = Timer::new(); for i in 0..loops { let inputs = vec![ - fields[i * 2].to_buffer().try_into().unwrap(), - fields[i * 2 + 1].to_buffer().try_into().unwrap(), + fields[i * 2].clone(), + fields[i * 2 + 1].clone(), ]; let _ = api.pedersen_hash(inputs, 0).expect("PedersenHash failed"); } @@ -98,9 +94,7 @@ fn test_pedersen_commit_perf() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let loops = 1000; let mut fields = Vec::with_capacity(loops * 2); @@ -111,8 +105,8 @@ fn test_pedersen_commit_perf() { let timer = Timer::new(); for i in 0..loops { let inputs = vec![ - fields[i * 2].to_buffer().try_into().unwrap(), - fields[i * 2 + 1].to_buffer().try_into().unwrap(), + fields[i * 2].clone(), + fields[i * 2 + 1].clone(), ]; let _ = api.pedersen_commit(inputs, 0).expect("PedersenCommit failed"); } diff --git a/barretenberg/rust/tests/src/pipe_test.rs b/barretenberg/rust/tests/src/pipe_test.rs index 3cb17bcc280..cecb5af334a 100644 --- a/barretenberg/rust/tests/src/pipe_test.rs +++ b/barretenberg/rust/tests/src/pipe_test.rs @@ -5,7 +5,9 @@ //! These tests require the BB binary to be built. They are skipped if the binary is not found. #[cfg(test)] -use barretenberg_rs::{backends::PipeBackend, BarretenbergApi, Fr}; +use barretenberg_rs::Fr; +#[cfg(test)] +use crate::utils::spawn_bb; #[cfg(test)] use crate::utils::get_bb_binary_path; #[cfg(test)] @@ -16,9 +18,7 @@ fn test_pipe_blake2s() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create pipe backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let input = b"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789"; let expected: [u8; 32] = [ @@ -43,13 +43,11 @@ fn test_pipe_pedersen_hash() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create pipe backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let inputs = vec![ - Fr::from_u64(4).to_buffer(), - Fr::from_u64(8).to_buffer(), + Fr::from_u64(4), + Fr::from_u64(8), ]; let response = api.pedersen_hash(inputs, 7).expect("PedersenHash failed"); @@ -66,13 +64,11 @@ fn test_pipe_poseidon2_hash() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create pipe backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let inputs = vec![ - Fr::from_u64(4).to_buffer(), - Fr::from_u64(8).to_buffer(), + Fr::from_u64(4), + Fr::from_u64(8), ]; let response = api.poseidon2_hash(inputs).expect("Poseidon2Hash failed"); @@ -89,9 +85,7 @@ fn test_pipe_grumpkin_add() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create pipe backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); // Grumpkin generator point (from precomputed_generators_grumpkin_impl.hpp) // x = 0x2df8b940e5890e4e1377e05373fae69a1d754f6935e6a780b666947431f2cdcd @@ -109,10 +103,10 @@ fn test_pipe_grumpkin_add() { 0x27, 0xca, 0x7e, 0xef, 0xb2, 0xc1, 0x90, 0x83, ]; - use barretenberg_rs::GrumpkinPoint; + use barretenberg_rs::generated::bb_types::GrumpkinPoint; let point_a = GrumpkinPoint { - x: generator_x.to_vec(), - y: generator_y.to_vec(), + x: generator_x.into(), + y: generator_y.into(), }; let point_b = point_a.clone(); @@ -129,9 +123,7 @@ fn test_pipe_error_response() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create pipe backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); // Create an invalid point (off-curve) to trigger an error // This point has x=1, y=1 which is NOT on the Grumpkin curve @@ -148,10 +140,10 @@ fn test_pipe_error_response() { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ]; - use barretenberg_rs::GrumpkinPoint; + use barretenberg_rs::generated::bb_types::GrumpkinPoint; let invalid_point = GrumpkinPoint { - x: invalid_x.to_vec(), - y: invalid_y.to_vec(), + x: invalid_x.into(), + y: invalid_y.into(), }; // This should fail because the point is not on the curve diff --git a/barretenberg/rust/tests/src/poseidon.rs b/barretenberg/rust/tests/src/poseidon.rs index e505fcfc5b1..b31cb2d5a1d 100644 --- a/barretenberg/rust/tests/src/poseidon.rs +++ b/barretenberg/rust/tests/src/poseidon.rs @@ -5,7 +5,9 @@ //! These tests require the BB binary to be built. They are skipped if the binary is not found. #[cfg(test)] -use barretenberg_rs::{backends::PipeBackend, BarretenbergApi, Fr}; +use barretenberg_rs::Fr; +#[cfg(test)] +use crate::utils::spawn_bb; #[cfg(test)] use crate::utils::{get_bb_binary_path, random_fr, Timer}; #[cfg(test)] @@ -16,13 +18,11 @@ fn test_poseidon2_hash() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let inputs = vec![ - Fr::from_u64(4).to_buffer(), - Fr::from_u64(8).to_buffer(), + Fr::from_u64(4), + Fr::from_u64(8), ]; let response = api.poseidon2_hash(inputs).expect("Poseidon2Hash failed"); @@ -40,14 +40,12 @@ fn test_poseidon2_hash_perf() { require_bb_binary!(); let bb_path = get_bb_binary_path(); - let backend = PipeBackend::new(&bb_path, Some(1)) - .expect("Failed to create backend"); - let mut api = BarretenbergApi::new(backend); + let mut api = spawn_bb(&bb_path); let loops = 1000; let mut fields = Vec::with_capacity(loops * 2); for _ in 0..loops * 2 { - fields.push(random_fr().to_buffer()); + fields.push(random_fr()); } let timer = Timer::new(); diff --git a/barretenberg/rust/tests/src/utils.rs b/barretenberg/rust/tests/src/utils.rs index 48e54c0b8bb..c8d2c385c06 100644 --- a/barretenberg/rust/tests/src/utils.rs +++ b/barretenberg/rust/tests/src/utils.rs @@ -1,7 +1,10 @@ //! Utility functions and helpers for tests +use std::path::Path; +use std::process::{Child, Command, Stdio}; use std::time::Instant; -use barretenberg_rs::Fr; + +use barretenberg_rs::{ipc_runtime::IpcClient, Backend, BarretenbergError, BbApi, Fr, Result}; /// Generate a pseudo-random Fr for testing (NOT cryptographically secure) pub fn random_fr() -> Fr { @@ -14,7 +17,7 @@ pub fn random_fr() -> Fr { let mut bytes = [0u8; 32]; bytes[0..16].copy_from_slice(&nanos.to_le_bytes()); bytes[16..24].copy_from_slice(&(nanos >> 64).to_le_bytes()[0..8]); - Fr(bytes) + Fr::from_be_bytes(bytes) } /// Timer for performance measurements @@ -54,6 +57,58 @@ impl Default for Timer { } } +/// A `bb msgpack run` child driven over its stdin/stdout pipe. +pub struct BbProcess { + client: IpcClient, + child: Option, +} + +impl BbProcess { + pub fn spawn(bb_path: impl AsRef) -> Self { + use std::os::fd::AsRawFd; + + let child = Command::new(bb_path.as_ref()) + .args(["msgpack", "run"]) + .env("HARDWARE_CONCURRENCY", "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("Failed to spawn bb"); + let out_fd = child.stdin.as_ref().unwrap().as_raw_fd(); + let in_fd = child.stdout.as_ref().unwrap().as_raw_fd(); + // SAFETY: both descriptors belong to `child`, which outlives the + // client; the client duplicates them. + let client = unsafe { IpcClient::from_fds(in_fd, out_fd) }.expect("Failed to connect to bb"); + Self { client, child: Some(child) } + } +} + +impl Backend for BbProcess { + fn call(&mut self, input: &[u8]) -> Result> { + self.client.call(input).map_err(|e| BarretenbergError::Backend(e.to_string())) + } + + fn destroy(&mut self) -> Result<()> { + if let Some(mut child) = self.child.take() { + let _ = child.kill(); + let _ = child.wait(); + } + Ok(()) + } +} + +impl Drop for BbProcess { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +/// Spawn bb and wrap it in a client. +pub fn spawn_bb(bb_path: &str) -> BbApi { + BbApi::new(BbProcess::spawn(bb_path)) +} + /// Get path to BB binary for testing pub fn get_bb_binary_path() -> String { std::env::var("BB_BINARY_PATH") diff --git a/barretenberg/ts/.gitignore b/barretenberg/ts/.gitignore index 9399d0021b0..ba0a722c837 100644 --- a/barretenberg/ts/.gitignore +++ b/barretenberg/ts/.gitignore @@ -18,4 +18,4 @@ package packages/ # Generated files -bb.js/src/cbind/generated/ +bb.js/src/generated/ diff --git a/barretenberg/ts/.rebuild_patterns b/barretenberg/ts/.rebuild_patterns index af9ebe7df71..7c88bc2f15a 100644 --- a/barretenberg/ts/.rebuild_patterns +++ b/barretenberg/ts/.rebuild_patterns @@ -3,4 +3,6 @@ ^ipc-codegen/src/.*\.ts$ ^ipc-codegen/templates/ ^barretenberg/cpp/src/barretenberg/avm/avm_schema\.json$ +^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$ diff --git a/barretenberg/ts/bb.js/.prettierignore b/barretenberg/ts/bb.js/.prettierignore new file mode 100644 index 00000000000..0d4529771b2 --- /dev/null +++ b/barretenberg/ts/bb.js/.prettierignore @@ -0,0 +1,4 @@ +# Codegen output: regenerated by `yarn generate`, never hand-edited, and not +# emitted in prettier's style. It is gitignored, but CI runners reuse working +# directories, so it can be present when `yarn formatting` runs. +src/generated/ diff --git a/barretenberg/ts/bb.js/bootstrap.sh b/barretenberg/ts/bb.js/bootstrap.sh index 895d59dad63..bd905c6953a 100755 --- a/barretenberg/ts/bb.js/bootstrap.sh +++ b/barretenberg/ts/bb.js/bootstrap.sh @@ -14,7 +14,9 @@ hash=$(hash_str \ function prepare_project { (cd .. && ./bootstrap.sh generate_packages) - (cd .. && npm_install_deps) + # Same cache-key inputs as barretenberg/ts/bootstrap.sh: the workspaces + # portal into ipc-runtime/ts, so its manifest belongs in the key. + (cd .. && npm_install_deps "^ipc-runtime/ts/package\.json$") } function formatting { diff --git a/barretenberg/ts/bb.js/eslint.config.js b/barretenberg/ts/bb.js/eslint.config.js index 59c9453bd6e..2f7fa274213 100644 --- a/barretenberg/ts/bb.js/eslint.config.js +++ b/barretenberg/ts/bb.js/eslint.config.js @@ -22,6 +22,8 @@ export default [ 'eslint.config.js', 'eslint.config.*.js', 'src/jest/*.mjs', + // Codegen output; see .prettierignore. + 'src/generated/**', ]), ...tseslint.config({ extends: [ @@ -94,7 +96,7 @@ export default [ 'error', { // Generated later in bootstrap; the tracked wasm symlinks are broken in a clean checkout until the C++ build runs. - ignore: ['cbind/generated', '\\.wasm\\.gz$'], + ignore: ['generated', '\\.wasm\\.gz$'], }, ], 'import-x/no-extraneous-dependencies': 'error', diff --git a/barretenberg/ts/bb.js/package.json b/barretenberg/ts/bb.js/package.json index ab371d99116..5086a901128 100644 --- a/barretenberg/ts/bb.js/package.json +++ b/barretenberg/ts/bb.js/package.json @@ -26,14 +26,14 @@ "README.md" ], "scripts": { - "clean": "rm -rf ./dest .tsbuildinfo .tsbuildinfo.cjs ./src/cbind/generated", + "clean": "rm -rf ./dest .tsbuildinfo .tsbuildinfo.cjs ./src/generated", "build": "yarn clean && yarn generate && yarn build:wasm && yarn build:native && yarn build:esm && yarn build:cjs && yarn build:browser", "build:wasm": "./scripts/copy_wasm.sh", "build:native": "./scripts/copy_native.sh", "build:esm": "tsgo -b tsconfig.esm.json && chmod +x ./dest/node/bin/index.js", "build:cjs": "tsgo -b tsconfig.cjs.json && ./scripts/cjs_postprocess.sh", "build:browser": "tsgo -b tsconfig.browser.json && ./scripts/browser_postprocess.sh", - "generate": "NODE_OPTIONS='--loader ts-node/esm' NODE_NO_WARNINGS=1 ts-node src/cbind/generate.ts", + "generate": "./scripts/generate.sh", "formatting": "prettier --check ./src && eslint --max-warnings 0 ./src", "formatting:fix": "eslint --fix ./src && prettier -w ./src", "test": "NODE_OPTIONS='--loader ts-node/esm' NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --passWithNoTests", @@ -70,6 +70,7 @@ "rootDir": "./src" }, "dependencies": { + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime", "comlink": "^4.4.1", "commander": "^12.1.0", "idb-keyval": "^6.2.1", diff --git a/barretenberg/ts/bb.js/scripts/copy_cross.sh b/barretenberg/ts/bb.js/scripts/copy_cross.sh index ffb0d5ac774..b39386b4e5b 100755 --- a/barretenberg/ts/bb.js/scripts/copy_cross.sh +++ b/barretenberg/ts/bb.js/scripts/copy_cross.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copies cross-compiled bb binary and napi module to dest. +# Copies the cross-compiled bb binaries to dest. set -e NO_CD=1 source $(git rev-parse --show-toplevel)/ci3/source @@ -9,7 +9,6 @@ if [ -n "${1:-}" ]; then arch="$1" mkdir -p ./build/$arch cp ../../cpp/build-$arch/bin/bb ./build/$arch - cp ../../cpp/build-$arch/lib/nodejs_module.node ./build/$arch elif semver check "${REF_NAME:-}" && [[ "$(arch)" == "amd64" ]]; then # We're building a release. # Copy all cross-compiled architectures for release builds. @@ -17,7 +16,6 @@ elif semver check "${REF_NAME:-}" && [[ "$(arch)" == "amd64" ]]; then for arch in arm64-linux amd64-macos arm64-macos; do mkdir -p ./build/$arch cp ../../cpp/build-$arch/bin/bb ./build/$arch - cp ../../cpp/build-$arch/lib/nodejs_module.node ./build/$arch done llvm-strip-20 ./build/*/* diff --git a/barretenberg/ts/bb.js/scripts/copy_native.sh b/barretenberg/ts/bb.js/scripts/copy_native.sh index 85487610e83..d5efca4dcc0 100755 --- a/barretenberg/ts/bb.js/scripts/copy_native.sh +++ b/barretenberg/ts/bb.js/scripts/copy_native.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copies native bb binary and napi module to dest. +# Copies the native bb binary to dest. set -e NO_CD=1 source $(git rev-parse --show-toplevel)/ci3/source @@ -9,10 +9,9 @@ cd $(dirname $0)/.. target="$(arch)-$(os)" if [ "${BUILD_CPP:-0}" -eq 1 ]; then - ../../cpp/bootstrap.sh build_preset clang20 --target bb --target nodejs_module + ../../cpp/bootstrap.sh build_preset clang20 --target bb fi mkdir -p ./build/$target cp ../../cpp/build/bin/bb ./build/$target -cp ../../cpp/build/lib/nodejs_module.node ./build/$target diff --git a/barretenberg/ts/bb.js/scripts/generate.sh b/barretenberg/ts/bb.js/scripts/generate.sh new file mode 100755 index 00000000000..5bb35ba4ca9 --- /dev/null +++ b/barretenberg/ts/bb.js/scripts/generate.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Generate bb.js's TypeScript client from the checked-in bb schema, via +# ipc-codegen. Other languages generate their own: see +# barretenberg/rust/bootstrap.sh for the Rust crate. +set -euo pipefail + +cd "$(dirname "$0")/.." +ROOT=$(git rev-parse --show-toplevel) +BBAPI="$ROOT/barretenberg/cpp/src/barretenberg/bbapi" + +# bb.js keeps its historical API surface (poseidon2Hash, Poseidon2Hash), so the +# Bb service prefix is stripped from identifiers; wire tags keep it. +node --experimental-strip-types --experimental-transform-types --no-warnings \ + "$ROOT/ipc-codegen/src/generate.ts" \ + --schema "$BBAPI/bb_schema.json" \ + --lang ts \ + --client \ + --strip-method-prefix \ + --strip-type-prefix \ + --out src/generated \ + --curve-constants "$BBAPI/bb_curve_constants.json" diff --git a/barretenberg/ts/bb.js/src/barretenberg/backend.ts b/barretenberg/ts/bb.js/src/barretenberg/backend.ts index ab612a201dc..bd622e087e6 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/backend.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/backend.ts @@ -1,8 +1,8 @@ import { Decoder, Encoder } from 'msgpackr'; import { ungzip } from 'pako'; -import { CircuitKind } from '../cbind/circuit_kind.js'; -import { ChonkProof, fromChonkProof, toChonkProof } from '../cbind/generated/api_types.js'; +import { CircuitKind } from '../circuit_kind.js'; +import { ChonkProof, fromChonkProof, toChonkProof } from '../generated/api_types.js'; import { ProofData, hexToUint8Array, uint8ArrayToHex } from '../proof/index.js'; import type { Barretenberg } from './index.js'; @@ -336,7 +336,10 @@ export class AztecClientBackend { throw new AztecClientBackendError('Witness and VKs must have the same stack depth!'); } - this.api.chonkStart({ kinds: this.circuitKinds }); + // Pipelined: the accumulation commands are issued back to back and only + // the final chonkProve is awaited, so a failure surfaces there. + // CircuitKind values travel as u8 on the wire. + void this.api.chonkStart({ kinds: Uint8Array.from(this.circuitKinds) }); const lastIdx = this.acirBuf.length - 1; for (let i = 0; i < this.acirBuf.length; i++) { @@ -351,7 +354,7 @@ export class AztecClientBackend { ); } - this.api.chonkLoad({ + void this.api.chonkLoad({ circuit: { name: functionName, bytecode: bytecode, @@ -360,7 +363,7 @@ export class AztecClientBackend { kind: this.circuitKinds[i], }); - this.api.chonkAccumulate({ + void this.api.chonkAccumulate({ witness, }); } diff --git a/barretenberg/ts/bb.js/src/barretenberg/index.ts b/barretenberg/ts/bb.js/src/barretenberg/index.ts index e79c5ccc6f8..7b1b17c05ef 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/index.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/index.ts @@ -1,9 +1,10 @@ import { BackendOptions, BackendType } from '../bb_backends/index.js'; import { IMsgpackBackendAsync, IMsgpackBackendSync } from '../bb_backends/interface.js'; import { createAsyncBackend, createSyncBackend } from '../bb_backends/node/index.js'; -import { AsyncApi } from '../cbind/generated/async.js'; -import { SyncApi } from '../cbind/generated/sync.js'; +import { BBApiException } from '../bbapi_exception.js'; import { Crs, GrumpkinCrs } from '../crs/index.js'; +import { AsyncApi } from '../generated/async.js'; +import { SyncApi } from '../generated/sync.js'; const DEFAULT_BB_CRS_SIZE = 2 ** 19; // Keep the iOS default separate so it can diverge when mobile memory limits require it. @@ -35,7 +36,7 @@ export class Barretenberg extends AsyncApi { private options: BackendOptions; constructor(backend: IMsgpackBackendAsync, options: BackendOptions) { - super(backend); + super(backend, message => new BBApiException(message)); this.options = options; } @@ -185,7 +186,7 @@ let barretenbergSyncSingleton: BarretenbergSync | undefined; export class BarretenbergSync extends SyncApi { constructor(backend: IMsgpackBackendSync) { - super(backend); + super(backend, message => new BBApiException(message)); } /** @@ -222,7 +223,9 @@ export class BarretenbergSync extends SyncApi { */ static async initSingleton(options: BackendOptions = {}) { if (!barretenbergSyncSingletonPromise) { - barretenbergSyncSingletonPromise = BarretenbergSync.new(options); + // unref for the same reason the async singleton does: nothing destroys a + // singleton, so its backend must not hold the event loop open at exit. + barretenbergSyncSingletonPromise = BarretenbergSync.new({ ...options, unref: true }); } try { diff --git a/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts b/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts index de1901950d3..a246f95d631 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts @@ -70,71 +70,40 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => { .fill(0) .map(() => Fr.random().toBuffer()); - // Benchmark 1: WASM (async) - let wasmTime = 0; - if (wasmApi) { - const wasmStart = performance.now(); - for (let i = 0; i < ITERATIONS; i++) { - await wasmApi.poseidon2Hash({ inputs }); - } - wasmTime = performance.now() - wasmStart; - } - - // Benchmark 2: Native Socket (async with non-blocking I/O) - let nativeSocketTime = 0; - if (nativeSocketApi) { - const nativeSocketStart = performance.now(); - for (let i = 0; i < ITERATIONS; i++) { - await nativeSocketApi.poseidon2Hash({ inputs }); - } - nativeSocketTime = performance.now() - nativeSocketStart; - } - - // Benchmark 3: Native Socket (async, request pipelined) - let nativeSocketPipelinedTime = 0; - if (nativeSocketApi) { - const nativeSocketPipelinedStart = performance.now(); - // Use promise.all to pipeline requests - const promises = []; - for (let i = 0; i < ITERATIONS; i++) { - promises.push(nativeSocketApi.poseidon2Hash({ inputs })); - } - await Promise.all(promises); - nativeSocketPipelinedTime = performance.now() - nativeSocketPipelinedStart; - } - - // Benchmark 4: Native Shared Memory (async) - let nativeShmTime = 0; - if (nativeShmApi) { - const nativeShmStart = performance.now(); + // Each mode runs once untimed first, so the JIT has seen the exact call + // pattern being timed (a pipelined burst exercises different code paths + // from sequential awaits) and the timing reflects steady state. + const timed = async (run: () => Promise | void) => { + await run(); + const start = performance.now(); + await run(); + return performance.now() - start; + }; + const sequential = (api: Barretenberg) => async () => { for (let i = 0; i < ITERATIONS; i++) { - await nativeShmApi.poseidon2Hash({ inputs }); + await api.poseidon2Hash({ inputs }); } - nativeShmTime = performance.now() - nativeShmStart; - } - - // Benchmark 5: Native Shared Memory (async, request pipelined) - let nativeShmPipelinedTime = 0; - if (nativeShmApi) { - const nativeShmPipelinedStart = performance.now(); - // Use promise.all to pipeline requests + }; + const pipelined = (api: Barretenberg) => async () => { const promises = []; for (let i = 0; i < ITERATIONS; i++) { - promises.push(nativeShmApi.poseidon2Hash({ inputs })); + promises.push(api.poseidon2Hash({ inputs })); } await Promise.all(promises); - nativeShmPipelinedTime = performance.now() - nativeShmPipelinedStart; - } + }; - // Benchmark 6: Native Shared Memory (sync) - let nativeShmSyncTime = 0; - if (nativeShmSyncApi) { - const nativeShmSyncStart = performance.now(); - for (let i = 0; i < ITERATIONS; i++) { - nativeShmSyncApi.poseidon2Hash({ inputs }); - } - nativeShmSyncTime = performance.now() - nativeShmSyncStart; - } + const wasmTime = wasmApi ? await timed(sequential(wasmApi)) : 0; + const nativeSocketTime = nativeSocketApi ? await timed(sequential(nativeSocketApi)) : 0; + const nativeSocketPipelinedTime = nativeSocketApi ? await timed(pipelined(nativeSocketApi)) : 0; + const nativeShmTime = nativeShmApi ? await timed(sequential(nativeShmApi)) : 0; + const nativeShmPipelinedTime = nativeShmApi ? await timed(pipelined(nativeShmApi)) : 0; + const nativeShmSyncTime = nativeShmSyncApi + ? await timed(() => { + for (let i = 0; i < ITERATIONS; i++) { + nativeShmSyncApi!.poseidon2Hash({ inputs }); + } + }) + : 0; // Calculate metrics (all relative to WASM baseline) const nativeSocketOverhead = ((nativeSocketTime - wasmTime) / wasmTime) * 100; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz index 213a2c2712a..e9fd64c59ea 120000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz +++ b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz @@ -1 +1 @@ -../../../cpp/build-wasm-threads/bin/barretenberg.wasm.gz \ No newline at end of file +../../../../cpp/build-wasm-threads/bin/barretenberg.wasm.gz \ No newline at end of file diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz index c6c4aeab616..e7a5a91393a 120000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz +++ b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz @@ -1 +1 @@ -../../../cpp/build-wasm/bin/barretenberg.wasm.gz \ No newline at end of file +../../../../cpp/build-wasm/bin/barretenberg.wasm.gz \ No newline at end of file diff --git a/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts b/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts index 4e4dba8dc71..10f21920d6c 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts @@ -5,7 +5,3 @@ export function findPackageRoot(): string | null { export function findBbBinary(_customPath?: string): string | null { throw new Error('Not implemented in browser environment.'); } - -export function findNapiBinary(_customPath?: string): string | null { - throw new Error('Not implemented in browser environment.'); -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/index.ts b/barretenberg/ts/bb.js/src/bb_backends/index.ts index a538a9b614a..f911f5865b6 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/index.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/index.ts @@ -49,10 +49,10 @@ export type BackendOptions = { /** * @description Replace the bb process when it dies, on the next call (NativeUnixSocket only). * - * There is no equivalent for the shared-memory backends, and no error either: a call to a bb - * process that has died never returns, because the receive loop cannot tell a dead server from a - * slow one. That is documented where it happens, in msgpack_client_wrapper.cpp. A caller that - * needs to survive a bb death should be on this backend. + * There is no equivalent for the shared-memory backends, which cannot tell a dead server from a + * slow one: a call to a bb process that has died fails only when ipc-runtime's call deadline + * expires, and as a timeout, not as a retryable death. A caller that needs to survive a bb death + * should be on this backend. * * Calls that were in flight still fail, with an error carrying `retry: true`; only later calls * see the replacement. diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/index.ts b/barretenberg/ts/bb.js/src/bb_backends/node/index.ts index 9ef091fb4e1..670fd1ad965 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/index.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/index.ts @@ -4,7 +4,7 @@ import { BarretenbergWasmAsyncBackend, BarretenbergWasmSyncBackend } from '../wa import { BarretenbergNativeShmSyncBackend } from './native_shm.js'; import { BarretenbergNativeShmAsyncBackend } from './native_shm_async.js'; import { BarretenbergNativeSocketAsyncBackend } from './native_socket.js'; -import { findBbBinary, findNapiBinary } from './platform.js'; +import { findBbBinary } from './platform.js'; /** * Create backend of specific type (no fallback) @@ -40,12 +40,14 @@ export async function createAsyncBackend( if (!bbPath) { throw new Error('Native backend requires bb binary.'); } - const napiPath = findNapiBinary(options.napiPath); - if (!napiPath) { - throw new Error('Native async backend requires napi client stub.'); - } logger(`Using native shared memory async backend: ${bbPath}`); - return await BarretenbergNativeShmAsyncBackend.new(bbPath, napiPath, options.threads, options.logger); + return await BarretenbergNativeShmAsyncBackend.new( + bbPath, + options.napiPath, + options.threads, + options.logger, + options.unref, + ); } case BackendType.Wasm: @@ -86,12 +88,14 @@ export async function createSyncBackend( if (!bbPath) { throw new Error('Native backend requires bb binary.'); } - const napiPath = findNapiBinary(options.napiPath); - if (!napiPath) { - throw new Error('Native sync backend requires napi client stub.'); - } logger(`Using native shared memory backend: ${bbPath}`); - return await BarretenbergNativeShmSyncBackend.new(bbPath, napiPath, options.threads, options.logger); + return await BarretenbergNativeShmSyncBackend.new( + bbPath, + options.napiPath, + options.threads, + options.logger, + options.unref, + ); } case BackendType.Wasm: { diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_pipe.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_pipe.ts deleted file mode 100644 index 05ddea618a4..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_pipe.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ChildProcess, spawn } from 'child_process'; - -import { IMsgpackBackendAsync } from '../interface.js'; - -/** - * Asynchronous native backend that communicates with bb binary via stdin/stdout. - * Uses event-based I/O with a state machine to handle partial reads. - * - * Protocol: - * - Request: 4-byte little-endian length + msgpack buffer - * - Response: 4-byte little-endian length + msgpack buffer - */ -export class BarretenbergNativePipeAsyncBackend implements IMsgpackBackendAsync { - private process: ChildProcess; - private pendingResolve: ((data: Uint8Array) => void) | null = null; - private pendingReject: ((error: Error) => void) | null = null; - - // State machine for reading responses - private readingLength: boolean = true; - private lengthBuffer: Buffer = Buffer.alloc(4); - private lengthBytesRead: number = 0; - private responseLength: number = 0; - private responseBuffer: Buffer | null = null; - private responseBytesRead: number = 0; - - constructor(bbBinaryPath: string) { - this.process = spawn(bbBinaryPath, ['msgpack', 'run'], { - stdio: ['pipe', 'pipe', 'inherit'], - }); - - this.process.stdout!.on('data', (chunk: Buffer) => { - this.handleData(chunk); - }); - - this.process.on('error', err => { - if (this.pendingReject) { - this.pendingReject(new Error(`Native backend process error: ${err.message}`)); - this.pendingReject = null; - this.pendingResolve = null; - } - }); - - this.process.on('exit', (code, signal) => { - if (this.pendingReject) { - if (code !== null && code !== 0) { - this.pendingReject(new Error(`Native backend process exited with code ${code}`)); - } else if (signal) { - if (signal != 'SIGTERM') { - this.pendingReject(new Error(`Native backend process killed with signal ${signal}`)); - } - } else { - this.pendingReject(new Error('Native backend process exited unexpectedly')); - } - this.pendingReject = null; - this.pendingResolve = null; - } - }); - } - - private handleData(chunk: Buffer): void { - let offset = 0; - - while (offset < chunk.length) { - if (this.readingLength) { - // Reading 4-byte length prefix - const bytesToCopy = Math.min(4 - this.lengthBytesRead, chunk.length - offset); - chunk.copy(this.lengthBuffer, this.lengthBytesRead, offset, offset + bytesToCopy); - this.lengthBytesRead += bytesToCopy; - offset += bytesToCopy; - - if (this.lengthBytesRead === 4) { - // Length is complete, switch to reading data - this.responseLength = this.lengthBuffer.readUInt32LE(0); - this.responseBuffer = Buffer.alloc(this.responseLength); - this.responseBytesRead = 0; - this.readingLength = false; - } - } else { - // Reading response data - const bytesToCopy = Math.min(this.responseLength - this.responseBytesRead, chunk.length - offset); - chunk.copy(this.responseBuffer!, this.responseBytesRead, offset, offset + bytesToCopy); - this.responseBytesRead += bytesToCopy; - offset += bytesToCopy; - - if (this.responseBytesRead === this.responseLength) { - // Response is complete - if (this.pendingResolve) { - this.pendingResolve(new Uint8Array(this.responseBuffer!)); - this.pendingResolve = null; - this.pendingReject = null; - } - - // Reset state for next message - this.readingLength = true; - this.lengthBytesRead = 0; - this.responseLength = 0; - this.responseBuffer = null; - this.responseBytesRead = 0; - } - } - } - } - - call(inputBuffer: Uint8Array): Promise { - if (this.pendingResolve) { - throw new Error('Cannot call while another call is pending (no pipelining supported)'); - } - - return new Promise((resolve, reject) => { - this.pendingResolve = resolve; - this.pendingReject = reject; - - // Write request: 4-byte little-endian length + msgpack data - const lengthBuf = Buffer.alloc(4); - lengthBuf.writeUInt32LE(inputBuffer.length, 0); - this.process.stdin!.write(lengthBuf); - this.process.stdin!.write(inputBuffer); - }); - } - - destroy(): Promise { - this.process.kill(); - return new Promise(resolve => { - this.process.once('exit', () => resolve()); - }); - } -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts index ef97be2e889..c0ce6472a38 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts @@ -1,215 +1,61 @@ -import { ChildProcess, spawn } from 'child_process'; -import { closeSync, openSync, unlinkSync } from 'fs'; -import { createRequire } from 'module'; -import { threadId } from 'worker_threads'; +import { SpawnedProcessBackendSync } from '@aztec-foundation/ipc-runtime'; import { IMsgpackBackendSync } from '../interface.js'; -import { findNapiBinary, findPackageRoot } from './platform.js'; -let instanceCounter = 0; +// Sync callers do short, one-at-a-time requests, so a single 4MB request ring +// is ample; the response ring keeps the runtime default. +const REQUEST_RING_SIZE = 1024 * 1024 * 4; /** - * Synchronous shared memory backend that communicates with bb binary via shared memory. - * Uses NAPI module to interface with shared memory IPC. - * - * Architecture: bb acts as the SERVER, TypeScript is the CLIENT - * - bb creates the shared memory region - * - TypeScript connects via NAPI wrapper - * - * Protocol: - * - Handled internally by IpcClient (no manual length prefixes needed) + * Synchronous native backend: bb serves over shared memory (`bb msgpack run + * --input .shm`) and @aztec-foundation/ipc-runtime's SpawnedProcessBackendSync owns + * the process lifecycle — stale-segment removal, spawn, retrying connect, + * death attribution and teardown. */ export class BarretenbergNativeShmSyncBackend implements IMsgpackBackendSync { - private process: ChildProcess; - private client: any; // NAPI MsgpackClient instance - private logFd?: number; // File descriptor for logs - - private constructor(process: ChildProcess, client: any, logFd?: number) { - this.process = process; - this.client = client; - this.logFd = logFd; - } + private constructor(private backend: SpawnedProcessBackendSync) {} /** * Create and initialize a shared memory backend. * @param bbBinaryPath Path to bb binary - * @param napiPath Path to NAPI binary + * @param napiPath Optional override for the ipc-runtime NAPI addon * @param threads Optional number of threads + * @param logger Optional receiver for bb's output */ static async new( bbBinaryPath: string, - napiPath: string, + napiPath?: string, threads?: number, logger?: (msg: string) => void, + unref?: boolean, ): Promise { - // Import the NAPI module - // The addon is built to the nodejs_module directory - const addonPath = findNapiBinary(napiPath); - // Try loading - let addon: any = null; - try { - const require = createRequire(findPackageRoot()!); - addon = require(addonPath!); - } catch { - // Addon not built yet or not available - throw new Error('Shared memory sync NAPI not available.'); - } - - // Create a unique shared memory name - const shmName = `bb-sync-${process.pid}-${threadId}-${instanceCounter++}`; - - // If threads not set use 1 thread. We're not expected to do long lived work on sync backends. - const hwc = threads ? threads.toString() : '1'; - const env = { ...process.env, HARDWARE_CONCURRENCY: hwc }; - - // Set up file logging if logger is provided. - // Direct file redirection bypasses Node event loop - logs are written even if process hangs. - let logFd: number | undefined; - let logPath: string | undefined; - if (logger) { - logPath = `/tmp/${shmName}.log`; - logFd = openSync(logPath, 'w'); - logger(`BB process logs redirected to: ${logPath}`); - } - - // Clean up any stale shared memory files from previous runs - // This handles the case where a previous process crashed without cleanup - const shmRequestPath = `/dev/shm/${shmName}_request`; - const shmResponsePath = `/dev/shm/${shmName}_response`; - try { - unlinkSync(shmRequestPath); - } catch (err) { - const isNotFound = err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT'; - if (!isNotFound) { - throw new Error(`Failed to clean up stale shared memory file ${shmRequestPath}: ${err}`); - } - } - - try { - unlinkSync(shmResponsePath); - } catch (err) { - const isNotFound = err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT'; - if (!isNotFound) { - throw new Error(`Failed to clean up stale shared memory file ${shmResponsePath}: ${err}`); - } - } - - // Spawn bb process with shared memory mode (SPSC-only, no max-clients needed) - const args = ['msgpack', 'run', '--input', `${shmName}.shm`, '--request-ring-size', `${1024 * 1024 * 4}`]; - const bbProcess = spawn(bbBinaryPath, args, { - stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'], - env, - }); - - // Disconnect from event loop so process can exit without waiting for bb - // The bb process has parent death monitoring (prctl on Linux, kqueue on macOS) - // so it will automatically exit when Node.js exits - bbProcess.unref(); - - // Track if process has exited - let processExited = false; - let exitError: Error | null = null; - - bbProcess.on('error', err => { - processExited = true; - exitError = new Error(`Native backend process error: ${err.message}`); - }); - - bbProcess.on('exit', (code, signal) => { - processExited = true; - if (code !== null && code !== 0) { - exitError = new Error(`Native backend process exited with code ${code}`); - } else if (signal && signal !== 'SIGTERM') { - exitError = new Error(`Native backend process killed with signal ${signal}`); - } + // Sync backends aren't expected to do long-lived work, so default to one thread. + const backend = await SpawnedProcessBackendSync.spawn({ + binaryPath: bbBinaryPath, + binaryName: 'bb', + instancePrefix: 'bb-sync', + ipcPathArgs: ['msgpack', 'run', '--input', '{path}'], + extraArgs: ['--request-ring-size', `${REQUEST_RING_SIZE}`], + transport: 'shm', + clientId: 0, + napiPath, + logger, + // bb monitors parent death (prctl/kqueue) and exits on its own, so it + // must not hold the Node event loop open; calls in flight still keep it + // alive. Without this a caller that never destroy()s the backend hangs + // at exit. + unref: true, + unrefStdio: unref, + env: { HARDWARE_CONCURRENCY: threads ? threads.toString() : '1' }, }); - - // Wait for bb to create shared memory - // Retry connection every 100ms for up to 3 seconds - const retryInterval = 100; // ms - const timeout = 3000; // ms - const maxAttempts = Math.floor(timeout / retryInterval); - let client: any = null; - - try { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - // Check if bb process has exited before attempting connection - if (processExited) { - throw exitError || new Error('Native backend process exited unexpectedly during startup'); - } - - // Wait before attempting connection (except first attempt) - if (attempt > 0) { - await new Promise(resolve => setTimeout(resolve, retryInterval)); - } - - try { - // Create NAPI client (SPSC-only, no max_clients needed) - client = new addon.MsgpackClient(shmName); - break; // Success! - } catch (err: any) { - // Connection failed, will retry - if (attempt === maxAttempts - 1) { - // Last attempt failed - check one more time if process exited - if (processExited && exitError) { - throw exitError as Error; - } - throw new Error(`Failed to connect to shared memory after ${timeout}ms: ${err.message}`); - } - } - } - - if (!client) { - throw new Error('Failed to create client connection'); - } - - return new BarretenbergNativeShmSyncBackend(bbProcess, client, logFd); - } finally { - // If we failed to connect, ensure the process is killed and log file closed - // kill() returns false if process already exited, but doesn't throw - if (!client) { - bbProcess.kill('SIGKILL'); - if (logFd !== undefined) { - try { - closeSync(logFd); - } catch { - // Ignore errors during cleanup - } - } - } - } + return new BarretenbergNativeShmSyncBackend(backend); } call(inputBuffer: Uint8Array): Uint8Array { - try { - const responseBuffer = this.client.call(Buffer.from(inputBuffer)); - return new Uint8Array(responseBuffer); - } catch (err: any) { - throw new Error(`Shared memory call failed: ${err.message}`); - } - } - - private cleanup(): void { - if (this.client) { - try { - this.client.close(); - } catch { - // Ignore errors during cleanup - } - } - if (this.logFd !== undefined) { - try { - closeSync(this.logFd); - } catch { - // Ignore errors during cleanup - } - } + return this.backend.call(inputBuffer); } destroy(): void { - this.cleanup(); - this.process.kill('SIGTERM'); - // Remove process event listeners to prevent hanging - this.process.removeAllListeners(); + this.backend.destroy(); } } diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts index 1f8eb77e498..68608fba1ac 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts @@ -1,266 +1,60 @@ -import { ChildProcess, spawn } from 'child_process'; -import { closeSync, openSync } from 'fs'; -import { createRequire } from 'module'; -import { threadId } from 'worker_threads'; +import { SpawnedProcessBackend } from '@aztec-foundation/ipc-runtime'; import { IMsgpackBackendAsync } from '../interface.js'; -import { findNapiBinary, findPackageRoot } from './platform.js'; -let instanceCounter = 0; +// Larger rings than the sync backend: this one pipelines, so several requests +// and responses can be in the rings at once. +const RING_SIZE = 1024 * 1024 * 4; /** - * Asynchronous shared memory backend that communicates with bb binary via shared memory. - * Uses NAPI module with background thread polling for async operations. - * Supports request pipelining - multiple requests can be in flight simultaneously. - * - * Architecture (matches socket backend pattern): - * - bb acts as the SERVER, TypeScript is the CLIENT - * - bb creates the shared memory region - * - TypeScript connects via NAPI wrapper (MsgpackClientAsync) - * - TypeScript manages promise queue (single-threaded, no mutex needed) - * - C++ background thread polls for responses, calls JavaScript callback - * - JavaScript callback pops queue and resolves promises in FIFO order + * Asynchronous native backend: bb serves over shared memory and + * @aztec-foundation/ipc-runtime's SpawnedProcessBackend owns the process lifecycle. + * Supports pipelining — responses are paired to callers by request id, so bb + * may complete them in any order. */ export class BarretenbergNativeShmAsyncBackend implements IMsgpackBackendAsync { - private process: ChildProcess; - private client: any; // NAPI MsgpackClientAsync instance - private logFd?: number; // File descriptor for logs - private logger: (msg: string) => void; - - // Queue of pending callbacks for pipelined requests - // Responses come back in FIFO order, so we match them with queued callbacks - private pendingCallbacks: Array<{ - resolve: (data: Uint8Array) => void; - reject: (error: Error) => void; - }> = []; - - private constructor(process: ChildProcess, client: any, logFd?: number, logger?: (msg: string) => void) { - this.process = process; - this.client = client; - this.logFd = logFd; - this.logger = logger ?? (() => {}); - - // Register our response handler with the C++ client - // This callback will be invoked from the background thread via ThreadSafeFunction - this.client.setResponseCallback((responseBuffer: Buffer) => { - this.handleResponse(responseBuffer); - }); - } - - /** - * Handle response from C++ background thread - * Dequeues the next pending callback and resolves it (FIFO order) - */ - private handleResponse(responseBuffer: Buffer): void { - // Response is complete - dequeue the next pending callback (FIFO) - const callback = this.pendingCallbacks.shift(); - if (callback) { - callback.resolve(new Uint8Array(responseBuffer)); - } else { - // This shouldn't happen - response without a pending request - this.logger('Received response but no pending callback'); - } - - // If no more pending callbacks, release ref to allow process to exit - if (this.pendingCallbacks.length === 0) { - this.client.release(); - } - } + private constructor(private backend: SpawnedProcessBackend) {} /** * Create and initialize an async shared memory backend. * @param bbBinaryPath Path to bb binary - * @param threads Optional number of threads (defaults to min(32, num_cpus)) - * @param logger Optional logger function for bb output + * @param napiPath Optional override for the ipc-runtime NAPI addon + * @param threads Optional number of threads (defaults to 16) + * @param logger Optional receiver for bb's output */ static async new( bbBinaryPath: string, - napiPath: string, + napiPath?: string, threads?: number, logger?: (msg: string) => void, + unref?: boolean, ): Promise { - // Import the NAPI module - // The addon is built to the nodejs_module directory - const addonPath = findNapiBinary(napiPath); - // Try loading - let addon: any = null; - try { - const require = createRequire(findPackageRoot()!); - addon = require(addonPath!); - } catch { - // Addon not built yet or not available - throw new Error('Shared memory async NAPI not available.'); - } - - // Create a unique shared memory name - const shmName = `bb-async-${process.pid}-${threadId}-${instanceCounter++}`; - - // If threads not set use num cpu cores, max 16 (same as socket backend) - const hwc = threads ? threads.toString() : '16'; - const env = { ...process.env, HARDWARE_CONCURRENCY: hwc }; - - // Set up file logging if logger is provided - // Direct file redirection bypasses Node event loop - logs are written even if process hangs - let logFd: number | undefined; - let logPath: string | undefined; - if (logger) { - logPath = `/tmp/${shmName}.log`; - logFd = openSync(logPath, 'w'); - logger(`BB process logs redirected to: ${logPath}`); - } - - // Spawn bb process with shared memory mode - // Use larger ring buffers for async mode to support pipelining - const args = [ - 'msgpack', - 'run', - '--input', - `${shmName}.shm`, - '--request-ring-size', - `${1024 * 1024 * 4}`, - '--response-ring-size', - `${1024 * 1024 * 4}`, - ]; - const bbProcess = spawn(bbBinaryPath, args, { - stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'], - env, - }); - - // Disconnect from event loop so process can exit without waiting for bb - // The bb process has parent death monitoring (prctl on Linux, kqueue on macOS) - // so it will automatically exit when Node.js exits - bbProcess.unref(); - - // Track if process has exited - let processExited = false; - let exitError: Error | null = null; - - bbProcess.on('error', err => { - processExited = true; - exitError = new Error(`Native backend process error: ${err.message}`); - }); - - bbProcess.on('exit', (code, signal) => { - processExited = true; - if (code !== null && code !== 0) { - exitError = new Error(`Native backend process exited with code ${code}`); - } else if (signal && signal !== 'SIGTERM') { - exitError = new Error(`Native backend process killed with signal ${signal}`); - } + const backend = await SpawnedProcessBackend.spawn({ + binaryPath: bbBinaryPath, + binaryName: 'bb', + instancePrefix: 'bb-async', + ipcPathArgs: ['msgpack', 'run', '--input', '{path}'], + extraArgs: ['--request-ring-size', `${RING_SIZE}`, '--response-ring-size', `${RING_SIZE}`], + transport: 'shm', + clientId: 0, + napiPath, + logger, + // bb monitors parent death (prctl/kqueue) and exits on its own, so it + // must not hold the Node event loop open; calls in flight still keep it + // alive. Without this a caller that never destroy()s the backend hangs + // at exit. + unref: true, + unrefStdio: unref, + env: { HARDWARE_CONCURRENCY: threads ? threads.toString() : '16' }, }); - - // Wait for bb to create shared memory - // Retry connection every 100ms for up to 5 seconds (longer than sync for thread startup) - const retryInterval = 100; // ms - const timeout = 5000; // ms - const maxAttempts = Math.floor(timeout / retryInterval); - let client: any = null; - - try { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - // Check if bb process has exited before attempting connection - if (processExited) { - throw exitError || new Error('Native backend process exited unexpectedly during startup'); - } - - // Wait before attempting connection (except first attempt) - if (attempt > 0) { - await new Promise(resolve => setTimeout(resolve, retryInterval)); - } - - try { - // Create NAPI async client - client = new addon.MsgpackClientAsync(shmName); - break; // Success! - } catch (err: any) { - // Connection failed, will retry - if (attempt === maxAttempts - 1) { - // Last attempt failed - check one more time if process exited - if (processExited && exitError) { - throw exitError as Error; - } - throw new Error(`Failed to connect to shared memory after ${timeout}ms: ${err.message}`); - } - } - } - - if (!client) { - throw new Error('Failed to create client connection'); - } - - return new BarretenbergNativeShmAsyncBackend(bbProcess, client, logFd, logger); - } finally { - // If we failed to connect, ensure the process is killed and log file closed - if (!client) { - bbProcess.kill('SIGKILL'); - if (logFd !== undefined) { - try { - closeSync(logFd); - } catch { - // Ignore errors during cleanup - } - } - } - } + return new BarretenbergNativeShmAsyncBackend(backend); } - /** - * Send a msgpack request asynchronously. - * Supports pipelining - can be called multiple times before awaiting responses. - * Use Promise.all() to send multiple requests concurrently. - * - * Example: - * const results = await Promise.all([ - * backend.call(buf1), - * backend.call(buf2), - * backend.call(buf3) - * ]); - * - * @param inputBuffer The msgpack-encoded request - * @returns Promise resolving to msgpack-encoded response - */ call(inputBuffer: Uint8Array): Promise { - return new Promise((resolve, reject) => { - // If this is the first pending callback, acquire ref to keep event loop alive - if (this.pendingCallbacks.length === 0) { - this.client.acquire(); - } - - // Enqueue this promise's callbacks (FIFO order) - this.pendingCallbacks.push({ resolve, reject }); - - try { - // Send request to shared memory (synchronous write) - // C++ call() no longer returns a promise - we manage them here - this.client.call(Buffer.from(inputBuffer)); - } catch (err: any) { - // Send failed - dequeue the callback we just added and reject - this.pendingCallbacks.pop(); - - // If queue is now empty, release ref to allow exit - if (this.pendingCallbacks.length === 0) { - this.client.release(); - } - - reject(new Error(`Shared memory async call failed: ${err.message}`)); - } - }); + return this.backend.call(inputBuffer); } destroy(): Promise { - // Kill the bb process - // Background thread and callbacks will be cleaned up by OS on process exit - this.process.kill('SIGTERM'); - this.process.removeAllListeners(); - - // Close log file if open - if (this.logFd !== undefined) { - try { - closeSync(this.logFd); - } catch { - // Ignore errors during cleanup - } - } - return Promise.resolve(); + return this.backend.destroy(); } } diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts index f8e856f8de5..9a25511e45a 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts @@ -86,13 +86,13 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { it('fails with the exit cause when bb dies before creating its socket', async () => { const fakeBb = writeFakeBbScript(`#!/bin/bash\nexit 17\n`); await expect(BarretenbergNativeSocketAsyncBackend.new(fakeBb)).rejects.toThrow( - /exited before socket connection was established \(code=17/, + /exited before IPC connection was ready \(code=17/, ); }); it('fails with the spawn error when the bb binary does not exist', async () => { await expect(BarretenbergNativeSocketAsyncBackend.new('/nonexistent/bb-binary')).rejects.toThrow( - /Native backend process error/, + /Failed to spawn bb/, ); }); @@ -103,7 +103,7 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { expect(await backend.call(new Uint8Array([1]))).toEqual(new Uint8Array([1])); process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !backend.isConnected()); + await waitUntil(() => !isProcessAlive(fake.pids()[0])); const err = await backend.call(new Uint8Array([2])).catch(e => e); expect(isRetryable(err)).toBe(true); @@ -119,7 +119,7 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { expect(await backend.call(new Uint8Array([1]))).toEqual(new Uint8Array([1])); process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !backend.isConnected()); + await waitUntil(() => !isProcessAlive(fake.pids()[0])); expect(await backend.call(new Uint8Array([2]))).toEqual(new Uint8Array([2])); const pids = fake.pids(); @@ -134,7 +134,7 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { await backend.call(new Uint8Array([1])); process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !backend.isConnected()); + await waitUntil(() => !isProcessAlive(fake.pids()[0])); const results = await Promise.all([1, 2, 3, 4].map(n => backend.call(new Uint8Array([n])))); expect(results).toEqual([1, 2, 3, 4].map(n => new Uint8Array([n]))); @@ -148,15 +148,14 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { await backend.call(new Uint8Array([1])); process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !backend.isConnected()); + await waitUntil(() => !isProcessAlive(fake.pids()[0])); const call = backend.call(new Uint8Array([2])).catch(e => e); await backend.destroy(); - expect(String(await call)).toMatch(/Backend connection closed/); + expect(String(await call)).toMatch(/destroy/); - // destroy() does not wait for the replacement, so watch for it to arrive and then go. - await waitUntil(() => fake.pids().length === 2); - await waitUntil(() => !isProcessAlive(fake.pids()[1])); + // Whether or not the replacement got as far as starting, nothing may be left running. + await waitUntil(() => fake.pids().every(pid => !isProcessAlive(pid))); }); it('fails retryably when the replacement cannot start either', async () => { @@ -177,12 +176,13 @@ describe('BarretenbergNativeSocketAsyncBackend', () => { const backend = await BarretenbergNativeSocketAsyncBackend.new(bb, undefined, undefined, undefined, true); await backend.call(new Uint8Array([1])); - process.kill(Number(fs.readFileSync(pidLog, 'utf-8').trim()), 'SIGKILL'); - await waitUntil(() => !backend.isConnected()); + const pid = Number(fs.readFileSync(pidLog, 'utf-8').trim()); + process.kill(pid, 'SIGKILL'); + await waitUntil(() => !isProcessAlive(pid)); const err = await backend.call(new Uint8Array([2])).catch(e => e); expect(isRetryable(err)).toBe(true); - expect(String(err)).toMatch(/exited before socket connection was established/); + expect(String(err)).toMatch(/exited before IPC connection was ready/); await backend.destroy(); }); diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts index 17c22e81622..d6a890c8088 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts @@ -1,153 +1,23 @@ -import { ChildProcess, spawn } from 'child_process'; -import { once } from 'events'; -import * as fs from 'fs'; -import * as net from 'net'; +import { SpawnedProcessBackend } from '@aztec-foundation/ipc-runtime'; import * as os from 'os'; -import * as path from 'path'; -import readline from 'readline'; -import { threadId } from 'worker_threads'; -import { BackendUnavailableError } from '../errors.js'; import { IMsgpackBackendAsync } from '../interface.js'; -let instanceCounter = 0; - -// Backstop for a bb process that is alive but wedged before listen(). Deliberately generous: -// on a fully loaded prover many bb processes can spawn simultaneously and startup time has no -// useful upper bound, so this must only ever fire when bb is genuinely stuck, never under load. -const STARTUP_TIMEOUT_MS = 60_000; - -/** What it takes to start a bb process, kept so a dead one can be replaced by an identical one. */ -interface SpawnOptions { - bbBinaryPath: string; - threads?: number; - logger?: (msg: string) => void; - unref?: boolean; - /** - * Replace the bb process when it dies, on the next call. Only safe where a bb process holds no - * state between calls: a replacement has no Chonk accumulation and no batch-verifier session. - * Off by default, so a caller that does hold such state keeps failing loudly instead of - * silently continuing against a fresh process. - */ - respawn?: boolean; -} - -/** A bb process and the connection to it. Replaced as a unit. */ -interface Incarnation { - proc: ChildProcess; - socket: net.Socket; - /** Kept so the path can be removed when the process is gone without having unlinked it. */ - socketPath: string; -} - /** - * Asynchronous native backend that communicates with bb binary via Unix Domain Socket. - * Uses event-based I/O with a state machine to handle partial reads. - * - * Architecture: bb acts as the SERVER, TypeScript is the CLIENT - * - bb creates the socket and listens for connections - * - TypeScript waits for socket file to exist, then connects + * Asynchronous native backend that communicates with the bb binary over a Unix + * Domain Socket, via @aztec-foundation/ipc-runtime's SpawnedProcessBackend: bb is spawned + * as the server (`bb msgpack run --input .sock`) and the runtime owns + * spawn, connect (raced against child death), envelope framing / request-id + * correlation, and teardown. * - * Protocol: - * - Request: 4-byte little-endian length + msgpack buffer - * - Response: 4-byte little-endian length + msgpack buffer + * The child and the idle socket are always unref'd: bb monitors parent death + * (prctl/kqueue) and exits on its own, so it must not hold the Node event loop + * open. `unref` additionally unrefs the log pipes, which would otherwise keep + * the loop alive while a logger is attached. */ export class BarretenbergNativeSocketAsyncBackend implements IMsgpackBackendAsync { - private socket: net.Socket | null; - - // Queue of pending callbacks for pipelined requests - // Responses come back in FIFO order, so we match them with queued callbacks - private pendingCallbacks: Array<{ - resolve: (data: Uint8Array) => void; - reject: (error: Error) => void; - }> = []; - - // State machine for reading responses - private readingLength: boolean = true; - private lengthBuffer: Buffer = Buffer.alloc(4); - private lengthBytesRead: number = 0; - private responseLength: number = 0; - private responseBuffer: Buffer | null = null; - private responseBytesRead: number = 0; - - private proc: ChildProcess | null = null; - /** Set when the process died; cleared when a replacement is adopted. */ - private death: Error | null = null; - /** Shared by every caller that finds the connection down, so one death causes one replacement. */ - private starting: Promise | null = null; - private destroyed = false; - - private constructor(private opts: SpawnOptions) { - this.socket = null; - this.logger = opts.logger ?? (() => {}); - } - - private logger: (msg: string) => void; - - /** Take ownership of a started process and its connection, and watch for its death. */ - private adopt(incarnation: Incarnation): void { - this.proc = incarnation.proc; - this.socket = incarnation.socket; - this.death = null; - this.readingLength = true; - this.lengthBytesRead = 0; - this.responseBuffer = null; - this.responseBytesRead = 0; - - const { proc, socket } = incarnation; - // Every listener is scoped to this incarnation and removed with it, so a death observed after - // a replacement was adopted cannot tear the replacement down. - proc.on('error', err => { - this.onDeath(incarnation, `Native backend process error: ${err.message}`); - }); - - proc.on('exit', (code, signal) => { - const reason = - code !== null && code !== 0 - ? `Native backend process exited with code ${code}` - : signal && signal !== 'SIGTERM' - ? `Native backend process killed with signal ${signal}` - : 'Native backend process exited unexpectedly'; - this.onDeath(incarnation, reason); - }); + private constructor(private backend: SpawnedProcessBackend) {} - socket.on('data', (chunk: Buffer) => { - this.handleData(chunk); - }); - - socket.on('error', err => { - this.onDeath(incarnation, `Socket error: ${err.message}`); - }); - - socket.on('end', () => { - this.onDeath(incarnation, 'Socket connection ended unexpectedly'); - }); - } - - /** - * This backend has lost the bb process it was talking to. In-flight calls fail as retryable, and - * the connection is dropped so the next call either starts a replacement or reports the death. - * - * Losing the connection does not mean the process is gone: bb's server keeps serving after a - * client disconnects, so it is killed here rather than left to outlive the backend that spawned - * it. Killing a process that has already exited is a no-op. - */ - private onDeath(incarnation: Incarnation, reason: string): void { - if (this.proc !== incarnation.proc) { - return; // A later incarnation is already in charge. - } - this.death = new BackendUnavailableError(reason); - this.proc = null; - retire(incarnation); - this.failAllPending(this.death); - } - - /** - * Spawn a bb process and wait until a socket connection to it is established. - * Waits as long as the bb process is alive (bb startup has no useful upper bound on a loaded - * machine), failing fast with the real cause if the process dies, and killing the process if - * it is still not accepting connections after the generous STARTUP_TIMEOUT_MS backstop. - */ static async new( bbBinaryPath: string, threads?: number, @@ -155,290 +25,28 @@ export class BarretenbergNativeSocketAsyncBackend implements IMsgpackBackendAsyn unref?: boolean, respawn?: boolean, ): Promise { - const opts: SpawnOptions = { bbBinaryPath, threads, logger, unref, respawn }; - const backend = new BarretenbergNativeSocketAsyncBackend(opts); - backend.adopt(await this.start(opts)); - return backend; - } - - /** Start a bb process and connect to it. The whole of what a replacement has to repeat. */ - private static async start(opts: SpawnOptions): Promise { - const { bbBinaryPath, threads, logger, unref } = opts; - // Create a unique socket path in temp directory - const socketPath = path.join(os.tmpdir(), `bb-${process.pid}-${threadId}-${instanceCounter++}.sock`); - - // Ensure socket path doesn't already exist (cleanup from previous crashes) - if (fs.existsSync(socketPath)) { - fs.unlinkSync(socketPath); - } - // If threads not set use num cpu cores, max 16. const hwc = threads ? threads.toString() : Math.min(16, os.cpus().length).toString(); - const env = { ...process.env, HARDWARE_CONCURRENCY: hwc }; - - // Spawn bb process - it will create the socket server - const args = ['msgpack', 'run', '--input', socketPath]; - const proc = spawn(bbBinaryPath, args, { - stdio: ['ignore', logger ? 'pipe' : 'ignore', logger ? 'pipe' : 'ignore'], - env, + const backend = await SpawnedProcessBackend.spawn({ + binaryPath: bbBinaryPath, + binaryName: 'bb', + instancePrefix: 'bb', + ipcPathArgs: ['msgpack', 'run', '--input', '{path}'], + transport: 'uds', + logger, + env: { HARDWARE_CONCURRENCY: hwc }, + unref: true, + unrefStdio: unref, + respawn, }); - - // Disconnect from event loop so process can exit without waiting for bb - // The bb process has parent death monitoring (prctl on Linux, kqueue on macOS) - // so it will automatically exit when Node.js exits - proc.unref(); - - if (logger) { - logger("Logger attached to bb process. DON'T FORGET TO DESTROY THE BACKEND to allow Node.js to exit."); - readline.createInterface({ input: proc.stdout! }).on('line', logger); - readline.createInterface({ input: proc.stderr! }).on('line', logger); - if (unref) { - (proc.stdout as any)?.unref?.(); - (proc.stderr as any)?.unref?.(); - } - } - - // Spawn failures (e.g. missing binary) surface only as an 'error' event, never as 'exit', - // so wait for the spawn/error outcome up front. Once 'spawn' has fired, every later death - // is observable via exitCode/signalCode in the connect loop below. - try { - await once(proc, 'spawn'); - } catch (err) { - // A missing or non-executable binary is a configuration fault: retrying cannot fix it. - const code = (err as NodeJS.ErrnoException).code; - const message = `Native backend process error: ${(err as Error).message}`; - throw code === 'ENOENT' || code === 'EACCES' ? new Error(message) : new BackendUnavailableError(message); - } - - try { - const socket = await this.waitForSocketAndConnect(socketPath, proc); - return { proc, socket, socketPath }; - } catch (err) { - proc.kill('SIGKILL'); - cleanUpSocketPath(socketPath); - // A bb that died starting up, never accepted a connection, or refused one failed for - // environmental reasons — a loaded machine, memory pressure — so it is worth another try. - throw new BackendUnavailableError((err as Error).message, { cause: err }); - } + return new BarretenbergNativeSocketAsyncBackend(backend); } - private static async waitForSocketAndConnect(socketPath: string, proc: ChildProcess): Promise { - const startTime = Date.now(); - for (;;) { - if (proc.exitCode !== null || proc.signalCode !== null) { - throw new Error( - `bb process exited before socket connection was established (code=${proc.exitCode} signal=${proc.signalCode})`, - ); - } - if (Date.now() - startTime > STARTUP_TIMEOUT_MS) { - throw new Error( - `bb process is alive but did not accept a socket connection within ${STARTUP_TIMEOUT_MS}ms: ${socketPath}`, - ); - } - - if (fs.existsSync(socketPath)) { - const stats = fs.statSync(socketPath); - if (!stats.isSocket()) { - throw new Error(`Path exists but is not a socket: ${socketPath}`); - } - try { - return await this.attemptConnect(socketPath); - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code !== 'ECONNREFUSED') { - throw new Error(`Failed to connect to bb socket: ${(err as Error).message}`); - } - // bb has bound the path but not yet called listen(); fall through and retry. - } - } - - await new Promise(resolve => setTimeout(resolve, 50)); - } - } - - private static attemptConnect(socketPath: string): Promise { - return new Promise((resolve, reject) => { - const socket = net.connect(socketPath); - socket.setNoDelay(true); - const onConnect = () => { - socket.removeListener('error', onError); - resolve(socket); - }; - const onError = (err: Error) => { - socket.removeListener('connect', onConnect); - socket.destroy(); - reject(err); - }; - socket.once('connect', onConnect); - socket.once('error', onError); - }); - } - - private failAllPending(error: Error): void { - for (const callback of this.pendingCallbacks) { - callback.reject(error); - } - this.pendingCallbacks = []; - if (this.socket) { - this.socket.destroy(); - this.socket = null; - } - } - - /** Whether a call can be made without first starting a replacement bb process. */ - isConnected(): boolean { - return this.socket !== null; - } - - private handleData(chunk: Buffer): void { - let offset = 0; - - while (offset < chunk.length) { - if (this.readingLength) { - // Reading 4-byte length prefix - const bytesToCopy = Math.min(4 - this.lengthBytesRead, chunk.length - offset); - chunk.copy(this.lengthBuffer, this.lengthBytesRead, offset, offset + bytesToCopy); - this.lengthBytesRead += bytesToCopy; - offset += bytesToCopy; - - if (this.lengthBytesRead === 4) { - // Length is complete, switch to reading data - this.responseLength = this.lengthBuffer.readUInt32LE(0); - this.responseBuffer = Buffer.alloc(this.responseLength); - this.responseBytesRead = 0; - this.readingLength = false; - } - } else { - // Reading response data - const bytesToCopy = Math.min(this.responseLength - this.responseBytesRead, chunk.length - offset); - chunk.copy(this.responseBuffer!, this.responseBytesRead, offset, offset + bytesToCopy); - this.responseBytesRead += bytesToCopy; - offset += bytesToCopy; - - if (this.responseBytesRead === this.responseLength) { - // Response is complete - dequeue the next pending callback (FIFO) - const callback = this.pendingCallbacks.shift(); - if (callback) { - callback.resolve(new Uint8Array(this.responseBuffer!)); - } else { - // This shouldn't happen - response without a pending request - this.logger('Received response but no pending callback'); - } - - // If no more pending callbacks, unref socket to allow process to exit - if (this.pendingCallbacks.length === 0 && this.socket) { - this.socket.unref(); - } - - // Reset state for next message - this.readingLength = true; - this.lengthBytesRead = 0; - this.responseLength = 0; - this.responseBuffer = null; - this.responseBytesRead = 0; - } - } - } - } - - async call(inputBuffer: Uint8Array): Promise { - const socket = await this.ensureConnected(); - - return new Promise((resolve, reject) => { - // If this is the first pending callback, ref the socket to keep event loop alive - if (this.pendingCallbacks.length === 0) { - socket.ref(); - } - - // Enqueue this promise's callbacks (FIFO order) - this.pendingCallbacks.push({ resolve, reject }); - - // Write request: 4-byte little-endian length + msgpack data - // Socket will buffer these if needed, maintaining order - const lengthBuf = Buffer.alloc(4); - lengthBuf.writeUInt32LE(inputBuffer.length, 0); - socket.write(lengthBuf); - socket.write(inputBuffer); - }); - } - - /** - * The connection to use for the next call, replacing a dead bb process when that is allowed. - * Concurrent callers share one replacement, so a single death costs a single process. - */ - private async ensureConnected(): Promise { - if (this.socket) { - return this.socket; - } - if (this.destroyed) { - throw new Error('Backend connection closed'); - } - if (!this.opts.respawn) { - // A fresh error each time: callers attach to what they are given, and one shared instance - // would carry one caller's stack and annotations to every other. - const death = this.death; - throw death - ? new BackendUnavailableError(death.message, { cause: death }) - : new BackendUnavailableError('Socket not connected'); - } - this.starting ??= this.replace().finally(() => { - this.starting = null; - }); - await this.starting; - // destroy() may have run while the replacement was starting. - if (!this.socket) { - throw new Error('Backend connection closed'); - } - return this.socket; - } - - private async replace(): Promise { - this.logger('bb process died; starting a replacement'); - const incarnation = await BarretenbergNativeSocketAsyncBackend.start(this.opts); - if (this.destroyed) { - // destroy() ran while this was starting: the owner is gone, so neither is this process. - retire(incarnation); - throw new Error('Backend connection closed'); - } - this.adopt(incarnation); + call(inputBuffer: Uint8Array): Promise { + return this.backend.call(inputBuffer); } destroy(): Promise { - this.destroyed = true; - this.failAllPending(new Error('Backend connection closed')); - // A replacement still starting is not waited for: replace() kills whatever it produces once it - // sees the backend destroyed. Waiting here would hold up shutdown for as long as a bb can take - // to come up, which has no useful upper bound. - const proc = this.proc; - this.proc = null; - // bb unlinks its own socket path when it shuts down cleanly, which SIGTERM gives it a chance to. - proc?.kill('SIGTERM'); - proc?.removeAllListeners(); - return Promise.resolve(); - } -} - -/** - * Stop watching an incarnation, drop its connection and kill its process. - * - * A bb killed this way never gets to unlink its socket path, so this does it instead; otherwise a - * long-lived backend that replaces its process leaves one file in the temp directory per death. - */ -function retire(incarnation: Incarnation): void { - incarnation.proc.removeAllListeners(); - incarnation.socket.removeAllListeners(); - incarnation.socket.destroy(); - incarnation.proc.kill('SIGKILL'); - const socketPath = incarnation.socketPath; - incarnation.proc.once('exit', () => cleanUpSocketPath(socketPath)); - cleanUpSocketPath(socketPath); -} - -/** Remove a socket path a bb process is no longer listening on. */ -function cleanUpSocketPath(socketPath: string): void { - try { - fs.unlinkSync(socketPath); - } catch { - // Already gone, which is the usual case: bb unlinks its own path when it exits cleanly. + return this.backend.destroy(); } } diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts b/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts index 3decbd611f5..966f8d82a93 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts @@ -128,38 +128,3 @@ export function findBbBinary(customPath?: string): string | null { return null; } - -export function findNapiBinary(customPath?: string): string | null { - // Check custom path first if provided - if (customPath) { - if (fs.existsSync(customPath)) { - return path.resolve(customPath); - } - // Custom path provided but doesn't exist - return null - return null; - } - - // Automatic detection - const platform = detectPlatform(); - if (!platform) { - return null; - } - - const buildDir = PLATFORM_TO_BUILD_DIR[platform]; - - // Get package root by climbing directory tree to find package.json - const packageRoot = findPackageRoot(); - - if (!packageRoot) { - return null; - } - - // Check in build//nodejs_module.node - const bbPath = path.join(packageRoot, 'build', buildDir, 'nodejs_module.node'); - - if (fs.existsSync(bbPath)) { - return bbPath; - } - - return null; -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/wasm.ts b/barretenberg/ts/bb.js/src/bb_backends/wasm.ts index d481de87d4e..5d08a545668 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/wasm.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/wasm.ts @@ -26,7 +26,7 @@ export class BarretenbergWasmSyncBackend implements IMsgpackBackendSync { } call(inputBuffer: Uint8Array): Uint8Array { - return this.wasm.cbindCall('bbapi', inputBuffer); + return this.wasm.cbindCall('ipc_ffi_entry', inputBuffer); } destroy(): void { @@ -96,7 +96,7 @@ export class BarretenbergWasmAsyncBackend implements IMsgpackBackendAsync { } call(inputBuffer: Uint8Array): Promise { - return Promise.resolve(this.wasm.cbindCall('bbapi', inputBuffer)); + return Promise.resolve(this.wasm.cbindCall('ipc_ffi_entry', inputBuffer)); } async destroy(): Promise { diff --git a/barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts b/barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts index 0e4cc335f91..05a63d2519f 100644 --- a/barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts +++ b/barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts @@ -16,7 +16,7 @@ interface RawStep { const TEST_TIMEOUT_MS = 30 * 60 * 1000; const DEFAULT_WASM_FLOW_LIMIT = 1; function findRepoRoot(): string { - return process.env.AZTEC_REPO_ROOT ?? resolve(process.cwd(), '../..'); + return process.env.AZTEC_REPO_ROOT ?? resolve(process.cwd(), '../../..'); } function ensurePinnedInputsRoot(): string { diff --git a/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts b/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts index 1a4785bc172..3c90963fd11 100644 --- a/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts +++ b/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts @@ -1,5 +1,5 @@ import { BarretenbergWasmSyncBackend } from '../bb_backends/wasm.js'; -import { SyncApi } from '../cbind/generated/sync.js'; +import { SyncApi } from '../generated/sync.js'; describe('BBApi Exception Handling from bb.js', () => { let backend: BarretenbergWasmSyncBackend; diff --git a/barretenberg/ts/bb.js/src/cbind/README.md b/barretenberg/ts/bb.js/src/cbind/README.md deleted file mode 100644 index 9cd309aa980..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/README.md +++ /dev/null @@ -1 +0,0 @@ -Derives bindings from the reported msgpack schema from bb. diff --git a/barretenberg/ts/bb.js/src/cbind/cpp_codegen.ts b/barretenberg/ts/bb.js/src/cbind/cpp_codegen.ts deleted file mode 100644 index ed584e83065..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/cpp_codegen.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * C++ IPC Client Code Generator - * - * Generates a C++ IPC client from a CompiledSchema. The generated client: - * - Connects to a server over Unix Domain Socket via ipc::IpcClient - * - Wraps each command in a NamedUnion, serializes with msgpack, sends, receives, deserializes - * - Has one method per command, returning the typed response - * - * Usage: - * const gen = new CppCodegen({ namespace: 'bb::cdb', prefix: 'Cdb' }); - * const header = gen.generateHeader(schema); - * const impl = gen.generateImpl(schema); - */ -import { toSnakeCase } from './naming.js'; -import type { Command, CompiledSchema } from './schema_visitor.js'; - -export interface CppCodegenOptions { - /** C++ namespace for generated code, e.g. 'bb::cdb' */ - namespace: string; - /** Prefix for command/response types, e.g. 'Cdb' */ - prefix: string; - /** Header path for the *_execute.hpp file that defines Command/CommandResponse NamedUnions */ - executeHeader: string; - /** Header path for the *_commands.hpp file that defines the command structs */ - commandsHeader: string; -} - -export class CppCodegen { - constructor(private opts: CppCodegenOptions) {} - - /** Convert a command name to a C++ method name (snake_case without prefix) */ - private methodName(commandName: string): string { - // Strip prefix: "CdbGetContractInstance" -> "GetContractInstance" -> "get_contract_instance" - const withoutPrefix = commandName.startsWith(this.opts.prefix) - ? commandName.slice(this.opts.prefix.length) - : commandName; - return toSnakeCase(withoutPrefix); - } - - /** Check if the response has fields (non-void return) */ - private hasResponseFields(command: Command, schema: CompiledSchema): boolean { - const resp = schema.responses.get(command.responseType); - return !!resp && resp.fields.length > 0; - } - - /** Generate the method signature using command struct types directly */ - private generateMethodSignature(command: Command, schema: CompiledSchema, className?: string): string { - const method = this.methodName(command.name); - const hasFields = this.hasResponseFields(command, schema); - const retType = hasFields ? `${command.name}::Response` : 'void'; - - // If the command has fields, take the whole command struct by value - const params = command.fields.length > 0 ? `${command.name} cmd` : ''; - - const prefix = className ? `${className}::` : ''; - const constSuffix = !this.isWriteCommand(command) ? ' const' : ''; - - return `${retType} ${prefix}${method}(${params})${constSuffix}`; - } - - /** Check if a command modifies state (non-const) */ - private isWriteCommand(command: Command): boolean { - const name = command.name.toLowerCase(); - return ( - name.includes('add') || - name.includes('create') || - name.includes('commit') || - name.includes('revert') || - name.includes('register') || - name.includes('shutdown') || - name.includes('delete') || - name.includes('sync') || - name.includes('rollback') || - name.includes('unwind') - ); - } - - /** Generate the header file */ - generateHeader(schema: CompiledSchema): string { - const { namespace: ns, prefix } = this.opts; - const className = `${prefix}IpcClient`; - const methods = schema.commands - .map(cmd => { - const sig = this.generateMethodSignature(cmd, schema); - return ` ${sig};`; - }) - .join('\n'); - - return `// AUTOGENERATED FILE - DO NOT EDIT -#pragma once - -#include "barretenberg/common/try_catch_shim.hpp" -#include "${this.opts.executeHeader}" -#include "barretenberg/ipc/ipc_client.hpp" - -#include -#include - -namespace ${ns} { - -/** - * @brief Auto-generated IPC client. - * - * Each method sends a msgpack-serialized command to the server over UDS - * and returns the typed response. All methods block until the response arrives. - */ -class ${className} { - public: - explicit ${className}(const std::string& socket_path); - ~${className}(); - - ${className}(const ${className}&) = delete; - ${className}& operator=(const ${className}&) = delete; - -${methods} - - private: - template - typename Cmd::Response send(Cmd&& cmd) const; - - mutable std::unique_ptr client_; -}; - -} // namespace ${ns} -`; - } - - /** Generate the implementation file */ - generateImpl(schema: CompiledSchema): string { - const { namespace: ns, prefix } = this.opts; - const className = `${prefix}IpcClient`; - const commandType = `${prefix}Command`; - const responseType = `${prefix}CommandResponse`; - const errorType = `${prefix}ErrorResponse`; - - const methods = schema.commands - .map(cmd => { - return this.generateMethodImpl(cmd, schema, className); - }) - .join('\n'); - - return `// AUTOGENERATED FILE - DO NOT EDIT - -#include "${this.headerIncludePath()}" -#include "${this.opts.executeHeader}" -#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" - -#include -#include - -namespace ${ns} { - -${className}::${className}(const std::string& socket_path) - : client_(ipc::IpcClient::create_socket(socket_path)) -{ - if (!client_->connect()) { - throw std::runtime_error("Failed to connect to server at " + socket_path); - } -} - -${className}::~${className}() -{ - if (client_) { - client_->close(); - } -} - -template -typename Cmd::Response ${className}::send(Cmd&& cmd) const -{ - // Wrap command in ${commandType} NamedUnion, then in a 1-element tuple (matches server expectations) - ${commandType} command = std::forward(cmd); - auto wrapped = std::make_tuple(std::move(command)); - - // Serialize to msgpack - msgpack::sbuffer send_buffer; - msgpack::pack(send_buffer, wrapped); - - // Send to server - constexpr uint64_t timeout_ns = 30'000'000'000ULL; // 30 seconds - if (!client_->send(send_buffer.data(), send_buffer.size(), timeout_ns)) { - throw std::runtime_error("Failed to send command to server"); - } - - // Receive response - auto response_span = client_->receive(timeout_ns); - if (response_span.empty()) { - throw std::runtime_error("Empty response from server"); - } - - // Deserialize response - auto unpacked = msgpack::unpack(reinterpret_cast(response_span.data()), response_span.size()); - auto response_obj = unpacked.get(); - - ${responseType} response; - response_obj.convert(response); - - // Release the receive buffer - client_->release(response_span.size()); - - // Check for error response - return std::move(response).visit([](auto&& resp) -> typename Cmd::Response { - using RespType = std::decay_t; - - if constexpr (std::is_same_v) { - throw std::runtime_error("Server error: " + resp.message); - } else if constexpr (std::is_same_v) { - return std::forward(resp); - } else { - throw std::runtime_error("Unexpected response type from server"); - } - }); -} - -${methods} -} // namespace ${ns} -`; - } - - /** Generate a single method implementation */ - private generateMethodImpl(command: Command, schema: CompiledSchema, className: string): string { - const sig = this.generateMethodSignature(command, schema, className); - const hasFields = this.hasResponseFields(command, schema); - - const cmdExpr = command.fields.length > 0 ? 'std::move(cmd)' : `${command.name}{}`; - - if (!hasFields) { - return `${sig} -{ - send(${cmdExpr}); -} -`; - } - - return `${sig} -{ - return send(${cmdExpr}); -} -`; - } - - /** Compute the include path for the generated header */ - private headerIncludePath(): string { - // Derive from the executeHeader path: replace _execute.hpp with _ipc_client_generated.hpp - const dir = this.opts.executeHeader.substring(0, this.opts.executeHeader.lastIndexOf('/')); - return `${dir}/${toSnakeCase(this.opts.prefix)}_ipc_client_generated.hpp`; - } -} diff --git a/barretenberg/ts/bb.js/src/cbind/generate.ts b/barretenberg/ts/bb.js/src/cbind/generate.ts deleted file mode 100644 index 4f7ceba46ab..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/generate.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Multi-language code generation from BB msgpack schema - * - * Architecture: - * Raw Schema → SchemaVisitor → CompiledSchema IR → Language Codegens → Files - */ -import { exec } from 'child_process'; -import { mkdirSync, writeFileSync } from 'fs'; -import { unpack } from 'msgpackr'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import { promisify } from 'util'; - -import { RustCodegen } from './rust_codegen.js'; -import { type CompiledSchema, SchemaVisitor } from './schema_visitor.js'; -import { TypeScriptCodegen } from './typescript_codegen.js'; - -const execAsync = promisify(exec); - -function log(message: string) { - process.stdout.write(`${message}\n`); -} - -// Language generators - all use the same CompiledSchema IR -interface LanguageGenerator { - name: string; - enabled: boolean; - generate: (compiled: CompiledSchema) => Array<{ path: string; content: string }>; -} - -const LANGUAGE_GENERATORS: LanguageGenerator[] = [ - { - name: 'TypeScript', - enabled: true, - generate: compiled => { - const tsGen = new TypeScriptCodegen(); - return [ - { path: 'generated/api_types.ts', content: tsGen.generateTypes(compiled) }, - { path: 'generated/sync.ts', content: tsGen.generateSyncApi(compiled) }, - { path: 'generated/async.ts', content: tsGen.generateAsyncApi(compiled) }, - ]; - }, - }, - { - name: 'Rust', - enabled: true, - generate: compiled => { - const rustGen = new RustCodegen(); - return [ - { path: '../../../../rust/barretenberg-rs/src/generated_types.rs', content: rustGen.generateTypes(compiled) }, - { path: '../../../../rust/barretenberg-rs/src/api.rs', content: rustGen.generateApi(compiled) }, - ]; - }, - }, -]; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- The same source is also compiled for CommonJS. -// @ts-ignore -- import.meta is valid when this generator runs as ESM, but TypeScript rejects it in the CJS build. -const __dirname = dirname(fileURLToPath(import.meta.url)); - -async function generate() { - const bbBuildPath = process.env.BB_BINARY_PATH || join(__dirname, '../../../../cpp/build/bin/bb'); - - // Get schema from bb - log('Fetching msgpack schema from bb...'); - const { stdout } = await execAsync(`${bbBuildPath} msgpack schema`); - const schema = JSON.parse(stdout.trim()); - - if (!schema.commands || !schema.responses) { - throw new Error('Invalid schema: missing commands or responses'); - } - - // Compile schema once using visitor pattern - log('Compiling schema...'); - const visitor = new SchemaVisitor(); - const compiled = visitor.visit(schema.commands, schema.responses); - - log(`Found ${compiled.commands.length} commands, ${compiled.structs.size} structs\n`); - - // Ensure output directory exists - const outputDir = join(__dirname, 'generated'); - mkdirSync(outputDir, { recursive: true }); - - // Generate all language bindings from compiled IR - for (const generator of LANGUAGE_GENERATORS) { - if (!generator.enabled) { - log(`⊘ ${generator.name}: disabled`); - continue; - } - - const files = generator.generate(compiled); - - for (const file of files) { - const outputPath = join(__dirname, file.path); - mkdirSync(dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, file.content); - log(`✓ ${generator.name}: ${outputPath}`); - } - } - - // Generate curve constants - log('\nGenerating curve constants...'); - await generateCurveConstants(bbBuildPath, outputDir); - - log('\n✨ Generation complete! Clean, maintainable, multi-language architecture.'); -} - -async function generateCurveConstants(bbBuildPath: string, outputDir: string) { - // Get curve constants from bb as msgpack binary - const { stdout: constantsBuffer } = await execAsync(`${bbBuildPath} msgpack curve_constants`, { - encoding: 'buffer', - maxBuffer: 10 * 1024 * 1024, // 10MB buffer - }); - - // Decode msgpack - const constants = unpack(constantsBuffer as Buffer); - - // Helper to convert Uint8Array to bigint (big-endian) - const toBigInt = (bytes: Uint8Array) => { - let result = 0n; - for (const byte of bytes) { - result = (result << 8n) | BigInt(byte); - } - return result; - }; - - // Helper to serialize point coordinate (handles both Uint8Array and array of Uint8Array for field2) - const serializeCoordinate = (coord: Uint8Array | Uint8Array[]) => { - if (Array.isArray(coord)) { - // For field2 (like BN254 G2), we have array of two Uint8Arrays - return `[${coord.map(c => `new Uint8Array([${Array.from(c).join(', ')}])`).join(', ')}]`; - } else { - // For regular fields, single Uint8Array - return `new Uint8Array([${Array.from(coord).join(', ')}])`; - } - }; - - // Generate TypeScript file - const content = `/** - * Curve constants generated from barretenberg native binary. - * DO NOT EDIT - This file is auto-generated by generate.ts - */ - -/** - * BN254 curve constants - */ -export const BN254_FR_MODULUS = ${toBigInt(constants.bn254_fr_modulus)}n; -export const BN254_FQ_MODULUS = ${toBigInt(constants.bn254_fq_modulus)}n; - -export const BN254_G1_GENERATOR = { - x: ${serializeCoordinate(constants.bn254_g1_generator.x)}, - y: ${serializeCoordinate(constants.bn254_g1_generator.y)}, -} as const; - -export const BN254_G2_GENERATOR = { - x: ${serializeCoordinate(constants.bn254_g2_generator.x)}, - y: ${serializeCoordinate(constants.bn254_g2_generator.y)}, -} as const; - -/** - * Grumpkin curve constants - */ -export const GRUMPKIN_FR_MODULUS = ${toBigInt(constants.grumpkin_fr_modulus)}n; -export const GRUMPKIN_FQ_MODULUS = ${toBigInt(constants.grumpkin_fq_modulus)}n; - -export const GRUMPKIN_G1_GENERATOR = { - x: ${serializeCoordinate(constants.grumpkin_g1_generator.x)}, - y: ${serializeCoordinate(constants.grumpkin_g1_generator.y)}, -} as const; - -/** - * Secp256k1 curve constants - */ -export const SECP256K1_FR_MODULUS = ${toBigInt(constants.secp256k1_fr_modulus)}n; -export const SECP256K1_FQ_MODULUS = ${toBigInt(constants.secp256k1_fq_modulus)}n; - -export const SECP256K1_G1_GENERATOR = { - x: ${serializeCoordinate(constants.secp256k1_g1_generator.x)}, - y: ${serializeCoordinate(constants.secp256k1_g1_generator.y)}, -} as const; - -/** - * Secp256r1 curve constants - */ -export const SECP256R1_FR_MODULUS = ${toBigInt(constants.secp256r1_fr_modulus)}n; -export const SECP256R1_FQ_MODULUS = ${toBigInt(constants.secp256r1_fq_modulus)}n; - -export const SECP256R1_G1_GENERATOR = { - x: ${serializeCoordinate(constants.secp256r1_g1_generator.x)}, - y: ${serializeCoordinate(constants.secp256r1_g1_generator.y)}, -} as const; -`; - - const outputPath = join(outputDir, 'curve_constants.ts'); - writeFileSync(outputPath, content); - log(`✓ Curve constants: ${outputPath}`); -} - -// Run the generator -generate().catch(error => { - const message = error instanceof Error ? (error.stack ?? error.message) : String(error); - process.stderr.write(`Generation failed: ${message}\n`); - process.exit(1); -}); diff --git a/barretenberg/ts/bb.js/src/cbind/naming.ts b/barretenberg/ts/bb.js/src/cbind/naming.ts deleted file mode 100644 index 27e3aa91d08..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/naming.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Shared naming utilities for code generators - */ - -/** - * Convert camelCase or PascalCase to snake_case - * @example toSnakeCase("Blake2s") -> "blake2s" - * @example toSnakeCase("poseidonHash") -> "poseidon_hash" - */ -export function toSnakeCase(name: string): string { - return name - .replace(/([A-Z])/g, '_$1') - .toLowerCase() - .replace(/^_/, ''); -} - -/** - * Convert snake_case to PascalCase - * @example toPascalCase("blake2s") -> "Blake2s" - * @example toPascalCase("poseidon_hash") -> "PoseidonHash" - */ -export function toPascalCase(name: string): string { - // Already PascalCase (no underscores and starts with uppercase) - if (!name.includes('_') && name[0] === name[0].toUpperCase()) { - return name; - } - return name - .split('_') - .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) - .join(''); -} diff --git a/barretenberg/ts/bb.js/src/cbind/rust_codegen.ts b/barretenberg/ts/bb.js/src/cbind/rust_codegen.ts deleted file mode 100644 index 972a23a4ed6..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/rust_codegen.ts +++ /dev/null @@ -1,563 +0,0 @@ -/** - * Rust Code Generator - String template based - * - * Philosophy: - * - String templates for file structure - * - Simple type mapping - * - Idiomatic Rust conventions - * - No complex abstraction - */ -import { toPascalCase, toSnakeCase } from './naming.js'; -import type { CompiledSchema, Field, Struct, Type } from './schema_visitor.js'; - -export class RustCodegen { - private errorTypeName: string = 'ErrorResponse'; - - // Type mapping: Schema type -> Rust type - private mapType(type: Type): string { - switch (type.kind) { - case 'primitive': - switch (type.primitive) { - case 'bool': - return 'bool'; - case 'u8': - return 'u8'; - case 'u16': - return 'u16'; - case 'u32': - return 'u32'; - case 'u64': - return 'u64'; - case 'f64': - return 'f64'; - case 'string': - return 'String'; - case 'bytes': - return 'Vec'; - case 'field2': - return '[Vec; 2]'; // Extension field (Fq2) - pair of 32-byte field elements - case 'enum_u32': - return 'u32'; // C++ enum serialized as uint32 - case 'map_u32_pair': - return 'std::collections::HashMap, u64)>'; - } - break; - - case 'vector': - return `Vec<${this.mapType(type.element!)}>`; - - case 'array': { - const elemType = this.mapType(type.element!); - // Large arrays become Vec for ergonomics - return type.size! > 32 ? `Vec<${elemType}>` : `[${elemType}; ${type.size}]`; - } - - case 'optional': - return `Option<${this.mapType(type.element!)}>`; - - case 'struct': - // Convert struct names to PascalCase for Rust conventions - return toPascalCase(type.struct!.name); - } - - return 'Unknown'; - } - - // Check if field needs serde(with = "serde_bytes") - private needsSerdeBytes(type: Type): boolean { - return type.kind === 'primitive' && type.primitive === 'bytes'; - } - - // Check if field needs serde(with = "serde_vec_bytes") - private needsSerdeVecBytes(type: Type): boolean { - return type.kind === 'vector' && this.needsSerdeBytes(type.element!); - } - - // Check if field needs serde(with = "serde_array2_bytes") - for [Vec; 2] (Fq2 extension field) - private needsSerdeArray2Bytes(type: Type): boolean { - return type.kind === 'primitive' && type.primitive === 'field2'; - } - - // Check if field needs serde(with = "serde_array4_bytes") - for [Vec; 4] (Poseidon2 state) - private needsSerdeArray4Bytes(type: Type): boolean { - return type.kind === 'array' && type.size === 4 && this.needsSerdeBytes(type.element!); - } - - // Generate struct field - private generateField(field: Field): string { - const rustName = toSnakeCase(field.name); - const rustType = this.mapType(field.type); - let attrs = ''; - - // Add serde rename if needed - if (field.name !== rustName) { - attrs += ` #[serde(rename = "${field.name}")]\n`; - } - - // Add serde bytes handling - if (this.needsSerdeArray2Bytes(field.type)) { - attrs += ` #[serde(with = "serde_array2_bytes")]\n`; - } else if (this.needsSerdeArray4Bytes(field.type)) { - attrs += ` #[serde(with = "serde_array4_bytes")]\n`; - } else if (this.needsSerdeVecBytes(field.type)) { - attrs += ` #[serde(with = "serde_vec_bytes")]\n`; - } else if (this.needsSerdeBytes(field.type)) { - attrs += ` #[serde(with = "serde_bytes")]\n`; - } - - return `${attrs} pub ${rustName}: ${rustType},`; - } - - // Generate a struct definition - private generateStruct(struct: Struct, isCommand: boolean): string { - const rustName = toPascalCase(struct.name); - const fields = struct.fields.map(f => this.generateField(f)).join('\n'); - - // Add serde rename if struct name changed - const serdeRename = struct.name !== rustName ? `\n#[serde(rename = "${struct.name}")]` : ''; - - // Commands need __typename field for struct identification, but skip it during serialization - const typenameField = isCommand - ? ` #[serde(rename = "__typename", skip_serializing)]\n pub type_name: String,\n` - : ''; - - // Generate constructor for commands - const constructor = isCommand ? this.generateConstructor(struct, rustName) : ''; - - return `/// ${struct.name} -#[derive(Debug, Clone, Serialize, Deserialize)]${serdeRename} -pub struct ${rustName} { -${typenameField}${fields} -}${constructor}`; - } - - // Generate constructor for command structs - private generateConstructor(struct: Struct, rustName: string): string { - const params = struct.fields.map(f => `${toSnakeCase(f.name)}: ${this.mapType(f.type)}`).join(', '); - - const fieldInits = [ - ` type_name: "${struct.name}".to_string(),`, - ...struct.fields.map(f => ` ${toSnakeCase(f.name)},`), - ].join('\n'); - - return ` - -impl ${rustName} { - pub fn new(${params}) -> Self { - Self { -${fieldInits} - } - } -}`; - } - - // Generate Command enum - private generateCommandEnum(schema: CompiledSchema): string { - const names = Array.from(schema.structs.keys()); - const variants = names - .map(name => { - const rustName = toPascalCase(name); - return ` ${rustName}(${rustName}),`; - }) - .join('\n'); - - const serializeCases = names - .map(name => { - const rustName = toPascalCase(name); - return ` Command::${rustName}(data) => { - tuple.serialize_element("${name}")?; - tuple.serialize_element(data)?; - }`; - }) - .join('\n'); - - const deserializeCases = names - .map(name => { - const rustName = toPascalCase(name); - return ` "${name}" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Command::${rustName}(data)) - }`; - }) - .join('\n'); - - const variantNames = names.map(name => `"${name}"`).join(', '); - - return `/// Command enum - wraps all possible commands -#[derive(Debug, Clone)] -pub enum Command { -${variants} -} - -impl Serialize for Command { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - use serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(2)?; - match self { -${serializeCases} - } - tuple.end() - } -} - -impl<'de> Deserialize<'de> for Command { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - use serde::de::{SeqAccess, Visitor}; - struct CommandVisitor; - - impl<'de> Visitor<'de> for CommandVisitor { - type Value = Command; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a 2-element array [name, payload]") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let name: String = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - match name.as_str() { -${deserializeCases} - _ => Err(serde::de::Error::unknown_variant(&name, &[${variantNames}])), - } - } - } - deserializer.deserialize_tuple(2, CommandVisitor) - } -}`; - } - - // Generate Response enum - private generateResponseEnum(schema: CompiledSchema): string { - // Include all response types from commands plus ErrorResponse if it exists - const commandResponseTypes = Array.from(new Set(schema.commands.map(c => c.responseType))); - const errorName = schema.errorTypeName || 'ErrorResponse'; - const responseTypes = schema.responses.has(errorName) ? [...commandResponseTypes, errorName] : commandResponseTypes; - const variants = responseTypes - .map(name => { - const rustName = toPascalCase(name); - return ` ${rustName}(${rustName}),`; - }) - .join('\n'); - - const serializeCases = responseTypes - .map(name => { - const rustName = toPascalCase(name); - return ` Response::${rustName}(data) => { - tuple.serialize_element("${name}")?; - tuple.serialize_element(data)?; - }`; - }) - .join('\n'); - - const deserializeCases = responseTypes - .map(name => { - const rustName = toPascalCase(name); - return ` "${name}" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::${rustName}(data)) - }`; - }) - .join('\n'); - - const variantNames = responseTypes.map(name => `"${name}"`).join(', '); - - return `/// Response enum - wraps all possible responses -#[derive(Debug, Clone)] -pub enum Response { -${variants} -} - -impl Serialize for Response { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - use serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(2)?; - match self { -${serializeCases} - } - tuple.end() - } -} - -impl<'de> Deserialize<'de> for Response { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - use serde::de::{SeqAccess, Visitor}; - struct ResponseVisitor; - - impl<'de> Visitor<'de> for ResponseVisitor { - type Value = Response; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a 2-element array [name, payload]") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let name: String = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - match name.as_str() { -${deserializeCases} - _ => Err(serde::de::Error::unknown_variant(&name, &[${variantNames}])), - } - } - } - deserializer.deserialize_tuple(2, ResponseVisitor) - } -}`; - } - - // Generate serde helper modules - private generateSerdeHelpers(): string { - return `mod serde_bytes { - use serde::{Deserialize, Deserializer, Serializer}; - pub fn serialize(bytes: &Vec, serializer: S) -> Result - where S: Serializer { serializer.serialize_bytes(bytes) } - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where D: Deserializer<'de> { >::deserialize(deserializer) } -} - -mod serde_vec_bytes { - use serde::{Deserialize, Deserializer, Serializer, Serialize}; - use serde::ser::SerializeSeq; - use serde::de::{SeqAccess, Visitor}; - - #[derive(Serialize, Deserialize)] - struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); - - pub fn serialize(vec: &Vec>, serializer: S) -> Result - where S: Serializer { - let mut seq = serializer.serialize_seq(Some(vec.len()))?; - for bytes in vec { - seq.serialize_element(&BytesWrapper(bytes.clone()))?; - } - seq.end() - } - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> - where D: Deserializer<'de> { - struct VecVecU8Visitor; - impl<'de> Visitor<'de> for VecVecU8Visitor { - type Value = Vec>; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a sequence of byte arrays") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let mut vec = Vec::new(); - while let Some(wrapper) = seq.next_element::()? { - vec.push(wrapper.0); - } - Ok(vec) - } - } - deserializer.deserialize_seq(VecVecU8Visitor) - } -} - -mod serde_array2_bytes { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use serde::ser::SerializeTuple; - use serde::de::{SeqAccess, Visitor}; - - #[derive(Serialize, Deserialize)] - struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); - - pub fn serialize(arr: &[Vec; 2], serializer: S) -> Result - where S: Serializer { - let mut tup = serializer.serialize_tuple(2)?; - for bytes in arr { - tup.serialize_element(&BytesWrapper(bytes.clone()))?; - } - tup.end() - } - pub fn deserialize<'de, D>(deserializer: D) -> Result<[Vec; 2], D::Error> - where D: Deserializer<'de> { - struct Array2Visitor; - impl<'de> Visitor<'de> for Array2Visitor { - type Value = [Vec; 2]; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an array of 2 byte arrays") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let mut arr: [Vec; 2] = Default::default(); - for (i, item) in arr.iter_mut().enumerate() { - *item = seq.next_element::()? - .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?.0; - } - Ok(arr) - } - } - deserializer.deserialize_tuple(2, Array2Visitor) - } -} - -mod serde_array4_bytes { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use serde::ser::SerializeTuple; - use serde::de::{SeqAccess, Visitor}; - - #[derive(Serialize, Deserialize)] - struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); - - pub fn serialize(arr: &[Vec; 4], serializer: S) -> Result - where S: Serializer { - let mut tup = serializer.serialize_tuple(4)?; - for bytes in arr { - tup.serialize_element(&BytesWrapper(bytes.clone()))?; - } - tup.end() - } - pub fn deserialize<'de, D>(deserializer: D) -> Result<[Vec; 4], D::Error> - where D: Deserializer<'de> { - struct Array4Visitor; - impl<'de> Visitor<'de> for Array4Visitor { - type Value = [Vec; 4]; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an array of 4 byte arrays") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let mut arr: [Vec; 4] = Default::default(); - for (i, item) in arr.iter_mut().enumerate() { - *item = seq.next_element::()? - .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?.0; - } - Ok(arr) - } - } - deserializer.deserialize_tuple(4, Array4Visitor) - } -}`; - } - - // Generate types file - generateTypes(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; - // Create set of top-level command struct names (only these need __typename) - const commandNames = new Set(schema.commands.map(c => c.name)); - - // Generate all structs (commands first, then responses) - const commandStructs = Array.from(schema.structs.values()) - .map(s => this.generateStruct(s, commandNames.has(s.name))) - .join('\n\n'); - - const responseStructs = Array.from(schema.responses.values()) - .map(s => this.generateStruct(s, false)) - .join('\n\n'); - - return `//! AUTOGENERATED - DO NOT EDIT -//! Generated from Barretenberg msgpack schema - -use serde::{Deserialize, Serialize}; - -${this.generateSerdeHelpers()} - -${commandStructs} - -${responseStructs} - -${this.generateCommandEnum(schema)} - -${this.generateResponseEnum(schema)} -`; - } - - // Generate API method - private generateApiMethod(command: { name: string; fields: Field[]; responseType: string }): string { - const methodName = toSnakeCase(command.name); - const cmdRustName = toPascalCase(command.name); - const respRustName = toPascalCase(command.responseType); - - const params = command.fields - .map(f => { - const rustType = this.mapType(f.type); - // Only convert simple Vec to &[u8], not nested types - const apiType = rustType === 'Vec' ? '&[u8]' : rustType; - return `${toSnakeCase(f.name)}: ${apiType}`; - }) - .join(', '); - - const paramConversions = command.fields - .map(f => { - const name = toSnakeCase(f.name); - const rustType = this.mapType(f.type); - // Only convert slices back to Vec - if (rustType === 'Vec') { - return `${name}.to_vec()`; - } - return name; - }) - .join(', '); - - return ` /// Execute ${command.name} command - pub fn ${methodName}(&mut self, ${params}) -> Result<${respRustName}> { - let cmd = Command::${cmdRustName}(${cmdRustName}::new(${paramConversions})); - match self.execute(cmd)? { - Response::${respRustName}(resp) => Ok(resp), - Response::${toPascalCase(this.errorTypeName)}(err) => Err(BarretenbergError::Backend( - err.message - )), - _ => Err(BarretenbergError::InvalidResponse( - "Expected ${command.responseType}".to_string() - )), - } - }`; - } - - // Generate API file - generateApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; - const apiMethods = schema.commands - .filter(c => c.name !== 'Shutdown') - .map(c => this.generateApiMethod(c)) - .join('\n\n'); - - return `//! AUTOGENERATED - DO NOT EDIT -//! High-level Barretenberg API - msgpack details hidden - -use crate::backend::Backend; -use crate::error::{BarretenbergError, Result}; -use crate::generated_types::*; - -/// High-level Barretenberg API -pub struct BarretenbergApi { - backend: B, -} - -impl BarretenbergApi { - /// Create API with custom backend - pub fn new(backend: B) -> Self { - Self { backend } - } - - fn execute(&mut self, command: Command) -> Result { - let input_buffer = rmp_serde::to_vec_named(&vec![command]) - .map_err(|e| BarretenbergError::Serialization(e.to_string()))?; - - let output_buffer = self.backend.call(&input_buffer)?; - - let response: Response = rmp_serde::from_slice(&output_buffer) - .map_err(|e| BarretenbergError::Deserialization(e.to_string()))?; - - Ok(response) - } - -${apiMethods} - - /// Shutdown backend gracefully - pub fn shutdown(&mut self) -> Result<()> { - let cmd = Command::Shutdown(Shutdown::new()); - let _ = self.execute(cmd)?; - self.backend.destroy() - } - - /// Destroy backend without shutdown command - pub fn destroy(&mut self) -> Result<()> { - self.backend.destroy() - } -} -`; - } -} diff --git a/barretenberg/ts/bb.js/src/cbind/schema_visitor.ts b/barretenberg/ts/bb.js/src/cbind/schema_visitor.ts deleted file mode 100644 index 733fb86dc1a..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/schema_visitor.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Schema Visitor - Minimal abstraction over raw msgpack schema - * - * Philosophy: - * - Keep raw schema structure - * - Resolve type references into a graph - * - No normalization - languages handle their own conventions - * - Output is "compiled schema" with resolved types - */ - -export type PrimitiveType = - | 'bool' - | 'u8' - | 'u16' - | 'u32' - | 'u64' - | 'f64' - | 'string' - | 'bytes' - | 'field2' - | 'enum_u32' - | 'map_u32_pair'; - -export interface Type { - kind: 'primitive' | 'vector' | 'array' | 'optional' | 'struct'; - primitive?: PrimitiveType; - element?: Type; // For vector, array, optional - size?: number; // For array - struct?: Struct; // For struct types -} - -export interface Field { - name: string; - type: Type; -} - -export interface Struct { - name: string; - fields: Field[]; -} - -export interface Command { - name: string; - fields: Field[]; - responseType: string; -} - -export interface CompiledSchema { - // All unique struct types discovered - structs: Map; - - // Command -> Response mappings - commands: Command[]; - - // Response types - responses: Map; - - // Error response type name (e.g. 'WsdbErrorResponse') - errorTypeName?: string; -} - -/** - * SchemaVisitor - Walks raw msgpack schema and resolves references - */ -export class SchemaVisitor { - private structs = new Map(); - private responses = new Map(); - - visit(commandsSchema: any, responsesSchema: any): CompiledSchema { - // Reset state - this.structs.clear(); - this.responses.clear(); - - const commands: Command[] = []; - - // Schema format: ["named_union", [[name, schema], ...]] - const commandPairs = commandsSchema[1] as Array<[string, any]>; - const responsePairs = responsesSchema[1] as Array<[string, any]>; - - // First, visit all response types (including ErrorResponse) - for (const [respName, respSchema] of responsePairs) { - if (typeof respSchema !== 'string') { - const respStruct = this.visitStruct(respName, respSchema); - this.responses.set(respName, respStruct); - } - } - - // Find the error response type name (e.g. 'WsdbErrorResponse') - const errorResponses = responsePairs.filter(([name]: [string, any]) => name.endsWith('ErrorResponse')); - const errorTypeName = errorResponses.length > 0 ? errorResponses[0][0] : undefined; - - // Visit all commands and pair with responses - const normalResponses = responsePairs.filter(([name]: [string, any]) => !name.endsWith('ErrorResponse')); - for (let i = 0; i < commandPairs.length; i++) { - const [cmdName, cmdSchema] = commandPairs[i]; - const [respName] = normalResponses[i]; - - // Discover command structure - const cmdStruct = this.visitStruct(cmdName, cmdSchema); - this.structs.set(cmdName, cmdStruct); - - // Create command mapping - commands.push({ - name: cmdName, - fields: cmdStruct.fields, - responseType: respName, - }); - } - - return { - structs: this.structs, - commands, - responses: this.responses, - errorTypeName, - }; - } - - private visitStruct(name: string, schema: any): Struct { - const fields: Field[] = []; - - // Schema is an object with __typename and fields - for (const [key, value] of Object.entries(schema)) { - if (key === '__typename') { - continue; - } - - fields.push({ - name: key, - type: this.visitType(value), - }); - } - - return { name, fields }; - } - - private visitType(schema: any): Type { - // Primitive string type - if (typeof schema === 'string') { - return this.resolvePrimitive(schema); - } - - // Array type descriptor: ['vector', [elementType]] - if (Array.isArray(schema)) { - const [kind, args] = schema; - - switch (kind) { - case 'vector': { - const [elemType] = args as [any]; - // Special case: vector = bytes - if (elemType === 'unsigned char') { - return { kind: 'primitive', primitive: 'bytes' }; - } - return { - kind: 'vector', - element: this.visitType(elemType), - }; - } - - case 'array': { - const [elemType, size] = args as [any, number]; - // Special case: array = bytes - if (elemType === 'unsigned char') { - return { kind: 'primitive', primitive: 'bytes' }; - } - return { - kind: 'array', - element: this.visitType(elemType), - size, - }; - } - - case 'optional': { - const [elemType] = args as [any]; - return { - kind: 'optional', - element: this.visitType(elemType), - }; - } - - case 'shared_ptr': { - // Dereference shared_ptr - just use inner type - const [innerType] = args as [any]; - return this.visitType(innerType); - } - - case 'alias': { - // Alias types (like uint256_t) are treated as bytes - return { kind: 'primitive', primitive: 'bytes' }; - } - - default: - throw new Error(`Unknown type kind: ${kind}`); - } - } - - // Inline struct definition - if (typeof schema === 'object' && schema.__typename) { - const structName = schema.__typename as string; - // Check if already visited - if (!this.structs.has(structName)) { - const struct = this.visitStruct(structName, schema); - this.structs.set(structName, struct); - } - return { - kind: 'struct', - struct: this.structs.get(structName)!, - }; - } - - throw new Error(`Cannot resolve type: ${JSON.stringify(schema)}`); - } - - private resolvePrimitive(name: string): Type { - const primitiveMap: Record = { - bool: 'bool', - int: 'u32', - 'unsigned int': 'u32', - 'unsigned short': 'u16', - 'unsigned long': 'u64', - 'unsigned long long': 'u64', - 'unsigned char': 'u8', - double: 'f64', - string: 'string', - bin32: 'bytes', - field2: 'field2', // Extension field (Fq2) - pair of field elements - MerkleTreeId: 'enum_u32', // C++ enum serialized as uint32 - CircuitKind: 'enum_u32', - ['unordered_map']: 'map_u32_pair', // StateReference: map> - }; - - const primitive = primitiveMap[name]; - if (primitive) { - return { kind: 'primitive', primitive }; - } - - // Unknown primitive - treat as struct reference - // This will be resolved later if it's a real struct - return { - kind: 'struct', - struct: { name, fields: [] }, // Placeholder - }; - } -} diff --git a/barretenberg/ts/bb.js/src/cbind/typescript_codegen.ts b/barretenberg/ts/bb.js/src/cbind/typescript_codegen.ts deleted file mode 100644 index 411b7172dc2..00000000000 --- a/barretenberg/ts/bb.js/src/cbind/typescript_codegen.ts +++ /dev/null @@ -1,447 +0,0 @@ -/** - * TypeScript Code Generator - String template based - * - * Philosophy: - * - String templates for file structure - * - Simple type mapping - * - Idiomatic TypeScript conventions - * - No complex abstraction - */ -import { toPascalCase } from './naming.js'; -import type { Command, CompiledSchema, Field, Struct, Type } from './schema_visitor.js'; - -function toCamelCase(name: string): string { - const pascal = toPascalCase(name); - return pascal.charAt(0).toLowerCase() + pascal.slice(1); -} - -export class TypeScriptCodegen { - private errorTypeName: string = 'ErrorResponse'; - - // Type mapping: Schema type -> TypeScript type - private mapType(type: Type): string { - switch (type.kind) { - case 'primitive': - switch (type.primitive) { - case 'bool': - return 'boolean'; - case 'u8': - return 'number'; - case 'u16': - return 'number'; - case 'u32': - return 'number'; - case 'u64': - return 'number'; - case 'f64': - return 'number'; - case 'string': - return 'string'; - case 'bytes': - return 'Uint8Array'; - case 'field2': - return '[Uint8Array, Uint8Array]'; // Extension field (Fq2) - case 'enum_u32': - return 'number'; // C++ enum as integer - case 'map_u32_pair': - return 'Record'; // map> - } - break; - - case 'vector': { - const inner = this.mapType(type.element!); - // Wrap union types in parens to avoid precedence issues: (Foo | undefined)[] - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; - } - - case 'array': { - const inner = this.mapType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; - } - - case 'optional': - return `${this.mapType(type.element!)} | undefined`; - - case 'struct': - return toPascalCase(type.struct!.name); - } - - return 'unknown'; - } - - // Type mapping for msgpack interfaces (uses Msgpack* prefix for structs) - private mapMsgpackType(type: Type): string { - switch (type.kind) { - case 'primitive': - switch (type.primitive) { - case 'bool': - return 'boolean'; - case 'u8': - return 'number'; - case 'u16': - return 'number'; - case 'u32': - return 'number'; - case 'u64': - return 'number'; - case 'f64': - return 'number'; - case 'string': - return 'string'; - case 'bytes': - return 'Uint8Array'; - case 'field2': - return '[Uint8Array, Uint8Array]'; - case 'enum_u32': - return 'number'; - case 'map_u32_pair': - return 'Record'; - } - break; - - case 'vector': { - const inner = this.mapMsgpackType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; - } - - case 'array': { - const inner = this.mapMsgpackType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; - } - - case 'optional': - return `${this.mapMsgpackType(type.element!)} | undefined`; - - case 'struct': - return `Msgpack${toPascalCase(type.struct!.name)}`; - } - - return 'unknown'; - } - - // Check if type needs conversion (has nested structs) - private needsConversion(type: Type): boolean { - switch (type.kind) { - case 'primitive': - return false; - case 'vector': - case 'array': - case 'optional': - return this.needsConversion(type.element!); - case 'struct': - return true; - } - return false; - } - - // Generate field - private generateField(field: Field): string { - const tsName = toCamelCase(field.name); - const tsType = this.mapType(field.type); - return ` ${tsName}: ${tsType};`; - } - - // Generate msgpack field (original names, uses Msgpack* types for structs) - private generateMsgpackField(field: Field): string { - const tsType = this.mapMsgpackType(field.type); - return ` ${field.name}: ${tsType};`; - } - - // Generate public interface - private generateInterface(struct: Struct): string { - const tsName = toPascalCase(struct.name); - const fields = struct.fields.map(f => this.generateField(f)).join('\n'); - - return `export interface ${tsName} { -${fields} -}`; - } - - // Generate msgpack interface (internal) - private generateMsgpackInterface(struct: Struct): string { - const tsName = toPascalCase(struct.name); - const fields = struct.fields.map(f => this.generateMsgpackField(f)).join('\n'); - - return `interface Msgpack${tsName} { -${fields} -}`; - } - - // Generate to* conversion function - private generateToFunction(struct: Struct): string { - const tsName = toPascalCase(struct.name); - - if (struct.fields.length === 0) { - return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { - return {}; -}`; - } - - const checks = struct.fields - .map( - f => ` if (o.${f.name} === undefined) { throw new Error("Expected ${f.name} in ${tsName} deserialization"); }`, - ) - .join('\n'); - - const conversions = struct.fields - .map(f => { - const tsFieldName = toCamelCase(f.name); - const converter = this.generateToConverter(f.type, `o.${f.name}`); - return ` ${tsFieldName}: ${converter},`; - }) - .join('\n'); - - return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { -${checks}; - return { -${conversions} - }; -}`; - } - - // Generate from* conversion function - private generateFromFunction(struct: Struct): string { - const tsName = toPascalCase(struct.name); - - if (struct.fields.length === 0) { - return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { - return {}; -}`; - } - - const checks = struct.fields - .map(f => { - const tsFieldName = toCamelCase(f.name); - return ` if (o.${tsFieldName} === undefined) { throw new Error("Expected ${tsFieldName} in ${tsName} serialization"); }`; - }) - .join('\n'); - - const conversions = struct.fields - .map(f => { - const tsFieldName = toCamelCase(f.name); - const converter = this.generateFromConverter(f.type, `o.${tsFieldName}`); - return ` ${f.name}: ${converter},`; - }) - .join('\n'); - - return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { -${checks}; - return { -${conversions} - }; -}`; - } - - // Generate converter for to* function - private generateToConverter(type: Type, value: string): string { - if (!this.needsConversion(type)) { - return value; - } - - switch (type.kind) { - case 'vector': - case 'array': - if (this.needsConversion(type.element!)) { - return `${value}.map((v: any) => ${this.generateToConverter(type.element!, 'v')})`; - } - return value; - case 'optional': - if (this.needsConversion(type.element!)) { - return `${value} != null ? ${this.generateToConverter(type.element!, value)} : undefined`; - } - return value; - case 'struct': - return `to${toPascalCase(type.struct!.name)}(${value})`; - } - return value; - } - - // Generate converter for from* function - private generateFromConverter(type: Type, value: string): string { - if (!this.needsConversion(type)) { - return value; - } - - switch (type.kind) { - case 'vector': - case 'array': - if (this.needsConversion(type.element!)) { - return `${value}.map((v: any) => ${this.generateFromConverter(type.element!, 'v')})`; - } - return value; - case 'optional': - if (this.needsConversion(type.element!)) { - return `${value} != null ? ${this.generateFromConverter(type.element!, value)} : undefined`; - } - return value; - case 'struct': - return `from${toPascalCase(type.struct!.name)}(${value})`; - } - return value; - } - - // Generate types file (api_types.ts) - generateTypes(schema: CompiledSchema): string { - const allStructs = [...schema.structs.values(), ...schema.responses.values()]; - - // Public interfaces - const publicInterfaces = allStructs.map(s => this.generateInterface(s)).join('\n\n'); - - // Msgpack interfaces - const msgpackInterfaces = allStructs.map(s => this.generateMsgpackInterface(s)).join('\n\n'); - - // Conversion functions - const toFunctions = allStructs.map(s => 'export ' + this.generateToFunction(s)).join('\n\n'); - - const fromFunctions = allStructs.map(s => 'export ' + this.generateFromFunction(s)).join('\n\n'); - - // BbApiBase interface - const apiMethods = schema.commands - .map(c => ` ${toCamelCase(c.name)}(command: ${toPascalCase(c.name)}): Promise<${toPascalCase(c.responseType)}>;`) - .join('\n'); - - return `// AUTOGENERATED FILE - DO NOT EDIT - -// Type aliases for primitive types -export type Field2 = [Uint8Array, Uint8Array]; - -// Public interfaces (exported) -${publicInterfaces} - -// Private Msgpack interfaces (not exported) -${msgpackInterfaces} - -// Conversion functions (exported) -${toFunctions} - -${fromFunctions} - -// Base API interface -export interface BbApiBase { -${apiMethods} - destroy(): Promise; -} -`; - } - - // Generate API method - private generateAsyncApiMethod(command: Command): string { - const methodName = toCamelCase(command.name); - const cmdType = toPascalCase(command.name); - const respType = toPascalCase(command.responseType); - - return ` ${methodName}(command: ${cmdType}): Promise<${respType}> { - const msgpackCommand = from${cmdType}(command); - return msgpackCall(this.backend, [["${command.name}", msgpackCommand]]).then(([variantName, result]: [string, any]) => { - if (variantName === '${this.errorTypeName}') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== '${command.responseType}') { - throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); - } - return to${respType}(result); - }); - }`; - } - - private generateSyncApiMethod(command: Command): string { - const methodName = toCamelCase(command.name); - const cmdType = toPascalCase(command.name); - const respType = toPascalCase(command.responseType); - - return ` ${methodName}(command: ${cmdType}): ${respType} { - const msgpackCommand = from${cmdType}(command); - const [variantName, result] = msgpackCall(this.backend, [["${command.name}", msgpackCommand]]); - if (variantName === 'ErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== '${command.responseType}') { - throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); - } - return to${respType}(result); - }`; - } - - // Generate async API file - generateAsyncApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; - const imports = this.generateApiImports(schema); - const methods = schema.commands.map(c => this.generateAsyncApiMethod(c)).join('\n\n'); - - return `// AUTOGENERATED FILE - DO NOT EDIT - -import { IMsgpackBackendAsync } from '../../bb_backends/interface.js'; -import { Decoder, Encoder } from 'msgpackr'; -import { BBApiException } from '../../bbapi_exception.js'; -${imports} - -async function msgpackCall(backend: IMsgpackBackendAsync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); - const encodedResult = await backend.call(inputBuffer); - return new Decoder({ useRecords: false }).unpack(encodedResult); -} - -export class AsyncApi implements BbApiBase { - constructor(protected backend: IMsgpackBackendAsync) {} - -${methods} - - destroy(): Promise { - return this.backend.destroy ? this.backend.destroy() : Promise.resolve(); - } -} -`; - } - - // Generate sync API file - generateSyncApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; - const imports = this.generateApiImports(schema); - const methods = schema.commands.map(c => this.generateSyncApiMethod(c)).join('\n\n'); - - return `// AUTOGENERATED FILE - DO NOT EDIT - -import { IMsgpackBackendSync } from '../../bb_backends/interface.js'; -import { Decoder, Encoder } from 'msgpackr'; -import { BBApiException } from '../../bbapi_exception.js'; -${imports} - -function msgpackCall(backend: IMsgpackBackendSync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); - const encodedResult = backend.call(inputBuffer); - return new Decoder({ useRecords: false }).unpack(encodedResult); -} - -export class SyncApi { - constructor(protected backend: IMsgpackBackendSync) {} - -${methods} - - destroy(): void { - if (this.backend.destroy) this.backend.destroy(); - } -} -`; - } - - // Generate import statement for API files - private generateApiImports(schema: CompiledSchema): string { - const types = new Set(); - - // Add command types and their conversion functions - for (const cmd of schema.commands) { - const cmdType = toPascalCase(cmd.name); - const respType = toPascalCase(cmd.responseType); - types.add(cmdType); - types.add(respType); - types.add(`from${cmdType}`); - types.add(`to${respType}`); - } - - // Add BbApiBase - types.add('BbApiBase'); - - const sortedTypes = Array.from(types).sort(); - return `import { ${sortedTypes.join(', ')} } from './api_types.js';`; - } -} diff --git a/barretenberg/ts/bb.js/src/cbind/circuit_kind.ts b/barretenberg/ts/bb.js/src/circuit_kind.ts similarity index 100% rename from barretenberg/ts/bb.js/src/cbind/circuit_kind.ts rename to barretenberg/ts/bb.js/src/circuit_kind.ts diff --git a/barretenberg/ts/bb.js/src/index.ts b/barretenberg/ts/bb.js/src/index.ts index 4a8fc436775..77e7aa41794 100644 --- a/barretenberg/ts/bb.js/src/index.ts +++ b/barretenberg/ts/bb.js/src/index.ts @@ -28,12 +28,17 @@ export type { GrumpkinPoint, Secp256k1Point, Secp256r1Point, - Field2, -} from './cbind/generated/api_types.js'; +} from './generated/api_types.js'; -export { toChonkProof } from './cbind/generated/api_types.js'; +export { toChonkProof } from './generated/api_types.js'; -export { CircuitKind } from './cbind/circuit_kind.js'; +/** + * @deprecated Fq2 coordinates are typed per curve now (see Bn254G2Point). + * Kept so consumers of the previous public API keep compiling. + */ +export type Field2 = [Uint8Array, Uint8Array]; + +export { CircuitKind } from './circuit_kind.js'; // Export curve constants for use in foundation export { @@ -50,6 +55,6 @@ export { SECP256R1_FR_MODULUS, SECP256R1_FQ_MODULUS, SECP256R1_G1_GENERATOR, -} from './cbind/generated/curve_constants.js'; +} from './generated/curve_constants.js'; -export { findBbBinary, findNapiBinary } from './bb_backends/node/platform.js'; +export { findBbBinary } from './bb_backends/node/platform.js'; diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index 20c7ff56775..97a4c764659 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -14,6 +14,12 @@ hash=$(hash_str \ $(cache_content_hash .rebuild_patterns) \ $(semver check $REF_NAME && echo 1 || echo 0)) +# The workspaces resolve @aztec-foundation/ipc-runtime through a portal into +# ipc-runtime/ts, so what node_modules ends up containing depends on that +# package's manifest. Include it in the node-modules cache key; without it a +# change there leaves a stale tree cached. +IPC_RUNTIME_PKG="^ipc-runtime/ts/package\.json$" + function generate_bb_avm_sim_package { node --experimental-strip-types --no-warnings \ "$ROOT/ipc-codegen/src/generate.ts" \ @@ -86,7 +92,7 @@ function build_bb_avm_sim { echo_header "bb-avm-sim package build" generate_packages copy_bb_avm_sim_native - npm_install_deps + npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$BB_AVM_SIM_PACKAGE" build prepare_bb_avm_sim_arch_packages "$(arch)-$(os)=build/$(arch)-$(os)/$BB_AVM_SIM_BINARY" } @@ -132,7 +138,7 @@ function release_bb_bin { function build_cdb { echo_header "cdb package build" generate_packages - npm_install_deps + npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$CDB_PACKAGE" build } @@ -161,7 +167,7 @@ function cross_copy_bb_js { function cross_copy_bb_avm_sim { generate_packages copy_bb_avm_sim_cross "$@" - npm_install_deps + npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$BB_AVM_SIM_PACKAGE" build prepare_bb_avm_sim_arch_packages } @@ -187,7 +193,7 @@ function release_bb_avm_sim { generate_packages copy_bb_avm_sim_native copy_bb_avm_sim_cross - npm_install_deps + npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$BB_AVM_SIM_PACKAGE" build prepare_bb_avm_sim_arch_packages for package_dir in bb-avm-sim/packages/*; do @@ -198,7 +204,7 @@ function release_bb_avm_sim { function release_cdb { generate_packages - npm_install_deps + npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$CDB_PACKAGE" build (cd cdb && retry "deploy_npm ${REF_NAME#v}") } diff --git a/barretenberg/ts/yarn.lock b/barretenberg/ts/yarn.lock index 462775f1ab2..eace48622f5 100644 --- a/barretenberg/ts/yarn.lock +++ b/barretenberg/ts/yarn.lock @@ -60,6 +60,7 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@workspace:bb.js" dependencies: + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" "@jest/globals": "npm:^30.0.0" "@swc/core": "npm:^1.10.1" "@swc/jest": "npm:^0.2.37" diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index c0f3e4264fb..3ff3949d12c 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -73,6 +73,7 @@ interface Args { ffi: boolean; curveConstants: string; stripMethodPrefix: boolean; + stripTypePrefix: boolean; } function usage(): never { @@ -101,6 +102,9 @@ Optional: --prefix Type prefix (auto-detected when >= 2 commands share one) --strip-method-prefix Strip the prefix from generated method names in all languages (e.g. BbCircuitProve -> circuitProve) + --strip-type-prefix Strip the prefix from generated type and converter + names too (e.g. BbCircuitProve -> CircuitProve). + Wire tags always keep the full schema name. --uds Copy UDS backend templates (rust, zig only) --ffi Copy in-process FFI backend templates (rust, zig only) --cpp-namespace C++ namespace (e.g. my::ns) @@ -132,6 +136,7 @@ function parseArgs(argv: string[]): Args { ffi: false, curveConstants: "", stripMethodPrefix: false, + stripTypePrefix: false, }; for (let i = 0; i < argv.length; i++) { @@ -205,6 +210,9 @@ function parseArgs(argv: string[]): Args { case "--strip-method-prefix": args.stripMethodPrefix = true; break; + case "--strip-type-prefix": + args.stripTypePrefix = true; + break; default: console.error(`Unknown flag: ${flag}`); process.exit(1); @@ -366,6 +374,7 @@ function generate(args: Args) { const serverPackage = !!args.packageDir && args.server && !args.client; const gen = new TypeScriptCodegen({ stripMethodPrefix: stripMethodPrefix ? prefix : undefined, + stripTypePrefix: args.stripTypePrefix ? prefix : undefined, }); writeFile("api_types.ts", gen.generateTypes(compiled, schemaHash)); if (args.server) { @@ -459,6 +468,7 @@ function generate(args: Args) { const gen = new RustCodegen({ prefix, stripMethodPrefix: stripMethodPrefix, + stripTypePrefix: args.stripTypePrefix, }); writeFile( `${toSnakeCase(prefix)}_types.rs`, diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts index 98b1d6442fe..876a533df3c 100644 --- a/ipc-codegen/src/rust_codegen.ts +++ b/ipc-codegen/src/rust_codegen.ts @@ -22,6 +22,8 @@ export interface RustCodegenOptions { backendImport?: string; /** Import path for error types. Defaults to 'crate::error::{IpcError, Result}' */ errorImport?: string; + /** Strip the prefix from generated type and variant names (e.g. BbBlake2s -> Blake2s) */ + stripTypePrefix?: boolean; /** Import path for generated types. Defaults to 'crate::types_gen::*' */ typesImport?: string; /** Module doc comment for types file */ @@ -32,6 +34,23 @@ export interface RustCodegenOptions { export class RustCodegen { private errorTypeName: string = "ErrorResponse"; + + /** + * The generated identifier for a schema name: PascalCase, with the service + * prefix stripped when asked. Wire strings always keep the schema name, so + * this must never be used where a tag is emitted. + */ + private typeName(schemaName: string): string { + let name = schemaName; + const prefix = this.opts?.stripTypePrefix ? this.opts.prefix : ""; + if (prefix && name.startsWith(prefix)) { + const rest = name.slice(prefix.length); + if (rest && rest[0] === rest[0].toUpperCase()) { + name = rest; + } + } + return toPascalCase(name); + } private opts: Required; constructor(options?: RustCodegenOptions) { @@ -40,6 +59,7 @@ export class RustCodegen { this.opts = { prefix, stripMethodPrefix: options?.stripMethodPrefix ?? false, + stripTypePrefix: options?.stripTypePrefix ?? false, apiStructName: options?.apiStructName ?? `${name}Api`, backendImport: options?.backendImport ?? "super::backend::Backend", errorImport: options?.errorImport ?? `super::error::{IpcError, Result}`, @@ -99,7 +119,7 @@ export class RustCodegen { case "struct": // Convert struct names to PascalCase for Rust conventions - return toPascalCase(type.struct!.name); + return this.typeName(type.struct!.name); } throw new Error(`Unsupported type kind: ${type.kind}`); @@ -136,6 +156,28 @@ export class RustCodegen { ); } + // Check if field needs serde(with = "serde_fixed_bytes") - for [u8; N]. + // These are raw bytes on the wire (msgpack bin), not a sequence of integers, + // so they need an explicit codec: serde's default for [u8; N] is a sequence, + // which the C++ side rejects. + private needsSerdeFixedBytes(type: Type): boolean { + return ( + type.kind === "array" && + type.size! <= 32 && + this.isU8(type.element!) + ); + } + + private isU8(type: Type): boolean { + return type.kind === "primitive" && type.primitive === "u8"; + } + + // Arrays of u8 above the size-32 cutoff map to Vec; they are still raw + // bytes on the wire, so they take the plain serde_bytes codec. + private needsSerdeLargeFixedBytes(type: Type): boolean { + return type.kind === "array" && type.size! > 32 && this.isU8(type.element!); + } + // Check if field needs serde(with = "serde_opt_bytes") private needsSerdeOptBytes(type: Type): boolean { return type.kind === "optional" && this.needsSerdeBytes(type.element!); @@ -153,7 +195,11 @@ export class RustCodegen { } // Add serde bytes handling - if (this.needsSerdeBytesArray(field.type)) { + if (this.needsSerdeLargeFixedBytes(field.type)) { + attrs += ` #[serde(with = "serde_bytes")]\n`; + } else if (this.needsSerdeFixedBytes(field.type)) { + attrs += ` #[serde(with = "serde_fixed_bytes")]\n`; + } else if (this.needsSerdeBytesArray(field.type)) { attrs += ` #[serde(with = "serde_bytes_array")]\n`; } else if ( this.needsSerdeVecBytes(field.type) || @@ -171,7 +217,7 @@ export class RustCodegen { // Generate a struct definition private generateStruct(struct: Struct, isCommand: boolean): string { - const rustName = toPascalCase(struct.name); + const rustName = this.typeName(struct.name); const fields = struct.fields.map((f) => this.generateField(f)).join("\n"); // Add serde rename if struct name changed @@ -216,14 +262,14 @@ ${fieldInits} const names = schema.commands.map((c) => c.name); const variants = names .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` ${rustName}(${rustName}),`; }) .join("\n"); const serializeCases = names .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` Command::${rustName}(data) => { tuple.serialize_element("${name}")?; tuple.serialize_element(data)?; @@ -233,7 +279,7 @@ ${fieldInits} const deserializeCases = names .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` "${name}" => { let data = seq.next_element()? .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; @@ -300,14 +346,14 @@ ${deserializeCases} : commandResponseTypes; const variants = responseTypes .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` ${rustName}(${rustName}),`; }) .join("\n"); const serializeCases = responseTypes .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` Response::${rustName}(data) => { tuple.serialize_element("${name}")?; tuple.serialize_element(data)?; @@ -317,7 +363,7 @@ ${deserializeCases} const deserializeCases = responseTypes .map((name) => { - const rustName = toPascalCase(name); + const rustName = this.typeName(name); return ` "${name}" => { let data = seq.next_element()? .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; @@ -457,6 +503,41 @@ mod serde_bytes_array { } } +mod serde_fixed_bytes { + use serde::{Deserializer, Serializer}; + use serde::de::Visitor; + + pub fn serialize(bytes: &[u8; N], serializer: S) -> Result + where S: Serializer { + serializer.serialize_bytes(bytes) + } + pub fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error> + where D: Deserializer<'de> { + struct FixedBytesVisitor; + impl<'de, const N: usize> Visitor<'de> for FixedBytesVisitor { + type Value = [u8; N]; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "{N} bytes") + } + fn visit_bytes(self, v: &[u8]) -> Result + where E: serde::de::Error { + v.try_into().map_err(|_| E::invalid_length(v.len(), &self)) + } + fn visit_seq(self, mut seq: A) -> Result + where A: serde::de::SeqAccess<'de> { + // Tolerated for peers that still send a sequence of integers. + let mut arr = [0u8; N]; + for (i, slot) in arr.iter_mut().enumerate() { + *slot = seq.next_element::()? + .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?; + } + Ok(arr) + } + } + deserializer.deserialize_bytes(FixedBytesVisitor::) + } +} + mod serde_opt_bytes { use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -542,6 +623,50 @@ impl Bin32 { pub fn as_slice(&self) -> &[u8] { &self.0 } } +impl From<[u8; 32]> for Bin32 { + fn from(bytes: [u8; 32]) -> Self { Self(bytes) } +} + +impl From<&[u8; 32]> for Bin32 { + fn from(bytes: &[u8; 32]) -> Self { Self(*bytes) } +} + +// Bin32 replaced loose Vec scalars. Deref plus the cross-type equalities +// below keep the byte-oriented surface callers already had - borrowing as a +// slice, len(), is_empty(), indexing, and comparison against a Vec or a fixed +// array - so upgrading does not force a rewrite at every read site. +impl TryFrom> for Bin32 { + type Error = Vec; + fn try_from(bytes: Vec) -> std::result::Result { + Ok(Self(<[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| bytes)?)) + } +} + +impl std::ops::Deref for Bin32 { + type Target = [u8]; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl PartialEq> for Bin32 { + fn eq(&self, other: &Vec) -> bool { self.0.as_slice() == other.as_slice() } +} + +impl PartialEq for Vec { + fn eq(&self, other: &Bin32) -> bool { self.as_slice() == other.0.as_slice() } +} + +impl PartialEq<[u8; N]> for Bin32 { + fn eq(&self, other: &[u8; N]) -> bool { self.0.as_slice() == other.as_slice() } +} + +impl PartialEq for [u8; N] { + fn eq(&self, other: &Bin32) -> bool { self.as_slice() == other.0.as_slice() } +} + +impl AsRef<[u8]> for Bin32 { + fn as_ref(&self) -> &[u8] { &self.0 } +} + impl Serialize for Bin32 { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer { @@ -591,8 +716,8 @@ ${this.generateResponseEnum(schema)} responseType: string; }): string { const methodName = this.methodName(command.name); - const cmdRustName = toPascalCase(command.name); - const respRustName = toPascalCase(command.responseType); + const cmdRustName = this.typeName(command.name); + const respRustName = this.typeName(command.responseType); const params = command.fields .map((f) => { @@ -624,7 +749,7 @@ ${this.generateResponseEnum(schema)} let cmd = Command::${cmdRustName}(${cmdRustName}::new(${paramConversions})); match self.execute(cmd)? { Response::${respRustName}(resp) => Ok(resp), - Response::${toPascalCase(this.errorTypeName)}(err) => Err(${errorType}::Backend( + Response::${this.typeName(this.errorTypeName)}(err) => Err(${errorType}::Backend( err.message )), _ => Err(${errorType}::InvalidResponse( @@ -698,13 +823,13 @@ ${apiMethods} generateServer(schema: CompiledSchema): string { this.errorTypeName = schema.errorTypeName; const { prefix, errorImport, typesImport } = this.opts; - const errorRespType = toPascalCase(this.errorTypeName); + const errorRespType = this.typeName(this.errorTypeName); const traitMethods = schema.commands .map((c) => { const methodName = this.methodName(c.name); - const cmdRustName = toPascalCase(c.name); - const respRustName = toPascalCase(c.responseType); + const cmdRustName = this.typeName(c.name); + const respRustName = this.typeName(c.responseType); return ` fn ${methodName}(&mut self, cmd: ${cmdRustName}, respond: Responder<${respRustName}>);`; }) .join("\n"); @@ -712,8 +837,8 @@ ${apiMethods} const dispatchArms = schema.commands .map((c) => { const methodName = this.methodName(c.name); - const cmdRustName = toPascalCase(c.name); - const respRustName = toPascalCase(c.responseType); + const cmdRustName = this.typeName(c.name); + const respRustName = this.typeName(c.responseType); return ` Command::${cmdRustName}(cmd) => { handler.${methodName}(cmd, Responder { raw, wrap: Response::${respRustName} }); }`; diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts index 100b0e1330d..5315a65e9ac 100644 --- a/ipc-codegen/src/typescript_codegen.ts +++ b/ipc-codegen/src/typescript_codegen.ts @@ -23,15 +23,49 @@ import { dedupeStructsByName, } from "./naming.ts"; +/** + * Above this length a fixed-size array is emitted as `T[]`, not a tuple. + * + * Pairs are the extension-field coordinates (Fq2), which callers build as + * two-element literals and pass positionally, so the arity carries its weight + * in the type. Longer fixed arrays are filled programmatically and read better + * as plain arrays — and typing them as tuples would reject the array + * expressions callers already pass. + */ +const TUPLE_SIZE_LIMIT = 2; + export class TypeScriptCodegen { private errorTypeName: string = "ErrorResponse"; /** Prefix to strip from command names when generating method names (e.g. "Bb" -> BbCircuitProve becomes circuitProve) */ private methodPrefix: string = ""; + /** Prefix to strip from generated type and converter names (e.g. "Bb" -> BbCircuitProve becomes CircuitProve) */ + private typePrefix: string = ""; - constructor(options?: { stripMethodPrefix?: string }) { + constructor(options?: { stripMethodPrefix?: string; stripTypePrefix?: string }) { if (options?.stripMethodPrefix) { this.methodPrefix = options.stripMethodPrefix; } + if (options?.stripTypePrefix) { + this.typePrefix = options.stripTypePrefix; + } + } + + /** + * The generated identifier for a schema name: PascalCase, with the service + * prefix stripped when asked. Wire strings always keep the schema name, so + * this must never be used where a tag is emitted. + */ + private typeName(schemaName: string): string { + let name = schemaName; + if (this.typePrefix && name.startsWith(this.typePrefix)) { + const rest = name.slice(this.typePrefix.length); + // Only strip when what remains still starts a PascalCase word, so a + // command legitimately beginning with the prefix letters is untouched. + if (rest && rest[0] === rest[0].toUpperCase()) { + name = rest; + } + } + return toPascalCase(name); } /** Strip the method prefix and convert to camelCase for API method names */ @@ -43,6 +77,19 @@ export class TypeScriptCodegen { return toCamelCase(name); } + /** + * A fixed-size array is a tuple: `[T, T]` rather than `T[]`. That preserves + * the arity in the type, and keeps a readonly tuple literal assignable, + * which a plain array type would reject. Long arrays stay `T[]` so the + * emitted types remain readable. + */ + private fixedArrayType(element: string, size?: number): string { + if (size === undefined || size > TUPLE_SIZE_LIMIT) { + return `${element}[]`; + } + return `[${Array(size).fill(element).join(", ")}]`; + } + private primitiveType(type: Type): string { switch (type.primitive) { case "bool": @@ -91,16 +138,16 @@ export class TypeScriptCodegen { return "Uint8Array"; } const inner = this.mapType(type.element!); - return type.element!.kind === "optional" - ? `(${inner})[]` - : `${inner}[]`; + const element = + type.element!.kind === "optional" ? `(${inner})` : inner; + return this.fixedArrayType(element, type.size); } case "optional": return `${this.mapType(type.element!)} | null`; case "struct": - return toPascalCase(type.struct!.name); + return this.typeName(type.struct!.name); } throw new Error(`Unsupported type kind: ${type.kind}`); @@ -126,14 +173,15 @@ export class TypeScriptCodegen { return "Uint8Array"; } const inner = this.mapMsgpackType(type.element!); - return inner.includes("|") ? `(${inner})[]` : `${inner}[]`; + const element = inner.includes("|") ? `(${inner})` : inner; + return this.fixedArrayType(element, type.size); } case "optional": return `${this.mapMsgpackType(type.element!)} | null`; case "struct": - return `Msgpack${toPascalCase(type.struct!.name)}`; + return `Msgpack${this.typeName(type.struct!.name)}`; } throw new Error(`Unsupported msgpack type kind: ${type.kind}`); @@ -169,7 +217,7 @@ export class TypeScriptCodegen { // Generate public interface private generateInterface(struct: Struct): string { - const tsName = toPascalCase(struct.name); + const tsName = this.typeName(struct.name); const fields = struct.fields.map((f) => this.generateField(f)).join("\n"); return `export interface ${tsName} { @@ -179,7 +227,7 @@ ${fields} // Generate msgpack interface (internal) private generateMsgpackInterface(struct: Struct): string { - const tsName = toPascalCase(struct.name); + const tsName = this.typeName(struct.name); const fields = struct.fields .map((f) => this.generateMsgpackField(f)) .join("\n"); @@ -191,7 +239,7 @@ ${fields} // Generate to* conversion function private generateToFunction(struct: Struct): string { - const tsName = toPascalCase(struct.name); + const tsName = this.typeName(struct.name); if (struct.fields.length === 0) { return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { @@ -225,7 +273,7 @@ ${conversions} // Generate from* conversion function private generateFromFunction(struct: Struct): string { - const tsName = toPascalCase(struct.name); + const tsName = this.typeName(struct.name); if (struct.fields.length === 0) { return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { @@ -289,7 +337,24 @@ ${conversions} return value; } const elem = this.generateConverter(dir, type.element!, "v"); - return elem === "v" ? value : `${value}.map((v: any) => ${elem})`; + if (elem === "v") { + return value; + } + const mapped = `${value}.map((v: any) => ${elem})`; + // map() widens a tuple to an array, so re-assert the arity for the + // fixed-size case; the length itself is fixed by the schema. + const target = + type.kind === "array" + ? dir === "to" + ? this.fixedArrayType(this.mapType(type.element!), type.size) + : this.fixedArrayType( + this.mapMsgpackType(type.element!), + type.size, + ) + : undefined; + return target && target.startsWith("[") + ? `(${mapped} as ${target})` + : mapped; } case "optional": { const inner = this.generateConverter(dir, type.element!, value); @@ -298,7 +363,7 @@ ${conversions} : `${value} != null ? ${inner} : null`; } case "struct": - return `${dir}${toPascalCase(type.struct!.name)}(${value})`; + return `${dir}${this.typeName(type.struct!.name)}(${value})`; } return value; } @@ -363,13 +428,13 @@ ${conversions} const asyncApiMethods = schema.commands .map( (c) => - ` ${this.toMethodName(c.name)}(command: ${toPascalCase(c.name)}): Promise<${toPascalCase(c.responseType)}>;`, + ` ${this.toMethodName(c.name)}(command: ${this.typeName(c.name)}): Promise<${this.typeName(c.responseType)}>;`, ) .join("\n"); const syncApiMethods = schema.commands .map( (c) => - ` ${this.toMethodName(c.name)}(command: ${toPascalCase(c.name)}): ${toPascalCase(c.responseType)};`, + ` ${this.toMethodName(c.name)}(command: ${this.typeName(c.name)}): ${this.typeName(c.responseType)};`, ) .join("\n"); @@ -441,8 +506,8 @@ ${syncApiMethods} // Generate API method private generateAsyncApiMethod(command: Command): string { const methodName = this.toMethodName(command.name); - const cmdType = toPascalCase(command.name); - const respType = toPascalCase(command.responseType); + const cmdType = this.typeName(command.name); + const respType = this.typeName(command.responseType); return ` ${methodName}(command: ${cmdType}): Promise<${respType}> { const msgpackCommand = from${cmdType}(command); @@ -460,8 +525,8 @@ ${syncApiMethods} private generateSyncApiMethod(command: Command): string { const methodName = this.toMethodName(command.name); - const cmdType = toPascalCase(command.name); - const respType = toPascalCase(command.responseType); + const cmdType = this.typeName(command.name); + const respType = this.typeName(command.responseType); return ` ${methodName}(command: ${cmdType}): ${respType} { const msgpackCommand = from${cmdType}(command); @@ -567,8 +632,8 @@ ${methods} // Add command types and their conversion functions for (const cmd of schema.commands) { - const cmdType = toPascalCase(cmd.name); - const respType = toPascalCase(cmd.responseType); + const cmdType = this.typeName(cmd.name); + const respType = this.typeName(cmd.responseType); types.add(cmdType); types.add(respType); types.add(`from${cmdType}`); @@ -588,14 +653,14 @@ ${methods} /** Generate a server handler interface and dispatch function */ generateServerApi(schema: CompiledSchema): string { this.errorTypeName = schema.errorTypeName; - const errorType = toPascalCase(this.errorTypeName); + const errorType = this.typeName(this.errorTypeName); // Generate handler interface const handlerMethods = schema.commands .map((c) => { const methodName = this.toMethodName(c.name); - const cmdType = toPascalCase(c.name); - const respType = toPascalCase(c.responseType); + const cmdType = this.typeName(c.name); + const respType = this.typeName(c.responseType); return ` ${methodName}(command: ${cmdType}): Promise<${respType}>;`; }) .join("\n"); @@ -604,8 +669,8 @@ ${methods} const dispatchCases = schema.commands .map((c) => { const methodName = this.toMethodName(c.name); - const cmdType = toPascalCase(c.name); - const respType = toPascalCase(c.responseType); + const cmdType = this.typeName(c.name); + const respType = this.typeName(c.responseType); return ` case '${c.name}': { const cmd = to${cmdType}(payload); const result = await handler.${methodName}(cmd); @@ -617,8 +682,8 @@ ${methods} const typeImports = new Set(); const valueImports = new Set(); for (const cmd of schema.commands) { - const cmdType = toPascalCase(cmd.name); - const respType = toPascalCase(cmd.responseType); + const cmdType = this.typeName(cmd.name); + const respType = this.typeName(cmd.responseType); typeImports.add(cmdType); typeImports.add(respType); valueImports.add(`to${cmdType}`); diff --git a/ipc-runtime/README.md b/ipc-runtime/README.md index db519a3a722..2d3360d3620 100644 --- a/ipc-runtime/README.md +++ b/ipc-runtime/README.md @@ -228,7 +228,7 @@ definition of the transport limits and defaults: | Constant | Value | Meaning | |----------|-------|---------| -| `MAX_FRAME_SIZE` | 256 MiB | Max length prefix accepted on receive; larger frames close the connection / fail the ring instead of allocating. | +| `MAX_FRAME_SIZE` | 4 GiB − 1 | The u32 length prefix's limit (it counts the 8-byte id plus the payload). Stream transports impose nothing smaller and grow receive buffers only as bytes arrive; SHM frames are bounded by half the ring capacity. | | `CONNECT_RETRY_BUDGET_MS` | 5000 | Total client connect retry budget (all transports). | | `DEFAULT_RING_SIZE` | 4 MiB | SHM ring size per direction per client. | | `SOCKET_BACKLOG` | 10 | Default UDS listen backlog. | diff --git a/ipc-runtime/bootstrap.sh b/ipc-runtime/bootstrap.sh index bd3fe351a3e..b7d9d7fb120 100755 --- a/ipc-runtime/bootstrap.sh +++ b/ipc-runtime/bootstrap.sh @@ -67,10 +67,42 @@ function cross_copy { ./ts/scripts/copy_cross.sh "$@" } +function crate_published { + curl -sf -H "User-Agent: aztec-packages-ci (tech@aztec-labs.com)" "https://crates.io/api/v1/crates/ipc-runtime/$1" | jq -e '.version.num' &>/dev/null +} + +# Publish the Rust crate to crates.io. barretenberg-rs depends on it at the +# same version, and its release runs after this one. +function release_crate { + local version=$1 + + if crate_published $version; then + echo "ipc-runtime@$version already published on crates.io. Skipping." + return 0 + fi + + # The crate compiles the C++ runtime itself, so it ships a copy of the sources. + rm -rf rust/cpp + mkdir -p rust/cpp + cp -r cpp/ipc_runtime rust/cpp/ + find rust/cpp -name '*.test.cpp' -delete + sed -i "0,/^version = \".*\"/s//version = \"$version\"/" rust/Cargo.toml + + # Verification builds the packaged crate on its own, which proves the copied + # sources are complete. + (cd rust && retry "denoise 'do_or_dryrun cargo publish --allow-dirty'") +} + function release { + local version=${REF_NAME#v} cross_copy - cd ts - retry "deploy_npm ${REF_NAME#v}" + (cd ts && retry "deploy_npm $version") + + # Private releases also run this function but publish only to the internal + # npm registry; crates.io is public-only. + if "$root/ci3/assert_public_release" 2>/dev/null; then + release_crate $version + fi } case "$cmd" in diff --git a/ipc-runtime/cpp/CMakeLists.txt b/ipc-runtime/cpp/CMakeLists.txt index 2666e1e3c17..77a4baadf70 100644 --- a/ipc-runtime/cpp/CMakeLists.txt +++ b/ipc-runtime/cpp/CMakeLists.txt @@ -46,6 +46,8 @@ else() ipc_runtime/c_abi.cpp ipc_runtime/ipc_client.cpp ipc_runtime/ipc_server.cpp + ipc_runtime/pipe_client.cpp + ipc_runtime/pipe_server.cpp ipc_runtime/serve_helper.cpp ipc_runtime/signal_handlers.cpp ipc_runtime/socket_client.cpp @@ -84,7 +86,7 @@ if(IPC_RUNTIME_BUILD_TESTS AND NOT WASM) FetchContent_MakeAvailable(GTest) endif() - add_executable(ipc_runtime_tests ipc_runtime/shm.test.cpp ipc_runtime/socket.test.cpp) + add_executable(ipc_runtime_tests ipc_runtime/pipe.test.cpp ipc_runtime/shm.test.cpp ipc_runtime/socket.test.cpp) target_link_libraries(ipc_runtime_tests PRIVATE ipc_runtime GTest::gtest_main) include(GoogleTest) diff --git a/ipc-runtime/cpp/ipc_runtime/c_abi.cpp b/ipc-runtime/cpp/ipc_runtime/c_abi.cpp index 0e5d3d3d85e..e09f09843fd 100644 --- a/ipc-runtime/cpp/ipc_runtime/c_abi.cpp +++ b/ipc-runtime/cpp/ipc_runtime/c_abi.cpp @@ -5,6 +5,8 @@ #include "ipc_runtime/serve_helper.hpp" #include "ipc_runtime/signal_handlers.hpp" +#include + #include #include #include @@ -211,6 +213,30 @@ ipc_client_t* ipc_client_create_mpsc_shm(const char* base_name, size_t client_id return wrap_client(ipc::IpcClient::create_mpsc_shm(base_name, client_id)); } +ipc_client_t* ipc_client_create_pipe(int in_fd, int out_fd) +{ + // PipeClient closes the descriptors it holds, but the caller (a spawning + // parent, typically) still owns the originals and will close them too. + // Duplicate so each side closes only its own: a double close frees an fd + // number the OS may already have reassigned, and the resulting EBADF + // surfaces far from here, in whatever unrelated code owns it by then. + int in_dup = ::dup(in_fd); + if (in_dup < 0) { + return nullptr; + } + int out_dup = ::dup(out_fd); + if (out_dup < 0) { + ::close(in_dup); + return nullptr; + } + auto* wrapped = wrap_client(ipc::IpcClient::create_pipe(in_dup, out_dup)); + if (wrapped == nullptr) { + ::close(in_dup); + ::close(out_dup); + } + return wrapped; +} + void ipc_client_destroy(ipc_client_t* client) { delete client; diff --git a/ipc-runtime/cpp/ipc_runtime/c_abi.h b/ipc-runtime/cpp/ipc_runtime/c_abi.h index 127ca6f45ce..6c06d6c011a 100644 --- a/ipc-runtime/cpp/ipc_runtime/c_abi.h +++ b/ipc-runtime/cpp/ipc_runtime/c_abi.h @@ -115,6 +115,14 @@ ipc_client_t* ipc_make_client(const char* path, size_t shm_client_id); ipc_client_t* ipc_client_create_socket(const char* socket_path); ipc_client_t* ipc_client_create_mpsc_shm(const char* base_name, size_t client_id); +/* Pipe transport over an already-open fd pair, for talking to a child process + * over its stdin/stdout. `in_fd` is read from, `out_fd` written to. There is + * no path form: a pipe endpoint is + * a pair of descriptors, not a name, so `ipc_make_client` cannot express it. + * The descriptors are duplicated: the caller keeps ownership of the originals + * and should close them as usual. */ +ipc_client_t* ipc_client_create_pipe(int in_fd, int out_fd); + void ipc_client_destroy(ipc_client_t* client); bool ipc_client_connect(ipc_client_t* client); diff --git a/ipc-runtime/cpp/ipc_runtime/constants.hpp b/ipc-runtime/cpp/ipc_runtime/constants.hpp index 77256e5ecfb..4ac0e792911 100644 --- a/ipc-runtime/cpp/ipc_runtime/constants.hpp +++ b/ipc-runtime/cpp/ipc_runtime/constants.hpp @@ -14,12 +14,14 @@ namespace ipc { /** - * Maximum length-prefix value accepted on receive, across all transports and - * languages. A frame claiming more than this is treated as corruption: the - * connection is closed (sockets) or the ring is declared corrupt (SHM), - * instead of allocating/awaiting the claimed size. + * Largest frame the wire format can express: the length prefix is a u32 and + * counts the request id plus the payload, so a payload can be at most + * MAX_FRAME_SIZE - FRAME_ID_SIZE bytes. The stream transports impose nothing + * smaller; they grow their receive buffers as bytes arrive (stream_io.hpp), so + * a corrupt prefix cannot force a large allocation. SHM is bounded separately by + * its ring capacity. */ -inline constexpr uint32_t MAX_FRAME_SIZE = 256U * 1024 * 1024; // 256 MiB +inline constexpr uint32_t MAX_FRAME_SIZE = UINT32_MAX; /** * Every frame carries a client-assigned request id (little-endian u64) between diff --git a/ipc-runtime/cpp/ipc_runtime/ipc_client.cpp b/ipc-runtime/cpp/ipc_runtime/ipc_client.cpp index 87d78583fc1..b0a6932c61a 100644 --- a/ipc-runtime/cpp/ipc_runtime/ipc_client.cpp +++ b/ipc-runtime/cpp/ipc_runtime/ipc_client.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/ipc_client.hpp" #include "ipc_runtime/mpsc_shm_client.hpp" +#include "ipc_runtime/pipe_client.hpp" #include "ipc_runtime/shm_client.hpp" #include "ipc_runtime/socket_client.hpp" #include @@ -23,4 +24,9 @@ std::unique_ptr IpcClient::create_mpsc_shm(const std::string& base_na return std::make_unique(base_name, client_id); } +std::unique_ptr IpcClient::create_pipe(int in_fd, int out_fd) +{ + return std::make_unique(in_fd, out_fd); +} + } // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/ipc_client.hpp b/ipc-runtime/cpp/ipc_runtime/ipc_client.hpp index b2d6692c6aa..bbd45541639 100644 --- a/ipc-runtime/cpp/ipc_runtime/ipc_client.hpp +++ b/ipc-runtime/cpp/ipc_runtime/ipc_client.hpp @@ -161,6 +161,11 @@ class IpcClient { // Multi-producer SHM: one request ring per client slot and one response // ring per client slot. This is what make_client("*.shm") selects. static std::unique_ptr create_mpsc_shm(const std::string& base_name, size_t client_id = kAutoClientId); + // Talk to a PipeServer over an already-open fd pair (typically the pipes of + // a child spawned with piped stdio). Same framing as the socket transport. + // Pipe transport over an fd pair. The client takes ownership: both + // descriptors are closed on close()/destruction. + static std::unique_ptr create_pipe(int in_fd, int out_fd); }; /** diff --git a/ipc-runtime/cpp/ipc_runtime/ipc_server.cpp b/ipc-runtime/cpp/ipc_runtime/ipc_server.cpp index b57760967e2..25ddaa6b11f 100644 --- a/ipc-runtime/cpp/ipc_runtime/ipc_server.cpp +++ b/ipc-runtime/cpp/ipc_runtime/ipc_server.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/ipc_server.hpp" #include "ipc_runtime/mpsc_shm_server.hpp" +#include "ipc_runtime/pipe_server.hpp" #include "ipc_runtime/shm_server.hpp" #include "ipc_runtime/socket_server.hpp" #include @@ -28,4 +29,9 @@ std::unique_ptr IpcServer::create_mpsc_shm(const std::string& base_na return std::make_unique(base_name, max_clients, request_ring_size, response_ring_size); } +std::unique_ptr IpcServer::create_pipe(int in_fd, int out_fd) +{ + return std::make_unique(in_fd, out_fd); +} + } // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp b/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp index 70981fdb4e9..b3f703a2c1d 100644 --- a/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp +++ b/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp @@ -85,8 +85,8 @@ class IpcServer { * reactor wakes promptly and sends it. Default is a no-op (a serial server * never blocks waiting on a cross-thread completion). Transports that back * run_reactor override this: sockets write the self-pipe; MPSC-SHM bumps the - * doorbell seq then futex_wakes (mirroring a publish — a bare wake without a - * seq bump would race). + * doorbell seq then futex_wakes a blocked reactor (mirroring a publish — a bare + * wake without a seq bump would race). */ virtual void notify() {} @@ -396,6 +396,10 @@ class IpcServer { size_t request_ring_size = DEFAULT_RING_SIZE, size_t response_ring_size = DEFAULT_RING_SIZE); // Multi-producer SHM: one request ring per client slot and one response + // Serve an already-open fd pair (e.g. stdin/stdout when spawned with piped + // stdio): a single implicit client, same framing as the socket transport. + // This is what make_server("-") selects, with fds 0 and 1. + static std::unique_ptr create_pipe(int in_fd, int out_fd); // ring per client slot. This is what make_server("*.shm") selects. static std::unique_ptr create_mpsc_shm(const std::string& base_name, size_t max_clients, diff --git a/ipc-runtime/cpp/ipc_runtime/mpsc_shm_server.hpp b/ipc-runtime/cpp/ipc_runtime/mpsc_shm_server.hpp index e3544429920..c82bc5494ba 100644 --- a/ipc-runtime/cpp/ipc_runtime/mpsc_shm_server.hpp +++ b/ipc-runtime/cpp/ipc_runtime/mpsc_shm_server.hpp @@ -124,8 +124,7 @@ class MpscShmServer : public IpcServer { // ring is corrupt — error out instead of waiting for it. A frame // shorter than the request-id field means the peer speaks the id-less // protocol. - if (msg_len > MAX_FRAME_SIZE || msg_len > request_ring_size_ / 2 - sizeof(uint32_t) || - msg_len < FRAME_ID_SIZE) { + if (msg_len > request_ring_size_ / 2 - sizeof(uint32_t) || msg_len < FRAME_ID_SIZE) { throw std::runtime_error("MpscShmServer::receive: invalid length prefix (" + std::to_string(msg_len) + " bytes) — corrupt ring or protocol mismatch"); } diff --git a/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp new file mode 100644 index 00000000000..76670d08cc0 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp @@ -0,0 +1,380 @@ +#include "ipc_runtime/ipc_client.hpp" +#include "ipc_runtime/ipc_server.hpp" +#include "ipc_runtime/serve_helper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ipc; + +namespace { + +// Pipe writes to a closed peer must yield EPIPE, not kill the test process — +// the documented requirement on the pipe transport (bb installs +// install_default_signal_handlers, which does the same). +struct IgnoreSigpipe { + IgnoreSigpipe() { std::signal(SIGPIPE, SIG_IGN); } +} ignore_sigpipe; + +// A client<->server fd-pair link built from two pipes, as a parent spawning a +// child with piped stdio would hold them. +struct PipeLink { + int server_in = -1; // child stdin, read end + int client_out = -1; // child stdin, write end + int client_in = -1; // child stdout, read end + int server_out = -1; // child stdout, write end + + PipeLink() + { + int to_server[2]; + int to_client[2]; + if (::pipe(to_server) != 0 || ::pipe(to_client) != 0) { + return; + } + server_in = to_server[0]; + client_out = to_server[1]; + client_in = to_client[0]; + server_out = to_client[1]; + } +}; + +// Minimal fixed-size thread pool used as a run_reactor() executor in tests. +class PipeTestPool { + public: + explicit PipeTestPool(size_t n) + { + for (size_t i = 0; i < n; i++) { + workers_.emplace_back([this] { + while (true) { + std::function job; + { + std::unique_lock lock(m_); + cv_.wait(lock, [this] { return stop_ || !q_.empty(); }); + if (stop_ && q_.empty()) { + return; + } + job = std::move(q_.front()); + q_.pop(); + } + job(); + } + }); + } + } + ~PipeTestPool() + { + { + std::lock_guard lock(m_); + stop_ = true; + } + cv_.notify_all(); + for (auto& w : workers_) { + w.join(); + } + } + void enqueue(std::function job) + { + { + std::lock_guard lock(m_); + q_.push(std::move(job)); + } + cv_.notify_one(); + } + + private: + std::vector workers_; + std::queue> q_; + std::mutex m_; + std::condition_variable cv_; + bool stop_ = false; +}; + +TEST(PipeTest, ServesEchoOverFdPair) +{ + PipeLink link; + ASSERT_GE(link.server_in, 0); + + auto server = IpcServer::create_pipe(link.server_in, link.server_out); + ASSERT_TRUE(server->listen()); + std::thread server_thread([&] { + server->run([](int, std::span req) { return std::vector(req.begin(), req.end()); }); + }); + + auto client = IpcClient::create_pipe(link.client_in, link.client_out); + ASSERT_TRUE(client->connect()); + + for (uint32_t i = 0; i < 100; i++) { + ASSERT_TRUE(client->send(&i, sizeof(i), 1'000'000'000ULL)); + auto resp = client->receive(5'000'000'000ULL); + ASSERT_EQ(resp.size(), sizeof(uint32_t)); + uint32_t got = 0; + std::memcpy(&got, resp.data(), sizeof(got)); + EXPECT_EQ(got, i); + client->release(resp.size()); + } + + // Zero-length round trip: an empty request/response is a valid frame. + ASSERT_TRUE(client->send(nullptr, 0, 1'000'000'000ULL)); + auto empty = client->receive(5'000'000'000ULL); + ASSERT_NE(empty.data(), nullptr); + EXPECT_EQ(empty.size(), 0U); + client->release(empty.size()); + + // Closing the client's fds is peer EOF on the server: its lifetime is the + // pipe, so run() must return without an explicit request_shutdown(). + client->close(); + server_thread.join(); + server->close(); +} + +// Serving "-" makes the process's stdout the frame stream, so anything else +// the process prints there (std::cout, printf, a raw write to fd 1) would +// corrupt it. make_server must take stdout for itself and send the rest to +// stderr. Runs in a child because it rewires the process's stdio. +TEST(PipeTest, StdioServerKeepsStrayStdoutOutOfTheFrameStream) +{ + PipeLink link; + ASSERT_GE(link.server_in, 0); + + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ::dup2(link.server_in, STDIN_FILENO); + ::dup2(link.server_out, STDOUT_FILENO); + ::close(link.server_in); + ::close(link.server_out); + ::close(link.client_in); + ::close(link.client_out); + // Keep the stray output off the test runner's terminal. + int devnull = ::open("/dev/null", O_WRONLY); + ::dup2(devnull, STDERR_FILENO); + + auto server = make_server("-", ServerOptions{}); + if (!server || !server->listen()) { + _exit(2); + } + server->run([](int, std::span req) { + std::cout << "stray cout" << std::endl; + std::printf("stray printf\n"); + std::fflush(stdout); + [[maybe_unused]] ssize_t n = ::write(STDOUT_FILENO, "stray write\n", 12); + return std::vector(req.begin(), req.end()); + }); + _exit(0); + } + + ::close(link.server_in); + ::close(link.server_out); + link.server_in = link.server_out = -1; + + auto client = IpcClient::create_pipe(link.client_in, link.client_out); + ASSERT_TRUE(client->connect()); + for (uint32_t i = 0; i < 3; i++) { + ASSERT_TRUE(client->send(&i, sizeof(i), 1'000'000'000ULL)); + auto resp = client->receive(5'000'000'000ULL); + ASSERT_EQ(resp.size(), sizeof(uint32_t)) << "response " << i << " was corrupted by stray stdout"; + uint32_t got = 0; + std::memcpy(&got, resp.data(), sizeof(got)); + EXPECT_EQ(got, i); + client->release(resp.size()); + } + client->close(); + link.client_in = link.client_out = -1; + + int status = 0; + ASSERT_EQ(::waitpid(pid, &status, 0), pid); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +TEST(PipeTest, PipelinedExplicitIdsEchoInOrder) +{ + PipeLink link; + auto server = IpcServer::create_pipe(link.server_in, link.server_out); + ASSERT_TRUE(server->listen()); + std::thread server_thread([&] { + server->run([](int, std::span req) { return std::vector(req.begin(), req.end()); }); + }); + + auto client = IpcClient::create_pipe(link.client_in, link.client_out); + ASSERT_TRUE(client->connect()); + + // Several requests in flight before the first receive; the serial run() + // loop answers FIFO and each response carries its request's echoed id. + constexpr uint32_t N = 16; + for (uint32_t id = 1; id <= N; id++) { + ASSERT_TRUE(client->send(id, &id, sizeof(id), 1'000'000'000ULL)); + } + for (uint32_t n = 0; n < N; n++) { + uint64_t rid = 0; + auto resp = client->receive(5'000'000'000ULL, rid); + ASSERT_EQ(resp.size(), sizeof(uint32_t)); + uint32_t got = 0; + std::memcpy(&got, resp.data(), sizeof(got)); + EXPECT_EQ(got, static_cast(rid)) << "response payload does not match its echoed request id"; + EXPECT_EQ(rid, n + 1) << "serial pipe server must answer in request order"; + client->release(resp.size()); + } + + client->close(); + server_thread.join(); + server->close(); +} + +TEST(PipeTest, RunReactorDeliversCompletionOrderWithEchoedIds) +{ + PipeLink link; + auto server = IpcServer::create_pipe(link.server_in, link.server_out); + ASSERT_TRUE(server->listen()); + + constexpr uint32_t N = 8; + PipeTestPool pool(4); + std::thread server_thread([&] { + server->run_reactor([&pool](int, std::span req, IpcServer::Respond respond) { + std::vector r(req.begin(), req.end()); + pool.enqueue([r = std::move(r), respond = std::move(respond)]() mutable { + uint32_t id = 0; + std::memcpy(&id, r.data(), sizeof(id)); + // Earlier ids sleep longer, so completions arrive roughly + // reversed — exercising the notify() self-pipe wake. + std::this_thread::sleep_for(std::chrono::milliseconds(5 + 5 * (N - id))); + respond(std::move(r)); + }); + }); + }); + + auto client = IpcClient::create_pipe(link.client_in, link.client_out); + ASSERT_TRUE(client->connect()); + + for (uint32_t id = 1; id <= N; id++) { + ASSERT_TRUE(client->send(id, &id, sizeof(id), 1'000'000'000ULL)); + } + std::vector seen(N + 1, false); + bool in_send_order = true; + uint64_t prev_rid = 0; + for (uint32_t n = 0; n < N; n++) { + uint64_t rid = 0; + auto resp = client->receive(5'000'000'000ULL, rid); + ASSERT_EQ(resp.size(), sizeof(uint32_t)); + uint32_t got = 0; + std::memcpy(&got, resp.data(), sizeof(got)); + ASSERT_GE(rid, 1U); + ASSERT_LE(rid, N); + EXPECT_EQ(got, static_cast(rid)); + EXPECT_FALSE(seen[rid]) << "duplicate response for request id " << rid; + seen[rid] = true; + if (rid < prev_rid) { + in_send_order = false; + } + prev_rid = rid; + client->release(resp.size()); + } + for (uint32_t id = 1; id <= N; id++) { + EXPECT_TRUE(seen[id]) << "request id " << id << " was never answered"; + } + // Reversed sleeps guarantee out-of-order completions; strictly in-order + // arrival would mean responses are being re-serialized somewhere. + EXPECT_FALSE(in_send_order) << "responses arrived strictly in send order over run_reactor"; + + client->close(); + server_thread.join(); + server->close(); +} + +TEST(PipeTest, SerialClientClosesOnForeignFrame) +{ + PipeLink link; + // A fake server that echoes a WRONG id: the serial convenience receive() + // must treat it as a desync (pipes cannot have stale ring leftovers) and + // close rather than deliver another request's payload. + std::thread fake_server([&] { + uint32_t len = 0; + uint64_t id = 0; + ASSERT_EQ(::read(link.server_in, &len, sizeof(len)), static_cast(sizeof(len))); + ASSERT_EQ(::read(link.server_in, &id, sizeof(id)), static_cast(sizeof(id))); + std::vector payload(len - sizeof(id)); + size_t got = 0; + while (got < payload.size()) { + ssize_t n = ::read(link.server_in, payload.data() + got, payload.size() - got); + ASSERT_GT(n, 0); + got += static_cast(n); + } + uint64_t wrong_id = id ^ 0xdeadbeefULL; + ASSERT_EQ(::write(link.server_out, &len, sizeof(len)), static_cast(sizeof(len))); + ASSERT_EQ(::write(link.server_out, &wrong_id, sizeof(wrong_id)), static_cast(sizeof(wrong_id))); + ASSERT_EQ(::write(link.server_out, payload.data(), payload.size()), static_cast(payload.size())); + ::close(link.server_in); + ::close(link.server_out); + }); + + auto client = IpcClient::create_pipe(link.client_in, link.client_out); + ASSERT_TRUE(client->connect()); + uint32_t msg = 42; + ASSERT_TRUE(client->send(&msg, sizeof(msg), 1'000'000'000ULL)); + auto resp = client->receive(5'000'000'000ULL); + EXPECT_EQ(resp.data(), nullptr) << "a mis-addressed frame over a pipe must fail the call, not deliver data"; + fake_server.join(); + client->close(); +} + +TEST(PipeTest, ServerRejectsIdlessFrameAsProtocolMismatch) +{ + PipeLink link; + auto server = IpcServer::create_pipe(link.server_in, link.server_out); + ASSERT_TRUE(server->listen()); + + // An old-protocol (id-less) frame: length prefix smaller than the id field. + uint32_t len = 4; + uint32_t payload = 7; + ASSERT_EQ(::write(link.client_out, &len, sizeof(len)), static_cast(sizeof(len))); + ASSERT_EQ(::write(link.client_out, &payload, sizeof(payload)), static_cast(sizeof(payload))); + + ASSERT_EQ(server->wait_for_data(1'000'000'000ULL), 0); + uint64_t rid = 0; + auto req = server->receive(0, rid); + EXPECT_TRUE(req.empty()); + + // The desync also ends the serve lifetime: a further wait reports no client. + EXPECT_EQ(server->wait_for_data(0), -1); + server->close(); + ::close(link.client_out); + ::close(link.client_in); +} + +TEST(PipeTest, NotifyWakesBlockedWait) +{ + PipeLink link; + auto server = IpcServer::create_pipe(link.server_in, link.server_out); + ASSERT_TRUE(server->listen()); + + std::atomic woke{ false }; + std::thread waiter([&] { + // 30s: only the notify() below can plausibly end this wait in time. + server->wait_for_data(30'000'000'000ULL); + woke.store(true); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_FALSE(woke.load()); + server->notify(); + waiter.join(); + EXPECT_TRUE(woke.load()); + + server->close(); + ::close(link.client_out); + ::close(link.client_in); +} + +} // namespace diff --git a/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp b/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp new file mode 100644 index 00000000000..df8658dd932 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp @@ -0,0 +1,208 @@ +#include "ipc_runtime/pipe_client.hpp" +#include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace ipc { + +PipeClient::PipeClient(int in_fd, int out_fd) + : in_fd_(in_fd) + , out_fd_(out_fd) +{} + +PipeClient::~PipeClient() +{ + close_internal(); +} + +bool PipeClient::connect() +{ + if (in_fd_ < 0 || out_fd_ < 0 || fcntl(in_fd_, F_GETFD) < 0 || fcntl(out_fd_, F_GETFD) < 0) { + errno = EBADF; + return false; + } + return true; +} + +int PipeClient::wait_fd(int fd, short events, uint64_t timeout_ns) +{ + // timeout_ns == 0 means "no timeout" (infinite), matching the socket + // client's SO_RCVTIMEO/SO_SNDTIMEO convention. + int timeout_ms = -1; + if (timeout_ns > 0) { + uint64_t ms = std::max(1, timeout_ns / 1000000ULL); + timeout_ms = static_cast(std::min(ms, INT_MAX)); + } + struct pollfd pfd = { .fd = fd, .events = events, .revents = 0 }; + while (true) { + int n = ::poll(&pfd, 1, timeout_ms); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + errno = EAGAIN; + return 0; + } + return 1; + } +} + +int PipeClient::read_exact(void* buf, size_t len, uint64_t timeout_ns, bool& partial) +{ + size_t total_read = 0; + while (total_read < len) { + // The timeout applies per poll round; a frame mid-delivery keeps making + // progress, so this bounds "no bytes at all", the case that matters. + int ready = wait_fd(in_fd_, POLLIN, timeout_ns); + if (ready <= 0) { + partial = total_read > 0; + return -1; + } + ssize_t n = ::read(in_fd_, static_cast(buf) + total_read, len - total_read); + if (n < 0) { + if (errno == EINTR) { + continue; + } + partial = total_read > 0; + return -1; + } + if (n == 0) { + partial = total_read > 0; + return 0; // Server closed the pipe + } + total_read += static_cast(n); + } + return 1; +} + +int PipeClient::write_exact(const void* buf, size_t len, uint64_t timeout_ns, bool& partial) +{ + size_t total_sent = 0; + while (total_sent < len) { + if (timeout_ns > 0) { + int ready = wait_fd(out_fd_, POLLOUT, timeout_ns); + if (ready <= 0) { + partial = total_sent > 0; + return -1; + } + } + ssize_t n = ::write(out_fd_, static_cast(buf) + total_sent, len - total_sent); + if (n < 0) { + if (errno == EINTR) { + continue; + } + partial = total_sent > 0; + return -1; + } + total_sent += static_cast(n); + } + return 1; +} + +bool PipeClient::send(uint64_t request_id, const void* data, size_t len, uint64_t timeout_ns) +{ + if (out_fd_ < 0) { + errno = EINVAL; + return false; + } + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { + errno = EMSGSIZE; + return false; + } + + // Write length prefix (4 bytes, little-endian), request id (8 bytes, + // little-endian), then message data, looping on partial writes. + auto msg_len = static_cast(FRAME_ID_SIZE + len); + bool partial = false; + if (write_exact(&msg_len, sizeof(msg_len), timeout_ns, partial) != 1 || + write_exact(&request_id, FRAME_ID_SIZE, timeout_ns, partial) != 1 || + write_exact(data, len, timeout_ns, partial) != 1) { + if (partial) { + // Part of the frame is on the wire — the stream is desynced and + // unusable. Close rather than silently corrupting later frames. + close_internal(); + } + return false; + } + return true; +} + +std::span PipeClient::receive(uint64_t timeout_ns, uint64_t& request_id) +{ + if (in_fd_ < 0) { + return {}; + } + + // Read length prefix (4 bytes) + uint32_t msg_len = 0; + bool partial = false; + if (read_exact(&msg_len, sizeof(msg_len), timeout_ns, partial) != 1) { + if (partial) { + // Mid-frame failure — stream desynced. + close_internal(); + } + return {}; + } + + // A frame shorter than the request-id field means the peer speaks the + // id-less protocol — close rather than misparse. + if (msg_len < FRAME_ID_SIZE) { + close_internal(); + return {}; + } + + // Read the echoed request id (8 bytes, little-endian). + request_id = 0; + if (read_exact(&request_id, FRAME_ID_SIZE, timeout_ns, partial) != 1) { + close_internal(); + return {}; + } + msg_len -= static_cast(FRAME_ID_SIZE); + + if (read_payload(recv_buffer_, 0, msg_len, [&](uint8_t* dst, size_t n) { + return read_exact(dst, n, timeout_ns, partial); + }) != 1) { + // Prefix consumed but payload incomplete — stream desynced. + close_internal(); + return {}; + } + + return std::span(recv_buffer_.data(), msg_len); +} + +void PipeClient::release(size_t /*message_size*/) +{ + // No-op for pipes — data is already consumed from the kernel buffer during + // receive(). +} + +void PipeClient::close() +{ + close_internal(); +} + +void PipeClient::close_internal() +{ + if (in_fd_ >= 0) { + ::close(in_fd_); + if (out_fd_ == in_fd_) { + out_fd_ = -1; + } + in_fd_ = -1; + } + if (out_fd_ >= 0) { + ::close(out_fd_); + out_fd_ = -1; + } +} + +} // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/pipe_client.hpp b/ipc-runtime/cpp/ipc_runtime/pipe_client.hpp new file mode 100644 index 00000000000..ae6d234f8f5 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_client.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "ipc_runtime/ipc_client.hpp" +#include +#include +#include + +namespace ipc { + +/** + * @brief IPC client over an already-open file-descriptor pair. + * + * The counterpart of PipeServer, with the same [4B LE length][8B LE request + * id][payload] framing as the socket transport. The typical producer of the fd + * pair is a parent that spawned the server with its stdin/stdout piped (the + * barretenberg-rs PipeBackend pattern); in-process tests use pipe/socketpair + * fd pairs. + * + * The receive timeout is honoured via poll(); writes block until the frame is + * fully on the pipe (poll(POLLOUT)-gated when a timeout is given). Writes rely + * on SIGPIPE being ignored so a closed peer yields EPIPE (see PipeServer). + */ +class PipeClient : public IpcClient { + public: + // Takes ownership of the fds; close() closes them (once, if they are equal). + PipeClient(int in_fd, int out_fd); + ~PipeClient() override; + + PipeClient(const PipeClient&) = delete; + PipeClient& operator=(const PipeClient&) = delete; + PipeClient(PipeClient&&) = delete; + PipeClient& operator=(PipeClient&&) = delete; + + bool connect() override; + bool send(uint64_t request_id, const void* data, size_t len, uint64_t timeout_ns) override; + using IpcClient::send; // serial auto-id convenience overload + std::span receive(uint64_t timeout_ns, uint64_t& request_id) override; + using IpcClient::receive; // serial echo-verifying convenience overload + void release(size_t message_size) override; + void close() override; + + private: + void close_internal(); + // Returns 1 on success, 0 on peer EOF, -1 on error/timeout. `partial` is set + // when some but not all bytes moved (stream desync). + int read_exact(void* buf, size_t len, uint64_t timeout_ns, bool& partial); + int write_exact(const void* buf, size_t len, uint64_t timeout_ns, bool& partial); + // Wait for the fd to become ready; 1 ready, 0 timeout, -1 error. + static int wait_fd(int fd, short events, uint64_t timeout_ns); + + int in_fd_; + int out_fd_; + std::vector recv_buffer_; +}; + +} // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp b/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp new file mode 100644 index 00000000000..f7763e29285 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp @@ -0,0 +1,295 @@ +#include "ipc_runtime/pipe_server.hpp" +#include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ipc { + +PipeServer::PipeServer(int in_fd, int out_fd) + : in_fd_(in_fd) + , out_fd_(out_fd) +{} + +PipeServer::~PipeServer() +{ + close_internal(); +} + +void PipeServer::close() +{ + close_internal(); +} + +void PipeServer::close_internal() +{ + connected_ = false; + if (in_fd_ >= 0) { + ::close(in_fd_); + if (out_fd_ == in_fd_) { + out_fd_ = -1; + } + in_fd_ = -1; + } + if (out_fd_ >= 0) { + ::close(out_fd_); + out_fd_ = -1; + } + if (wake_read_fd_ >= 0) { + ::close(wake_read_fd_); + wake_read_fd_ = -1; + } + if (wake_write_fd_ >= 0) { + ::close(wake_write_fd_); + wake_write_fd_ = -1; + } +} + +void PipeServer::disconnect() +{ + // The pipe is the connection and the connection is the server's lifetime: + // peer EOF (or a mid-frame stream desync) means no further request can ever + // arrive, so ask the serve loop to exit rather than spinning on a + // permanently-readable EOF fd. + connected_ = false; + request_shutdown(); +} + +bool PipeServer::setup_wake_pipe() +{ + int fds[2]; + if (::pipe(fds) < 0) { + return false; + } + // Both ends non-blocking + close-on-exec: the non-blocking read end bounds + // drain_wake_pipe()'s loop, and the non-blocking write end keeps notify() + // from ever blocking (a full pipe already means a wake is pending). + for (int fd : fds) { + int fl = fcntl(fd, F_GETFL, 0); + if (fl < 0 || fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0) { + ::close(fds[0]); + ::close(fds[1]); + return false; + } + int fdfl = fcntl(fd, F_GETFD, 0); + if (fdfl >= 0) { + fcntl(fd, F_SETFD, fdfl | FD_CLOEXEC); + } + } + wake_read_fd_ = fds[0]; + wake_write_fd_ = fds[1]; + return true; +} + +void PipeServer::drain_wake_pipe() +{ + if (wake_read_fd_ < 0) { + return; + } + uint8_t buf[256]; + while (::read(wake_read_fd_, buf, sizeof(buf)) > 0) { + // Non-blocking: drain everything so we don't spin on a stale wake. + } +} + +void PipeServer::notify() +{ + if (wake_write_fd_ < 0) { + return; + } + // One byte is enough to make the read end readable; ignore EAGAIN (pipe + // already full ⇒ a wake is already pending) so notify() never blocks. + const uint8_t one = 1; + [[maybe_unused]] ssize_t n = ::write(wake_write_fd_, &one, 1); +} + +bool PipeServer::listen() +{ + if (connected_) { + return true; // Already listening + } + if (in_fd_ < 0 || out_fd_ < 0 || fcntl(in_fd_, F_GETFD) < 0 || fcntl(out_fd_, F_GETFD) < 0) { + errno = EBADF; + return false; + } + if (wake_read_fd_ < 0 && !setup_wake_pipe()) { + return false; + } + connected_ = true; + return true; +} + +int PipeServer::wait_for_data(uint64_t timeout_ns) +{ + if (!connected_) { + errno = ENOTCONN; + return -1; + } + + // 0 = non-blocking poll (matches the interface doc and the socket + // transport). Sub-millisecond timeouts round up to 1ms; large timeouts + // clamp to INT_MAX ms. + int timeout_ms = 0; + if (timeout_ns > 0) { + uint64_t ms = std::max(1, timeout_ns / 1000000ULL); + timeout_ms = static_cast(std::min(ms, INT_MAX)); + } + + struct pollfd fds[2]; + fds[0] = { .fd = in_fd_, .events = POLLIN, .revents = 0 }; + fds[1] = { .fd = wake_read_fd_, .events = POLLIN, .revents = 0 }; + + int n = ::poll(fds, 2, timeout_ms); + if (n <= 0) { + return -1; + } + + // Completion wakeup from notify(): drain the self-pipe and report "no client + // request". The caller drains its completion queue on every wake. + if ((fds[1].revents & POLLIN) != 0) { + drain_wake_pipe(); + if ((fds[0].revents & (POLLIN | POLLHUP | POLLERR)) == 0) { + return -1; + } + } + + // POLLHUP/POLLERR are reported readable too: read() will deliver any + // remaining buffered bytes and then EOF, which receive() turns into a + // disconnect. + if ((fds[0].revents & (POLLIN | POLLHUP | POLLERR)) != 0) { + return 0; + } + return -1; +} + +std::span PipeServer::receive(int client_id, uint64_t& request_id) +{ + if (client_id != 0 || !connected_) { + return {}; + } + + // Read length prefix (4 bytes), looping on partial reads. + uint32_t msg_len = 0; + size_t total_read = 0; + while (total_read < sizeof(msg_len)) { + ssize_t n = ::read(in_fd_, reinterpret_cast(&msg_len) + total_read, sizeof(msg_len) - total_read); + if (n < 0) { + if (errno == EINTR) { + continue; // Interrupted, retry + } + return {}; + } + if (n == 0) { + // Peer closed the pipe. + disconnect(); + return {}; + } + total_read += static_cast(n); + } + + // A frame shorter than the request-id field means the peer speaks the + // id-less protocol — treat as a fatal desync rather than misparse. + if (msg_len < FRAME_ID_SIZE) { + fprintf(stderr, "ipc: pipe peer sent an invalid frame (len=%u) — protocol mismatch?\n", msg_len); + disconnect(); + return {}; + } + + // Read the request id (8 bytes, little-endian). + request_id = 0; + total_read = 0; + while (total_read < FRAME_ID_SIZE) { + ssize_t n = ::read(in_fd_, reinterpret_cast(&request_id) + total_read, FRAME_ID_SIZE - total_read); + if (n <= 0) { + if (n < 0 && errno == EINTR) { + continue; // Interrupted, retry + } + disconnect(); + return {}; + } + total_read += static_cast(n); + } + msg_len -= static_cast(FRAME_ID_SIZE); + + int result = read_payload(recv_buffer_, 0, msg_len, [this](uint8_t* dst, size_t n) { + size_t got = 0; + while (got < n) { + ssize_t r = ::read(in_fd_, dst + got, n - got); + if (r < 0) { + if (errno == EINTR) { + continue; // Interrupted, retry + } + return -1; + } + if (r == 0) { + return 0; // Peer closed mid-message + } + got += static_cast(r); + } + return 1; + }); + if (result != 1) { + disconnect(); + return {}; + } + + return std::span(recv_buffer_.data(), msg_len); +} + +void PipeServer::release(int client_id, size_t message_size) +{ + // No-op for pipes — the message was consumed from the kernel buffer during + // receive(). + (void)client_id; + (void)message_size; +} + +bool PipeServer::send(int client_id, uint64_t request_id, const void* data, size_t len) +{ + if (client_id != 0 || !connected_ || out_fd_ < 0) { + errno = EINVAL; + return false; + } + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { + errno = EMSGSIZE; + return false; + } + + // Write length prefix (4 bytes), echoed request id (8 bytes), then message + // data, looping on partial writes — a short write after the prefix would + // permanently desync the stream. A closed peer yields EPIPE (SIGPIPE must + // be ignored; see class comment). + auto msg_len = static_cast(FRAME_ID_SIZE + len); + const uint8_t* parts[3] = { reinterpret_cast(&msg_len), + reinterpret_cast(&request_id), + static_cast(data) }; + size_t part_lens[3] = { sizeof(msg_len), FRAME_ID_SIZE, len }; + for (int part = 0; part < 3; part++) { + size_t total_sent = 0; + while (total_sent < part_lens[part]) { + ssize_t n = ::write(out_fd_, parts[part] + total_sent, part_lens[part] - total_sent); + if (n < 0) { + if (errno == EINTR) { + continue; // Interrupted, retry + } + if (part > 0 || total_sent > 0) { + // Frame partially on the wire — stream desynced. + disconnect(); + } + return false; + } + total_sent += static_cast(n); + } + } + return true; +} + +} // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/pipe_server.hpp b/ipc-runtime/cpp/ipc_runtime/pipe_server.hpp new file mode 100644 index 00000000000..331cc0431a8 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_server.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "ipc_runtime/ipc_server.hpp" +#include +#include +#include + +namespace ipc { + +/** + * @brief IPC server over an already-open file-descriptor pair (e.g. stdin/stdout). + * + * A single implicit client (id 0) that is connected from listen() onward: the fd + * pair IS the connection, so there is no accept step. Framing is identical to the + * socket transport ([4B LE length][8B LE request id][payload]) — a process holding + * the other end of the pipes is a peer exactly like a UDS client, served by the + * same run()/run_reactor() code. + * + * Peer EOF on the read fd requests shutdown: a pipe server's lifetime is its + * pipe (the spawning parent closing stdin is how it tells the child to exit). + * + * Writes rely on SIGPIPE being ignored so a closed peer yields EPIPE instead of + * killing the process — install_default_signal_handlers() does this; standalone + * users (tests) must ignore SIGPIPE themselves. + */ +class PipeServer : public IpcServer { + public: + // Takes ownership of the fds; close() closes them (once, if they are equal — + // a socketpair end can serve as both). + PipeServer(int in_fd, int out_fd); + ~PipeServer() override; + + PipeServer(const PipeServer&) = delete; + PipeServer& operator=(const PipeServer&) = delete; + PipeServer(PipeServer&&) = delete; + PipeServer& operator=(PipeServer&&) = delete; + + bool listen() override; + int wait_for_data(uint64_t timeout_ns) override; + std::span receive(int client_id, uint64_t& request_id) override; + void release(int client_id, size_t message_size) override; + bool send(int client_id, uint64_t request_id, const void* data, size_t len) override; + void close() override; + + // Wake a thread blocked in wait_for_data() by writing the self-pipe that + // sits in the same poll set as the input fd. Used by run_reactor() to + // surface a worker-thread completion promptly. + void notify() override; + + private: + void close_internal(); + void disconnect(); + bool setup_wake_pipe(); + void drain_wake_pipe(); + + int in_fd_; + int out_fd_; + int wake_read_fd_ = -1; // self-pipe read end (in the poll set) + int wake_write_fd_ = -1; // self-pipe write end (poked by notify()) + bool connected_ = false; + std::vector recv_buffer_; +}; + +} // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp b/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp index 4943e50ba60..7f4f0bf243d 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp @@ -1,7 +1,12 @@ #include "ipc_runtime/serve_helper.hpp" +#include +#include +#include #include #include +#include +#include namespace ipc { @@ -16,6 +21,23 @@ bool ends_with(const std::string& s, const std::string& suffix) std::unique_ptr make_server(const std::string& input_path, const ServerOptions& opts) { + // "-" is the conventional CLI spelling for stdio: serve the process's own + // stdin/stdout (a parent spawned us with piped stdio, PipeBackend-style). + if (input_path == "-") { + // stdout becomes the frame stream, so anything else the process writes + // there (std::cout, printf, a library's diagnostics) would corrupt it. + // Serve on a private duplicate and point fd 1 at stderr instead. + std::fflush(stdout); + int frame_out = ::fcntl(STDOUT_FILENO, F_DUPFD_CLOEXEC, STDERR_FILENO + 1); + if (frame_out < 0 || ::dup2(STDERR_FILENO, STDOUT_FILENO) < 0) { + int err = errno; + if (frame_out >= 0) { + ::close(frame_out); + } + throw std::system_error(err, std::generic_category(), "make_server: cannot take stdout for the pipe"); + } + return IpcServer::create_pipe(STDIN_FILENO, frame_out); + } if (ends_with(input_path, ".sock")) { return IpcServer::create_socket(input_path, opts.socket_backlog); } diff --git a/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp b/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp index 8c5d690bf70..8d5f94bfa6a 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp @@ -35,7 +35,10 @@ struct ServerOptions { /** * @brief Construct an IpcServer based on the input path's suffix. * - * Recognised suffixes: + * Recognised inputs: + * - "-" → IpcServer::create_pipe over the process's own stdin and a + * duplicate of stdout (parent spawned us with piped stdio). fd 1 is + * then pointed at stderr so stray stdout writes can't corrupt frames. * - "*.sock" → IpcServer::create_socket(path, opts.socket_backlog) * - "*.shm" → IpcServer::create_mpsc_shm(, opts.max_shm_clients, * opts.shm_request_ring_size, diff --git a/ipc-runtime/cpp/ipc_runtime/shm.test.cpp b/ipc-runtime/cpp/ipc_runtime/shm.test.cpp index 701696e77b4..0d8b2682953 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm.test.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm.test.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/ipc_client.hpp" #include "ipc_runtime/ipc_server.hpp" +#include "ipc_runtime/shm/mpsc_shm.hpp" #include "ipc_runtime/shm/spsc_shm.hpp" #include "ipc_runtime/shm_client.hpp" #include "ipc_runtime/shm_common.hpp" @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -193,6 +196,43 @@ TEST(ShmTest, SingleClientSmallRingHighVolume) * A handler returning an empty vector must still produce a (zero-length) * response frame, otherwise the client deadlocks waiting for it. */ +// A segment is mappable from the moment its creator ftruncates it, before its control block is initialised. A client +// that attached then could write state (its blocked flag) that the creator's initialisation overwrites, losing the wake +// it was waiting for. Connecting must therefore refuse a segment that is sized but not yet initialised. +TEST(ShmTest, ConnectRefusesSizedButUninitialisedSegments) +{ + auto make_zeroed = [](const std::string& name, size_t len) { + shm_unlink(name.c_str()); + int fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); + ASSERT_GE(fd, 0); + ASSERT_EQ(ftruncate(fd, static_cast(len)), 0); + ::close(fd); + }; + + std::string ring_name = "shm_uninit_ring_" + std::to_string(getpid()); + make_zeroed(ring_name, sizeof(SpscCtrl) + 4096); + EXPECT_THROW(SpscShm::connect(ring_name), std::runtime_error); + shm_unlink(ring_name.c_str()); + + // Once create() has finished, the same name connects. + { + SpscShm created = SpscShm::create(ring_name, 4096); + EXPECT_NO_THROW(SpscShm::connect(ring_name)); + } + SpscShm::unlink(ring_name); + + // The MPSC doorbell: a producer must not attach before the consumer has published num_slots, even when the ring + // it would use is already initialised. + std::string base = "shm_uninit_mpsc_" + std::to_string(getpid()); + make_zeroed(base + "_doorbell", sizeof(MpscDoorbell) + sizeof(std::atomic)); + { + SpscShm ring = SpscShm::create(base + "_ring_0", 4096); + EXPECT_THROW(MpscProducer::connect(base, 0), std::runtime_error); + } + shm_unlink((base + "_doorbell").c_str()); + SpscShm::unlink(base + "_ring_0"); +} + TEST(ShmTest, ZeroLengthResponseRoundTrip) { constexpr size_t RING_SIZE = 4UL * 1024; diff --git a/ipc-runtime/cpp/ipc_runtime/shm/README.md b/ipc-runtime/cpp/ipc_runtime/shm/README.md index ac15655f051..0b906f1bb64 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/README.md +++ b/ipc-runtime/cpp/ipc_runtime/shm/README.md @@ -271,19 +271,21 @@ The consumer's `peek()` automatically skips padding, so callers never see it. Instead of busy-waiting forever: 1. **Producer**: Spins briefly checking for space, then sleeps on the `tail` futex (armed against the current tail value) 2. **Consumer**: Spins briefly checking for data, then sleeps on the `head` futex (armed against the current head value) -3. **Wakeup**: The other side calls `futex_wake` unconditionally after publishing/releasing. `futex_wait` re-checks the armed value atomically under the futex bucket lock, so a publish/release that races the sleep returns `EAGAIN` instead of sleeping. +3. **Wakeup**: Before sleeping, the waiter sets `consumer_blocked`/`producer_blocked` and re-checks the ring; it clears the flag when it wakes. The other side calls `futex_wake` after publishing/releasing only while that flag is set, so a busy channel makes no syscalls at all. Each side stores (data index or flag) and then loads the other's, so both put a `seq_cst` fence between the two: release/acquire alone lets the load run ahead of the store, and both sides could then miss each other. `futex_wait` re-checks the armed value atomically under the futex bucket lock, so a publish/release that races the sleep returns `EAGAIN` instead of sleeping. This provides: -- Low latency when active (spin catches transitions) +- Low latency when active (spin catches transitions, and no wake syscalls) - Low power when idle (futex sleep) - No thundering herd (one waker, one sleeper) -> The wake is intentionally unconditional — do **not** gate it on a -> `consumer_blocked`/`producer_blocked` flag to skip the syscall when no one is -> waiting. That would be a cross-process Dekker handshake between the flag and -> the head/tail word, which races and can drop the wake, stranding a waiter on -> already-published data. A `futex_wake` with no waiter is a cheap no-op, so the -> unconditional wake costs effectively nothing on the idle-consumer path. +> The conditional wake relies on nothing overwriting a waiter's flag. Segments +> are mappable from `ftruncate` on, before `create()` has initialised them, so a +> client that attached then could set its flag and have the creator's +> initialisation reset it, after which the matching publish skips the wake and +> the client sleeps on published data forever. `connect()` therefore refuses a +> segment until it is fully initialised: `create()` writes `capacity` (rings) and +> `num_slots` (doorbell) last, with release ordering, and a zero value means "not +> ready, retry". ### MPSC Doorbell @@ -299,7 +301,7 @@ struct alignas(64) MpscDoorbell { **Protocol:** 1. Producer publishes data to its SPSC ring -2. Producer increments the doorbell seq and calls `futex_wake` unconditionally +2. Producer increments the doorbell seq, and calls `futex_wake` if the consumer is blocked on it 3. Consumer wakes up, polls all rings in round-robin 4. Consumer sleeps on the doorbell seq only when all rings are empty diff --git a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp index b0a0db35771..2c34a5ae6a0 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp @@ -113,13 +113,16 @@ MpscConsumer MpscConsumer::create(const std::string& name, size_t num_producers, // Initialize doorbell (use placement new to avoid memset on non-trivial type) new (doorbell) MpscDoorbell{}; - doorbell->seq.store(0, std::memory_order_release); - doorbell->num_slots.store(static_cast(num_producers), std::memory_order_release); + doorbell->seq.store(0, std::memory_order_relaxed); + doorbell->consumer_blocked.store(false, std::memory_order_relaxed); // Initialize the per-slot owner table to "free" (pid 0). std::atomic* slot_owners = mpsc_slot_owners(doorbell); for (size_t i = 0; i < num_producers; i++) { new (&slot_owners[i]) std::atomic(0); } + // Published last: clients refuse the doorbell while num_slots is zero (MpscSlotClaim::claim, + // MpscProducer::connect), so none attaches before the fields above are set. Same scheme as SpscShm's capacity. + doorbell->num_slots.store(static_cast(num_producers), std::memory_order_release); // Create all SPSC rings std::vector rings; @@ -202,8 +205,11 @@ int MpscConsumer::wait_for_data(uint64_t timeout_ns, const std::function // post-spin predicate check runs promptly — without this, every // low-concurrency request eats the full spin before its response is sent. uint32_t spin_seq = doorbell_->seq.load(std::memory_order_acquire); - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - do { + // Read the clock only every TIME_CHECK_INTERVAL iterations, as SpscShm's spin does: a clock_gettime per + // iteration dominated the loop. + constexpr uint32_t TIME_CHECK_INTERVAL = 256; + uint32_t iterations = 0; + while (true) { for (size_t i = 0; i < num_rings; i++) { size_t idx = (last_served_ + 1 + i) % num_rings; if (rings_[idx].available() > 0) { @@ -216,7 +222,13 @@ int MpscConsumer::wait_for_data(uint64_t timeout_ns, const std::function break; // a completion may be ready — fall through to the predicate check } IPC_PAUSE(); - } while ((mono_ns_now() - start) < spin_duration); + if (++iterations >= TIME_CHECK_INTERVAL) { + iterations = 0; + if ((mono_ns_now() - start) >= spin_duration) { + break; + } + } + } // Check after spin for (size_t i = 0; i < num_rings; i++) { @@ -236,10 +248,9 @@ int MpscConsumer::wait_for_data(uint64_t timeout_ns, const std::function } // About to block. Capture the doorbell seq and arm the futex against it. - // Producers bump seq on every publish and wake unconditionally (see - // MpscProducer::publish), and futex_wait re-checks *seq == seq atomically, so - // a publish that lands before we sleep returns EAGAIN. The arm-value plus the - // unconditional wake are the whole protocol — no "blocked" flag. + // Producers bump seq on every publish and wake while consumer_blocked is set + // (see MpscProducer::publish), and futex_wait re-checks *seq == seq atomically, + // so a publish that lands before we sleep returns EAGAIN. uint32_t seq = doorbell_->seq.load(std::memory_order_acquire); // Final check before blocking @@ -261,7 +272,15 @@ int MpscConsumer::wait_for_data(uint64_t timeout_ns, const std::function return -1; } + // Set blocked flag RIGHT BEFORE futex_wait + doorbell_->consumer_blocked.store(true, std::memory_order_release); + // Pairs with the fences after the producers' seq bumps: either a producer + // sees this flag and wakes us, or the seq it bumped is visible to the + // futex_wait comparison below and we do not sleep. + std::atomic_thread_fence(std::memory_order_seq_cst); futex_wait_timeout(reinterpret_cast(&doorbell_->seq), seq, remaining_timeout); + // Clear blocked flag RIGHT AFTER futex_wait returns + doorbell_->consumer_blocked.store(false, std::memory_order_relaxed); // After waking, poll again for (size_t i = 0; i < num_rings; i++) { @@ -321,7 +340,12 @@ void MpscConsumer::notify() // enough here — without a seq bump a wake landing in the consumer's pre-sleep // window is lost. doorbell_->seq.fetch_add(1, std::memory_order_release); - futex_wake(reinterpret_cast(&doorbell_->seq), 1); + // Same conditional wake as MpscProducer::publish: only a consumer actually blocked on the doorbell needs the + // syscall. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (doorbell_->consumer_blocked.load(std::memory_order_relaxed)) { + futex_wake(reinterpret_cast(&doorbell_->seq), 1); + } } // ----- MpscProducer Implementation ----- @@ -401,6 +425,13 @@ MpscProducer MpscProducer::connect(const std::string& name, size_t producer_id) std::string(std::strerror(errno))); } + // The file exists from shm_open but is only sized by ftruncate; reading a mapping past its end raises SIGBUS. + struct stat st; + if (fstat(doorbell_fd, &st) != 0 || static_cast(st.st_size) < doorbell_len) { + ::close(doorbell_fd); + throw std::runtime_error("MpscProducer::connect: doorbell '" + doorbell_name + "' is not sized yet"); + } + auto* doorbell = static_cast(mmap(nullptr, doorbell_len, PROT_READ | PROT_WRITE, MAP_SHARED, doorbell_fd, 0)); if (doorbell == MAP_FAILED) { @@ -408,6 +439,12 @@ MpscProducer MpscProducer::connect(const std::string& name, size_t producer_id) ::close(doorbell_fd); throw std::runtime_error("MpscProducer::connect: mmap doorbell failed: " + std::string(std::strerror(e))); } + // Refuse a doorbell the consumer has not finished initialising (num_slots is published last in create()). + if (doorbell->num_slots.load(std::memory_order_acquire) == 0) { + munmap(doorbell, doorbell_len); + ::close(doorbell_fd); + throw std::runtime_error("MpscProducer::connect: doorbell '" + doorbell_name + "' is not initialised yet"); + } // Connect to assigned ring std::string ring_name = name + "_ring_" + std::to_string(producer_id); @@ -433,10 +470,14 @@ void MpscProducer::publish(size_t n) // Ring doorbell to wake the consumer. Bump seq (release) so a consumer // mid-block sees the value change and its futex_wait returns immediately, - // then wake unconditionally — never gated on a "consumer blocked" flag (see - // SpscShm::publish for why that handshake is unsafe across processes). + // then wake only if the consumer is blocked on the futex. doorbell_->seq.fetch_add(1, std::memory_order_release); - futex_wake(reinterpret_cast(&doorbell_->seq), 1); + + // Store-then-load against the consumer's flag-then-seq; see SpscShm::publish. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (doorbell_->consumer_blocked.load(std::memory_order_relaxed)) { + futex_wake(reinterpret_cast(&doorbell_->seq), 1); + } } // ----- MpscSlotClaim Implementation ----- diff --git a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp index 408eb6105f3..ba5213add98 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp @@ -26,13 +26,15 @@ namespace ipc { */ struct alignas(64) MpscDoorbell { // Producer-written (written by producers in publish()). Consumers wait on - // this seq with a futex; producers bump it and wake unconditionally. + // this seq with a futex; producers bump it and wake it while consumer_blocked is set. alignas(64) std::atomic seq; // Number of producer slots (= num_producers). Written once by the consumer at // create(); read by clients to bound the slot-claim scan. Kept here (offset 4) // so `seq` stays at offset 0 and the doorbell mapping/wakeup path is unchanged. std::atomic num_slots; - std::array _pad0; + // Set by the consumer right before futex_wait, cleared right after; producers wake only while it is set. + std::atomic consumer_blocked; + std::array _pad0; }; static_assert(sizeof(MpscDoorbell) == 64, "MpscDoorbell layout must stay 64 bytes (seq at offset 0)"); @@ -146,8 +148,8 @@ class MpscConsumer { * @brief Wake the consumer blocked in wait_for_data, without delivering ring * data. * - * Bumps the doorbell seq (release) then futex_wakes it — identical to the - * doorbell ring in MpscProducer::publish. The seq bump is what makes a + * Bumps the doorbell seq (release), then futex_wakes it if the consumer is + * blocked — identical to the doorbell ring in MpscProducer::publish. The seq bump is what makes a * consumer that is mid-wait see a value change and return from futex_wait * instead of sleeping; a bare futex_wake (as in wakeup_all) would race. */ diff --git a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp index d549abcc12c..143e189c25c 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp @@ -150,14 +150,16 @@ SpscShm SpscShm::create(const std::string& name, size_t min_capacity) } auto* ctrl = static_cast(mem); - // Initialize non-atomic fields first - ctrl->capacity = cap; + // The segment is visible (and mappable) from ftruncate on, so a client may map it before the fields below are set. + // connect() refuses a ring until capacity is non-zero, so capacity is published last: a client only ever attaches + // to a fully initialised ring, and nothing here can overwrite state a client has already written. ctrl->mask = cap - 1; ctrl->wrap_head = UINT64_MAX; - - // Initialize atomics with release ordering to ensure capacity/mask/wrap_head are visible - ctrl->head.store(0ULL, std::memory_order_release); - ctrl->tail.store(0ULL, std::memory_order_release); + ctrl->head.store(0ULL, std::memory_order_relaxed); + ctrl->tail.store(0ULL, std::memory_order_relaxed); + ctrl->consumer_blocked.store(false, std::memory_order_relaxed); + ctrl->producer_blocked.store(false, std::memory_order_relaxed); + std::atomic_ref(ctrl->capacity).store(cap, std::memory_order_release); auto* buf = reinterpret_cast(ctrl + 1); return SpscShm(fd, map_len, ctrl, buf); @@ -192,8 +194,14 @@ SpscShm SpscShm::connect(const std::string& name) auto* ctrl = static_cast(mem); auto* buf = reinterpret_cast(ctrl + 1); - // Ensure initialization is visible before use (pairs with release in create) - (void)ctrl->head.load(std::memory_order_acquire); + // A zero capacity means the creator has not finished initialising the ring (see create()). Refuse it, so the caller + // retries rather than using a ring whose initialisation could still overwrite what it writes. The acquire pairs + // with create()'s release, making every other field visible. + if (map_len < sizeof(SpscCtrl) || std::atomic_ref(ctrl->capacity).load(std::memory_order_acquire) == 0) { + munmap(mem, map_len); + ::close(fd); + throw std::runtime_error("SpscShm::connect: '" + name + "' is not initialised yet"); + } return SpscShm(fd, map_len, ctrl, buf); } @@ -254,12 +262,16 @@ void SpscShm::publish(size_t n) // synchronizes with the data and wrap_head writes above. ctrl_->head.store(head + total_advance, std::memory_order_release); - // Wake any blocked consumer unconditionally: futex_wake with no waiter is a - // cheap no-op, and the consumer's head value-check keeps the fast path - // syscall-free. Do not gate the wake on a "consumer blocked" flag — that - // cross-process flag/head handshake races and can drop the wake, stranding - // the consumer asleep on already-published data. - futex_wake(reinterpret_cast(&ctrl_->head), 1); + // The wake is skipped unless the consumer has flagged that it is about to + // sleep. That is a store-then-load on each side (here head then the flag; + // the waiter the flag then head), and release/acquire lets each load run + // ahead of its own store, so both sides could miss the other's write and + // the consumer sleep on published data. A seq_cst fence on both sides + // guarantees at least one of them sees the other. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (ctrl_->consumer_blocked.load(std::memory_order_relaxed)) { + futex_wake(reinterpret_cast(&ctrl_->head), 1); + } } void* SpscShm::peek(size_t want, uint64_t timeout_ns) @@ -314,9 +326,11 @@ void SpscShm::release(size_t n) uint64_t new_tail = tail + total_release; ctrl_->tail.store(new_tail, std::memory_order_release); - // Wake any producer blocked on a full ring, unconditionally — see publish() - // for why the wake is never gated on a "producer blocked" flag. - futex_wake(reinterpret_cast(&ctrl_->tail), 1); + // Pairs with the fence in wait_for_space(); see publish(). + std::atomic_thread_fence(std::memory_order_seq_cst); + if (ctrl_->producer_blocked.load(std::memory_order_relaxed)) { + futex_wake(reinterpret_cast(&ctrl_->tail), 1); + } } bool SpscShm::wait_for_data(size_t need, uint64_t timeout_ns) @@ -403,21 +417,23 @@ bool SpscShm::wait_for_data(size_t need, uint64_t timeout_ns) return false; } - // About to block. Capture the head value we're waiting to change and arm the - // futex against it. The producer wakes unconditionally on every publish (see - // publish()), and futex_wait re-checks *head == head_now atomically under the - // futex bucket lock, so a publish that lands between this load and the - // syscall returns EAGAIN immediately rather than sleeping. No separate - // "blocked" flag is involved — the arm-value and the producer's wake are the - // whole protocol. + // About to block - load seq, final check, then block uint32_t head_now = static_cast(ctrl_->head.load(std::memory_order_acquire)); + ctrl_->consumer_blocked.store(true, std::memory_order_release); + // Pairs with the fence in publish(): the flag must be visible before the + // re-check reads head. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (check_available()) { + ctrl_->consumer_blocked.store(false, std::memory_order_relaxed); previous_had_data_ = true; // Found data before blocking return true; } + // Wait on futex for producer to signal new data futex_wait_timeout(reinterpret_cast(&ctrl_->head), head_now, remaining_timeout); + ctrl_->consumer_blocked.store(false, std::memory_order_relaxed); bool result = check_available(); previous_had_data_ = result; // Update flag based on final result @@ -508,19 +524,23 @@ bool SpscShm::wait_for_space(size_t need, uint64_t timeout_ns) return false; } - // About to block. Arm the futex against the current tail; release() wakes - // unconditionally and futex_wait re-checks *tail == tail_now atomically, so a - // release between this load and the syscall returns EAGAIN rather than - // sleeping. Mirrors wait_for_data — the arm-value plus the unconditional wake - // are the whole protocol. + // About to block - load seq, final check, then block uint32_t tail_now = static_cast(ctrl_->tail.load(std::memory_order_acquire)); + // Wait on futex for consumer to signal freed space + ctrl_->producer_blocked.store(true, std::memory_order_release); + // Pairs with the fence in release(): the flag must be visible before the + // re-check reads tail. + std::atomic_thread_fence(std::memory_order_seq_cst); + if (check_space()) { + ctrl_->producer_blocked.store(false, std::memory_order_relaxed); previous_had_space_ = true; // Found space before blocking return true; } futex_wait_timeout(reinterpret_cast(&ctrl_->tail), tail_now, remaining_timeout); + ctrl_->producer_blocked.store(false, std::memory_order_relaxed); bool result = check_space(); previous_had_space_ = result; // Update flag based on final result diff --git a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.hpp b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.hpp index 886ec912e70..26c15c76848 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.hpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.hpp @@ -29,11 +29,13 @@ struct alignas(SPSC_CACHELINE) SpscCtrl { // Producer-owned (written by producer, read by consumer) alignas(SPSC_CACHELINE) std::atomic head; // bytes written uint64_t wrap_head; // Head value when last message wrapped (UINT64_MAX = no wrap), synchronized by head - std::array _pad0; + std::atomic producer_blocked; // Written by producer in wait_for_space() + std::array _pad0; // Consumer-owned (written by consumer, read by producer) alignas(SPSC_CACHELINE) std::atomic tail; // bytes consumed - std::array _pad1; + std::atomic consumer_blocked; // Written by consumer in wait_for_data() + std::array _pad1; // Immutable capacity information alignas(SPSC_CACHELINE) uint64_t capacity; // power of two diff --git a/ipc-runtime/cpp/ipc_runtime/shm_common.hpp b/ipc-runtime/cpp/ipc_runtime/shm_common.hpp index 4dc12cc885a..2e1cead881a 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm_common.hpp +++ b/ipc-runtime/cpp/ipc_runtime/shm_common.hpp @@ -61,7 +61,7 @@ inline std::span ring_receive_msg(SpscShm& ring, uint64_t timeout // legally publish more than capacity/2 - 4 bytes, so a larger prefix // means the ring is corrupt — and a frame shorter than the request-id // field means the peer speaks the id-less protocol. - if (msg_len > MAX_FRAME_SIZE || msg_len > ring.capacity() / 2 - 4 || msg_len < FRAME_ID_SIZE) { + if (msg_len > ring.capacity() / 2 - 4 || msg_len < FRAME_ID_SIZE) { throw std::runtime_error("ring_receive_msg: invalid length prefix (" + std::to_string(msg_len) + " bytes) — corrupt ring or protocol mismatch"); } diff --git a/ipc-runtime/cpp/ipc_runtime/socket.test.cpp b/ipc-runtime/cpp/ipc_runtime/socket.test.cpp index 4c4b4eed782..f66e6f599ef 100644 --- a/ipc-runtime/cpp/ipc_runtime/socket.test.cpp +++ b/ipc-runtime/cpp/ipc_runtime/socket.test.cpp @@ -1,5 +1,7 @@ #include "ipc_runtime/ipc_client.hpp" #include "ipc_runtime/ipc_server.hpp" +#include "ipc_runtime/stream_io.hpp" +#include #include #include #include @@ -216,11 +218,53 @@ TEST(SocketTest, ZeroLengthResponseRoundTrip) server->close(); } -// A corrupt/malicious length prefix must cause the server to drop the -// connection, not allocate the claimed amount. -TEST(SocketTest, ServerRejectsOversizedLengthPrefix) +// Stream frames have no size cap below the u32 length field, so a corrupt +// prefix can claim ~4 GiB. The payload buffer must grow with the bytes that +// actually arrive rather than allocating the claimed size up front. +TEST(StreamIoTest, ReadPayloadGrowsWithReceivedBytesNotClaimedLength) { - std::string path = test_socket_path("oversize_srv"); + constexpr size_t claimed = 0xFFFFFFF7; // largest payload a u32 frame can carry + constexpr size_t supplied = 3 * PAYLOAD_READ_CHUNK + 17; + size_t delivered = 0; + std::vector buffer; + int result = read_payload(buffer, 4, claimed, [&](uint8_t* dst, size_t n) { + if (delivered + n > supplied) { + return 0; // peer closes before the claimed length arrives + } + std::memset(dst, 0xab, n); + delivered += n; + return 1; + }); + EXPECT_EQ(result, 0); + EXPECT_LE(buffer.size(), 4 + 2 * supplied + PAYLOAD_READ_CHUNK); +} + +TEST(StreamIoTest, ReadPayloadReadsExactLengthAndKeepsZeroLengthNonNull) +{ + std::vector source(5 * PAYLOAD_READ_CHUNK + 3); + for (size_t i = 0; i < source.size(); i++) { + source[i] = static_cast(i * 31); + } + size_t pos = 0; + auto reader = [&](uint8_t* dst, size_t n) { + std::memcpy(dst, source.data() + pos, n); + pos += n; + return 1; + }; + std::vector buffer; + ASSERT_EQ(read_payload(buffer, 0, source.size(), reader), 1); + EXPECT_TRUE(std::equal(source.begin(), source.end(), buffer.begin())); + + std::vector empty; + ASSERT_EQ(read_payload(empty, 0, 0, reader), 1); + EXPECT_NE(empty.data(), nullptr); +} + +// A peer that claims a huge frame and then hangs up mid-payload must cost the +// server only that connection: it disconnects the client and keeps serving. +TEST(SocketTest, ServerDropsClientThatClosesMidFrame) +{ + std::string path = test_socket_path("midframe_srv"); auto server = IpcServer::create_socket(path, 2); ASSERT_TRUE(server->listen()); @@ -228,7 +272,7 @@ TEST(SocketTest, ServerRejectsOversizedLengthPrefix) server->run([](int, std::span req) { return std::vector(req.begin(), req.end()); }); }); - // Raw client so we can write a bogus frame. + // Raw client so we can write a truncated frame. int fd = socket(AF_UNIX, SOCK_STREAM, 0); ASSERT_GE(fd, 0); struct sockaddr_un addr; @@ -237,28 +281,34 @@ TEST(SocketTest, ServerRejectsOversizedLengthPrefix) std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); ASSERT_EQ(::connect(fd, reinterpret_cast(&addr), sizeof(addr)), 0); - uint32_t bogus_len = 0x7FFFFFFF; // ~2 GiB, way over MAX_FRAME_SIZE - ASSERT_EQ(::send(fd, &bogus_len, sizeof(bogus_len), 0), static_cast(sizeof(bogus_len))); - - // Server should close the connection. recv with a timeout so a buggy - // server (waiting for 2 GiB of payload) fails the test instead of hanging. - struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - uint8_t buf[4]; - ssize_t n = ::recv(fd, buf, sizeof(buf), 0); - EXPECT_EQ(n, 0) << "server should have closed the connection on oversized frame"; + uint32_t claimed_len = 0xFFFFFFFF; + uint64_t request_id = 7; + uint8_t payload[16] = {}; + ASSERT_EQ(::send(fd, &claimed_len, sizeof(claimed_len), 0), static_cast(sizeof(claimed_len))); + ASSERT_EQ(::send(fd, &request_id, sizeof(request_id), 0), static_cast(sizeof(request_id))); + ASSERT_EQ(::send(fd, payload, sizeof(payload), 0), static_cast(sizeof(payload))); ::close(fd); + auto client = IpcClient::create_socket(path); + ASSERT_TRUE(client->connect()); + const std::vector request = { 1, 2, 3 }; + ASSERT_TRUE(client->send(request.data(), request.size(), 2'000'000'000ULL)); + auto resp = client->receive(2'000'000'000ULL); + ASSERT_NE(resp.data(), nullptr); + EXPECT_EQ(std::vector(resp.begin(), resp.end()), request); + client->release(resp.size()); + client->close(); + server->request_shutdown(); server_thread.join(); server->close(); } -// Same on the client side: a bogus length prefix from the server must be -// rejected (connection closed), not trusted as an allocation size. -TEST(SocketTest, ClientRejectsOversizedLengthPrefix) +// Same on the client side: a server that claims a huge frame and closes +// mid-payload is an error, not a hang or an up-front allocation. +TEST(SocketTest, ClientErrorsWhenServerClosesMidFrame) { - std::string path = test_socket_path("oversize_cli"); + std::string path = test_socket_path("midframe_cli"); int listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); ASSERT_GE(listen_fd, 0); @@ -275,19 +325,19 @@ TEST(SocketTest, ClientRejectsOversizedLengthPrefix) if (conn_fd < 0) { return; } - uint32_t bogus_len = 0x7FFFFFFF; - ::send(conn_fd, &bogus_len, sizeof(bogus_len), 0); - // Leave the connection open: a buggy client would block waiting for - // ~2 GiB of payload (bounded by its receive timeout). - uint8_t buf[1]; - ::recv(conn_fd, buf, sizeof(buf), 0); // returns when client closes + uint32_t claimed_len = 0xFFFFFFFF; + uint64_t request_id = 7; + uint8_t payload[16] = {}; + ::send(conn_fd, &claimed_len, sizeof(claimed_len), 0); + ::send(conn_fd, &request_id, sizeof(request_id), 0); + ::send(conn_fd, payload, sizeof(payload), 0); ::close(conn_fd); }); auto client = IpcClient::create_socket(path); ASSERT_TRUE(client->connect()); auto resp = client->receive(2'000'000'000ULL); - EXPECT_EQ(resp.data(), nullptr) << "oversized frame must be an error"; + EXPECT_EQ(resp.data(), nullptr) << "a truncated frame must be an error"; client->close(); fake_server.join(); diff --git a/ipc-runtime/cpp/ipc_runtime/socket_client.cpp b/ipc-runtime/cpp/ipc_runtime/socket_client.cpp index 3adde5e9f04..900cec0a24c 100644 --- a/ipc-runtime/cpp/ipc_runtime/socket_client.cpp +++ b/ipc-runtime/cpp/ipc_runtime/socket_client.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/socket_client.hpp" #include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" #include #include #include @@ -125,7 +126,7 @@ bool SocketClient::send(uint64_t request_id, const void* data, size_t len, uint6 errno = EINVAL; return false; } - if (len > MAX_FRAME_SIZE) { + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { errno = EMSGSIZE; return false; } @@ -167,10 +168,9 @@ std::span SocketClient::receive(uint64_t timeout_ns, uint64_t& re return {}; } - // A corrupt/malicious prefix must not drive the allocation below. A frame - // shorter than the request-id field means the peer speaks the id-less - // protocol — close rather than misparse. - if (msg_len > MAX_FRAME_SIZE || msg_len < FRAME_ID_SIZE) { + // A frame shorter than the request-id field means the peer speaks the + // id-less protocol — close rather than misparse. + if (msg_len < FRAME_ID_SIZE) { close_internal(); return {}; } @@ -181,16 +181,10 @@ std::span SocketClient::receive(uint64_t timeout_ns, uint64_t& re close_internal(); return {}; } - msg_len -= FRAME_ID_SIZE; + msg_len -= static_cast(FRAME_ID_SIZE); - // Ensure buffer is large enough. Keep at least one byte so data() is - // non-null for zero-length messages (null data() signals failure). - if (recv_buffer_.size() < msg_len || recv_buffer_.empty()) { - recv_buffer_.resize(std::max(msg_len, 1)); - } - - // Read message data into internal buffer - if (recv_exact(recv_buffer_.data(), msg_len, partial) != 1) { + if (read_payload(recv_buffer_, 0, msg_len, [&](uint8_t* dst, size_t n) { return recv_exact(dst, n, partial); }) != + 1) { // Prefix consumed but payload incomplete — stream desynced. close_internal(); return {}; diff --git a/ipc-runtime/cpp/ipc_runtime/socket_server.cpp b/ipc-runtime/cpp/ipc_runtime/socket_server.cpp index e284425551a..7b31acd8c05 100644 --- a/ipc-runtime/cpp/ipc_runtime/socket_server.cpp +++ b/ipc-runtime/cpp/ipc_runtime/socket_server.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/socket_server.hpp" #include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" #include #include #include @@ -148,7 +149,7 @@ bool SocketServer::send(int client_id, uint64_t request_id, const void* data, si return false; } - if (len > MAX_FRAME_SIZE) { + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { errno = EMSGSIZE; return false; } @@ -231,10 +232,9 @@ std::span SocketServer::receive(int client_id, uint64_t& request_ total_read += static_cast(n); } - // A corrupt/malicious prefix must not drive the allocation below. A frame - // shorter than the request-id field means the peer speaks the id-less - // protocol — disconnect rather than misparse. - if (msg_len > MAX_FRAME_SIZE || msg_len < FRAME_ID_SIZE) { + // A frame shorter than the request-id field means the peer speaks the + // id-less protocol — disconnect rather than misparse. + if (msg_len < FRAME_ID_SIZE) { fprintf(stderr, "ipc: client %d sent an invalid frame (len=%u) — protocol mismatch?\n", client_id, msg_len); disconnect_client(client_id); return {}; @@ -254,35 +254,32 @@ std::span SocketServer::receive(int client_id, uint64_t& request_ } total_read += static_cast(n); } - msg_len -= FRAME_ID_SIZE; + msg_len -= static_cast(FRAME_ID_SIZE); - // Resize buffer if needed to fit length prefix + message - size_t total_size = sizeof(uint32_t) + msg_len; - if (recv_buffer.size() < total_size) { - recv_buffer.resize(total_size); - } - - // Store length prefix in buffer - std::memcpy(recv_buffer.data(), &msg_len, sizeof(uint32_t)); - - // Read message data - must loop until all bytes received (MSG_WAITALL unreliable on macOS) - total_read = 0; - while (total_read < msg_len) { - ssize_t n = ::recv(fd, recv_buffer.data() + sizeof(uint32_t) + total_read, msg_len - total_read, 0); - if (n < 0) { - if (errno == EINTR) { - continue; // Interrupted, retry + // Read message data after the stored length prefix - must loop until all + // bytes received (MSG_WAITALL unreliable on macOS). + int result = read_payload(recv_buffer, sizeof(uint32_t), msg_len, [fd](uint8_t* dst, size_t n) { + size_t got = 0; + while (got < n) { + ssize_t r = ::recv(fd, dst + got, n - got, 0); + if (r < 0) { + if (errno == EINTR) { + continue; // Interrupted, retry + } + return -1; } - disconnect_client(client_id); - return {}; - } - if (n == 0) { - // Client disconnected mid-message - disconnect_client(client_id); - return {}; + if (r == 0) { + return 0; // Client disconnected mid-message + } + got += static_cast(r); } - total_read += static_cast(n); + return 1; + }); + if (result != 1) { + disconnect_client(client_id); + return {}; } + std::memcpy(recv_buffer.data(), &msg_len, sizeof(uint32_t)); return std::span(recv_buffer.data() + sizeof(uint32_t), msg_len); } diff --git a/ipc-runtime/cpp/ipc_runtime/stream_io.hpp b/ipc-runtime/cpp/ipc_runtime/stream_io.hpp new file mode 100644 index 00000000000..03c9de81126 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/stream_io.hpp @@ -0,0 +1,53 @@ +#pragma once +/** + * @file stream_io.hpp + * @brief Payload reads shared by the stream transports (UDS and pipe). + */ + +#include +#include +#include +#include + +namespace ipc { + +/** First allocation step when reading a payload; later steps double. */ +inline constexpr size_t PAYLOAD_READ_CHUNK = 1024 * 1024; + +/** + * Read a `len`-byte payload into `buffer` at `offset`, growing the buffer only + * as bytes arrive. + * + * Stream frames carry no limit below the u32 length field, so a desynced or + * corrupt prefix can claim up to 4 GiB. Allocating the claimed size up front + * would commit that memory before a single payload byte shows up; growing in + * doubling steps keeps the allocation within twice what the peer actually sent. + * + * @param read_exact Reads exactly `n` bytes into `dst`, returning 1 on success, + * 0 on EOF and -1 on error (the transports' existing read helpers). + * @return The first non-success result of `read_exact`, or 1. + */ +template +int read_payload(std::vector& buffer, size_t offset, size_t len, ReadExact&& read_exact) +{ + // Keep at least one byte so data() is non-null for zero-length messages + // (null data() signals failure to callers). + if (buffer.size() < std::max(offset, 1)) { + buffer.resize(std::max(offset, 1)); + } + size_t received = 0; + while (received < len) { + size_t step = std::min(len - received, std::max(PAYLOAD_READ_CHUNK, received)); + if (buffer.size() < offset + received + step) { + buffer.resize(offset + received + step); + } + int result = read_exact(buffer.data() + offset + received, step); + if (result != 1) { + return result; + } + received += step; + } + return 1; +} + +} // namespace ipc diff --git a/ipc-runtime/cpp/napi/msgpack_client_async.cpp b/ipc-runtime/cpp/napi/msgpack_client_async.cpp index db0b1d27077..a72cb47723e 100644 --- a/ipc-runtime/cpp/napi/msgpack_client_async.cpp +++ b/ipc-runtime/cpp/napi/msgpack_client_async.cpp @@ -88,7 +88,9 @@ void MsgpackClientAsync::poll_responses() tsfn_.NonBlockingCall(response_data, [](Napi::Env env, Napi::Function js_callback, Completion* completion) { auto js_buffer = Napi::Buffer::Copy(env, completion->payload.data(), completion->payload.size()); - js_callback.Call({ Napi::BigInt::New(env, completion->request_id), js_buffer }); + // Live ids are below 2^30 (see NapiShmAsyncClient); a larger id is a stale + // frame and only needs to not match, so the double conversion is fine. + js_callback.Call({ Napi::Number::New(env, static_cast(completion->request_id)), js_buffer }); delete completion; }); if (status != napi_ok) { @@ -102,18 +104,14 @@ Napi::Value MsgpackClientAsync::call(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); - if (info.Length() < 2 || !info[0].IsBigInt() || !info[1].IsBuffer()) { - throw Napi::TypeError::New(env, "Expected (request_id: bigint, payload: Buffer)"); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsBuffer()) { + throw Napi::TypeError::New(env, "Expected (request_id: number, payload: Buffer)"); } if (shutdown_.load(std::memory_order_acquire)) { throw Napi::Error::New(env, "Client is closed"); } - bool lossless = false; - uint64_t request_id = info[0].As().Uint64Value(&lossless); - if (!lossless) { - throw Napi::TypeError::New(env, "request_id must fit in an unsigned 64-bit integer"); - } + uint64_t request_id = info[0].As().Uint32Value(); auto input_buffer = info[1].As>(); const uint8_t* input_data = input_buffer.Data(); size_t input_len = input_buffer.Length(); diff --git a/ipc-runtime/rust/.gitignore b/ipc-runtime/rust/.gitignore index 2c96eb1b651..8eea25754ca 100644 --- a/ipc-runtime/rust/.gitignore +++ b/ipc-runtime/rust/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +/cpp/ diff --git a/ipc-runtime/rust/Cargo.toml b/ipc-runtime/rust/Cargo.toml index 0e5a437f2fc..089ca07876f 100644 --- a/ipc-runtime/rust/Cargo.toml +++ b/ipc-runtime/rust/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" edition = "2021" description = "Safe Rust bindings to the ipc-runtime C ABI (UDS + MPSC-SHM transport)." license = "Apache-2.0" +repository = "https://github.com/AztecProtocol/aztec-packages/tree/next/ipc-runtime" +# cpp/ is not checked in: the release step copies ../cpp/ipc_runtime there so +# the published crate can build without the rest of the repo. +include = ["src/**/*.rs", "build.rs", "cpp/ipc_runtime/**"] [lib] name = "ipc_runtime" diff --git a/ipc-runtime/rust/build.rs b/ipc-runtime/rust/build.rs index c4cd25a5b30..f118bdcf671 100644 --- a/ipc-runtime/rust/build.rs +++ b/ipc-runtime/rust/build.rs @@ -7,24 +7,45 @@ // system clang; macOS gets libc++ via Apple clang. Either way, no external // IPC_RUNTIME_LIB_DIR dependency. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +/// Every non-test .cpp under `dir`, sorted for reproducible builds. +/// +/// Discovered rather than listed: a hand-maintained copy of the CMake target's +/// sources silently drifts when a file is added there, and the symptom is an +/// undefined reference at link time in whichever consumer links this archive, +/// far from the change that caused it. +fn collect_sources(dir: &Path, out: &mut Vec) { + let entries = std::fs::read_dir(dir) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display())); + for entry in entries { + let path = entry.expect("cannot read dir entry").path(); + if path.is_dir() { + collect_sources(&path, out); + } else if path.extension().is_some_and(|e| e == "cpp") + && !path.to_string_lossy().ends_with(".test.cpp") + { + out.push(path); + } + } + out.sort(); +} fn main() { let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let cpp_dir = crate_dir.join("../cpp"); + // In the repo the sources sit beside this crate. A published crate has no + // sibling directory, so its release step copies them into cpp/ instead. + let cpp_dir = [crate_dir.join("../cpp"), crate_dir.join("cpp")] + .into_iter() + .find(|dir| dir.join("ipc_runtime").is_dir()) + .expect("ipc-runtime C++ sources not found in ../cpp or cpp"); let src_dir = cpp_dir.join("ipc_runtime"); - let sources = [ - "c_abi.cpp", - "ipc_client.cpp", - "ipc_server.cpp", - "serve_helper.cpp", - "signal_handlers.cpp", - "socket_client.cpp", - "socket_server.cpp", - "shm/mpsc_shm.cpp", - "shm/spsc_shm.cpp", - ]; + let mut sources = Vec::new(); + collect_sources(&src_dir, &mut sources); + assert!(!sources.is_empty(), "no C++ sources found in {}", src_dir.display()); + // Re-run when a source is added or removed, not just edited. + println!("cargo:rerun-if-changed={}", src_dir.display()); let mut build = cc::Build::new(); build @@ -33,9 +54,8 @@ fn main() { .flag_if_supported("-fPIC") .include(&cpp_dir); - for src in sources { - let path = src_dir.join(src); - build.file(&path); + for path in &sources { + build.file(path); println!("cargo:rerun-if-changed={}", path.display()); } println!("cargo:rerun-if-changed=build.rs"); diff --git a/ipc-runtime/rust/src/lib.rs b/ipc-runtime/rust/src/lib.rs index b83ba3cc64b..03a28355ea1 100644 --- a/ipc-runtime/rust/src/lib.rs +++ b/ipc-runtime/rust/src/lib.rs @@ -10,6 +10,7 @@ #![allow(non_camel_case_types)] use std::ffi::{c_void, CString}; +use std::os::fd::RawFd; use std::os::raw::{c_char, c_int}; use std::ptr::NonNull; @@ -64,6 +65,7 @@ mod sys { pub fn ipc_install_default_signal_handlers(server: *mut ipc_server); pub fn ipc_make_client(path: *const c_char, shm_client_id: usize) -> *mut ipc_client; + pub fn ipc_client_create_pipe(in_fd: c_int, out_fd: c_int) -> *mut ipc_client; pub fn ipc_client_destroy(client: *mut ipc_client); pub fn ipc_client_connect(client: *mut ipc_client) -> bool; pub fn ipc_client_close(client: *mut ipc_client); @@ -242,6 +244,22 @@ impl IpcClient { Self::from_path_with_id(path, 0) } + /// Construct a client over an already-open fd pair, for talking to a child + /// process over its stdin/stdout. `in_fd` is read from, `out_fd` written + /// to; both stay owned by the caller and must outlive the client. + /// + /// # Safety + /// The descriptors must be valid and open for the client's lifetime. + pub unsafe fn from_fds(in_fd: RawFd, out_fd: RawFd) -> Result { + let raw = unsafe { sys::ipc_client_create_pipe(in_fd, out_fd) }; + let inner = NonNull::new(raw).ok_or(Error::Connect("pipe".to_string()))?; + let client = IpcClient { inner }; + if !unsafe { sys::ipc_client_connect(client.inner.as_ptr()) } { + return Err(Error::Connect("pipe".to_string())); + } + Ok(client) + } + pub fn from_path_with_id(path: &str, shm_client_id: usize) -> Result { let c_path = CString::new(path).map_err(|_| Error::InvalidPath(path.to_string()))?; let raw = unsafe { sys::ipc_make_client(c_path.as_ptr(), shm_client_id) }; diff --git a/ipc-runtime/ts/src/frame_reader.test.ts b/ipc-runtime/ts/src/frame_reader.test.ts new file mode 100644 index 00000000000..968e45049b5 --- /dev/null +++ b/ipc-runtime/ts/src/frame_reader.test.ts @@ -0,0 +1,39 @@ +import { test } from "node:test"; +import * as assert from "node:assert/strict"; +import { FrameReader } from "./frame_reader.js"; + +function frame(payload: Buffer): Buffer { + const header = Buffer.allocUnsafe(4); + header.writeUInt32LE(payload.length, 0); + return Buffer.concat([header, payload]); +} + +test("reassembles a frame split across chunks, including a split prefix", () => { + const payload = Buffer.alloc(300_000); + for (let i = 0; i < payload.length; i++) payload[i] = i & 0xff; + const bytes = frame(payload); + const reader = new FrameReader(); + const cuts = [1, 3, 9, 1000, 150_000, bytes.length]; + let start = 0; + for (const cut of cuts) { + assert.equal(reader.next(), undefined); + reader.push(bytes.subarray(start, cut)); + start = cut; + } + assert.deepEqual(reader.next(), bytes); + assert.equal(reader.next(), undefined); +}); + +test("yields several frames from one chunk and keeps the remainder", () => { + const a = frame(Buffer.from([1, 2])); + const b = frame(Buffer.from([])); + const c = frame(Buffer.from([3, 4, 5])); + const reader = new FrameReader(); + reader.push(Buffer.concat([a, b, c.subarray(0, 5)])); + assert.deepEqual(reader.next(), a); + assert.deepEqual(reader.next(), b); + assert.equal(reader.next(), undefined); + assert.equal(reader.peekLength(), 3); + reader.push(c.subarray(5)); + assert.deepEqual(reader.next(), c); +}); diff --git a/ipc-runtime/ts/src/frame_reader.ts b/ipc-runtime/ts/src/frame_reader.ts new file mode 100644 index 00000000000..09f96fa6ac9 --- /dev/null +++ b/ipc-runtime/ts/src/frame_reader.ts @@ -0,0 +1,72 @@ +/** + * Reassembles length-prefixed frames (`u32 len | len bytes`) from stream + * chunks. Chunks are queued as they arrive and each frame is copied out once + * it is complete, so a large frame costs one copy rather than one per chunk, + * and memory grows only with the bytes the peer has actually sent. + */ +export class FrameReader { + private chunks: Buffer[] = []; + private buffered = 0; + + push(chunk: Buffer): void { + this.chunks.push(chunk); + this.buffered += chunk.length; + } + + /** The next frame's length prefix, once its 4 bytes have arrived. */ + peekLength(): number | undefined { + if (this.buffered < 4) { + return undefined; + } + const head = this.chunks[0]; + if (head.length >= 4) { + return head.readUInt32LE(0); + } + return this.copyOut(4, /*consume=*/ false).readUInt32LE(0); + } + + /** The next whole frame, length prefix included, once it has fully arrived. */ + next(): Buffer | undefined { + const len = this.peekLength(); + if (len === undefined || this.buffered < 4 + len) { + return undefined; + } + return this.copyOut(4 + len, /*consume=*/ true); + } + + private copyOut(n: number, consume: boolean): Buffer { + const head = this.chunks[0]; + if (head.length >= n) { + if (consume) { + this.consume(n); + } + return head.subarray(0, n); + } + const out = Buffer.allocUnsafe(n); + let copied = 0; + for (const chunk of this.chunks) { + copied += chunk.copy(out, copied, 0, Math.min(chunk.length, n - copied)); + if (copied === n) { + break; + } + } + if (consume) { + this.consume(n); + } + return out; + } + + private consume(n: number): void { + this.buffered -= n; + while (n > 0) { + const head = this.chunks[0]; + if (head.length <= n) { + n -= head.length; + this.chunks.shift(); + } else { + this.chunks[0] = head.subarray(n); + n = 0; + } + } + } +} diff --git a/ipc-runtime/ts/src/index.ts b/ipc-runtime/ts/src/index.ts index ebfc6cea6da..3a095cfd47f 100644 --- a/ipc-runtime/ts/src/index.ts +++ b/ipc-runtime/ts/src/index.ts @@ -14,6 +14,7 @@ export { } from "./errors.js"; export { SpawnedProcessBackend, + SpawnedProcessBackendSync, type SpawnedProcessBackendOptions, type SpawnedTransport, } from "./spawned_backend.js"; diff --git a/ipc-runtime/ts/src/pending_queue.ts b/ipc-runtime/ts/src/pending_queue.ts new file mode 100644 index 00000000000..fa9c5fe25a9 --- /dev/null +++ b/ipc-runtime/ts/src/pending_queue.ts @@ -0,0 +1,55 @@ +/** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */ +export const REQUEST_ID_SPACE = 2 ** 30; + +/** A call awaiting its response. */ +export interface PendingCall { + requestId: number; + resolve: (data: Uint8Array) => void; + reject: (error: unknown) => void; +} + +/** + * In-flight calls in issue order, paired to responses by request id. Servers + * that answer in order (e.g. a serial `run()` loop) always match the head, so + * the common path is a shift and an integer compare; out-of-order responses + * fall back to a search. Ids start at a random point so a stale frame from a + * previous occupant of the channel is unlikely to pair with a live call. + */ +export class PendingQueue { + private calls: PendingCall[] = []; + private nextRequestId = Math.floor(Math.random() * REQUEST_ID_SPACE); + + get length(): number { + return this.calls.length; + } + + /** Enqueues a call under a fresh request id and returns the id. */ + push(resolve: PendingCall["resolve"], reject: PendingCall["reject"]): number { + const requestId = this.nextRequestId; + this.nextRequestId = (requestId + 1) % REQUEST_ID_SPACE; + this.calls.push({ requestId, resolve, reject }); + return requestId; + } + + /** Removes the most recently pushed call (to unwind a failed send). */ + pop(): PendingCall | undefined { + return this.calls.pop(); + } + + /** Removes and returns the call for `requestId`, or undefined if none is pending. */ + take(requestId: number): PendingCall | undefined { + const calls = this.calls; + if (calls.length > 0 && calls[0].requestId === requestId) { + return calls.shift(); + } + const i = calls.findIndex((c) => c.requestId === requestId); + return i === -1 ? undefined : calls.splice(i, 1)[0]; + } + + /** Removes and returns every pending call. */ + drain(): PendingCall[] { + const calls = this.calls; + this.calls = []; + return calls; + } +} diff --git a/ipc-runtime/ts/src/shm_client.test.ts b/ipc-runtime/ts/src/shm_client.test.ts index 02d8b999394..b52ca36dadf 100644 --- a/ipc-runtime/ts/src/shm_client.test.ts +++ b/ipc-runtime/ts/src/shm_client.test.ts @@ -7,16 +7,16 @@ import { NapiShmAsyncClient, NapiMsgpackClientAsync } from "./shm_client.js"; * testable without the native module or a live server. */ class MockAddon implements NapiMsgpackClientAsync { - public deliver!: (requestId: bigint, response: Buffer) => void; - public sent: Array<{ requestId: bigint; input: Buffer }> = []; + public deliver!: (requestId: number, response: Buffer) => void; + public sent: Array<{ requestId: number; input: Buffer }> = []; public acquires = 0; public releases = 0; public closed = false; - setResponseCallback(cb: (requestId: bigint, response: Buffer) => void): void { + setResponseCallback(cb: (requestId: number, response: Buffer) => void): void { this.deliver = cb; } - call(requestId: bigint, input: Buffer): void { + call(requestId: number, input: Buffer): void { this.sent.push({ requestId, input }); } acquire(): void { @@ -40,7 +40,7 @@ test("shm async client discards a stale frame and still resolves the live call", // A leftover frame from a ring's previous occupant: unknown id. Must be // discarded — not resolve the live call, not reject anything. - addon.deliver(liveId ^ 0xdeadbeefn, Buffer.from([0xba, 0xad])); + addon.deliver(liveId ^ 0x5eadbee, Buffer.from([0xba, 0xad])); // The real response still pairs and resolves. addon.deliver(liveId, Buffer.from([9, 9])); diff --git a/ipc-runtime/ts/src/shm_client.ts b/ipc-runtime/ts/src/shm_client.ts index 53b43456afc..aee5b268048 100644 --- a/ipc-runtime/ts/src/shm_client.ts +++ b/ipc-runtime/ts/src/shm_client.ts @@ -1,5 +1,6 @@ import { loadIpcRuntimeNapi } from "./native_loader.js"; -import { IpcClientAsync, IpcClientSync } from "./types.js"; +import { PendingQueue } from "./pending_queue.js"; +import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js"; /** * Minimum surface a NAPI msgpack client must expose. Satisfied by the @@ -24,8 +25,8 @@ export interface NapiMsgpackClientSync { } export interface NapiMsgpackClientAsync { - setResponseCallback(cb: (requestId: bigint, response: Buffer) => void): void; - call(requestId: bigint, input: Buffer): void; + setResponseCallback(cb: (requestId: number, response: Buffer) => void): void; + call(requestId: number, input: Buffer): void; acquire(): void; release(): void; /** Stop the native poll thread, release any held TSFN ref, close the client. */ @@ -49,42 +50,34 @@ export class NapiShmSyncClient implements IpcClientSync { } } -interface PendingCallback { - resolve: (data: Uint8Array) => void; - reject: (error: Error) => void; -} - /** * Wraps the fire-and-forget async NAPI msgpack client behind the - * `IpcClientAsync` interface. Owns a map of pending calls keyed by request - * id; the C++ background polling thread invokes `setResponseCallback` once - * per response (in completion order), and this wrapper pairs it to its - * caller by the echoed id. Ids start at a random point per client so a - * stale frame left in a recycled SHM ring slot by a previous occupant - * cannot pair with a live call. + * `IpcClientAsync` interface. The C++ background polling thread invokes + * `setResponseCallback` once per response (in completion order), and this + * wrapper pairs it to its caller by the echoed id (see PendingQueue). * * `acquire` / `release` are reference-count hooks the NAPI exposes so the * libuv loop is kept alive only while requests are outstanding — without * them a `node script.js` would never exit naturally. */ export class NapiShmAsyncClient implements IpcClientAsync { - private readonly pending = new Map(); - private nextRequestId = - (BigInt(Math.floor(Math.random() * 0xffffffff)) << 16n) + 1n; + private readonly pending = new PendingQueue(); private destroyed = false; - constructor(private inner: NapiMsgpackClientAsync) { - this.inner.setResponseCallback((requestId: bigint, response: Buffer) => { + constructor( + private inner: NapiMsgpackClientAsync, + private readonly mapError?: IpcErrorMapper, + ) { + this.inner.setResponseCallback((requestId: number, response: Buffer) => { if (this.destroyed) { // Late response delivered after destroy(); the native close already // balanced the TSFN reference. return; } - const cb = this.pending.get(requestId); + const cb = this.pending.take(requestId); if (cb) { - this.pending.delete(requestId); cb.resolve(new Uint8Array(response)); - if (this.pending.size === 0) { + if (this.pending.length === 0) { this.inner.release(); } } else { @@ -104,28 +97,31 @@ export class NapiShmAsyncClient implements IpcClientAsync { call(input: Uint8Array): Promise { if (this.destroyed) { - return Promise.reject( - new Error("NapiShmAsyncClient: call() after destroy()"), + return new Promise((_, reject) => + this.fail( + reject, + new Error("NapiShmAsyncClient: call() after destroy()"), + ), ); } const buf = Buffer.isBuffer(input) ? input : Buffer.from(input.buffer, input.byteOffset, input.byteLength); return new Promise((resolve, reject) => { - const requestId = this.nextRequestId++; - if (this.pending.size === 0) { + if (this.pending.length === 0) { this.inner.acquire(); } - this.pending.set(requestId, { resolve, reject }); + const requestId = this.pending.push(resolve, reject); try { this.inner.call(requestId, buf); } catch (err: any) { - // Send failed — unwind the map entry we just added. - this.pending.delete(requestId); - if (this.pending.size === 0) { + // Send failed — unwind the entry we just added. + this.pending.pop(); + if (this.pending.length === 0) { this.inner.release(); } - reject( + this.fail( + reject, err instanceof Error ? err : new Error(`SHM async call failed: ${String(err)}`), @@ -134,6 +130,14 @@ export class NapiShmAsyncClient implements IpcClientAsync { }); } + private fail(reject: (error: unknown) => void, err: unknown): void { + if (this.mapError) { + this.mapError(err).then(reject, reject); + } else { + reject(err); + } + } + async destroy(): Promise { if (this.destroyed) { return; @@ -141,12 +145,11 @@ export class NapiShmAsyncClient implements IpcClientAsync { this.destroyed = true; // Reject anything still in flight. const err = new Error("ipc-runtime SHM client destroyed before response"); - for (const cb of this.pending.values()) { - cb.reject(err); + for (const cb of this.pending.drain()) { + this.fail(cb.reject, err); } - this.pending.clear(); // Stops the native poll thread and releases the TSFN reference taken - // when the map went 0 → 1 — without this, Node never exits when + // when the queue went 0 → 1 — without this, Node never exits when // destroyed with calls in flight. this.inner.close(); } @@ -157,6 +160,8 @@ export interface CreateNapiShmOptions { clientId?: number; /** Override addon path lookup. Rarely needed; useful for tests / unusual deployments. */ customAddonPath?: string; + /** Applied to every rejected call's error. */ + mapError?: IpcErrorMapper; } /** @@ -190,5 +195,6 @@ export function createNapiShmAsyncClient( options.clientId === undefined ? new napi.MsgpackClientAsync(shmName) : new napi.MsgpackClientAsync(shmName, options.clientId), + options.mapError, ); } diff --git a/ipc-runtime/ts/src/spawned_backend.test.ts b/ipc-runtime/ts/src/spawned_backend.test.ts index 472492d9a6c..fd59bd3fe57 100644 --- a/ipc-runtime/ts/src/spawned_backend.test.ts +++ b/ipc-runtime/ts/src/spawned_backend.test.ts @@ -223,3 +223,54 @@ test("destroy during a pending respawn does not leak the fresh process", async ( ); } }); + +// Hangs up on every connection's first request but keeps serving: its client +// is disconnected, its process is not. Writes its pid to the path in argv[3]. +const HANGUP_SERVER = join(scratch, "hangup_server.cjs"); +writeFileSync( + HANGUP_SERVER, + ` +require('node:fs').writeFileSync(process.argv[3], String(process.pid)); +require('node:net') + .createServer(conn => conn.on('data', () => conn.destroy())) + .listen(process.argv[2]); +`, +); + +test("a broken connection to a live server kills it, and respawn replaces it", async () => { + const pidFile = join(scratch, "hangup.pid"); + const backend = await SpawnedProcessBackend.spawn({ + binaryPath: process.execPath, + binaryName: "hangup_server", + instancePrefix: "hangup-test", + ipcPathArgs: [HANGUP_SERVER, "{path}", pidFile], + transport: "uds", + respawn: true, + }); + const { readFileSync } = await import("node:fs"); + const firstPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10); + try { + const err = await backend.call(Uint8Array.from([1])).then( + () => undefined, + (e: Error) => e, + ); + assert.equal((err as { retry?: boolean } | undefined)?.retry, true); + assert.equal( + processAlive(firstPid), + false, + "the stranded server was killed", + ); + + // The next call gets a fresh process rather than the dead connection. + await assert.rejects(backend.call(Uint8Array.from([2]))); + const secondPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10); + assert.notEqual(secondPid, firstPid); + await backend.destroy(); + assert.equal(processAlive(secondPid), false); + } finally { + await backend.destroy(); + if (processAlive(firstPid)) { + process.kill(firstPid, "SIGKILL"); + } + } +}); diff --git a/ipc-runtime/ts/src/spawned_backend.ts b/ipc-runtime/ts/src/spawned_backend.ts index 4943eb28867..bffd0cf3da8 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -3,9 +3,17 @@ import { open, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { threadId } from "node:worker_threads"; -import { IpcError, IpcProcessExitedError, IpcSpawnError } from "./errors.js"; -import { createNapiShmAsyncClient } from "./shm_client.js"; -import { IpcClientAsync } from "./types.js"; +import { + IpcError, + IpcProcessExitedError, + IpcSpawnError, + IpcTransportError, +} from "./errors.js"; +import { + createNapiShmAsyncClient, + createNapiShmSyncClient, +} from "./shm_client.js"; +import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js"; import { UdsIpcClient } from "./uds_client.js"; export type SpawnedTransport = "uds" | "shm"; @@ -33,6 +41,20 @@ export interface SpawnedProcessBackendOptions { * still reject (with retry=true); only later calls see the fresh process. */ respawn?: boolean; + /** + * Unref the child process (and, over UDS, the idle socket) so a backend + * that is never destroy()ed cannot hold the Node event loop open. Calls in + * flight still keep the loop alive until their response arrives. + */ + unref?: boolean; + /** + * Also unref the child's stdout/stderr pipes, which exist only when + * `logger` is set. Separate from `unref` because those pipes are how the + * caller sees the child's output: unref'ing them lets the process exit with + * log lines still unread, so it is opt-in even when the child itself is + * unref'd. + */ + unrefStdio?: boolean; /** SHM only: fixed client slot id. When unset the client self-allocates a free slot. */ clientId?: number; /** SHM only: override the native addon path. */ @@ -55,6 +77,124 @@ const SIGTERM_GRACE_MS = 5_000; // so without this grace a death would be misreported as a bare transport error. const EXIT_ATTRIBUTION_GRACE_MS = 250; +/** POSIX shm segment names an shm server creates for `.shm`. */ +function shmSegmentPaths(ipcPath: string): string[] { + const shmName = ipcPath.replace(/\.shm$/, ""); + return ["_request", "_response"].map( + (suffix) => `/dev/shm/${shmName}${suffix}`, + ); +} + +/** + * Remove any ipc path left behind by a previous occupant of this name. + * + * Run before spawning as well as after teardown: a server killed outright + * (SIGKILL, OOM) never reaches its own cleanup, and shm segments are created + * with O_EXCL, so a leftover segment makes the next server fail at startup + * with a bare "File exists". Instance names embed the pid, so this only bites + * once pids recycle — which is routine in containers and CI. + */ +async function removeStaleIpcPath( + transport: SpawnedTransport, + ipcPath: string, +): Promise { + try { + const paths = transport === "shm" ? shmSegmentPaths(ipcPath) : [ipcPath]; + await Promise.all(paths.map((p) => rm(p, { force: true }))); + } catch { + // Best effort: a stale path we cannot remove surfaces as the server's own + // startup error, which carries more context than anything we could throw. + } +} + +/** + * Spawn the server process with the caller's argv/env, wiring stdio to the + * live logger when there is one and to `logFd` otherwise. Shared by the async + * and sync backends so process setup has exactly one implementation. + */ +function spawnServerProcess( + options: SpawnedProcessBackendOptions, + ipcPath: string, + logFd?: number, +): ChildProcess { + const child = spawn( + options.binaryPath, + [ + ...options.ipcPathArgs.map((arg) => (arg === "{path}" ? ipcPath : arg)), + ...(options.extraArgs ?? []), + ], + { + stdio: [ + "ignore", + options.logger ? "pipe" : logFd!, + options.logger ? "pipe" : logFd!, + ], + env: { ...process.env, ...(options.env ?? {}) }, + }, + ); + if (options.logger) { + child.stdout?.on("data", (data: Buffer) => + options.logger?.( + `[${options.binaryName} stdout] ${data.toString().trimEnd()}`, + ), + ); + child.stderr?.on("data", (data: Buffer) => + options.logger?.( + `[${options.binaryName} stderr] ${data.toString().trimEnd()}`, + ), + ); + } + if (options.unref) { + child.unref(); + } + if (options.unrefStdio) { + // The stdio pipes are net.Sockets at runtime but typed as Readable. + (child.stdout as unknown as { unref?: () => void } | null)?.unref?.(); + (child.stderr as unknown as { unref?: () => void } | null)?.unref?.(); + } + return child; +} + +/** + * A promise that rejects if the child fails to spawn or dies before its IPC + * endpoint is ready, for racing against the connect. Already observed, since + * it can reject before the caller attaches a handler (spawn failures land on + * nextTick) and would otherwise count as an unhandled rejection. + */ +function childReadyFailurePromise( + child: ChildProcess, + binaryName: string, +): Promise { + const failure = new Promise((_, reject) => { + // Spawn syscall failures are the one place errno distinguishes a + // configuration error (missing/non-executable binary → retrying cannot + // help) from an environmental one. + child.once("error", (err) => { + const code = (err as NodeJS.ErrnoException).code; + const retry = code !== "ENOENT" && code !== "EACCES"; + reject( + new IpcSpawnError( + `Failed to spawn ${binaryName}: ${err.message}`, + retry, + { + cause: err, + }, + ), + ); + }); + child.once("exit", (code, signal) => { + reject( + new IpcSpawnError( + `${binaryName} exited before IPC connection was ready (code=${code}, signal=${signal})`, + /*retry=*/ true, + ), + ); + }); + }); + failure.catch(() => {}); + return failure; +} + let instanceCounter = 0; /** One spawned process together with the connection into it. */ @@ -116,13 +256,21 @@ export class SpawnedProcessBackend implements IpcClientAsync { return this.ipcPath; } - async call(input: Uint8Array): Promise { - const incarnation = await this.ensureUp(); - try { - return await incarnation.client.call(input); - } catch (err) { - throw await this.attributeCallError(incarnation, err); + call(input: Uint8Array): Promise { + // Fast path for a live process: no async frame, no await on ensureUp(), + // and no extra promise. This runs once per call, and a pipelined caller + // issuing thousands of calls feels every extra promise hop. Errors are + // attributed by the client itself (see connectClient's mapError). + const incarnation = this.current; + if (incarnation !== undefined && !this.destroying) { + return incarnation.client.call(input); } + return this.callWithRespawn(input); + } + + private async callWithRespawn(input: Uint8Array): Promise { + const incarnation = await this.ensureUp(); + return await incarnation.client.call(input); } sendProcessSignal(signal: NodeJS.Signals): void { @@ -206,6 +354,12 @@ export class SpawnedProcessBackend implements IpcClientAsync { * actually happened: the socket breaks before the child's 'exit' event * lands, so wait a short grace for exit attribution before giving up and * rethrowing the transport error as-is. + * + * A broken connection to a process that is still running is retired by + * killing that process. The client cannot reconnect, so otherwise the + * backend would keep the dead connection, failing every later call, never + * respawning, and leaving the server running for the life of the caller. + * Killing it routes the failure through the ordinary death path instead. */ private async attributeCallError( incarnation: Incarnation, @@ -229,15 +383,21 @@ export class SpawnedProcessBackend implements IpcClientAsync { this.logPath, ); } + if ( + err instanceof IpcTransportError && + !this.destroying && + this.current === incarnation + ) { + incarnation.child.kill("SIGKILL"); + await incarnation.exitPromise; + } return err; } /** Spawn the server process and connect to it; kills the child on any failure. */ private async spawnIncarnation(): Promise { const { options } = this; - if (options.transport === "uds") { - await rm(this.ipcPath, { force: true }); - } + await removeStaleIpcPath(options.transport, this.ipcPath); // Without a live logger, capture the child's stdout/stderr to the backend's // log file (a plain fd, not a pipe — a pipe would keep the libuv loop @@ -248,35 +408,7 @@ export class SpawnedProcessBackend implements IpcClientAsync { // whole event loop. const logFile = this.logPath !== undefined ? await open(this.logPath, "a") : undefined; - const child = spawn( - options.binaryPath, - [ - ...options.ipcPathArgs.map((arg) => - arg === "{path}" ? this.ipcPath : arg, - ), - ...(options.extraArgs ?? []), - ], - { - stdio: [ - "ignore", - options.logger ? "pipe" : logFile!.fd, - options.logger ? "pipe" : logFile!.fd, - ], - env: { ...process.env, ...(options.env ?? {}) }, - }, - ); - if (options.logger) { - child.stdout?.on("data", (data: Buffer) => - options.logger?.( - `[${options.binaryName} stdout] ${data.toString().trimEnd()}`, - ), - ); - child.stderr?.on("data", (data: Buffer) => - options.logger?.( - `[${options.binaryName} stderr] ${data.toString().trimEnd()}`, - ), - ); - } + const child = spawnServerProcess(options, this.ipcPath, logFile?.fd); const incarnation: Partial & { child: ChildProcess } = { child, @@ -290,35 +422,10 @@ export class SpawnedProcessBackend implements IpcClientAsync { }); incarnation.exitPromise = exitPromise; - const childReadyFailure = new Promise((_, reject) => { - // Spawn syscall failures are the one place errno distinguishes a - // configuration error (missing/non-executable binary → retrying cannot - // help) from an environmental one. - child.once("error", (err) => { - const code = (err as NodeJS.ErrnoException).code; - const retry = code !== "ENOENT" && code !== "EACCES"; - reject( - new IpcSpawnError( - `Failed to spawn ${options.binaryName}: ${err.message}`, - retry, - { cause: err }, - ), - ); - }); - child.once("exit", (code, signal) => { - reject( - new IpcSpawnError( - `${options.binaryName} exited before IPC connection was ready (code=${code}, signal=${signal})`, - /*retry=*/ true, - ), - ); - }); - }); - - // Observe immediately: childReadyFailure can reject during the awaits - // below (spawn failures land on nextTick), before Promise.race attaches - // its handler — without this it would count as an unhandled rejection. - childReadyFailure.catch(() => {}); + const childReadyFailure = childReadyFailurePromise( + child, + options.binaryName, + ); if (logFile !== undefined) { // spawn() dups the fd synchronously; the parent's handle isn't needed. @@ -333,12 +440,19 @@ export class SpawnedProcessBackend implements IpcClientAsync { // and fail immediately with the real cause if the process dies first. On // any failure, reap the child and its ipc path so a failed spawn cannot // leak an orphan process still holding sockets or database locks. + // The connect retries on a timer until its budget expires. When the child + // dies first the race rejects, but nothing stops that loop — so abort it, + // or it keeps dialling a dead server and holds the event loop open. + const connectAbort = new AbortController(); try { incarnation.client = await Promise.race([ - this.connectClient(), + this.connectClient(connectAbort.signal, (err) => + this.attributeCallError(incarnation as Incarnation, err), + ), childReadyFailure, ]); } catch (err) { + connectAbort.abort(); if (child.pid !== undefined) { // SIGKILL, not SIGTERM: the process never became ready, so it has no // state to flush, and a wedged process may not honour SIGTERM. @@ -385,7 +499,10 @@ export class SpawnedProcessBackend implements IpcClientAsync { } } - private async connectClient(): Promise { + private async connectClient( + signal: AbortSignal, + mapError: IpcErrorMapper, + ): Promise { const { options } = this; const timeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS; if (options.transport === "uds") { @@ -396,17 +513,21 @@ export class SpawnedProcessBackend implements IpcClientAsync { // wait with its real exit cause. return await UdsIpcClient.connect(this.ipcPath, { connectTimeoutMs: timeoutMs, + unref: options.unref, + signal, + mapError, }); } // The SHM client attaches to server-created rings, so creation can race // server startup; retry until the backstop expires. const deadline = Date.now() + timeoutMs; let lastError: unknown; - while (Date.now() <= deadline) { + while (Date.now() <= deadline && !signal?.aborted) { try { return createNapiShmAsyncClient(this.ipcPath.replace(/\.shm$/, ""), { clientId: options.clientId, customAddonPath: options.napiPath, + mapError, }); } catch (err) { lastError = err; @@ -443,18 +564,152 @@ export class SpawnedProcessBackend implements IpcClientAsync { } private async cleanupIpcPath(): Promise { + await removeStaleIpcPath(this.options.transport, this.ipcPath); + } +} + +/** + * A synchronous IpcClientSync backed by a spawned server process, for callers + * whose API cannot await (BarretenbergSync and friends). Shares process setup, + * stale-path removal and the connect backstop with SpawnedProcessBackend; only + * the request path differs, blocking in the NAPI client instead of returning a + * promise. + * + * SHM only: a synchronous request needs the shared-memory client, as a socket + * round trip cannot block the event loop without deadlocking it. Construction + * is still async — the server has to come up before the first call — so only + * call() and destroy() are synchronous. + */ +export class SpawnedProcessBackendSync implements IpcClientSync { + private destroyed = false; + + private constructor( + private readonly options: SpawnedProcessBackendOptions, + private readonly child: ChildProcess, + private readonly client: IpcClientSync, + private readonly ipcPath: string, + private readonly logPath?: string, + ) {} + + static async spawn( + options: SpawnedProcessBackendOptions, + ): Promise { + if (options.transport !== "shm") { + throw new IpcError( + `SpawnedProcessBackendSync requires the shm transport (got ${options.transport})`, + /*retry=*/ false, + ); + } + const instanceId = `${options.instancePrefix}-${process.pid}-${threadId}-${instanceCounter++}`; + const ipcPath = `${instanceId}.shm`; + const logPath = options.logger + ? undefined + : join(tmpdir(), `${instanceId}.log`); + + await removeStaleIpcPath("shm", ipcPath); + const logFile = + logPath !== undefined ? await open(logPath, "a") : undefined; + const child = spawnServerProcess(options, ipcPath, logFile?.fd); + const childReadyFailure = childReadyFailurePromise( + child, + options.binaryName, + ); + await logFile?.close(); + try { - if (this.options.transport === "uds") { - await rm(this.ipcPath, { force: true }); + const client = await Promise.race([ + connectShmSyncClient(options, ipcPath), + childReadyFailure, + ]); + return new SpawnedProcessBackendSync( + options, + child, + client, + ipcPath, + logPath, + ); + } catch (err) { + if (child.exitCode === null && child.signalCode === null) { + child.kill("SIGKILL"); } - if (this.options.transport === "shm") { - const shmName = this.ipcPath.replace(/\.shm$/, ""); - for (const suffix of ["_request", "_response"]) { - await rm(`/dev/shm/${shmName}${suffix}`, { force: true }); - } + child.removeAllListeners(); + await removeStaleIpcPath("shm", ipcPath); + throw err instanceof IpcError + ? err + : new IpcSpawnError( + `Failed to start ${options.binaryName}: ${String(err)}`, + /*retry=*/ true, + { cause: err }, + ); + } + } + + call(input: Uint8Array): Uint8Array { + if (this.destroyed) { + throw new IpcError( + `${this.options.binaryName} backend destroyed`, + /*retry=*/ false, + ); + } + try { + return this.client.call(input); + } catch (err) { + // A dead server is the likeliest cause of a failed shm call; report it as + // such (with the log path) rather than as an opaque transport error. + if (this.child.exitCode !== null || this.child.signalCode !== null) { + throw new IpcProcessExitedError( + `${this.options.binaryName} exited unexpectedly (code=${this.child.exitCode}, signal=${this.child.signalCode})` + + (this.logPath !== undefined ? `; see logs: ${this.logPath}` : ""), + this.child.exitCode, + this.child.signalCode, + this.logPath, + ); } - } catch { - // Cleanup is best-effort; the paths live under tmpdir. + throw err; + } + } + + destroy(): void { + if (this.destroyed) { + return; + } + this.destroyed = true; + this.client.destroy(); + if (this.child.exitCode === null && this.child.signalCode === null) { + this.child.kill("SIGTERM"); + } + this.child.removeAllListeners(); + // Teardown is synchronous by contract, so the segments are reaped in the + // background; the pre-spawn removal is what actually guarantees a clean + // start for the next occupant. + void removeStaleIpcPath("shm", this.ipcPath); + } +} + +/** Retry attaching the sync shm client until the server has created its rings. */ +async function connectShmSyncClient( + options: SpawnedProcessBackendOptions, + ipcPath: string, +): Promise { + const deadline = + Date.now() + (options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS); + let lastError: unknown; + while (Date.now() <= deadline) { + try { + return createNapiShmSyncClient(ipcPath.replace(/\.shm$/, ""), { + clientId: options.clientId, + customAddonPath: options.napiPath, + }); + } catch (err) { + lastError = err; + await new Promise((resolve) => setTimeout(resolve, 50)); } } + const message = + lastError instanceof Error ? lastError.message : String(lastError); + throw new IpcSpawnError( + `Timed out connecting to ${options.binaryName}: ${message}`, + /*retry=*/ true, + { cause: lastError }, + ); } diff --git a/ipc-runtime/ts/src/types.ts b/ipc-runtime/ts/src/types.ts index de67744cd41..1fdbbc06628 100644 --- a/ipc-runtime/ts/src/types.ts +++ b/ipc-runtime/ts/src/types.ts @@ -7,6 +7,13 @@ export interface IpcClientAsync { destroy(): Promise; } +/** + * Rewrites a failed call's error before the caller sees it, e.g. to blame a + * dead server process instead of the broken transport. Clients apply it on + * the rejection path only, so successful calls pay nothing for it. + */ +export type IpcErrorMapper = (err: unknown) => Promise; + export interface IpcClientSync { call(input: Uint8Array): Uint8Array; destroy(): void; @@ -16,11 +23,12 @@ export interface IpcClientSync { // keep the two in sync. /** - * Maximum length-prefix value accepted on receive. A frame claiming more - * than this is treated as corruption and the connection is closed instead - * of buffering the claimed size. + * Largest frame the wire format can express: the u32 length prefix counts the + * request id plus the payload, so a payload can be at most MAX_FRAME_SIZE - 8 + * bytes. Stream receivers buffer only what has arrived, so nothing smaller is + * imposed; SHM is bounded separately by its ring capacity. */ -export const MAX_FRAME_SIZE = 256 * 1024 * 1024; // 256 MiB +export const MAX_FRAME_SIZE = 0xffffffff; /** * Total budget (ms) for connect() retry loops, covering the window where diff --git a/ipc-runtime/ts/src/uds.test.ts b/ipc-runtime/ts/src/uds.test.ts index eed1a6fd2c5..64dfc297e5e 100644 --- a/ipc-runtime/ts/src/uds.test.ts +++ b/ipc-runtime/ts/src/uds.test.ts @@ -1,5 +1,5 @@ // In-process UDS transport tests: UdsIpcServer + UdsIpcClient round-trips, -// zero-length responses, disconnect handling and oversized-frame rejection. +// zero-length responses, disconnect handling and truncated/oversized frames. // Run via `yarn test` (node --test against the compiled dest/ output). import { test } from "node:test"; @@ -8,6 +8,7 @@ import * as net from "node:net"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { IpcError, IpcTransportError } from "./errors.js"; import { UdsIpcClient } from "./uds_client.js"; import { UdsIpcServer } from "./uds_server.js"; @@ -85,15 +86,15 @@ test("disconnect rejects pending calls and fails fast afterwards", async () => { } }); -test("client rejects oversized frame from server", async () => { - const socketPath = tmpSocketPath("oversize_cli"); - // Raw server that answers any request with a corrupt 0xFFFFFFFF length - // prefix. +test("client fails pending calls when the server closes mid-frame", async () => { + const socketPath = tmpSocketPath("midframe_cli"); + // Raw server that claims a ~4 GiB response, sends a few bytes of it and + // hangs up. const rawServer = net.createServer((conn) => { conn.once("data", () => { - const bogus = Buffer.allocUnsafe(4); - bogus.writeUInt32LE(0xffffffff, 0); - conn.write(bogus); + const partial = Buffer.alloc(12 + 16); + partial.writeUInt32LE(0xffffffff, 0); + conn.end(partial); }); }); await new Promise((resolve) => @@ -102,7 +103,7 @@ test("client rejects oversized frame from server", async () => { const client = await UdsIpcClient.connect(socketPath); try { - await assert.rejects(client.call(new Uint8Array([1])), /oversized frame/); + await assert.rejects(client.call(new Uint8Array([1])), IpcTransportError); } finally { await client.destroy(); rawServer.close(); @@ -110,6 +111,28 @@ test("client rejects oversized frame from server", async () => { } }); +test("a request too large for the frame header rejects without breaking the connection", async () => { + const socketPath = tmpSocketPath("too_large"); + const server = await UdsIpcServer.listen(socketPath, (_id, req) => req); + const client = await UdsIpcClient.connect(socketPath); + try { + // Only the length is consulted before the request is refused, so a stand-in + // avoids allocating 4 GiB. + const tooLarge = { length: 2 ** 32 } as unknown as Uint8Array; + await assert.rejects(client.call(tooLarge), (err: unknown) => { + assert.ok(err instanceof IpcError); + assert.ok(!(err instanceof IpcTransportError)); + assert.equal(err.retry, false); + return true; + }); + const payload = new Uint8Array([4, 5, 6]); + assert.deepEqual(await client.call(payload), payload); + } finally { + await client.destroy(); + await server.close(); + } +}); + test("client fails all pending calls on a response with an unknown request id", async () => { const socketPath = tmpSocketPath("unknown_id"); // Raw server that answers with a well-formed frame whose request id matches @@ -166,33 +189,26 @@ test("client fails loudly on an id-less (old-protocol) frame", async () => { } }); -test("server drops connection on oversized frame", async () => { - const socketPath = tmpSocketPath("oversize_srv"); +test("server keeps serving after a client closes mid-frame", async () => { + const socketPath = tmpSocketPath("midframe_srv"); const server = await UdsIpcServer.listen(socketPath, (_id, req) => req); const conn = net.createConnection(socketPath); - try { + const client = await (async () => { await new Promise((resolve, reject) => { conn.once("connect", () => resolve()); conn.once("error", reject); }); - const bogus = Buffer.allocUnsafe(4); - bogus.writeUInt32LE(0xffffffff, 0); - conn.write(bogus); - await new Promise((resolve, reject) => { - const timer = setTimeout( - () => reject(new Error("server did not close the connection")), - 5000, - ); - conn.once("close", () => { - clearTimeout(timer); - resolve(); - }); - conn.once("error", () => { - /* RST is fine — close follows */ - }); - }); + const partial = Buffer.alloc(12 + 16); + partial.writeUInt32LE(0xffffffff, 0); + conn.end(partial); + return UdsIpcClient.connect(socketPath); + })(); + try { + const payload = new Uint8Array([1, 2, 3]); + assert.deepEqual(await client.call(payload), payload); } finally { conn.destroy(); + await client.destroy(); await server.close(); } }); diff --git a/ipc-runtime/ts/src/uds_client.ts b/ipc-runtime/ts/src/uds_client.ts index 4c617357222..e14b966a8f9 100644 --- a/ipc-runtime/ts/src/uds_client.ts +++ b/ipc-runtime/ts/src/uds_client.ts @@ -1,18 +1,20 @@ import * as net from "node:net"; -import { IpcTransportError } from "./errors.js"; +import { IpcError, IpcTransportError } from "./errors.js"; +import { FrameReader } from "./frame_reader.js"; +import { PendingQueue } from "./pending_queue.js"; import { IpcClientAsync, + IpcErrorMapper, CONNECT_RETRY_BUDGET_MS, MAX_FRAME_SIZE, } from "./types.js"; -interface PendingCall { - resolve: (resp: Uint8Array) => void; - reject: (err: Error) => void; -} - export interface UdsIpcClientConnectOptions { - /** Mark the socket as unref'd so it doesn't keep the Node event loop alive when idle. */ + /** + * Unref the socket while idle so it doesn't keep the Node event loop + * alive; it is re-ref'd while calls are in flight so a response can never + * be lost to an early process exit. + */ unref?: boolean; /** * Retry budget (ms) for the initial connect when the server has bound the @@ -20,6 +22,15 @@ export interface UdsIpcClientConnectOptions { * ECONNREFUSED. Default CONNECT_RETRY_BUDGET_MS (5000). */ connectTimeoutMs?: number; + /** + * Abandons the connect. Callers that race the connect against something + * else (a spawned server dying, say) must abort the loser: otherwise it + * keeps retrying on a timer until its budget expires, holding the event + * loop open long after the caller gave up. + */ + signal?: AbortSignal; + /** Applied to every rejected call's error. */ + mapError?: IpcErrorMapper; } /** @@ -30,19 +41,20 @@ export interface UdsIpcClientConnectOptions { * * Supports pipelining: each call carries a unique request id which the * server echoes on the response, so responses are paired to callers by id - * and the server may complete requests in any order. Ids start at a random - * point per connection. + * and the server may complete requests in any order (see PendingQueue). */ export class UdsIpcClient implements IpcClientAsync { - private buffer: Buffer = Buffer.alloc(0); - private pending = new Map(); - private nextRequestId = - (BigInt(Math.floor(Math.random() * 0xffffffff)) << 16n) + 1n; + private frames = new FrameReader(); + private readonly pending = new PendingQueue(); private destroyed = false; /** Set once the socket has errored/closed; new calls fail fast. */ private closed = false; - private constructor(private conn: net.Socket) { + private constructor( + private conn: net.Socket, + private readonly idleUnref: boolean, + private readonly mapError?: IpcErrorMapper, + ) { conn.on("data", (chunk) => this.onData(chunk)); conn.on("error", (err) => this.failAll( @@ -63,15 +75,16 @@ export class UdsIpcClient implements IpcClientAsync { const conn = await connectWithRetry( socketPath, opts?.connectTimeoutMs ?? CONNECT_RETRY_BUDGET_MS, + opts?.signal, ); conn.setNoDelay(true); if (opts?.unref) conn.unref(); - return new UdsIpcClient(conn); + return new UdsIpcClient(conn, opts?.unref ?? false, opts?.mapError); } /** Number of in-flight calls awaiting a response. */ get inflight(): number { - return this.pending.size; + return this.pending.length; } /** Underlying socket — exposed for ref/unref control (event-loop tuning). */ @@ -81,19 +94,35 @@ export class UdsIpcClient implements IpcClientAsync { async call(input: Uint8Array): Promise { if (this.destroyed) { - throw new IpcTransportError("UdsIpcClient: call() after destroy()"); + throw await this.mapped( + new IpcTransportError("UdsIpcClient: call() after destroy()"), + ); } if (this.closed) { - throw new IpcTransportError( - "UdsIpcClient: call() on a closed/errored socket", + throw await this.mapped( + new IpcTransportError( + "UdsIpcClient: call() on a closed/errored socket", + ), + ); + } + // The u32 length prefix cannot describe a larger frame. This is the + // caller's error and the connection is still healthy, so it is not a + // transport error (which would retire the server) and is not mapped. + if (input.length + 8 > MAX_FRAME_SIZE) { + throw new IpcError( + `UdsIpcClient: request of ${input.length} bytes exceeds the ${MAX_FRAME_SIZE}-byte frame limit`, + /*retry=*/ false, ); } return new Promise((resolve, reject) => { - const requestId = this.nextRequestId++; - this.pending.set(requestId, { resolve, reject }); + if (this.idleUnref && this.pending.length === 0) { + this.conn.ref(); + } + const requestId = this.pending.push(resolve, reject); const header = Buffer.allocUnsafe(12); header.writeUInt32LE(input.length + 8, 0); // length counts id + payload - header.writeBigUInt64LE(requestId, 4); + header.writeUInt32LE(requestId, 4); // u64 id; live ids fit in the low word + header.writeUInt32LE(0, 8); this.conn.write(header); this.conn.write(input); }); @@ -107,23 +136,10 @@ export class UdsIpcClient implements IpcClientAsync { } private onData(chunk: Buffer): void { - this.buffer = - this.buffer.length === 0 - ? Buffer.from(chunk) - : Buffer.concat([this.buffer, chunk]); - while (this.buffer.length >= 4) { - const len = this.buffer.readUInt32LE(0); - if (len > MAX_FRAME_SIZE) { - // Corrupt/malicious frame — close instead of buffering up to the - // claimed size. - this.conn.destroy(); - this.failAll( - new IpcTransportError( - `UdsIpcClient: oversized frame (${len} bytes exceeds MAX_FRAME_SIZE)`, - ), - ); - return; - } + this.frames.push(chunk); + for (;;) { + const len = this.frames.peekLength(); + if (len === undefined) return; if (len < 8) { // Shorter than the request-id field: the server speaks the id-less // protocol. Fail loudly instead of misparsing. @@ -136,13 +152,17 @@ export class UdsIpcClient implements IpcClientAsync { ); return; } - if (this.buffer.length < 4 + len) return; - const requestId = this.buffer.readBigUInt64LE(4); - const payload = this.buffer.subarray(12, 4 + len); - this.buffer = this.buffer.subarray(4 + len); - const next = this.pending.get(requestId); + const frame = this.frames.next(); + if (frame === undefined) return; + // Live ids are below 2^30, so a nonzero high word can't match a call. + const requestId = + frame.readUInt32LE(8) === 0 ? frame.readUInt32LE(4) : -1; + const payload = frame.subarray(12); + const next = this.pending.take(requestId); if (next) { - this.pending.delete(requestId); + if (this.idleUnref && this.pending.length === 0) { + this.conn.unref(); + } next.resolve(new Uint8Array(payload)); } else { // A response that pairs with no pending call means the stream's @@ -161,9 +181,17 @@ export class UdsIpcClient implements IpcClientAsync { private failAll(err: Error): void { this.closed = true; - const pending = [...this.pending.values()]; - this.pending.clear(); - for (const p of pending) p.reject(err); + for (const p of this.pending.drain()) { + if (this.mapError) { + this.mapError(err).then(p.reject, p.reject); + } else { + p.reject(err); + } + } + } + + private async mapped(err: unknown): Promise { + return this.mapError ? await this.mapError(err) : err; } } @@ -180,11 +208,15 @@ export class UdsIpcClient implements IpcClientAsync { async function connectWithRetry( socketPath: string, timeoutMs: number, + signal?: AbortSignal, ): Promise { const deadline = Date.now() + timeoutMs; let attempt = 0; let lastErr: Error | undefined; while (true) { + if (signal?.aborted) { + throw new IpcTransportError("UdsIpcClient: connect aborted"); + } try { const remainingMs = Math.max(1, deadline - Date.now()); return await attemptConnect(socketPath, remainingMs); @@ -210,7 +242,7 @@ async function connectWithRetry( ); } const delay = Math.min(50, 5 * 2 ** attempt++); - await new Promise((resolve) => setTimeout(resolve, delay)); + await sleep(delay, signal); } } } @@ -248,3 +280,18 @@ function attemptConnect( conn.once("error", onError); }); } + +/** Wait `ms`, returning early (and clearing the timer) if `signal` aborts. */ +function sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + resolve(); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + }); +} diff --git a/ipc-runtime/ts/src/uds_server.ts b/ipc-runtime/ts/src/uds_server.ts index 6d07a874643..005af6bcec2 100644 --- a/ipc-runtime/ts/src/uds_server.ts +++ b/ipc-runtime/ts/src/uds_server.ts @@ -1,6 +1,6 @@ import * as net from "node:net"; import * as fs from "node:fs"; -import { MAX_FRAME_SIZE } from "./types.js"; +import { FrameReader } from "./frame_reader.js"; /** * Handler signature mirrors the C++ ipc::IpcServer::Handler: receive raw @@ -101,26 +101,14 @@ export class UdsIpcServer { const clientId = this.nextClientId++; this.connections.add(conn); conn.on("close", () => this.connections.delete(conn)); - let buffer = Buffer.alloc(0); + const frames = new FrameReader(); let chain: Promise = Promise.resolve(); conn.on("data", (chunk: Buffer) => { - buffer = - buffer.length === 0 - ? Buffer.from(chunk) - : Buffer.concat([buffer, chunk]); - while (buffer.length >= 4) { - const len = buffer.readUInt32LE(0); - if (len > MAX_FRAME_SIZE) { - // Corrupt/malicious frame — drop the connection instead of - // buffering up to the claimed size. - conn.destroy( - new Error( - `UdsIpcServer: oversized frame (${len} bytes exceeds MAX_FRAME_SIZE)`, - ), - ); - return; - } + frames.push(chunk); + for (;;) { + const len = frames.peekLength(); + if (len === undefined) break; if (len < 8) { // Shorter than the request-id field: the peer speaks the id-less // protocol. Drop the connection with a clear reason. @@ -132,12 +120,12 @@ export class UdsIpcServer { ); return; } - if (buffer.length < 4 + len) break; - const requestId = buffer.readBigUInt64LE(4); + const frame = frames.next(); + if (frame === undefined) break; + const requestId = frame.readBigUInt64LE(4); // Copy into a standalone Buffer (not a subarray view, and not a plain Uint8Array): handlers // decode with msgpackr, which relies on Buffer semantics for correct string/binary decoding. - const payload = Buffer.from(buffer.subarray(12, 4 + len)); - buffer = buffer.subarray(4 + len); + const payload = Buffer.from(frame.subarray(12)); const prev = chain; chain = (async () => { diff --git a/ipc-runtime/zig/build.zig b/ipc-runtime/zig/build.zig index e93af3f3144..7415271520c 100644 --- a/ipc-runtime/zig/build.zig +++ b/ipc-runtime/zig/build.zig @@ -24,17 +24,7 @@ pub fn build(b: *std.Build) void { runtime_mod.addIncludePath(cpp_root); runtime_mod.addCSourceFiles(.{ .root = cpp_root, - .files = &.{ - "ipc_runtime/c_abi.cpp", - "ipc_runtime/ipc_client.cpp", - "ipc_runtime/ipc_server.cpp", - "ipc_runtime/serve_helper.cpp", - "ipc_runtime/signal_handlers.cpp", - "ipc_runtime/socket_client.cpp", - "ipc_runtime/socket_server.cpp", - "ipc_runtime/shm/mpsc_shm.cpp", - "ipc_runtime/shm/spsc_shm.cpp", - }, + .files = collectCppSources(b, cpp_root, "ipc_runtime"), .flags = &.{ "-std=c++20", "-fPIC" }, }); const runtime = b.addLibrary(.{ @@ -69,3 +59,30 @@ pub fn build(b: *std.Build) void { smoke.linkLibrary(runtime); b.installArtifact(smoke); } + +/// Every non-test .cpp under the runtime's source tree, relative to `root`. +/// +/// Discovered rather than listed: a hand-maintained copy of the CMake target's +/// sources silently drifts when a file is added there, and the symptom is an +/// undefined symbol at link time in whichever consumer links this archive, far +/// from the change that caused it. +fn collectCppSources(b: *std.Build, root: std.Build.LazyPath, subdir: []const u8) []const []const u8 { + var files: std.ArrayList([]const u8) = .empty; + // Only the runtime's own sources: cpp/ also holds the NAPI addon (needs + // node headers) and CMake build directories. + const root_path = b.pathJoin(&.{ root.getPath(b), subdir }); + var dir = std.fs.cwd().openDir(root_path, .{ .iterate = true }) catch |err| + std.debug.panic("cannot open {s}: {s}", .{ root_path, @errorName(err) }); + defer dir.close(); + var walker = dir.walk(b.allocator) catch @panic("out of memory"); + defer walker.deinit(); + while (walker.next() catch @panic("failed walking ipc-runtime sources")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".cpp")) continue; + if (std.mem.endsWith(u8, entry.path, ".test.cpp")) continue; + // Paths are relative to `root`, which is what addCSourceFiles expects. + files.append(b.allocator, b.pathJoin(&.{ subdir, entry.path })) catch @panic("out of memory"); + } + if (files.items.len == 0) std.debug.panic("no C++ sources under {s}", .{root_path}); + return files.toOwnedSlice(b.allocator) catch @panic("out of memory"); +}