From 1c10d71a0f9164c36589dc7d37b94d93b346f0c1 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Sun, 30 Aug 2026 06:58:12 +0000 Subject: [PATCH 01/19] refactor(bbapi): migrate bb + bb.js to ipc-codegen/ipc-runtime; delete legacy bb::ipc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combined redo of #23612 + #23614 + #23613. Those predate envelope ids (the 8-byte request-id frame prefix), which broke wire compatibility between the old and new stacks, so bb, bb.js and bb-rs have to move together. bb now serves msgpack through ipc-runtime on every transport: `bb msgpack run` maps ""/"-" to a stdio pipe server, .sock to UDS and .shm to shared memory, all dispatching through one generated handler with envelope framing (a plain file keeps the offline bare-frame replay). The command surface comes from a checked-in bb_schema.json in the friendly ipc-codegen dialect, with handlers implemented as thin adapters over the existing domain commands. The typed cbind entrypoint is replaced by the ipc-codegen FFI contract ipc_ffi_entry, so wasm and static-linking consumers speak the same payload as the transports. That makes the legacy machinery consumer-free, so it goes: the in-tree ipc library and its benchmark, the nodejs_module msgpack client (bb.js now uses ipc-runtime's NAPI addon, and nodejs_module only exports LMDBStore), the Command/CommandResponse named unions with their execute dispatcher, and the func_traits/schema reflection behind the CBIND macros. Both client libraries now generate from ipc-codegen rather than forks of it. bb.js/src/cbind (a stale copy of the generator: schema visitor, TS and rust backends, naming, friendly-schema lowering) is deleted in favour of calling ipc-codegen, and barretenberg-rs drops its hand-written Backend trait, error type, Fr/Point types and both backends — codegen emits those, and ipc_runtime::IpcClient plugs in as the Backend, so the crate is ~89% generated with a deprecated BarretenbergApi shim keeping the published surface. The hand-rolled PipeBackend goes with them; its tests now run over ipc-runtime's UDS transport. ipc-codegen gains --strip-type-prefix (bb.js and barretenberg-rs publish unprefixed names while wire tags keep the service prefix) and three rust serde fixes: fq[2] pairs, [u8; N] and above-cutoff byte arrays were encoded as sequences of integers rather than msgpack bin, which the C++ side rejects. ipc-runtime gains the pipe transport (fd pair or stdio, peer EOF requests shutdown), a sync spawned-process backend for callers that cannot await, pre-spawn removal of stale shm segments (they are created O_EXCL, so a killed server's leftovers blocked the next one), and an unref option so a spawned backend cannot hold the Node event loop open while idle. Its rust and zig build files now discover the C++ sources instead of keeping hand-written copies of the CMake list, which had silently drifted. Also fixes two latent breakages from the ts/ -> ts/bb.js/ restructure that blocked wasm and pinned-flow tests: the barretenberg_wasm symlinks and the chonk pinned-inputs repo-root resolution were both one directory short. Raises MAX_FRAME_SIZE from 256 MiB to 1 GiB. bb.js reaches bb over ipc-runtime's socket transport now rather than the deleted nodejs_module msgpack client, so proving requests are newly subject to the frame cap: the rollup circuits exceed it, and bb_prover_full_rollup failed with the server refusing a 314525552-byte frame and the client seeing the closed connection as "write EPIPE". The TS client also checks the outgoing size, so a request over the cap names itself instead of surfacing as an unexplained EPIPE on the next write. Fixes two gaps in unref handling that left the Node event loop open. The shared-memory backends dropped the caller's option -- their new() did not take it and createBackend never passed it -- so unrefStdio was never set and the child's stdout/stderr pipes (which exist whenever a logger is set) kept the loop alive; they thread it through as the socket backend already does. And BarretenbergSync.initSingleton did not force unref: true, unlike its async counterpart, so the sync backend it creates held the loop for the life of the process. Nothing destroys a singleton, so it opts in now too. Together these had a labs pxe test pass all 7 cases and then hang until the job timed out. Rebased onto next, which added two things to the surface this replaces. #25487 hardened the msgpack entrypoints: the generated dispatch already answers a malformed envelope, an unknown command and a throwing handler with an error frame, but an unpack or convert failure still escapes it, so api_msgpack.cpp and ipc_ffi_entry catch that rather than lose the response. Poseidon2AbsorbChain was a new command registered in the NamedUnion this deletes, so it moves to bb_schema.json with a handler alongside the other crypto ones, and its malformed -length test is reexpressed against the FFI entrypoint. barretenberg/acir_tests is its own yarn project and consumes bb.js through a portal, so it has to resolve bb.js's dependencies itself. bb.js declares @aztec-foundation/ipc-runtime with the placeholder specifier that only a resolution gives meaning to, which barretenberg/ts has and acir_tests did not: `yarn install` there failed with "isn't supported by any available resolver". It gets the same resolution. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016einpgfthfjLYGwCB3iQqD --- Makefile | 20 +- barretenberg/.gitignore | 3 +- barretenberg/acir_tests/package.json | 3 +- barretenberg/acir_tests/yarn.lock | 8 + barretenberg/cpp/.gitignore | 1 + barretenberg/cpp/CMakePresets.json | 10 +- barretenberg/cpp/bootstrap.sh | 2 +- barretenberg/cpp/src/CMakeLists.txt | 6 +- .../cpp/src/barretenberg/api/CMakeLists.txt | 2 +- .../cpp/src/barretenberg/api/api_msgpack.cpp | 332 ++------- .../cpp/src/barretenberg/api/api_msgpack.hpp | 55 +- .../cpp/src/barretenberg/bb/CMakeLists.txt | 4 +- barretenberg/cpp/src/barretenberg/bb/cli.cpp | 3 +- .../cpp/src/barretenberg/bbapi/CMakeLists.txt | 50 ++ .../bbapi/bb_curve_constants.json | 36 + .../cpp/src/barretenberg/bbapi/bb_schema.json | 646 ++++++++++++++++++ .../barretenberg/bbapi/bb_schema_embed.hpp.in | 7 + .../cpp/src/barretenberg/bbapi/bbapi.hpp | 12 +- .../cpp/src/barretenberg/bbapi/bbapi.test.cpp | 1 - .../bbapi/bbapi_chonk_handlers.cpp | 170 +++++ .../src/barretenberg/bbapi/bbapi_execute.cpp | 15 - .../src/barretenberg/bbapi/bbapi_execute.hpp | 179 ----- .../src/barretenberg/bbapi/bbapi_handlers.cpp | 579 ++++++++++++++++ .../src/barretenberg/bbapi/bbapi_handlers.hpp | 173 +++++ .../src/barretenberg/bbapi/bbapi_schema.cpp | 16 + .../src/barretenberg/bbapi/bbapi_schema.hpp | 9 + .../bbapi/bbapi_ultra_honk_handlers.cpp | 139 ++++ .../barretenberg/bbapi/bbapi_wire_convert.hpp | 296 ++++++++ .../cpp/src/barretenberg/bbapi/c_bind.cpp | 73 +- .../cpp/src/barretenberg/bbapi/c_bind.hpp | 21 +- .../bbapi/c_bind_exception.test.cpp | 113 +-- .../src/barretenberg/benchmark/CMakeLists.txt | 1 - .../benchmark/ipc_bench/CMakeLists.txt | 1 - .../benchmark/ipc_bench/ipc.bench.cpp | 327 --------- .../cpp/src/barretenberg/ipc/CMakeLists.txt | 2 - .../cpp/src/barretenberg/ipc/README.md | 389 ----------- .../cpp/src/barretenberg/ipc/grind_ipc.sh | 17 - .../cpp/src/barretenberg/ipc/ipc_client.cpp | 26 - .../cpp/src/barretenberg/ipc/ipc_client.hpp | 78 --- .../cpp/src/barretenberg/ipc/ipc_server.cpp | 31 - .../cpp/src/barretenberg/ipc/ipc_server.hpp | 214 ------ .../src/barretenberg/ipc/mpsc_shm_client.hpp | 111 --- .../src/barretenberg/ipc/mpsc_shm_server.hpp | 154 ----- .../cpp/src/barretenberg/ipc/shm.test.cpp | 308 --------- .../cpp/src/barretenberg/ipc/shm/README.md | 439 ------------ .../cpp/src/barretenberg/ipc/shm/futex.hpp | 111 --- .../cpp/src/barretenberg/ipc/shm/mpsc_shm.cpp | 384 ----------- .../cpp/src/barretenberg/ipc/shm/mpsc_shm.hpp | 155 ----- .../cpp/src/barretenberg/ipc/shm/spsc_shm.cpp | 545 --------------- .../cpp/src/barretenberg/ipc/shm/spsc_shm.hpp | 180 ----- .../src/barretenberg/ipc/shm/utilities.hpp | 40 -- .../cpp/src/barretenberg/ipc/shm_client.hpp | 108 --- .../cpp/src/barretenberg/ipc/shm_common.hpp | 61 -- .../cpp/src/barretenberg/ipc/shm_server.hpp | 153 ----- .../src/barretenberg/ipc/socket_client.cpp | 119 ---- .../src/barretenberg/ipc/socket_client.hpp | 43 -- .../src/barretenberg/ipc/socket_server.cpp | 573 ---------------- .../src/barretenberg/ipc/socket_server.hpp | 55 -- .../src/barretenberg/nodejs_module/.gitignore | 1 - .../barretenberg/nodejs_module/.yarnrc.yml | 5 - .../barretenberg/nodejs_module/CMakeLists.txt | 35 - .../nodejs_module/init_module.cpp | 15 - .../msgpack_client/msgpack_client_async.cpp | 171 ----- .../msgpack_client/msgpack_client_async.hpp | 94 --- .../msgpack_client/msgpack_client_wrapper.cpp | 113 --- .../msgpack_client/msgpack_client_wrapper.hpp | 39 -- .../barretenberg/nodejs_module/package.json | 15 - .../src/barretenberg/nodejs_module/yarn.lock | 579 ---------------- ...schema.test.cpp => msgpack_check.test.cpp} | 25 +- .../barretenberg/serialize/msgpack_impl.hpp | 72 -- .../serialize/msgpack_impl/func_traits.hpp | 39 -- .../serialize/msgpack_impl/schema_impl.hpp | 220 ------ .../barretenberg/serialize/test_helper.hpp | 21 - barretenberg/rust/.rebuild_patterns | 4 + barretenberg/rust/README.md | 25 +- barretenberg/rust/barretenberg-rs/Cargo.toml | 19 +- .../rust/barretenberg-rs/src/backend.rs | 44 -- .../rust/barretenberg-rs/src/backends/ffi.rs | 163 ----- .../rust/barretenberg-rs/src/backends/pipe.rs | 112 --- .../rust/barretenberg-rs/src/error.rs | 32 - .../rust/barretenberg-rs/src/fr_ext.rs | 53 ++ .../rust/barretenberg-rs/src/legacy.rs | 319 +++++++++ .../rust/barretenberg-rs/src/legacy_pipe.rs | 97 +++ .../rust/barretenberg-rs/src/legacy_types.rs | 152 +++++ barretenberg/rust/barretenberg-rs/src/lib.rs | 97 ++- .../rust/barretenberg-rs/src/types.rs | 53 -- barretenberg/rust/bootstrap.sh | 38 +- barretenberg/rust/scripts/run_test.sh | 6 +- barretenberg/rust/tests/src/debug_msgpack.rs | 5 +- barretenberg/ts/.gitignore | 2 +- barretenberg/ts/.rebuild_patterns | 2 + barretenberg/ts/bb.js/.prettierignore | 4 + barretenberg/ts/bb.js/bootstrap.sh | 4 +- barretenberg/ts/bb.js/eslint.config.js | 4 +- barretenberg/ts/bb.js/package.json | 5 +- barretenberg/ts/bb.js/scripts/copy_cross.sh | 4 +- barretenberg/ts/bb.js/scripts/copy_native.sh | 5 +- barretenberg/ts/bb.js/scripts/generate.sh | 21 + .../ts/bb.js/src/barretenberg/backend.ts | 13 +- .../ts/bb.js/src/barretenberg/index.ts | 13 +- .../barretenberg-threads.wasm.gz | 2 +- .../barretenberg_wasm/barretenberg.wasm.gz | 2 +- .../ts/bb.js/src/bb_backends/index.ts | 8 +- .../ts/bb.js/src/bb_backends/node/index.ts | 26 +- .../bb.js/src/bb_backends/node/native_pipe.ts | 127 ---- .../bb.js/src/bb_backends/node/native_shm.ts | 222 +----- .../src/bb_backends/node/native_shm_async.ts | 274 +------- .../bb_backends/node/native_socket.test.ts | 26 +- .../src/bb_backends/node/native_socket.ts | 444 +----------- barretenberg/ts/bb.js/src/bb_backends/wasm.ts | 4 +- .../src/bbapi/chonk_pinned_inputs.test.ts | 2 +- .../src/bbapi/exception_handling.test.ts | 2 +- barretenberg/ts/bb.js/src/cbind/README.md | 1 - .../ts/bb.js/src/cbind/cpp_codegen.ts | 249 ------- barretenberg/ts/bb.js/src/cbind/generate.ts | 204 ------ barretenberg/ts/bb.js/src/cbind/naming.ts | 31 - .../ts/bb.js/src/cbind/rust_codegen.ts | 563 --------------- .../ts/bb.js/src/cbind/schema_visitor.ts | 243 ------- .../ts/bb.js/src/cbind/typescript_codegen.ts | 447 ------------ .../ts/bb.js/src/{cbind => }/circuit_kind.ts | 0 barretenberg/ts/bb.js/src/index.ts | 15 +- barretenberg/ts/bootstrap.sh | 16 +- barretenberg/ts/yarn.lock | 1 + ipc-codegen/src/generate.ts | 10 + ipc-codegen/src/rust_codegen.ts | 159 ++++- ipc-codegen/src/typescript_codegen.ts | 121 +++- ipc-runtime/cpp/CMakeLists.txt | 4 +- ipc-runtime/cpp/ipc_runtime/c_abi.cpp | 26 + ipc-runtime/cpp/ipc_runtime/c_abi.h | 8 + ipc-runtime/cpp/ipc_runtime/constants.hpp | 2 +- ipc-runtime/cpp/ipc_runtime/ipc_client.cpp | 6 + ipc-runtime/cpp/ipc_runtime/ipc_client.hpp | 5 + ipc-runtime/cpp/ipc_runtime/ipc_server.cpp | 6 + ipc-runtime/cpp/ipc_runtime/ipc_server.hpp | 4 + ipc-runtime/cpp/ipc_runtime/pipe.test.cpp | 316 +++++++++ ipc-runtime/cpp/ipc_runtime/pipe_client.cpp | 212 ++++++ ipc-runtime/cpp/ipc_runtime/pipe_client.hpp | 56 ++ ipc-runtime/cpp/ipc_runtime/pipe_server.cpp | 297 ++++++++ ipc-runtime/cpp/ipc_runtime/pipe_server.hpp | 64 ++ ipc-runtime/cpp/ipc_runtime/serve_helper.cpp | 6 + ipc-runtime/cpp/ipc_runtime/serve_helper.hpp | 4 +- ipc-runtime/cpp/ipc_runtime/socket_client.cpp | 2 +- ipc-runtime/cpp/ipc_runtime/socket_server.cpp | 2 +- ipc-runtime/rust/build.rs | 45 +- ipc-runtime/rust/src/lib.rs | 18 + ipc-runtime/ts/src/index.ts | 1 + ipc-runtime/ts/src/spawned_backend.test.ts | 51 ++ ipc-runtime/ts/src/spawned_backend.ts | 386 ++++++++--- ipc-runtime/ts/src/types.ts | 2 +- ipc-runtime/ts/src/uds_client.ts | 56 +- ipc-runtime/zig/build.zig | 39 +- 151 files changed, 5060 insertions(+), 10106 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bb_schema_embed.hpp.in create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_chonk_handlers.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_execute.hpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.cpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_schema.hpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_ultra_honk_handlers.cpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bbapi_wire_convert.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/CMakeLists.txt delete mode 100644 barretenberg/cpp/src/barretenberg/benchmark/ipc_bench/ipc.bench.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/CMakeLists.txt delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/README.md delete mode 100755 barretenberg/cpp/src/barretenberg/ipc/grind_ipc.sh delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/ipc_client.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/ipc_client.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/ipc_server.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/ipc_server.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_client.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/mpsc_shm_server.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm.test.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/README.md delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/futex.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/mpsc_shm.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/spsc_shm.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm/utilities.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm_client.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm_common.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/shm_server.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/socket_client.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/socket_client.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/socket_server.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/ipc/socket_server.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/.gitignore delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/.yarnrc.yml delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/package.json delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/yarn.lock rename barretenberg/cpp/src/barretenberg/serialize/{msgpack_schema.test.cpp => msgpack_check.test.cpp} (65%) delete mode 100644 barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/func_traits.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp delete mode 100644 barretenberg/rust/barretenberg-rs/src/backend.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/backends/ffi.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/backends/pipe.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/error.rs create mode 100644 barretenberg/rust/barretenberg-rs/src/fr_ext.rs create mode 100644 barretenberg/rust/barretenberg-rs/src/legacy.rs create mode 100644 barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs create mode 100644 barretenberg/rust/barretenberg-rs/src/legacy_types.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/types.rs create mode 100644 barretenberg/ts/bb.js/.prettierignore create mode 100755 barretenberg/ts/bb.js/scripts/generate.sh delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_pipe.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/README.md delete mode 100644 barretenberg/ts/bb.js/src/cbind/cpp_codegen.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/generate.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/naming.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/rust_codegen.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/schema_visitor.ts delete mode 100644 barretenberg/ts/bb.js/src/cbind/typescript_codegen.ts rename barretenberg/ts/bb.js/src/{cbind => }/circuit_kind.ts (100%) create mode 100644 ipc-runtime/cpp/ipc_runtime/pipe.test.cpp create mode 100644 ipc-runtime/cpp/ipc_runtime/pipe_client.cpp create mode 100644 ipc-runtime/cpp/ipc_runtime/pipe_client.hpp create mode 100644 ipc-runtime/cpp/ipc_runtime/pipe_server.cpp create mode 100644 ipc-runtime/cpp/ipc_runtime/pipe_server.hpp 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/.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/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..393f13a8070 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp +++ b/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp @@ -1,23 +1,18 @@ #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/log.hpp" -#include "barretenberg/serialize/msgpack.hpp" #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 +22,102 @@ 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); + server->run_reactor([&handler](int /*client_id*/, + std::span raw, + ipc::IpcServer::Respond respond) { handler(raw, std::move(respond)); }); + + 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..5ed6fd7b687 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, completion-order responses via run_reactor). * - * @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/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/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/README.md b/barretenberg/rust/README.md index 177c295090f..d9e3bbb9714 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")?; @@ -34,7 +36,7 @@ use barretenberg_rs::{BarretenbergApi, backends::FfiBackend}; let backend = FfiBackend::new()?; let mut api = BarretenbergApi::new(backend); -// Same API as PipeBackend +// Same API as the IPC transports let response = api.blake2s(b"hello world")?; println!("Hash: {:?}", response.hash); ``` @@ -46,7 +48,8 @@ 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 @@ -64,7 +67,7 @@ The crate provides a pluggable backend system: │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ -│ PipeBackend │ │ FfiBackend │ +│ IpcClient │ │ FfiBackend │ │ (bb process) │ │ (libbarretenberg)│ └─────────────────┘ └─────────────────┘ ``` @@ -76,7 +79,7 @@ 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 @@ -89,6 +92,6 @@ cd ../ts && yarn 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 diff --git a/barretenberg/rust/barretenberg-rs/Cargo.toml b/barretenberg/rust/barretenberg-rs/Cargo.toml index f637a2a82a5..72ded0456c7 100644 --- a/barretenberg/rust/barretenberg-rs/Cargo.toml +++ b/barretenberg/rust/barretenberg-rs/Cargo.toml @@ -21,21 +21,26 @@ 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", optional = true } # Utilities thiserror.workspace = true -tracing = { workspace = true, optional = true } hex.workspace = true [features] default = ["native", "ffi"] -native = ["tokio", "nix", "tracing"] -async = ["tokio"] # FFI backend - links against libbarretenberg from cpp build ffi = [] +# Transport backends, provided by ipc-runtime. +ipc-runtime = ["dep:ipc-runtime"] +# Pre-migration feature names. `native` used to pull in the in-crate pipe +# backend and its async runtime; the transport now comes from ipc-runtime and +# needs no runtime of its own, so `async` is inert. Kept so existing manifests +# resolve unchanged. +native = ["ipc-runtime"] +async = [] 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..09587a458e8 --- /dev/null +++ b/barretenberg/rust/barretenberg-rs/src/fr_ext.rs @@ -0,0 +1,53 @@ +//! Extra constructors / accessors on the generated `Fr` type that downstream +//! callers (tests, ports of TS helpers) already depend on. 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/legacy.rs b/barretenberg/rust/barretenberg-rs/src/legacy.rs new file mode 100644 index 00000000000..b5b30983480 --- /dev/null +++ b/barretenberg/rust/barretenberg-rs/src/legacy.rs @@ -0,0 +1,319 @@ +//! Back-compat shim mirroring the pre-codegen `BarretenbergApi` surface. +//! +//! The codegen migration replaced loose `&[u8]` / `Vec>` scalar +//! parameters with typed newtypes (`Fr`, `Fq`, `Secp256k1Fr`, ...). External +//! consumers were already depending on the old surface, so this shim +//! preserves it: callers that did +//! +//! ```ignore +//! use barretenberg_rs::{BarretenbergApi, FfiBackend}; +//! let mut api = BarretenbergApi::new(FfiBackend::new()?); +//! api.schnorr_compute_public_key(&private_key_bytes)?; +//! ``` +//! +//! continue to compile against this crate while they migrate to the new +//! [`crate::BbApi`] surface (typed scalars, `Vec` for hash inputs, +//! etc.). +//! +//! Wire format is identical — only the Rust call surface changed. Methods +//! whose signature did not change reach `BbApi` through `Deref` (no +//! explicit wrapper here). + +#![allow(deprecated)] + +use std::ops::{Deref, DerefMut}; + +use crate::generated::backend::Backend; +use crate::generated::bb_client::BbApi; +use crate::generated::bb_types::{ + AesDecryptResponse, + AesEncryptResponse, + Bn254FqSqrtResponse, + Bn254FrSqrtResponse, + Bn254G1MulResponse, + Bn254G2MulResponse, + EcdsaSecp256k1ComputePublicKeyResponse, + EcdsaSecp256k1ConstructSignatureResponse, + EcdsaSecp256r1ComputePublicKeyResponse, + EcdsaSecp256r1ConstructSignatureResponse, + Fq, + Fr, + GrumpkinAddResponse, + GrumpkinBatchMulResponse, + GrumpkinMulResponse, + PedersenCommitResponse, + PedersenHashResponse, + Poseidon2HashResponse, + Poseidon2PermutationResponse, + EcdsaSecp256k1VerifySignatureResponse, + SchnorrComputePublicKeyResponse, + SchnorrConstructSignatureResponse, + SchnorrVerifySignatureResponse, + Secp256k1Fr, + Secp256k1MulResponse, + Secp256r1Fr, +}; +use crate::generated::bb_types as wire; +use crate::generated::error::Result; +#[allow(deprecated)] +use crate::legacy_types::{Bn254G1Point, Bn254G2Point, GrumpkinPoint, Secp256k1Point}; + +/// Deprecated alias for [`crate::BbApi`] preserving the pre-migration call +/// surface (`&[u8]` scalars, `Vec>` hash inputs). Forwards unchanged +/// methods to `BbApi` via `Deref`; overrides methods whose signature changed. +#[deprecated( + note = "use `BbApi` directly; typed scalars (Fr/Fq/Secp256k1Fr) replace raw `&[u8]` parameters" +)] +pub struct BarretenbergApi(BbApi); + +impl BarretenbergApi { + pub fn new(backend: B) -> Self { + Self(BbApi::new(backend)) + } + + pub fn into_inner(self) -> BbApi { + self.0 + } +} + +impl Deref for BarretenbergApi { + type Target = BbApi; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BarretenbergApi { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +fn to_fr_array(s: &[u8]) -> Fr { + let arr: [u8; 32] = s.try_into().expect("expected 32-byte scalar"); + Fr::from_be_bytes(arr) +} + +fn to_fq_array(s: &[u8]) -> Fq { + let arr: [u8; 32] = s.try_into().expect("expected 32-byte scalar"); + Fq::from_bytes(arr) +} + +fn to_secp256k1_fr(s: &[u8]) -> Secp256k1Fr { + let arr: [u8; 32] = s.try_into().expect("expected 32-byte secp256k1 scalar"); + Secp256k1Fr::from_bytes(arr) +} + +fn to_secp256r1_fr(s: &[u8]) -> Secp256r1Fr { + let arr: [u8; 32] = s.try_into().expect("expected 32-byte secp256r1 scalar"); + Secp256r1Fr::from_bytes(arr) +} + +fn to_16_array(s: &[u8]) -> [u8; 16] { + s.try_into().expect("expected 16-byte value") +} + +fn to_32_array(s: &[u8]) -> [u8; 32] { + s.try_into().expect("expected 32-byte value") +} + +fn fr_vec(inputs: Vec>) -> Vec { + inputs.into_iter().map(|b| to_fr_array(&b)).collect() +} + +// Old-surface methods. These shadow the same-named methods reached through +// `Deref`, so callers picking up `BarretenbergApi` get the legacy signature. +#[allow(deprecated)] +impl BarretenbergApi { + pub fn poseidon2_hash(&mut self, inputs: Vec>) -> Result { + self.0.poseidon2_hash(fr_vec(inputs)) + } + + pub fn poseidon2_permutation( + &mut self, + inputs: [Vec; 4], + ) -> Result { + let typed: [Fr; 4] = inputs.map(|b| to_fr_array(&b)); + self.0.poseidon2_permutation(typed) + } + + pub fn pedersen_commit( + &mut self, + inputs: Vec>, + hash_index: u32, + ) -> Result { + self.0.pedersen_commit(fr_vec(inputs), hash_index) + } + + pub fn pedersen_hash( + &mut self, + inputs: Vec>, + hash_index: u32, + ) -> Result { + self.0.pedersen_hash(fr_vec(inputs), hash_index) + } + + pub fn grumpkin_mul( + &mut self, + point: impl Into, + scalar: &[u8], + ) -> Result { + self.0.grumpkin_mul(point.into(), to_fq_array(scalar)) + } + + pub fn grumpkin_add( + &mut self, + a: impl Into, + b: impl Into, + ) -> Result { + self.0.grumpkin_add(a.into(), b.into()) + } + + pub fn grumpkin_batch_mul( + &mut self, + points: Vec>, + scalar: &[u8], + ) -> Result { + self.0.grumpkin_batch_mul(points.into_iter().map(Into::into).collect(), to_fq_array(scalar)) + } + + pub fn secp256k1_mul( + &mut self, + point: impl Into, + scalar: &[u8], + ) -> Result { + self.0.secp256k1_mul(point.into(), to_secp256k1_fr(scalar)) + } + + pub fn bn254_fr_sqrt(&mut self, input: &[u8]) -> Result { + self.0.bn254_fr_sqrt(to_fr_array(input)) + } + + pub fn bn254_fq_sqrt(&mut self, input: &[u8]) -> Result { + self.0.bn254_fq_sqrt(to_fq_array(input)) + } + + pub fn bn254_g1_mul( + &mut self, + point: impl Into, + scalar: &[u8], + ) -> Result { + self.0.bn254_g1_mul(point.into(), to_fr_array(scalar)) + } + + pub fn bn254_g2_mul( + &mut self, + point: impl Into, + scalar: &[u8], + ) -> Result { + self.0.bn254_g2_mul(point.into(), to_fr_array(scalar)) + } + + pub fn schnorr_compute_public_key( + &mut self, + private_key: &[u8], + ) -> Result { + Ok(self.0.schnorr_compute_public_key(to_fq_array(private_key))?.into()) + } + + pub fn schnorr_construct_signature( + &mut self, + message: &[u8], + private_key: &[u8], + ) -> Result { + self.0 + .schnorr_construct_signature(to_fr_array(message), to_fq_array(private_key)) + } + + pub fn ecdsa_secp256k1_compute_public_key( + &mut self, + private_key: &[u8], + ) -> Result { + Ok(self + .0 + .ecdsa_secp256k1_compute_public_key(to_secp256k1_fr(private_key))? + .into()) + } + + pub fn ecdsa_secp256r1_compute_public_key( + &mut self, + private_key: &[u8], + ) -> Result { + Ok(self + .0 + .ecdsa_secp256r1_compute_public_key(to_secp256r1_fr(private_key))? + .into()) + } + + pub fn ecdsa_secp256k1_construct_signature( + &mut self, + message: &[u8], + private_key: &[u8], + ) -> Result { + self.0 + .ecdsa_secp256k1_construct_signature(message, to_secp256k1_fr(private_key)) + } + + pub fn ecdsa_secp256r1_construct_signature( + &mut self, + message: &[u8], + private_key: &[u8], + ) -> Result { + self.0 + .ecdsa_secp256r1_construct_signature(message, to_secp256r1_fr(private_key)) + } + pub fn aes_encrypt( + &mut self, + plaintext: &[u8], + iv: &[u8], + key: &[u8], + length: u32, + ) -> Result { + self.0 + .aes_encrypt(plaintext, to_16_array(iv), to_16_array(key), length) + } + + pub fn aes_decrypt( + &mut self, + ciphertext: &[u8], + iv: &[u8], + key: &[u8], + length: u32, + ) -> Result { + self.0 + .aes_decrypt(ciphertext, to_16_array(iv), to_16_array(key), length) + } + + pub fn schnorr_verify_signature( + &mut self, + message: &[u8], + public_key: impl Into, + s: &[u8], + e: &[u8], + ) -> Result { + self.0.schnorr_verify_signature( + to_fr_array(message), + public_key.into(), + to_fq_array(s), + to_fq_array(e), + ) + } + + pub fn ecdsa_secp256k1_verify_signature( + &mut self, + message: &[u8], + public_key: impl Into, + r: &[u8], + s: &[u8], + v: u8, + ) -> Result { + self.0.ecdsa_secp256k1_verify_signature( + message, + public_key.into(), + to_32_array(r), + to_32_array(s), + v, + ) + } + +} diff --git a/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs b/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs new file mode 100644 index 00000000000..99643afb6ec --- /dev/null +++ b/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs @@ -0,0 +1,97 @@ +//! Back-compat `PipeBackend`, preserving the pre-migration constructor. +//! +//! The transport itself now lives in ipc-runtime: this spawns bb the way it +//! always did and drives the connection through `ipc_runtime::IpcClient`'s +//! pipe client, so no framing or fd handling is duplicated here. + +use std::path::Path; +use std::process::{Child, Command, Stdio}; + +use crate::generated::backend::Backend; +use crate::generated::error::{IpcError, Result}; + +/// Deprecated: spawns bb and talks to it over its stdin/stdout. +/// +/// Prefer driving [`ipc_runtime::IpcClient`] directly — it implements +/// [`Backend`] and also offers the UDS and shared-memory transports. +#[deprecated(note = "use ipc_runtime::IpcClient (from_fds / from_path) as the Backend")] +pub struct PipeBackend { + client: ipc_runtime::IpcClient, + process: Option, +} + +#[allow(deprecated)] +impl PipeBackend { + /// Spawn bb in msgpack mode and connect over its stdio pipes. + pub fn new(bb_binary_path: impl AsRef, threads: Option) -> Result { + use std::os::fd::AsRawFd; + + let mut cmd = Command::new(bb_binary_path.as_ref()); + cmd.arg("msgpack") + .arg("run") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + // bb reads thread count from the environment. + if let Some(t) = threads { + cmd.env("HARDWARE_CONCURRENCY", t.to_string()); + } + + let mut process = cmd + .spawn() + .map_err(|e| IpcError::Backend(format!("Failed to spawn BB process: {e}")))?; + + let out_fd = process + .stdin + .as_ref() + .ok_or_else(|| IpcError::Backend("Failed to get stdin handle".to_string()))? + .as_raw_fd(); + let in_fd = process + .stdout + .as_ref() + .ok_or_else(|| IpcError::Backend("Failed to get stdout handle".to_string()))? + .as_raw_fd(); + + if let Ok(Some(status)) = process.try_wait() { + return Err(IpcError::Backend(format!( + "BB process exited immediately with status: {status}" + ))); + } + + // SAFETY: both descriptors belong to `process`, which this struct owns + // for as long as the client lives. The client duplicates them, so the + // two sides close independently. + let client = unsafe { ipc_runtime::IpcClient::from_fds(in_fd, out_fd) } + .map_err(|e| IpcError::Backend(format!("Failed to connect to BB process: {e}")))?; + + Ok(Self { + client, + process: Some(process), + }) + } +} + +#[allow(deprecated)] +impl Backend for PipeBackend { + fn call(&mut self, input: &[u8]) -> Result> { + self.client + .call(input) + .map_err(|e| IpcError::Backend(e.to_string())) + } + + fn destroy(&mut self) -> Result<()> { + if let Some(mut process) = self.process.take() { + let _ = process.kill(); + let _ = process.wait(); + } + Ok(()) + } +} + +#[allow(deprecated)] +impl Drop for PipeBackend { + fn drop(&mut self) { + let _ = self.destroy(); + } +} diff --git a/barretenberg/rust/barretenberg-rs/src/legacy_types.rs b/barretenberg/rust/barretenberg-rs/src/legacy_types.rs new file mode 100644 index 00000000000..ea0d531d903 --- /dev/null +++ b/barretenberg/rust/barretenberg-rs/src/legacy_types.rs @@ -0,0 +1,152 @@ +//! Pre-codegen value types, kept so existing callers compile unchanged. +//! +//! Codegen models 32-byte scalars as `Bin32` and wire structs as holding it. +//! The types here keep the older `[u8; 32]` / `Vec` shapes and convert at +//! the API boundary in [`crate::legacy`]. New code should use the generated +//! types directly. + +use crate::generated::bb_types as wire; + +/// Deprecated: a 32-byte field element. Prefer the generated `Fr`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Fr(pub [u8; 32]); + +impl Fr { + /// 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) + } + + pub fn from_be_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + pub fn from_le_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// 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) + } + + /// Truncates or zero-pads to 32 bytes. + 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) + } + + pub fn to_buffer(&self) -> Vec { + self.0.to_vec() + } +} + +impl From for wire::Bin32 { + fn from(value: Fr) -> Self { + wire::Bin32(value.0) + } +} + +impl From for Fr { + fn from(value: wire::Bin32) -> Self { + Self(value.0) + } +} + +/// Scalars arrive from callers as loose bytes; the wire type is fixed size. +fn to_bin32(bytes: &[u8]) -> wire::Bin32 { + wire::Bin32(bytes.try_into().expect("expected a 32-byte scalar")) +} + +macro_rules! legacy_point { + ($name:ident, $wire:ident, $doc:literal) => { + #[doc = $doc] + #[derive(Debug, Clone, PartialEq, Eq, Default)] + pub struct $name { + pub x: Vec, + pub y: Vec, + } + + impl From<$name> for wire::$wire { + fn from(value: $name) -> Self { + wire::$wire { + x: to_bin32(&value.x), + y: to_bin32(&value.y), + } + } + } + + impl From for $name { + fn from(value: wire::$wire) -> Self { + Self { + x: value.x.0.to_vec(), + y: value.y.0.to_vec(), + } + } + } + }; +} + +legacy_point!(GrumpkinPoint, GrumpkinPoint, "Deprecated: byte-valued Grumpkin point."); +legacy_point!(Bn254G1Point, Bn254G1Point, "Deprecated: byte-valued BN254 G1 point."); +legacy_point!(Secp256k1Point, Secp256k1Point, "Deprecated: byte-valued secp256k1 point."); +legacy_point!(Secp256r1Point, Secp256r1Point, "Deprecated: byte-valued secp256r1 point."); + +/// Deprecated: byte-valued BN254 G2 point (coordinates are Fq pairs). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Bn254G2Point { + pub x: [Vec; 2], + pub y: [Vec; 2], +} + +impl From for wire::Bn254G2Point { + fn from(value: Bn254G2Point) -> Self { + wire::Bn254G2Point { + x: value.x.map(|c| to_bin32(&c)), + y: value.y.map(|c| to_bin32(&c)), + } + } +} + +/// Deprecated: byte-valued responses that carry a point, so callers can keep +/// destructuring `.x` / `.y` as `Vec`. +macro_rules! legacy_point_response { + ($name:ident, $wire:ident, $field:ident, $point:ident) => { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct $name { + pub $field: $point, + } + + impl From for $name { + fn from(value: wire::$wire) -> Self { + Self { + $field: value.$field.into(), + } + } + } + }; +} + +legacy_point_response!( + EcdsaSecp256k1ComputePublicKeyResponse, + EcdsaSecp256k1ComputePublicKeyResponse, + public_key, + Secp256k1Point +); +legacy_point_response!( + EcdsaSecp256r1ComputePublicKeyResponse, + EcdsaSecp256r1ComputePublicKeyResponse, + public_key, + Secp256r1Point +); +legacy_point_response!( + SchnorrComputePublicKeyResponse, + SchnorrComputePublicKeyResponse, + public_key, + GrumpkinPoint +); diff --git a/barretenberg/rust/barretenberg-rs/src/lib.rs b/barretenberg/rust/barretenberg-rs/src/lib.rs index ffccd08c514..949400e0f51 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")?; +//! let response = api.blake2s(b"hello world".to_vec())?; //! println!("Hash: {:?}", response.hash); -//! -//! // Cleanup -//! api.destroy()?; //! ``` //! //! ## Custom Backend @@ -46,30 +45,66 @@ //! } //! ``` -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; -// Generated types from msgpack schema -// Run: cd ../ts && yarn generate -pub mod generated_types; + #[cfg(feature = "ffi")] + pub mod ffi_backend; +} -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}; +mod fr_ext; +pub mod legacy; +pub mod legacy_types; +#[cfg(feature = "ipc-runtime")] +pub mod legacy_pipe; -/// Backend implementations -pub mod backends { - #[cfg(feature = "native")] - pub mod pipe; - #[cfg(feature = "native")] - pub use pipe::PipeBackend; +pub use generated::backend::Backend; +pub use generated::bb_client::BbApi; +pub use generated::bb_types::{Bin32, Command, Response}; +// Pre-codegen value shapes: byte-valued points and a constructible Fr. The +// generated equivalents remain available as `generated::bb_types::*`. +#[allow(deprecated)] +pub use legacy_types::{ + Bn254G1Point, Bn254G2Point, Fr, GrumpkinPoint, Secp256k1Point, Secp256r1Point, +}; +pub use generated::error::{IpcError as BarretenbergError, Result}; +// Pre-codegen surface kept around so external consumers can migrate at their +// own pace; see [`legacy`] for the deprecation notes and the typed-scalar +// replacements on [`BbApi`]. +#[allow(deprecated)] +pub use legacy::BarretenbergApi; + +// Preserved module path for callers that imported types via +// `barretenberg_rs::generated_types::*`. Explicit re-exports shadow the glob, +// so the byte-valued shapes win for the types whose fields changed. +pub mod generated_types { + pub use crate::generated::bb_types::*; + #[allow(deprecated)] + pub use crate::legacy_types::{ + Bn254G1Point, Bn254G2Point, Fr, GrumpkinPoint, Secp256k1Point, Secp256r1Point, + }; +} + +#[cfg(feature = "ffi")] +pub use generated::ffi_backend::FfiBackend; + +// Pre-codegen module path. +pub mod backends { #[cfg(feature = "ffi")] - pub mod ffi; - #[cfg(feature = "ffi")] - pub use ffi::FfiBackend; + pub use crate::generated::ffi_backend::FfiBackend; + #[allow(deprecated)] + #[cfg(feature = "ipc-runtime")] + pub use crate::legacy_pipe::PipeBackend; } + +// 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..42c05dc8720 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 @@ -58,9 +74,9 @@ 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 # Check if this version is already published on crates.io (idempotent re-runs). 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/src/debug_msgpack.rs b/barretenberg/rust/tests/src/debug_msgpack.rs index 5ab317a5d8b..f0da98a91b1 100644 --- a/barretenberg/rust/tests/src/debug_msgpack.rs +++ b/barretenberg/rust/tests/src/debug_msgpack.rs @@ -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/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_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/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/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..571793f8c41 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'; 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/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..1a1459d1c64 100644 --- a/ipc-runtime/cpp/ipc_runtime/constants.hpp +++ b/ipc-runtime/cpp/ipc_runtime/constants.hpp @@ -19,7 +19,7 @@ namespace ipc { * connection is closed (sockets) or the ring is declared corrupt (SHM), * instead of allocating/awaiting the claimed size. */ -inline constexpr uint32_t MAX_FRAME_SIZE = 256U * 1024 * 1024; // 256 MiB +inline constexpr uint32_t MAX_FRAME_SIZE = 1024U * 1024 * 1024; // 1 GiB /** * 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..4077acad15a 100644 --- a/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp +++ b/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp @@ -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/pipe.test.cpp b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp new file mode 100644 index 00000000000..cce8b70fbd8 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp @@ -0,0 +1,316 @@ +#include "ipc_runtime/ipc_client.hpp" +#include "ipc_runtime/ipc_server.hpp" +#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(); +} + +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..438089fa626 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp @@ -0,0 +1,212 @@ +#include "ipc_runtime/pipe_client.hpp" +#include "ipc_runtime/constants.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) { + 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 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) { + 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); + + // 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)); + } + + if (read_exact(recv_buffer_.data(), msg_len, 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..3cd32577952 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp @@ -0,0 +1,297 @@ +#include "ipc_runtime/pipe_server.hpp" +#include "ipc_runtime/constants.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 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 — treat as a fatal desync rather than misparse. + if (msg_len > MAX_FRAME_SIZE || 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); + + if (recv_buffer_.size() < msg_len || recv_buffer_.empty()) { + // Keep at least one byte so data() is non-null for zero-length messages + // (null data() signals failure). + recv_buffer_.resize(std::max(msg_len, 1)); + } + + total_read = 0; + while (total_read < msg_len) { + ssize_t n = ::read(in_fd_, recv_buffer_.data() + total_read, msg_len - total_read); + if (n < 0) { + if (errno == EINTR) { + continue; // Interrupted, retry + } + disconnect(); + return {}; + } + if (n == 0) { + // Peer closed mid-message. + disconnect(); + return {}; + } + total_read += static_cast(n); + } + + 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) { + 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..d62741ae55a 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace ipc { @@ -16,6 +17,11 @@ 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 == "-") { + return IpcServer::create_pipe(STDIN_FILENO, STDOUT_FILENO); + } 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..c6d41ab37fb 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp @@ -35,7 +35,9 @@ struct ServerOptions { /** * @brief Construct an IpcServer based on the input path's suffix. * - * Recognised suffixes: + * Recognised inputs: + * - "-" → IpcServer::create_pipe(STDIN_FILENO, STDOUT_FILENO) — serve the + * process's own stdio (parent spawned us with piped stdio) * - "*.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/socket_client.cpp b/ipc-runtime/cpp/ipc_runtime/socket_client.cpp index 3adde5e9f04..8af262a736a 100644 --- a/ipc-runtime/cpp/ipc_runtime/socket_client.cpp +++ b/ipc-runtime/cpp/ipc_runtime/socket_client.cpp @@ -181,7 +181,7 @@ 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). diff --git a/ipc-runtime/cpp/ipc_runtime/socket_server.cpp b/ipc-runtime/cpp/ipc_runtime/socket_server.cpp index e284425551a..b24ddc3991a 100644 --- a/ipc-runtime/cpp/ipc_runtime/socket_server.cpp +++ b/ipc-runtime/cpp/ipc_runtime/socket_server.cpp @@ -254,7 +254,7 @@ 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; diff --git a/ipc-runtime/rust/build.rs b/ipc-runtime/rust/build.rs index c4cd25a5b30..de6cde60365 100644 --- a/ipc-runtime/rust/build.rs +++ b/ipc-runtime/rust/build.rs @@ -7,24 +7,40 @@ // 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"); 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 +49,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/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/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..0744b5a7d89 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 } 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,119 @@ 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. */ @@ -206,6 +341,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 +370,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 +395,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 +409,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 +427,17 @@ 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), 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 +484,7 @@ export class SpawnedProcessBackend implements IpcClientAsync { } } - private async connectClient(): Promise { + private async connectClient(signal?: AbortSignal): Promise { const { options } = this; const timeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS; if (options.transport === "uds") { @@ -396,13 +495,15 @@ export class SpawnedProcessBackend implements IpcClientAsync { // wait with its real exit cause. return await UdsIpcClient.connect(this.ipcPath, { connectTimeoutMs: timeoutMs, + unref: options.unref, + signal, }); } // 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, @@ -443,18 +544,151 @@ 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..6771954e702 100644 --- a/ipc-runtime/ts/src/types.ts +++ b/ipc-runtime/ts/src/types.ts @@ -20,7 +20,7 @@ export interface IpcClientSync { * than this is treated as corruption and the connection is closed instead * of buffering the claimed size. */ -export const MAX_FRAME_SIZE = 256 * 1024 * 1024; // 256 MiB +export const MAX_FRAME_SIZE = 1024 * 1024 * 1024; // 1 GiB /** * Total budget (ms) for connect() retry loops, covering the window where diff --git a/ipc-runtime/ts/src/uds_client.ts b/ipc-runtime/ts/src/uds_client.ts index 4c617357222..2bdc09fdc64 100644 --- a/ipc-runtime/ts/src/uds_client.ts +++ b/ipc-runtime/ts/src/uds_client.ts @@ -12,7 +12,11 @@ interface PendingCall { } 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 +24,13 @@ 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; } /** @@ -42,7 +53,10 @@ export class UdsIpcClient implements IpcClientAsync { /** 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, + ) { conn.on("data", (chunk) => this.onData(chunk)); conn.on("error", (err) => this.failAll( @@ -63,10 +77,11 @@ 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); } /** Number of in-flight calls awaiting a response. */ @@ -88,8 +103,19 @@ export class UdsIpcClient implements IpcClientAsync { "UdsIpcClient: call() on a closed/errored socket", ); } + // The peer rejects an oversized frame by closing the connection, which + // reaches the caller as an unexplained EPIPE on the next write. Fail here + // instead, naming the size that was refused. + if (input.length + 8 > MAX_FRAME_SIZE) { + throw new IpcTransportError( + `UdsIpcClient: request of ${input.length} bytes exceeds MAX_FRAME_SIZE (${MAX_FRAME_SIZE})`, + ); + } return new Promise((resolve, reject) => { const requestId = this.nextRequestId++; + if (this.idleUnref && this.pending.size === 0) { + this.conn.ref(); + } this.pending.set(requestId, { resolve, reject }); const header = Buffer.allocUnsafe(12); header.writeUInt32LE(input.length + 8, 0); // length counts id + payload @@ -143,6 +169,9 @@ export class UdsIpcClient implements IpcClientAsync { const next = this.pending.get(requestId); if (next) { this.pending.delete(requestId); + if (this.idleUnref && this.pending.size === 0) { + this.conn.unref(); + } next.resolve(new Uint8Array(payload)); } else { // A response that pairs with no pending call means the stream's @@ -180,11 +209,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 +243,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 +281,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/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"); +} From e3fd5344f3bd142ed1f1b14c7b6aa17965946de6 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:43:47 +0000 Subject: [PATCH 02/19] test(bb-docs): budget the recursive example for running alongside a full bb build It takes ~35s idle but exceeded its 360s jest timeout (408s) when the lane ran concurrently with a full bb rebuild. It uses the published bb.js, so the slowdown is load, not the tree: raise the per-test timeout to 20 minutes and the lane's to 25. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- barretenberg/docs/bootstrap.sh | 10 ++++++---- barretenberg/docs/examples/recursive.test.ts | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) 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); }); From 5cd3ac0eff75c31b4d8973ebb8022adb68103a85 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:19:53 +0000 Subject: [PATCH 03/19] perf(ipc): conditional SHM wakes made safe, and bb served by the serial run() loop The shared-memory transport woke the other side with a futex syscall on every publish and release, about six per bb request, which roughly doubled the pipelined poseidon benchmark against next. Those wakes were made unconditional to fix an intermittent hang, whose cause turned out to be a startup race rather than the conditional wake itself: segments are mappable from ftruncate, before create() initialises them, so a fast client could attach, set its blocked flag on a response ring, and have the server's initialisation reset it; the next publish then skipped the wake and the client slept on published data forever. connect() now refuses a segment until it is fully initialised. create() writes the ring capacity, and the doorbell's num_slots, last with release ordering, and a zero value means not ready: SpscShm::connect and MpscProducer::connect throw and the existing connect-retry loops try again. With that in place the conditional wake is restored on the rings, the doorbell and the reactor's notify(). bb also served every request through run_reactor, paying a request copy, a heap-allocated respond closure, a locked completion queue and a notify() (a self-pipe write on sockets) per request, although every bb handler responds before returning. It now uses the serial run() loop, asserting that. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- .../cpp/src/barretenberg/api/api_msgpack.cpp | 14 +++- .../cpp/src/barretenberg/api/api_msgpack.hpp | 2 +- ipc-runtime/cpp/ipc_runtime/ipc_server.hpp | 4 +- ipc-runtime/cpp/ipc_runtime/shm.test.cpp | 40 +++++++++++ ipc-runtime/cpp/ipc_runtime/shm/README.md | 20 +++--- ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp | 45 ++++++++++--- ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp | 10 +-- ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp | 67 +++++++++++-------- ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.hpp | 6 +- 9 files changed, 148 insertions(+), 60 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp b/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp index 393f13a8070..eefdf533090 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp +++ b/barretenberg/cpp/src/barretenberg/api/api_msgpack.cpp @@ -2,10 +2,12 @@ #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 #include #include +#include #include #include #include @@ -102,9 +104,15 @@ int execute_msgpack_run(const std::string& msgpack_input_file, // sequences (ChonkStart/Load/Accumulate/Prove) share IVC state. bb::bbapi::BBApiRequest request; auto handler = bb::bbapi::make_bb_handler(request); - server->run_reactor([&handler](int /*client_id*/, - std::span raw, - ipc::IpcServer::Respond respond) { handler(raw, std::move(respond)); }); + // 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; diff --git a/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp b/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp index 5ed6fd7b687..aa4d0fde9a7 100644 --- a/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp +++ b/barretenberg/cpp/src/barretenberg/api/api_msgpack.hpp @@ -30,7 +30,7 @@ int process_msgpack_commands(std::istream& input_stream); * - existing file → offline replay of bare length-prefixed commands * * All live transports use the shared ipc-runtime server (request-id envelope - * framing, completion-order responses via run_reactor). + * framing), served by its serial run() loop: every bb handler responds inline. * * @param msgpack_input_file Input path as above * @param max_clients Maximum concurrent clients for IPC servers diff --git a/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp b/ipc-runtime/cpp/ipc_runtime/ipc_server.hpp index 4077acad15a..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() {} 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..f2c9600bcca 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. `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..275aec2bd1f 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; @@ -236,10 +239,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 +263,11 @@ 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); 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 +327,11 @@ 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. + if (doorbell_->consumer_blocked.load(std::memory_order_acquire)) { + futex_wake(reinterpret_cast(&doorbell_->seq), 1); + } } // ----- MpscProducer Implementation ----- @@ -401,6 +411,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 +425,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 +456,12 @@ 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); + + if (doorbell_->consumer_blocked.load(std::memory_order_acquire)) { + 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..3065a944873 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,11 @@ 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); + 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, uint64_t timeout_ns) @@ -314,9 +321,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); + 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, uint64_t timeout_ns) @@ -403,21 +412,20 @@ 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); + 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 +516,20 @@ 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); + 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 From 84c1b0efed41bb9823cfec4505c9e33e6b8bd351 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:30:24 +0000 Subject: [PATCH 04/19] perf(ipc-runtime): skip the async hop in SpawnedProcessBackend.call, and stop the MPSC spin reading the clock every iteration Pipelined bb.js calls over shared memory were still ~50% slower than next on the poseidon benchmark after the futex fix. Layer by layer, the transport itself was at parity; the extra cost was main-thread JavaScript per call, which left the main thread saturated so the API's own encode/decode no longer overlapped with bb's work. Most of it was SpawnedProcessBackend.call: an async function awaiting ensureUp() before every call. A live process now takes a direct path with no async frame; only a missing or dead process goes through ensureUp(). On the benchmark's pattern (the first pipelined burst after 10k sequential calls) this takes the fixed build from 17.3 to 13.4 us/call (min of 8, next 14.1). MpscConsumer::wait_for_data's spin also called clock_gettime on every iteration, about a tenth of bb's instructions while serving; it now checks the clock every 256 iterations, as SpscShm's spin does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp | 15 ++++++++++++--- ipc-runtime/ts/src/spawned_backend.ts | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp index 275aec2bd1f..47bbebc766a 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp @@ -205,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) { @@ -219,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++) { diff --git a/ipc-runtime/ts/src/spawned_backend.ts b/ipc-runtime/ts/src/spawned_backend.ts index 0744b5a7d89..d08a1dcef89 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -251,7 +251,20 @@ export class SpawnedProcessBackend implements IpcClientAsync { return this.ipcPath; } - async call(input: Uint8Array): Promise { + call(input: Uint8Array): Promise { + // Fast path for a live process: no async frame and no await on + // ensureUp(). This runs once per call, and a pipelined caller issuing + // thousands of calls feels every extra promise hop. + const incarnation = this.current; + if (incarnation !== undefined && !this.destroying) { + return incarnation.client.call(input).catch(async (err) => { + throw await this.attributeCallError(incarnation, err); + }); + } + return this.callWithRespawn(input); + } + + private async callWithRespawn(input: Uint8Array): Promise { const incarnation = await this.ensureUp(); try { return await incarnation.client.call(input); From 528c6dc4e3015e42eb8be24630b1f667d93c0d78 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:45:21 +0000 Subject: [PATCH 05/19] perf(ipc-runtime): pair SHM async responses through an in-order queue with small-int ids NapiShmAsyncClient kept pending calls in a Map. Each call allocated two heap BigInts (the id and the one napi hands back) and hashed them three times. Under jest, where the main thread saturates during a pipelined burst, that overhead was directly visible in bb.js's poseidon bench. Ids are now numbers below 2^30, so V8 keeps them as small integers. Pending calls sit in an issue-order queue: in-order servers (bb's serial run() loop) always match the head, and out-of-order responses fall back to a search. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/cpp/napi/msgpack_client_async.cpp | 14 ++--- ipc-runtime/ts/src/shm_client.test.ts | 10 +-- ipc-runtime/ts/src/shm_client.ts | 62 ++++++++++++------- 3 files changed, 49 insertions(+), 37 deletions(-) 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/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..230b9b70cf8 100644 --- a/ipc-runtime/ts/src/shm_client.ts +++ b/ipc-runtime/ts/src/shm_client.ts @@ -24,8 +24,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. */ @@ -50,41 +50,45 @@ export class NapiShmSyncClient implements IpcClientSync { } interface PendingCallback { + requestId: number; resolve: (data: Uint8Array) => void; reject: (error: Error) => void; } +/** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */ +const REQUEST_ID_SPACE = 2 ** 30; + /** * 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. Pending calls are kept in issue order; 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. Servers that answer in order (e.g. a serial `run()` loop) always + * match the head of the queue, 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 per client so a stale frame left in a recycled SHM ring slot + * by a previous occupant is unlikely to pair with a live call. * * `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 pending: PendingCallback[] = []; + private nextRequestId = Math.floor(Math.random() * REQUEST_ID_SPACE); private destroyed = false; constructor(private inner: NapiMsgpackClientAsync) { - this.inner.setResponseCallback((requestId: bigint, response: Buffer) => { + 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.takePending(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 { @@ -102,6 +106,14 @@ export class NapiShmAsyncClient implements IpcClientAsync { }); } + private takePending(requestId: number): PendingCallback | undefined { + if (this.pending.length > 0 && this.pending[0].requestId === requestId) { + return this.pending.shift(); + } + const i = this.pending.findIndex((p) => p.requestId === requestId); + return i === -1 ? undefined : this.pending.splice(i, 1)[0]; + } + call(input: Uint8Array): Promise { if (this.destroyed) { return Promise.reject( @@ -112,17 +124,18 @@ export class NapiShmAsyncClient implements IpcClientAsync { ? input : Buffer.from(input.buffer, input.byteOffset, input.byteLength); return new Promise((resolve, reject) => { - const requestId = this.nextRequestId++; - if (this.pending.size === 0) { + const requestId = this.nextRequestId; + this.nextRequestId = (requestId + 1) % REQUEST_ID_SPACE; + if (this.pending.length === 0) { this.inner.acquire(); } - this.pending.set(requestId, { resolve, reject }); + this.pending.push({ requestId, 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( @@ -141,12 +154,13 @@ 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()) { + const pending = this.pending; + this.pending = []; + for (const cb of pending) { 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(); } From 88ca7b71f43daf727aa5a1722b534b5f517a2ce7 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:57:08 +0000 Subject: [PATCH 06/19] perf(ipc-runtime): attribute call errors inside the client, not with a per-call .catch SpawnedProcessBackend.call chained .catch(attributeCallError) onto every call, so each successful call paid for an extra promise and microtask. In a pipelined burst under jest that was ~0.8us/call of main-thread CPU in steady state and ~2us in the first burst. The SHM and UDS clients now take an optional mapError hook, applied only on their rejection paths. SpawnedProcessBackend passes attributeCallError for the incarnation when it connects, and the fast path returns the client's promise directly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/ts/src/shm_client.ts | 32 +++++++++++++---- ipc-runtime/ts/src/spawned_backend.ts | 50 ++++++++++++++++----------- ipc-runtime/ts/src/types.ts | 7 ++++ ipc-runtime/ts/src/uds_client.ts | 36 ++++++++++++++----- 4 files changed, 89 insertions(+), 36 deletions(-) diff --git a/ipc-runtime/ts/src/shm_client.ts b/ipc-runtime/ts/src/shm_client.ts index 230b9b70cf8..1ce925fcd9f 100644 --- a/ipc-runtime/ts/src/shm_client.ts +++ b/ipc-runtime/ts/src/shm_client.ts @@ -1,5 +1,5 @@ import { loadIpcRuntimeNapi } from "./native_loader.js"; -import { IpcClientAsync, IpcClientSync } from "./types.js"; +import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js"; /** * Minimum surface a NAPI msgpack client must expose. Satisfied by the @@ -52,7 +52,7 @@ export class NapiShmSyncClient implements IpcClientSync { interface PendingCallback { requestId: number; resolve: (data: Uint8Array) => void; - reject: (error: Error) => void; + reject: (error: unknown) => void; } /** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */ @@ -78,7 +78,10 @@ export class NapiShmAsyncClient implements IpcClientAsync { private nextRequestId = Math.floor(Math.random() * REQUEST_ID_SPACE); private destroyed = false; - constructor(private inner: NapiMsgpackClientAsync) { + 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 @@ -116,8 +119,11 @@ 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) @@ -138,7 +144,8 @@ export class NapiShmAsyncClient implements IpcClientAsync { if (this.pending.length === 0) { this.inner.release(); } - reject( + this.fail( + reject, err instanceof Error ? err : new Error(`SHM async call failed: ${String(err)}`), @@ -147,6 +154,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; @@ -157,7 +172,7 @@ export class NapiShmAsyncClient implements IpcClientAsync { const pending = this.pending; this.pending = []; for (const cb of pending) { - cb.reject(err); + this.fail(cb.reject, err); } // Stops the native poll thread and releases the TSFN reference taken // when the queue went 0 → 1 — without this, Node never exits when @@ -171,6 +186,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; } /** @@ -204,5 +221,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.ts b/ipc-runtime/ts/src/spawned_backend.ts index d08a1dcef89..bffd0cf3da8 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -13,7 +13,7 @@ import { createNapiShmAsyncClient, createNapiShmSyncClient, } from "./shm_client.js"; -import { IpcClientAsync, IpcClientSync } from "./types.js"; +import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js"; import { UdsIpcClient } from "./uds_client.js"; export type SpawnedTransport = "uds" | "shm"; @@ -80,7 +80,9 @@ 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}`); + return ["_request", "_response"].map( + (suffix) => `/dev/shm/${shmName}${suffix}`, + ); } /** @@ -97,8 +99,7 @@ async function removeStaleIpcPath( ipcPath: string, ): Promise { try { - const paths = - transport === "shm" ? shmSegmentPaths(ipcPath) : [ipcPath]; + 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 @@ -172,9 +173,13 @@ function childReadyFailurePromise( 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, - }), + new IpcSpawnError( + `Failed to spawn ${binaryName}: ${err.message}`, + retry, + { + cause: err, + }, + ), ); }); child.once("exit", (code, signal) => { @@ -252,25 +257,20 @@ export class SpawnedProcessBackend implements IpcClientAsync { } call(input: Uint8Array): Promise { - // Fast path for a live process: no async frame and no await on - // ensureUp(). This runs once per call, and a pipelined caller issuing - // thousands of calls feels every extra promise hop. + // 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).catch(async (err) => { - throw await this.attributeCallError(incarnation, err); - }); + return incarnation.client.call(input); } return this.callWithRespawn(input); } private async callWithRespawn(input: Uint8Array): Promise { const incarnation = await this.ensureUp(); - try { - return await incarnation.client.call(input); - } catch (err) { - throw await this.attributeCallError(incarnation, err); - } + return await incarnation.client.call(input); } sendProcessSignal(signal: NodeJS.Signals): void { @@ -446,7 +446,9 @@ export class SpawnedProcessBackend implements IpcClientAsync { const connectAbort = new AbortController(); try { incarnation.client = await Promise.race([ - this.connectClient(connectAbort.signal), + this.connectClient(connectAbort.signal, (err) => + this.attributeCallError(incarnation as Incarnation, err), + ), childReadyFailure, ]); } catch (err) { @@ -497,7 +499,10 @@ export class SpawnedProcessBackend implements IpcClientAsync { } } - private async connectClient(signal?: AbortSignal): Promise { + private async connectClient( + signal: AbortSignal, + mapError: IpcErrorMapper, + ): Promise { const { options } = this; const timeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS; if (options.transport === "uds") { @@ -510,6 +515,7 @@ export class SpawnedProcessBackend implements IpcClientAsync { connectTimeoutMs: timeoutMs, unref: options.unref, signal, + mapError, }); } // The SHM client attaches to server-created rings, so creation can race @@ -521,6 +527,7 @@ export class SpawnedProcessBackend implements IpcClientAsync { return createNapiShmAsyncClient(this.ipcPath.replace(/\.shm$/, ""), { clientId: options.clientId, customAddonPath: options.napiPath, + mapError, }); } catch (err) { lastError = err; @@ -600,7 +607,8 @@ export class SpawnedProcessBackendSync implements IpcClientSync { : join(tmpdir(), `${instanceId}.log`); await removeStaleIpcPath("shm", ipcPath); - const logFile = logPath !== undefined ? await open(logPath, "a") : undefined; + const logFile = + logPath !== undefined ? await open(logPath, "a") : undefined; const child = spawnServerProcess(options, ipcPath, logFile?.fd); const childReadyFailure = childReadyFailurePromise( child, diff --git a/ipc-runtime/ts/src/types.ts b/ipc-runtime/ts/src/types.ts index 6771954e702..ad5e4a06639 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; diff --git a/ipc-runtime/ts/src/uds_client.ts b/ipc-runtime/ts/src/uds_client.ts index 2bdc09fdc64..ba43e8acb48 100644 --- a/ipc-runtime/ts/src/uds_client.ts +++ b/ipc-runtime/ts/src/uds_client.ts @@ -2,13 +2,14 @@ import * as net from "node:net"; import { IpcTransportError } from "./errors.js"; import { IpcClientAsync, + IpcErrorMapper, CONNECT_RETRY_BUDGET_MS, MAX_FRAME_SIZE, } from "./types.js"; interface PendingCall { resolve: (resp: Uint8Array) => void; - reject: (err: Error) => void; + reject: (err: unknown) => void; } export interface UdsIpcClientConnectOptions { @@ -31,6 +32,8 @@ export interface UdsIpcClientConnectOptions { * loop open long after the caller gave up. */ signal?: AbortSignal; + /** Applied to every rejected call's error. */ + mapError?: IpcErrorMapper; } /** @@ -56,6 +59,7 @@ export class UdsIpcClient implements IpcClientAsync { 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) => @@ -81,7 +85,7 @@ export class UdsIpcClient implements IpcClientAsync { ); conn.setNoDelay(true); if (opts?.unref) conn.unref(); - return new UdsIpcClient(conn, opts?.unref ?? false); + return new UdsIpcClient(conn, opts?.unref ?? false, opts?.mapError); } /** Number of in-flight calls awaiting a response. */ @@ -96,19 +100,25 @@ 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 peer rejects an oversized frame by closing the connection, which // reaches the caller as an unexplained EPIPE on the next write. Fail here // instead, naming the size that was refused. if (input.length + 8 > MAX_FRAME_SIZE) { - throw new IpcTransportError( - `UdsIpcClient: request of ${input.length} bytes exceeds MAX_FRAME_SIZE (${MAX_FRAME_SIZE})`, + throw await this.mapped( + new IpcTransportError( + `UdsIpcClient: request of ${input.length} bytes exceeds MAX_FRAME_SIZE (${MAX_FRAME_SIZE})`, + ), ); } return new Promise((resolve, reject) => { @@ -192,7 +202,17 @@ export class UdsIpcClient implements IpcClientAsync { this.closed = true; const pending = [...this.pending.values()]; this.pending.clear(); - for (const p of pending) p.reject(err); + for (const p of pending) { + 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; } } From 3ac1601a88a8f9b1dd5019de28d7f6c1d374d9a1 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:01:47 +0000 Subject: [PATCH 07/19] test(bb.js): warm each poseidon bench mode before timing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every mode was timed on its first run, so the numbers included JIT warm-up for that call pattern. A pipelined burst exercises different code paths from the sequential warm-up that preceded it, so it was the most affected. Its first-burst result swung by several µs between CI runs. Each mode now runs once untimed first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- .../src/barretenberg/poseidon.bench.test.ts | 85 ++++++------------- 1 file changed, 27 insertions(+), 58 deletions(-) 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..7ab78d0de9f 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) => { + 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(async () => { + for (let i = 0; i < ITERATIONS; i++) { + nativeShmSyncApi!.poseidon2Hash({ inputs }); + } + }) + : 0; // Calculate metrics (all relative to WASM baseline) const nativeSocketOverhead = ((nativeSocketTime - wasmTime) / wasmTime) * 100; From 20c04b314366348ae55621ec8479ecb8d0545f18 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:06:14 +0000 Subject: [PATCH 08/19] perf(ipc-runtime): share the in-order pending queue with the UDS client UdsIpcClient still kept pending calls in a Map, with the same per-call BigInt allocation and hashing the SHM client had. Both clients now use one PendingQueue: small-int ids, head-of-queue fast path, search fallback. The UDS wire id stays u64; live ids sit in the low word, and a nonzero high word never matches a call. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/ts/src/pending_queue.ts | 55 +++++++++++++++++++++++++++++ ipc-runtime/ts/src/shm_client.ts | 42 +++++----------------- ipc-runtime/ts/src/uds_client.ts | 36 ++++++++----------- 3 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 ipc-runtime/ts/src/pending_queue.ts 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.ts b/ipc-runtime/ts/src/shm_client.ts index 1ce925fcd9f..aee5b268048 100644 --- a/ipc-runtime/ts/src/shm_client.ts +++ b/ipc-runtime/ts/src/shm_client.ts @@ -1,4 +1,5 @@ import { loadIpcRuntimeNapi } from "./native_loader.js"; +import { PendingQueue } from "./pending_queue.js"; import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js"; /** @@ -49,33 +50,18 @@ export class NapiShmSyncClient implements IpcClientSync { } } -interface PendingCallback { - requestId: number; - resolve: (data: Uint8Array) => void; - reject: (error: unknown) => void; -} - -/** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */ -const REQUEST_ID_SPACE = 2 ** 30; - /** * Wraps the fire-and-forget async NAPI msgpack client behind the - * `IpcClientAsync` interface. Pending calls are kept in issue order; 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. Servers that answer in order (e.g. a serial `run()` loop) always - * match the head of the queue, 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 per client so a stale frame left in a recycled SHM ring slot - * by a previous occupant is unlikely to 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 pending: PendingCallback[] = []; - private nextRequestId = Math.floor(Math.random() * REQUEST_ID_SPACE); + private readonly pending = new PendingQueue(); private destroyed = false; constructor( @@ -88,7 +74,7 @@ export class NapiShmAsyncClient implements IpcClientAsync { // balanced the TSFN reference. return; } - const cb = this.takePending(requestId); + const cb = this.pending.take(requestId); if (cb) { cb.resolve(new Uint8Array(response)); if (this.pending.length === 0) { @@ -109,14 +95,6 @@ export class NapiShmAsyncClient implements IpcClientAsync { }); } - private takePending(requestId: number): PendingCallback | undefined { - if (this.pending.length > 0 && this.pending[0].requestId === requestId) { - return this.pending.shift(); - } - const i = this.pending.findIndex((p) => p.requestId === requestId); - return i === -1 ? undefined : this.pending.splice(i, 1)[0]; - } - call(input: Uint8Array): Promise { if (this.destroyed) { return new Promise((_, reject) => @@ -130,12 +108,10 @@ export class NapiShmAsyncClient implements IpcClientAsync { ? input : Buffer.from(input.buffer, input.byteOffset, input.byteLength); return new Promise((resolve, reject) => { - const requestId = this.nextRequestId; - this.nextRequestId = (requestId + 1) % REQUEST_ID_SPACE; if (this.pending.length === 0) { this.inner.acquire(); } - this.pending.push({ requestId, resolve, reject }); + const requestId = this.pending.push(resolve, reject); try { this.inner.call(requestId, buf); } catch (err: any) { @@ -169,9 +145,7 @@ export class NapiShmAsyncClient implements IpcClientAsync { this.destroyed = true; // Reject anything still in flight. const err = new Error("ipc-runtime SHM client destroyed before response"); - const pending = this.pending; - this.pending = []; - for (const cb of pending) { + for (const cb of this.pending.drain()) { this.fail(cb.reject, err); } // Stops the native poll thread and releases the TSFN reference taken diff --git a/ipc-runtime/ts/src/uds_client.ts b/ipc-runtime/ts/src/uds_client.ts index ba43e8acb48..45744056b8b 100644 --- a/ipc-runtime/ts/src/uds_client.ts +++ b/ipc-runtime/ts/src/uds_client.ts @@ -1,5 +1,6 @@ import * as net from "node:net"; import { IpcTransportError } from "./errors.js"; +import { PendingQueue } from "./pending_queue.js"; import { IpcClientAsync, IpcErrorMapper, @@ -7,11 +8,6 @@ import { MAX_FRAME_SIZE, } from "./types.js"; -interface PendingCall { - resolve: (resp: Uint8Array) => void; - reject: (err: unknown) => void; -} - export interface UdsIpcClientConnectOptions { /** * Unref the socket while idle so it doesn't keep the Node event loop @@ -44,14 +40,11 @@ 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 readonly pending = new PendingQueue(); private destroyed = false; /** Set once the socket has errored/closed; new calls fail fast. */ private closed = false; @@ -90,7 +83,7 @@ export class UdsIpcClient implements IpcClientAsync { /** 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). */ @@ -122,14 +115,14 @@ export class UdsIpcClient implements IpcClientAsync { ); } return new Promise((resolve, reject) => { - const requestId = this.nextRequestId++; - if (this.idleUnref && this.pending.size === 0) { + if (this.idleUnref && this.pending.length === 0) { this.conn.ref(); } - this.pending.set(requestId, { resolve, reject }); + 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); }); @@ -173,13 +166,14 @@ export class UdsIpcClient implements IpcClientAsync { return; } if (this.buffer.length < 4 + len) return; - const requestId = this.buffer.readBigUInt64LE(4); + // Live ids are below 2^30, so a nonzero high word can't match a call. + const requestId = + this.buffer.readUInt32LE(8) === 0 ? this.buffer.readUInt32LE(4) : -1; const payload = this.buffer.subarray(12, 4 + len); this.buffer = this.buffer.subarray(4 + len); - const next = this.pending.get(requestId); + const next = this.pending.take(requestId); if (next) { - this.pending.delete(requestId); - if (this.idleUnref && this.pending.size === 0) { + if (this.idleUnref && this.pending.length === 0) { this.conn.unref(); } next.resolve(new Uint8Array(payload)); @@ -200,9 +194,7 @@ 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) { + for (const p of this.pending.drain()) { if (this.mapError) { this.mapError(err).then(p.reject, p.reject); } else { From 9a35cb5d75f17a0997fa75871685b152e2bce2a4 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:15:19 +0000 Subject: [PATCH 09/19] test(bb.js): don't mark the sync bench body async (require-await) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7ab78d0de9f..a246f95d631 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts @@ -73,7 +73,7 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => { // 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) => { + const timed = async (run: () => Promise | void) => { await run(); const start = performance.now(); await run(); @@ -98,7 +98,7 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => { const nativeShmTime = nativeShmApi ? await timed(sequential(nativeShmApi)) : 0; const nativeShmPipelinedTime = nativeShmApi ? await timed(pipelined(nativeShmApi)) : 0; const nativeShmSyncTime = nativeShmSyncApi - ? await timed(async () => { + ? await timed(() => { for (let i = 0; i < ITERATIONS; i++) { nativeShmSyncApi!.poseidon2Hash({ inputs }); } From aa396f450891a9e0eb3f962cc0c48d316f9b8616 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:33:15 +0000 Subject: [PATCH 10/19] refactor(bb-rs): drop the pre-codegen compatibility layer The 7.0 line is a breaking release, and older barretenberg-rs versions stay on crates.io for anyone who needs the old API (semver keeps `"6"` manifests off 7.x). Removes the legacy BarretenbergApi/PipeBackend/byte-valued types, the old module paths and the inert `native`/`async` features, so the crate exposes only the generated BbApi. The tests move to BbApi with typed scalars; the pipe tests spawn bb and drive it through ipc_runtime::IpcClient::from_fds. The README gains a 6.x migration note. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- barretenberg/rust/Cargo.toml | 2 - barretenberg/rust/README.md | 34 +- barretenberg/rust/barretenberg-rs/Cargo.toml | 12 +- .../rust/barretenberg-rs/src/fr_ext.rs | 7 +- .../rust/barretenberg-rs/src/legacy.rs | 319 ------------------ .../rust/barretenberg-rs/src/legacy_pipe.rs | 97 ------ .../rust/barretenberg-rs/src/legacy_types.rs | 152 --------- barretenberg/rust/barretenberg-rs/src/lib.rs | 40 +-- barretenberg/rust/tests/Cargo.toml | 2 +- barretenberg/rust/tests/src/blake2s.rs | 14 +- barretenberg/rust/tests/src/debug_msgpack.rs | 2 +- barretenberg/rust/tests/src/ffi/aes.rs | 26 +- barretenberg/rust/tests/src/ffi/blake2s.rs | 8 +- barretenberg/rust/tests/src/ffi/bn254.rs | 94 +++--- barretenberg/rust/tests/src/ffi/ecdsa.rs | 36 +- barretenberg/rust/tests/src/ffi/grumpkin.rs | 50 +-- barretenberg/rust/tests/src/ffi/mod.rs | 2 +- barretenberg/rust/tests/src/ffi/pedersen.rs | 74 ++-- barretenberg/rust/tests/src/ffi/poseidon.rs | 46 +-- barretenberg/rust/tests/src/ffi/schnorr.rs | 44 +-- barretenberg/rust/tests/src/ffi/secp256k1.rs | 52 +-- barretenberg/rust/tests/src/pedersen.rs | 38 +-- barretenberg/rust/tests/src/pipe_test.rs | 44 +-- barretenberg/rust/tests/src/poseidon.rs | 18 +- barretenberg/rust/tests/src/utils.rs | 59 +++- 25 files changed, 359 insertions(+), 913 deletions(-) delete mode 100644 barretenberg/rust/barretenberg-rs/src/legacy.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs delete mode 100644 barretenberg/rust/barretenberg-rs/src/legacy_types.rs 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 d9e3bbb9714..53b6958301d 100644 --- a/barretenberg/rust/README.md +++ b/barretenberg/rust/README.md @@ -30,11 +30,11 @@ 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 the IPC transports let response = api.blake2s(b"hello world")?; @@ -55,7 +55,7 @@ The crate provides a pluggable backend system: ``` ┌─────────────────────────────────────────┐ -│ BarretenbergApi │ +│ BbApi │ │ (generated, type-safe Rust API) │ └─────────────────────────────────────────┘ │ @@ -84,14 +84,36 @@ 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 - `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 72ded0456c7..1c7abff31d7 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] @@ -33,14 +33,8 @@ thiserror.workspace = true hex.workspace = true [features] -default = ["native", "ffi"] +default = ["ipc-runtime", "ffi"] # FFI backend - links against libbarretenberg from cpp build ffi = [] -# Transport backends, provided by ipc-runtime. +# Transport backends (pipe, UDS, shared memory), provided by ipc-runtime. ipc-runtime = ["dep:ipc-runtime"] -# Pre-migration feature names. `native` used to pull in the in-crate pipe -# backend and its async runtime; the transport now comes from ipc-runtime and -# needs no runtime of its own, so `async` is inert. Kept so existing manifests -# resolve unchanged. -native = ["ipc-runtime"] -async = [] diff --git a/barretenberg/rust/barretenberg-rs/src/fr_ext.rs b/barretenberg/rust/barretenberg-rs/src/fr_ext.rs index 09587a458e8..81914e4f862 100644 --- a/barretenberg/rust/barretenberg-rs/src/fr_ext.rs +++ b/barretenberg/rust/barretenberg-rs/src/fr_ext.rs @@ -1,7 +1,6 @@ -//! Extra constructors / accessors on the generated `Fr` type that downstream -//! callers (tests, ports of TS helpers) already depend on. Kept as a separate -//! impl block here rather than inside `bb_types.rs` so the generated -//! file stays a pure regen target. +//! 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}; diff --git a/barretenberg/rust/barretenberg-rs/src/legacy.rs b/barretenberg/rust/barretenberg-rs/src/legacy.rs deleted file mode 100644 index b5b30983480..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/legacy.rs +++ /dev/null @@ -1,319 +0,0 @@ -//! Back-compat shim mirroring the pre-codegen `BarretenbergApi` surface. -//! -//! The codegen migration replaced loose `&[u8]` / `Vec>` scalar -//! parameters with typed newtypes (`Fr`, `Fq`, `Secp256k1Fr`, ...). External -//! consumers were already depending on the old surface, so this shim -//! preserves it: callers that did -//! -//! ```ignore -//! use barretenberg_rs::{BarretenbergApi, FfiBackend}; -//! let mut api = BarretenbergApi::new(FfiBackend::new()?); -//! api.schnorr_compute_public_key(&private_key_bytes)?; -//! ``` -//! -//! continue to compile against this crate while they migrate to the new -//! [`crate::BbApi`] surface (typed scalars, `Vec` for hash inputs, -//! etc.). -//! -//! Wire format is identical — only the Rust call surface changed. Methods -//! whose signature did not change reach `BbApi` through `Deref` (no -//! explicit wrapper here). - -#![allow(deprecated)] - -use std::ops::{Deref, DerefMut}; - -use crate::generated::backend::Backend; -use crate::generated::bb_client::BbApi; -use crate::generated::bb_types::{ - AesDecryptResponse, - AesEncryptResponse, - Bn254FqSqrtResponse, - Bn254FrSqrtResponse, - Bn254G1MulResponse, - Bn254G2MulResponse, - EcdsaSecp256k1ComputePublicKeyResponse, - EcdsaSecp256k1ConstructSignatureResponse, - EcdsaSecp256r1ComputePublicKeyResponse, - EcdsaSecp256r1ConstructSignatureResponse, - Fq, - Fr, - GrumpkinAddResponse, - GrumpkinBatchMulResponse, - GrumpkinMulResponse, - PedersenCommitResponse, - PedersenHashResponse, - Poseidon2HashResponse, - Poseidon2PermutationResponse, - EcdsaSecp256k1VerifySignatureResponse, - SchnorrComputePublicKeyResponse, - SchnorrConstructSignatureResponse, - SchnorrVerifySignatureResponse, - Secp256k1Fr, - Secp256k1MulResponse, - Secp256r1Fr, -}; -use crate::generated::bb_types as wire; -use crate::generated::error::Result; -#[allow(deprecated)] -use crate::legacy_types::{Bn254G1Point, Bn254G2Point, GrumpkinPoint, Secp256k1Point}; - -/// Deprecated alias for [`crate::BbApi`] preserving the pre-migration call -/// surface (`&[u8]` scalars, `Vec>` hash inputs). Forwards unchanged -/// methods to `BbApi` via `Deref`; overrides methods whose signature changed. -#[deprecated( - note = "use `BbApi` directly; typed scalars (Fr/Fq/Secp256k1Fr) replace raw `&[u8]` parameters" -)] -pub struct BarretenbergApi(BbApi); - -impl BarretenbergApi { - pub fn new(backend: B) -> Self { - Self(BbApi::new(backend)) - } - - pub fn into_inner(self) -> BbApi { - self.0 - } -} - -impl Deref for BarretenbergApi { - type Target = BbApi; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for BarretenbergApi { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -fn to_fr_array(s: &[u8]) -> Fr { - let arr: [u8; 32] = s.try_into().expect("expected 32-byte scalar"); - Fr::from_be_bytes(arr) -} - -fn to_fq_array(s: &[u8]) -> Fq { - let arr: [u8; 32] = s.try_into().expect("expected 32-byte scalar"); - Fq::from_bytes(arr) -} - -fn to_secp256k1_fr(s: &[u8]) -> Secp256k1Fr { - let arr: [u8; 32] = s.try_into().expect("expected 32-byte secp256k1 scalar"); - Secp256k1Fr::from_bytes(arr) -} - -fn to_secp256r1_fr(s: &[u8]) -> Secp256r1Fr { - let arr: [u8; 32] = s.try_into().expect("expected 32-byte secp256r1 scalar"); - Secp256r1Fr::from_bytes(arr) -} - -fn to_16_array(s: &[u8]) -> [u8; 16] { - s.try_into().expect("expected 16-byte value") -} - -fn to_32_array(s: &[u8]) -> [u8; 32] { - s.try_into().expect("expected 32-byte value") -} - -fn fr_vec(inputs: Vec>) -> Vec { - inputs.into_iter().map(|b| to_fr_array(&b)).collect() -} - -// Old-surface methods. These shadow the same-named methods reached through -// `Deref`, so callers picking up `BarretenbergApi` get the legacy signature. -#[allow(deprecated)] -impl BarretenbergApi { - pub fn poseidon2_hash(&mut self, inputs: Vec>) -> Result { - self.0.poseidon2_hash(fr_vec(inputs)) - } - - pub fn poseidon2_permutation( - &mut self, - inputs: [Vec; 4], - ) -> Result { - let typed: [Fr; 4] = inputs.map(|b| to_fr_array(&b)); - self.0.poseidon2_permutation(typed) - } - - pub fn pedersen_commit( - &mut self, - inputs: Vec>, - hash_index: u32, - ) -> Result { - self.0.pedersen_commit(fr_vec(inputs), hash_index) - } - - pub fn pedersen_hash( - &mut self, - inputs: Vec>, - hash_index: u32, - ) -> Result { - self.0.pedersen_hash(fr_vec(inputs), hash_index) - } - - pub fn grumpkin_mul( - &mut self, - point: impl Into, - scalar: &[u8], - ) -> Result { - self.0.grumpkin_mul(point.into(), to_fq_array(scalar)) - } - - pub fn grumpkin_add( - &mut self, - a: impl Into, - b: impl Into, - ) -> Result { - self.0.grumpkin_add(a.into(), b.into()) - } - - pub fn grumpkin_batch_mul( - &mut self, - points: Vec>, - scalar: &[u8], - ) -> Result { - self.0.grumpkin_batch_mul(points.into_iter().map(Into::into).collect(), to_fq_array(scalar)) - } - - pub fn secp256k1_mul( - &mut self, - point: impl Into, - scalar: &[u8], - ) -> Result { - self.0.secp256k1_mul(point.into(), to_secp256k1_fr(scalar)) - } - - pub fn bn254_fr_sqrt(&mut self, input: &[u8]) -> Result { - self.0.bn254_fr_sqrt(to_fr_array(input)) - } - - pub fn bn254_fq_sqrt(&mut self, input: &[u8]) -> Result { - self.0.bn254_fq_sqrt(to_fq_array(input)) - } - - pub fn bn254_g1_mul( - &mut self, - point: impl Into, - scalar: &[u8], - ) -> Result { - self.0.bn254_g1_mul(point.into(), to_fr_array(scalar)) - } - - pub fn bn254_g2_mul( - &mut self, - point: impl Into, - scalar: &[u8], - ) -> Result { - self.0.bn254_g2_mul(point.into(), to_fr_array(scalar)) - } - - pub fn schnorr_compute_public_key( - &mut self, - private_key: &[u8], - ) -> Result { - Ok(self.0.schnorr_compute_public_key(to_fq_array(private_key))?.into()) - } - - pub fn schnorr_construct_signature( - &mut self, - message: &[u8], - private_key: &[u8], - ) -> Result { - self.0 - .schnorr_construct_signature(to_fr_array(message), to_fq_array(private_key)) - } - - pub fn ecdsa_secp256k1_compute_public_key( - &mut self, - private_key: &[u8], - ) -> Result { - Ok(self - .0 - .ecdsa_secp256k1_compute_public_key(to_secp256k1_fr(private_key))? - .into()) - } - - pub fn ecdsa_secp256r1_compute_public_key( - &mut self, - private_key: &[u8], - ) -> Result { - Ok(self - .0 - .ecdsa_secp256r1_compute_public_key(to_secp256r1_fr(private_key))? - .into()) - } - - pub fn ecdsa_secp256k1_construct_signature( - &mut self, - message: &[u8], - private_key: &[u8], - ) -> Result { - self.0 - .ecdsa_secp256k1_construct_signature(message, to_secp256k1_fr(private_key)) - } - - pub fn ecdsa_secp256r1_construct_signature( - &mut self, - message: &[u8], - private_key: &[u8], - ) -> Result { - self.0 - .ecdsa_secp256r1_construct_signature(message, to_secp256r1_fr(private_key)) - } - pub fn aes_encrypt( - &mut self, - plaintext: &[u8], - iv: &[u8], - key: &[u8], - length: u32, - ) -> Result { - self.0 - .aes_encrypt(plaintext, to_16_array(iv), to_16_array(key), length) - } - - pub fn aes_decrypt( - &mut self, - ciphertext: &[u8], - iv: &[u8], - key: &[u8], - length: u32, - ) -> Result { - self.0 - .aes_decrypt(ciphertext, to_16_array(iv), to_16_array(key), length) - } - - pub fn schnorr_verify_signature( - &mut self, - message: &[u8], - public_key: impl Into, - s: &[u8], - e: &[u8], - ) -> Result { - self.0.schnorr_verify_signature( - to_fr_array(message), - public_key.into(), - to_fq_array(s), - to_fq_array(e), - ) - } - - pub fn ecdsa_secp256k1_verify_signature( - &mut self, - message: &[u8], - public_key: impl Into, - r: &[u8], - s: &[u8], - v: u8, - ) -> Result { - self.0.ecdsa_secp256k1_verify_signature( - message, - public_key.into(), - to_32_array(r), - to_32_array(s), - v, - ) - } - -} diff --git a/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs b/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs deleted file mode 100644 index 99643afb6ec..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/legacy_pipe.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Back-compat `PipeBackend`, preserving the pre-migration constructor. -//! -//! The transport itself now lives in ipc-runtime: this spawns bb the way it -//! always did and drives the connection through `ipc_runtime::IpcClient`'s -//! pipe client, so no framing or fd handling is duplicated here. - -use std::path::Path; -use std::process::{Child, Command, Stdio}; - -use crate::generated::backend::Backend; -use crate::generated::error::{IpcError, Result}; - -/// Deprecated: spawns bb and talks to it over its stdin/stdout. -/// -/// Prefer driving [`ipc_runtime::IpcClient`] directly — it implements -/// [`Backend`] and also offers the UDS and shared-memory transports. -#[deprecated(note = "use ipc_runtime::IpcClient (from_fds / from_path) as the Backend")] -pub struct PipeBackend { - client: ipc_runtime::IpcClient, - process: Option, -} - -#[allow(deprecated)] -impl PipeBackend { - /// Spawn bb in msgpack mode and connect over its stdio pipes. - pub fn new(bb_binary_path: impl AsRef, threads: Option) -> Result { - use std::os::fd::AsRawFd; - - let mut cmd = Command::new(bb_binary_path.as_ref()); - cmd.arg("msgpack") - .arg("run") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()); - - // bb reads thread count from the environment. - if let Some(t) = threads { - cmd.env("HARDWARE_CONCURRENCY", t.to_string()); - } - - let mut process = cmd - .spawn() - .map_err(|e| IpcError::Backend(format!("Failed to spawn BB process: {e}")))?; - - let out_fd = process - .stdin - .as_ref() - .ok_or_else(|| IpcError::Backend("Failed to get stdin handle".to_string()))? - .as_raw_fd(); - let in_fd = process - .stdout - .as_ref() - .ok_or_else(|| IpcError::Backend("Failed to get stdout handle".to_string()))? - .as_raw_fd(); - - if let Ok(Some(status)) = process.try_wait() { - return Err(IpcError::Backend(format!( - "BB process exited immediately with status: {status}" - ))); - } - - // SAFETY: both descriptors belong to `process`, which this struct owns - // for as long as the client lives. The client duplicates them, so the - // two sides close independently. - let client = unsafe { ipc_runtime::IpcClient::from_fds(in_fd, out_fd) } - .map_err(|e| IpcError::Backend(format!("Failed to connect to BB process: {e}")))?; - - Ok(Self { - client, - process: Some(process), - }) - } -} - -#[allow(deprecated)] -impl Backend for PipeBackend { - fn call(&mut self, input: &[u8]) -> Result> { - self.client - .call(input) - .map_err(|e| IpcError::Backend(e.to_string())) - } - - fn destroy(&mut self) -> Result<()> { - if let Some(mut process) = self.process.take() { - let _ = process.kill(); - let _ = process.wait(); - } - Ok(()) - } -} - -#[allow(deprecated)] -impl Drop for PipeBackend { - fn drop(&mut self) { - let _ = self.destroy(); - } -} diff --git a/barretenberg/rust/barretenberg-rs/src/legacy_types.rs b/barretenberg/rust/barretenberg-rs/src/legacy_types.rs deleted file mode 100644 index ea0d531d903..00000000000 --- a/barretenberg/rust/barretenberg-rs/src/legacy_types.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Pre-codegen value types, kept so existing callers compile unchanged. -//! -//! Codegen models 32-byte scalars as `Bin32` and wire structs as holding it. -//! The types here keep the older `[u8; 32]` / `Vec` shapes and convert at -//! the API boundary in [`crate::legacy`]. New code should use the generated -//! types directly. - -use crate::generated::bb_types as wire; - -/// Deprecated: a 32-byte field element. Prefer the generated `Fr`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Fr(pub [u8; 32]); - -impl Fr { - /// 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) - } - - pub fn from_be_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - pub fn from_le_bytes(bytes: [u8; 32]) -> Self { - Self(bytes) - } - - /// 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) - } - - /// Truncates or zero-pads to 32 bytes. - 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) - } - - pub fn to_buffer(&self) -> Vec { - self.0.to_vec() - } -} - -impl From for wire::Bin32 { - fn from(value: Fr) -> Self { - wire::Bin32(value.0) - } -} - -impl From for Fr { - fn from(value: wire::Bin32) -> Self { - Self(value.0) - } -} - -/// Scalars arrive from callers as loose bytes; the wire type is fixed size. -fn to_bin32(bytes: &[u8]) -> wire::Bin32 { - wire::Bin32(bytes.try_into().expect("expected a 32-byte scalar")) -} - -macro_rules! legacy_point { - ($name:ident, $wire:ident, $doc:literal) => { - #[doc = $doc] - #[derive(Debug, Clone, PartialEq, Eq, Default)] - pub struct $name { - pub x: Vec, - pub y: Vec, - } - - impl From<$name> for wire::$wire { - fn from(value: $name) -> Self { - wire::$wire { - x: to_bin32(&value.x), - y: to_bin32(&value.y), - } - } - } - - impl From for $name { - fn from(value: wire::$wire) -> Self { - Self { - x: value.x.0.to_vec(), - y: value.y.0.to_vec(), - } - } - } - }; -} - -legacy_point!(GrumpkinPoint, GrumpkinPoint, "Deprecated: byte-valued Grumpkin point."); -legacy_point!(Bn254G1Point, Bn254G1Point, "Deprecated: byte-valued BN254 G1 point."); -legacy_point!(Secp256k1Point, Secp256k1Point, "Deprecated: byte-valued secp256k1 point."); -legacy_point!(Secp256r1Point, Secp256r1Point, "Deprecated: byte-valued secp256r1 point."); - -/// Deprecated: byte-valued BN254 G2 point (coordinates are Fq pairs). -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct Bn254G2Point { - pub x: [Vec; 2], - pub y: [Vec; 2], -} - -impl From for wire::Bn254G2Point { - fn from(value: Bn254G2Point) -> Self { - wire::Bn254G2Point { - x: value.x.map(|c| to_bin32(&c)), - y: value.y.map(|c| to_bin32(&c)), - } - } -} - -/// Deprecated: byte-valued responses that carry a point, so callers can keep -/// destructuring `.x` / `.y` as `Vec`. -macro_rules! legacy_point_response { - ($name:ident, $wire:ident, $field:ident, $point:ident) => { - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct $name { - pub $field: $point, - } - - impl From for $name { - fn from(value: wire::$wire) -> Self { - Self { - $field: value.$field.into(), - } - } - } - }; -} - -legacy_point_response!( - EcdsaSecp256k1ComputePublicKeyResponse, - EcdsaSecp256k1ComputePublicKeyResponse, - public_key, - Secp256k1Point -); -legacy_point_response!( - EcdsaSecp256r1ComputePublicKeyResponse, - EcdsaSecp256r1ComputePublicKeyResponse, - public_key, - Secp256r1Point -); -legacy_point_response!( - SchnorrComputePublicKeyResponse, - SchnorrComputePublicKeyResponse, - public_key, - GrumpkinPoint -); diff --git a/barretenberg/rust/barretenberg-rs/src/lib.rs b/barretenberg/rust/barretenberg-rs/src/lib.rs index 949400e0f51..9d479100122 100644 --- a/barretenberg/rust/barretenberg-rs/src/lib.rs +++ b/barretenberg/rust/barretenberg-rs/src/lib.rs @@ -15,7 +15,7 @@ //! let client = IpcClient::from_path("/tmp/bb.sock")?; //! let mut api = BbApi::new(client); //! -//! let response = api.blake2s(b"hello world".to_vec())?; +//! let response = api.blake2s(b"hello world")?; //! println!("Hash: {:?}", response.hash); //! ``` //! @@ -60,51 +60,15 @@ pub mod generated { } mod fr_ext; -pub mod legacy; -pub mod legacy_types; -#[cfg(feature = "ipc-runtime")] -pub mod legacy_pipe; pub use generated::backend::Backend; pub use generated::bb_client::BbApi; -pub use generated::bb_types::{Bin32, Command, Response}; -// Pre-codegen value shapes: byte-valued points and a constructible Fr. The -// generated equivalents remain available as `generated::bb_types::*`. -#[allow(deprecated)] -pub use legacy_types::{ - Bn254G1Point, Bn254G2Point, Fr, GrumpkinPoint, Secp256k1Point, Secp256r1Point, -}; +pub use generated::bb_types::{Bin32, Command, Fq, Fr, Response}; pub use generated::error::{IpcError as BarretenbergError, Result}; -// Pre-codegen surface kept around so external consumers can migrate at their -// own pace; see [`legacy`] for the deprecation notes and the typed-scalar -// replacements on [`BbApi`]. -#[allow(deprecated)] -pub use legacy::BarretenbergApi; - -// Preserved module path for callers that imported types via -// `barretenberg_rs::generated_types::*`. Explicit re-exports shadow the glob, -// so the byte-valued shapes win for the types whose fields changed. -pub mod generated_types { - pub use crate::generated::bb_types::*; - #[allow(deprecated)] - pub use crate::legacy_types::{ - Bn254G1Point, Bn254G2Point, Fr, GrumpkinPoint, Secp256k1Point, Secp256r1Point, - }; -} - #[cfg(feature = "ffi")] pub use generated::ffi_backend::FfiBackend; -// Pre-codegen module path. -pub mod backends { - #[cfg(feature = "ffi")] - pub use crate::generated::ffi_backend::FfiBackend; - #[allow(deprecated)] - #[cfg(feature = "ipc-runtime")] - pub use crate::legacy_pipe::PipeBackend; -} - // 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/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 f0da98a91b1..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() { 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") From 1db7ebf7a6933fd25cc0577da1e5f3322f26757e Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:33:15 +0000 Subject: [PATCH 11/19] fix(bb-rs): publish the ipc-runtime crate so barretenberg-rs can release barretenberg-rs now depends on the ipc-runtime crate, which had no version and was not on crates.io, so `cargo publish` refused the package. The release step now publishes ipc-runtime at the same version first and pins it exactly. ipc-runtime's build compiles the C++ sources from ../cpp, which a published crate does not have; the release copies them into the crate's cpp/ directory and build.rs falls back to it. Publishing happens from the barretenberg-rs release rather than ipc-runtime's, because the latter also runs for private releases, which must not reach crates.io. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- barretenberg/rust/barretenberg-rs/Cargo.toml | 2 +- barretenberg/rust/bootstrap.sh | 35 +++++++++++++++++++- ipc-runtime/rust/.gitignore | 1 + ipc-runtime/rust/Cargo.toml | 4 +++ ipc-runtime/rust/build.rs | 7 +++- 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/barretenberg/rust/barretenberg-rs/Cargo.toml b/barretenberg/rust/barretenberg-rs/Cargo.toml index 1c7abff31d7..3c495e9d212 100644 --- a/barretenberg/rust/barretenberg-rs/Cargo.toml +++ b/barretenberg/rust/barretenberg-rs/Cargo.toml @@ -26,7 +26,7 @@ libc.workspace = 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", optional = true } +ipc-runtime = { path = "../../../ipc-runtime/rust", version = "0.1.0", optional = true } # Utilities thiserror.workspace = true diff --git a/barretenberg/rust/bootstrap.sh b/barretenberg/rust/bootstrap.sh index 42c05dc8720..6097da13ff9 100755 --- a/barretenberg/rust/bootstrap.sh +++ b/barretenberg/rust/bootstrap.sh @@ -62,6 +62,35 @@ 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 +} + +# barretenberg-rs takes its transport from the ipc-runtime crate, so that crate +# has to be on crates.io at the same version first. It is published from here +# rather than from ipc-runtime/bootstrap.sh because that release also runs for +# private releases, which must never reach crates.io. +function release_ipc_runtime_crate { + local version=$1 + local crate_dir=$root/ipc-runtime/rust + + if crate_published ipc-runtime $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 $crate_dir/cpp + mkdir -p $crate_dir/cpp + cp -r $root/ipc-runtime/cpp/ipc_runtime $crate_dir/cpp/ + find $crate_dir/cpp -name '*.test.cpp' -delete + sed -i "0,/^version = \".*\"/s//version = \"$version\"/" $crate_dir/Cargo.toml + + # Verification builds the packaged crate on its own, which proves the copied + # sources are complete. + (cd $crate_dir && retry "denoise 'do_or_dryrun cargo publish --allow-dirty'") +} + function release { echo_header "barretenberg-rs release" @@ -79,8 +108,12 @@ function release { generate fi + release_ipc_runtime_crate $version + # Pin the exact build published alongside this one. + 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/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 de6cde60365..f118bdcf671 100644 --- a/ipc-runtime/rust/build.rs +++ b/ipc-runtime/rust/build.rs @@ -33,7 +33,12 @@ fn collect_sources(dir: &Path, out: &mut Vec) { 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 mut sources = Vec::new(); From 01df6ae9962f27271f8b4951f819cedff070d0cc Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:39:54 +0000 Subject: [PATCH 12/19] refactor(ipc-runtime): publish the Rust crate from ipc-runtime's own release ipc-runtime owns its crate, so its release publishes it. The release also runs for private releases, which publish only to the internal npm registry, so the crates.io step is skipped when assert_public_release refuses. ipc-runtime is released before barretenberg/rust, whose release now only pins the version. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- barretenberg/rust/bootstrap.sh | 29 ++------------------------- ipc-runtime/bootstrap.sh | 36 ++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/barretenberg/rust/bootstrap.sh b/barretenberg/rust/bootstrap.sh index 6097da13ff9..b49097e57f4 100755 --- a/barretenberg/rust/bootstrap.sh +++ b/barretenberg/rust/bootstrap.sh @@ -66,31 +66,6 @@ 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 } -# barretenberg-rs takes its transport from the ipc-runtime crate, so that crate -# has to be on crates.io at the same version first. It is published from here -# rather than from ipc-runtime/bootstrap.sh because that release also runs for -# private releases, which must never reach crates.io. -function release_ipc_runtime_crate { - local version=$1 - local crate_dir=$root/ipc-runtime/rust - - if crate_published ipc-runtime $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 $crate_dir/cpp - mkdir -p $crate_dir/cpp - cp -r $root/ipc-runtime/cpp/ipc_runtime $crate_dir/cpp/ - find $crate_dir/cpp -name '*.test.cpp' -delete - sed -i "0,/^version = \".*\"/s//version = \"$version\"/" $crate_dir/Cargo.toml - - # Verification builds the packaged crate on its own, which proves the copied - # sources are complete. - (cd $crate_dir && retry "denoise 'do_or_dryrun cargo publish --allow-dirty'") -} - function release { echo_header "barretenberg-rs release" @@ -108,8 +83,8 @@ function release { generate fi - release_ipc_runtime_crate $version - # Pin the exact build published alongside this one. + # 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). 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 From 5d7a4ce696852c59457fef85ad03578034440036 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:31:34 +0000 Subject: [PATCH 13/19] fix(ipc-runtime): fence the SHM conditional wake handshake The wake is skipped unless the waiter has flagged that it is about to sleep. Each side stores (head/tail/seq, or the flag) and then loads the other's value, and release/acquire lets each load run ahead of its own store. Both sides could then miss each other, leaving the waiter asleep on published data, with no timeout on the default call path. A seq_cst fence between the store and the load on both sides guarantees one of them sees the other. The futex lock already fenced the waiter in practice; the SPSC producer had no such cover. No measurable cost in a pipelined poseidon A/B on a loaded host. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/cpp/ipc_runtime/shm/README.md | 2 +- ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp | 11 ++++++++-- ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp | 23 +++++++++++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/ipc-runtime/cpp/ipc_runtime/shm/README.md b/ipc-runtime/cpp/ipc_runtime/shm/README.md index f2c9600bcca..0b906f1bb64 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/README.md +++ b/ipc-runtime/cpp/ipc_runtime/shm/README.md @@ -271,7 +271,7 @@ 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**: 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. `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, and no wake syscalls) diff --git a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp index 47bbebc766a..2c34a5ae6a0 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.cpp @@ -274,6 +274,10 @@ int MpscConsumer::wait_for_data(uint64_t timeout_ns, const std::function // 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); @@ -338,7 +342,8 @@ void MpscConsumer::notify() doorbell_->seq.fetch_add(1, std::memory_order_release); // Same conditional wake as MpscProducer::publish: only a consumer actually blocked on the doorbell needs the // syscall. - if (doorbell_->consumer_blocked.load(std::memory_order_acquire)) { + 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); } } @@ -468,7 +473,9 @@ void MpscProducer::publish(size_t n) // then wake only if the consumer is blocked on the futex. doorbell_->seq.fetch_add(1, std::memory_order_release); - if (doorbell_->consumer_blocked.load(std::memory_order_acquire)) { + // 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); } } diff --git a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp index 3065a944873..143e189c25c 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/spsc_shm.cpp @@ -262,9 +262,14 @@ 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); - 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); + // 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); } } @@ -321,9 +326,9 @@ void SpscShm::release(size_t 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); + // 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); } } @@ -416,6 +421,9 @@ bool SpscShm::wait_for_data(size_t need, uint64_t timeout_ns) 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); @@ -521,6 +529,9 @@ bool SpscShm::wait_for_space(size_t need, uint64_t timeout_ns) // 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); From 0051dc8094c30b7e06ebb127fb0108b512a3d996 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:31:34 +0000 Subject: [PATCH 14/19] fix(ipc-runtime): drop the stream frame cap, buffer only what arrives UDS and pipe frames were capped at 1 GiB (256 MiB until bb proving requests outgrew it). Nothing in a stream transport needs a limit below the u32 length field; the cap only guarded against a corrupt prefix driving a huge up-front allocation. Receivers now grow their buffer as payload bytes arrive (C++ read_payload, TS FrameReader), so that guard is no longer needed and the limit is the wire format's own. SHM stays bounded by half its ring capacity. FrameReader also fixes the TS client and server re-concatenating their whole buffer on every chunk, which made receiving a large frame quadratic. A request too large for the length field now rejects with a non-transport IpcError: it is the caller's mistake on a healthy connection, and as a transport error SpawnedProcessBackend killed the server for it. Senders also check against the payload limit (frame limit minus the 8-byte id) rather than the frame limit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/README.md | 2 +- ipc-runtime/cpp/ipc_runtime/constants.hpp | 12 ++- .../cpp/ipc_runtime/mpsc_shm_server.hpp | 3 +- ipc-runtime/cpp/ipc_runtime/pipe_client.cpp | 20 ++-- ipc-runtime/cpp/ipc_runtime/pipe_server.cpp | 48 ++++----- ipc-runtime/cpp/ipc_runtime/shm_common.hpp | 2 +- ipc-runtime/cpp/ipc_runtime/socket.test.cpp | 102 +++++++++++++----- ipc-runtime/cpp/ipc_runtime/socket_client.cpp | 20 ++-- ipc-runtime/cpp/ipc_runtime/socket_server.cpp | 55 +++++----- ipc-runtime/cpp/ipc_runtime/stream_io.hpp | 53 +++++++++ ipc-runtime/ts/src/frame_reader.test.ts | 39 +++++++ ipc-runtime/ts/src/frame_reader.ts | 72 +++++++++++++ ipc-runtime/ts/src/types.ts | 9 +- ipc-runtime/ts/src/uds.test.ts | 72 ++++++++----- ipc-runtime/ts/src/uds_client.ts | 47 +++----- ipc-runtime/ts/src/uds_server.ts | 32 ++---- 16 files changed, 390 insertions(+), 198 deletions(-) create mode 100644 ipc-runtime/cpp/ipc_runtime/stream_io.hpp create mode 100644 ipc-runtime/ts/src/frame_reader.test.ts create mode 100644 ipc-runtime/ts/src/frame_reader.ts 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/cpp/ipc_runtime/constants.hpp b/ipc-runtime/cpp/ipc_runtime/constants.hpp index 1a1459d1c64..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 = 1024U * 1024 * 1024; // 1 GiB +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/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_client.cpp b/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp index 438089fa626..df8658dd932 100644 --- a/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp +++ b/ipc-runtime/cpp/ipc_runtime/pipe_client.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/pipe_client.hpp" #include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" #include #include #include @@ -113,7 +114,7 @@ bool PipeClient::send(uint64_t request_id, const void* data, size_t len, uint64_ errno = EINVAL; return false; } - if (len > MAX_FRAME_SIZE) { + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { errno = EMSGSIZE; return false; } @@ -152,10 +153,9 @@ std::span PipeClient::receive(uint64_t timeout_ns, uint64_t& requ 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 {}; } @@ -168,13 +168,9 @@ std::span PipeClient::receive(uint64_t timeout_ns, uint64_t& requ } 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)); - } - - if (read_exact(recv_buffer_.data(), msg_len, timeout_ns, partial) != 1) { + 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 {}; diff --git a/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp b/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp index 3cd32577952..f7763e29285 100644 --- a/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp +++ b/ipc-runtime/cpp/ipc_runtime/pipe_server.cpp @@ -1,5 +1,6 @@ #include "ipc_runtime/pipe_server.hpp" #include "ipc_runtime/constants.hpp" +#include "ipc_runtime/stream_io.hpp" #include #include #include @@ -194,10 +195,9 @@ std::span PipeServer::receive(int client_id, uint64_t& request_id 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 — treat as a fatal desync 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 — 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 {}; @@ -219,28 +219,26 @@ std::span PipeServer::receive(int client_id, uint64_t& request_id } msg_len -= static_cast(FRAME_ID_SIZE); - if (recv_buffer_.size() < msg_len || recv_buffer_.empty()) { - // Keep at least one byte so data() is non-null for zero-length messages - // (null data() signals failure). - recv_buffer_.resize(std::max(msg_len, 1)); - } - - total_read = 0; - while (total_read < msg_len) { - ssize_t n = ::read(in_fd_, recv_buffer_.data() + total_read, msg_len - total_read); - if (n < 0) { - if (errno == EINTR) { - continue; // Interrupted, retry + 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; } - disconnect(); - return {}; - } - if (n == 0) { - // Peer closed mid-message. - disconnect(); - return {}; + if (r == 0) { + return 0; // Peer closed mid-message + } + got += static_cast(r); } - total_read += static_cast(n); + return 1; + }); + if (result != 1) { + disconnect(); + return {}; } return std::span(recv_buffer_.data(), msg_len); @@ -260,7 +258,7 @@ bool PipeServer::send(int client_id, uint64_t request_id, const void* data, size errno = EINVAL; return false; } - if (len > MAX_FRAME_SIZE) { + if (len > MAX_FRAME_SIZE - FRAME_ID_SIZE) { errno = EMSGSIZE; return false; } 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 8af262a736a..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 {}; } @@ -183,14 +183,8 @@ std::span SocketClient::receive(uint64_t timeout_ns, uint64_t& re } 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 b24ddc3991a..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 {}; @@ -256,33 +256,30 @@ std::span SocketServer::receive(int client_id, uint64_t& request_ } 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/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/types.ts b/ipc-runtime/ts/src/types.ts index ad5e4a06639..1fdbbc06628 100644 --- a/ipc-runtime/ts/src/types.ts +++ b/ipc-runtime/ts/src/types.ts @@ -23,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 = 1024 * 1024 * 1024; // 1 GiB +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 45744056b8b..e14b966a8f9 100644 --- a/ipc-runtime/ts/src/uds_client.ts +++ b/ipc-runtime/ts/src/uds_client.ts @@ -1,5 +1,6 @@ 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, @@ -43,7 +44,7 @@ export interface UdsIpcClientConnectOptions { * and the server may complete requests in any order (see PendingQueue). */ export class UdsIpcClient implements IpcClientAsync { - private buffer: Buffer = Buffer.alloc(0); + private frames = new FrameReader(); private readonly pending = new PendingQueue(); private destroyed = false; /** Set once the socket has errored/closed; new calls fail fast. */ @@ -104,14 +105,13 @@ export class UdsIpcClient implements IpcClientAsync { ), ); } - // The peer rejects an oversized frame by closing the connection, which - // reaches the caller as an unexplained EPIPE on the next write. Fail here - // instead, naming the size that was refused. + // 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 await this.mapped( - new IpcTransportError( - `UdsIpcClient: request of ${input.length} bytes exceeds MAX_FRAME_SIZE (${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) => { @@ -136,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. @@ -165,12 +152,12 @@ export class UdsIpcClient implements IpcClientAsync { ); return; } - if (this.buffer.length < 4 + len) return; + 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 = - this.buffer.readUInt32LE(8) === 0 ? this.buffer.readUInt32LE(4) : -1; - const payload = this.buffer.subarray(12, 4 + len); - this.buffer = this.buffer.subarray(4 + len); + frame.readUInt32LE(8) === 0 ? frame.readUInt32LE(4) : -1; + const payload = frame.subarray(12); const next = this.pending.take(requestId); if (next) { if (this.idleUnref && this.pending.length === 0) { 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 () => { From 0fb7928ba1ef4df0c79624576f2aa033b8004a9c Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:39:59 +0000 Subject: [PATCH 15/19] fix(ipc-runtime): keep stray stdout out of the stdio frame stream Serving "-" made the process's stdout the frame stream, so any other write to it (std::cout, printf, a library's diagnostics) landed inside a frame and desynced the peer. next's bb guarded this by redirecting std::cout; that guard did not carry over to the ipc-runtime pipe path. make_server now serves on a private duplicate of stdout and points fd 1 at stderr, which also covers C stdio and raw writes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- ipc-runtime/cpp/ipc_runtime/pipe.test.cpp | 64 ++++++++++++++++++++ ipc-runtime/cpp/ipc_runtime/serve_helper.cpp | 18 +++++- ipc-runtime/cpp/ipc_runtime/serve_helper.hpp | 5 +- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp index cce8b70fbd8..76670d08cc0 100644 --- a/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp +++ b/ipc-runtime/cpp/ipc_runtime/pipe.test.cpp @@ -1,15 +1,20 @@ #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 @@ -136,6 +141,65 @@ TEST(PipeTest, ServesEchoOverFdPair) 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; diff --git a/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp b/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp index d62741ae55a..7f4f0bf243d 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.cpp @@ -1,7 +1,11 @@ #include "ipc_runtime/serve_helper.hpp" +#include +#include +#include #include #include +#include #include namespace ipc { @@ -20,7 +24,19 @@ std::unique_ptr make_server(const std::string& input_path, const Serv // "-" 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 == "-") { - return IpcServer::create_pipe(STDIN_FILENO, STDOUT_FILENO); + // 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 c6d41ab37fb..8d5f94bfa6a 100644 --- a/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp +++ b/ipc-runtime/cpp/ipc_runtime/serve_helper.hpp @@ -36,8 +36,9 @@ struct ServerOptions { * @brief Construct an IpcServer based on the input path's suffix. * * Recognised inputs: - * - "-" → IpcServer::create_pipe(STDIN_FILENO, STDOUT_FILENO) — serve the - * process's own stdio (parent spawned us with piped stdio) + * - "-" → 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, From 018e97f3cdfde105d83727163a1bf67269b65959 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:39:59 +0000 Subject: [PATCH 16/19] refactor(bb.js): remove findNapiBinary It looked for nodejs_module.node, which no longer ships now that bb.js uses ipc-runtime's addon, so it could only ever return null. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4 --- .../bb.js/src/bb_backends/browser/platform.ts | 4 --- .../ts/bb.js/src/bb_backends/node/platform.ts | 35 ------------------- barretenberg/ts/bb.js/src/index.ts | 2 +- 3 files changed, 1 insertion(+), 40 deletions(-) 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/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/index.ts b/barretenberg/ts/bb.js/src/index.ts index 571793f8c41..77e7aa41794 100644 --- a/barretenberg/ts/bb.js/src/index.ts +++ b/barretenberg/ts/bb.js/src/index.ts @@ -57,4 +57,4 @@ export { SECP256R1_G1_GENERATOR, } from './generated/curve_constants.js'; -export { findBbBinary, findNapiBinary } from './bb_backends/node/platform.js'; +export { findBbBinary } from './bb_backends/node/platform.js'; From b8a89cab30785ec585c5bfa796737a367c7e40aa Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:17:32 +0000 Subject: [PATCH 17/19] docs(bb): drop stale nodejs_module and findNapiBinary references Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NwZD6M5xfpDdAFkdbkWT67 --- barretenberg/.claude/skills/benchmark-avm/SKILL.md | 6 ++---- barretenberg/CLAUDE.md | 2 +- barretenberg/cpp/src/barretenberg/common/CMakeLists.txt | 1 - .../cpp/src/barretenberg/crypto/keccak/CMakeLists.txt | 1 - .../cpp/src/barretenberg/crypto/poseidon2/CMakeLists.txt | 1 - .../cpp/src/barretenberg/crypto/sha256/CMakeLists.txt | 1 - barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md | 4 ++-- barretenberg/cpp/src/barretenberg/vm2/CMakeLists.txt | 1 - .../src/barretenberg/world_state_reference/CMakeLists.txt | 1 - 9 files changed, 5 insertions(+), 13 deletions(-) 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/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/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/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) From 6c44ef6f1225cef035e696da46588cb88d512aca Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:13:04 +0000 Subject: [PATCH 18/19] fix(ipc-runtime): sync SHM calls wait for a live server instead of a 60s deadline The sync NAPI client gave up on any call after 60s, failing a slow but healthy server and reporting a dead one only as a timeout. It now has no deadline: given the server's pid, it checks between 1s waits whether the server has exited (waitid with WNOWAIT, so Node still reaps the child) and fails with its exit code or signal, which SpawnedProcessBackendSync reports as IpcProcessExitedError. A request too large for the ring now fails at once rather than waiting for space that never frees. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Prk3bcqkopGENJUuUpT52q --- .../ts/bb.js/src/bb_backends/index.ts | 5 +- .../src/bb_backends/node/native_shm.test.ts | 56 ++++++++ ipc-runtime/cpp/ipc_runtime/blocking_call.hpp | 96 +++++++++++++ .../cpp/ipc_runtime/mpsc_shm_client.hpp | 8 ++ ipc-runtime/cpp/ipc_runtime/shm.test.cpp | 133 ++++++++++++++++++ ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp | 3 + .../cpp/napi/msgpack_client_wrapper.cpp | 50 +++---- .../cpp/napi/msgpack_client_wrapper.hpp | 7 +- ipc-runtime/ts/src/native_loader.ts | 6 +- ipc-runtime/ts/src/shm_client.ts | 15 +- ipc-runtime/ts/src/spawned_backend.ts | 45 ++++-- 11 files changed, 379 insertions(+), 45 deletions(-) create mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts create mode 100644 ipc-runtime/cpp/ipc_runtime/blocking_call.hpp diff --git a/barretenberg/ts/bb.js/src/bb_backends/index.ts b/barretenberg/ts/bb.js/src/bb_backends/index.ts index f911f5865b6..9991aa2d3c8 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/index.ts +++ b/barretenberg/ts/bb.js/src/bb_backends/index.ts @@ -49,9 +49,8 @@ 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, 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 + * There is no equivalent for the shared-memory backends: a call to a bb process that has died fails + * there with a retryable error, but later calls fail too. 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 diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts new file mode 100644 index 00000000000..9678a60aa32 --- /dev/null +++ b/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts @@ -0,0 +1,56 @@ +import { IpcProcessExitedError } from '@aztec-foundation/ipc-runtime'; +import { readFileSync, readdirSync } from 'fs'; + +import { BarretenbergNativeShmSyncBackend } from './native_shm.js'; +import { findBbBinary } from './platform.js'; + +const bbPath = findBbBinary(); +// Linux only: the test finds bb's pid through /proc. +const describeIfBb = bbPath && process.platform === 'linux' ? describe : describe.skip; + +/** The pids of this process's children named bb. */ +function bbChildPids(): number[] { + const pids: number[] = []; + for (const entry of readdirSync('/proc')) { + if (!/^\d+$/.test(entry)) { + continue; + } + let stat: string; + try { + stat = readFileSync(`/proc/${entry}/stat`, 'utf-8'); + } catch { + continue; // exited since the listing + } + // " () ...": comm may contain spaces, so split after its closing paren. + const comm = stat.slice(stat.indexOf('(') + 1, stat.lastIndexOf(')')); + const ppid = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1]); + if (comm === 'bb' && ppid === process.pid) { + pids.push(Number(entry)); + } + } + return pids; +} + +describeIfBb('BarretenbergNativeShmSyncBackend', () => { + it('fails a call to a bb that has died with its exit, promptly rather than after a deadline', async () => { + const before = new Set(bbChildPids()); + const backend = await BarretenbergNativeShmSyncBackend.new(bbPath!); + const [pid] = bbChildPids().filter(p => !before.has(p)); + expect(pid).toBeDefined(); + + // Kill bb and call it in the same tick: the sync call holds the main thread, so the child's + // exit event can't run, and only the native client can notice the death. + process.kill(pid, 'SIGKILL'); + const start = Date.now(); + let error: unknown; + try { + backend.call(new Uint8Array([0x90])); + } catch (err) { + error = err; + } + expect(Date.now() - start).toBeLessThan(10_000); + expect(error).toBeInstanceOf(IpcProcessExitedError); + expect((error as IpcProcessExitedError).signal).toBe('SIGKILL'); + backend.destroy(); + }, 120_000); +}); diff --git a/ipc-runtime/cpp/ipc_runtime/blocking_call.hpp b/ipc-runtime/cpp/ipc_runtime/blocking_call.hpp new file mode 100644 index 00000000000..25c9f773c78 --- /dev/null +++ b/ipc-runtime/cpp/ipc_runtime/blocking_call.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "ipc_runtime/ipc_client.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ipc { + +/** How a server process ended: its exit code, or the signal that killed it. -1 where it doesn't apply or is unknown. */ +struct ProcessExit { + int code = -1; + int signal = -1; +}; + +/** + * Whether process `pid` has exited, without reaping it: the process that spawned + * it (e.g. Node's child_process) still collects its status as usual. While + * unreaped, an exited child keeps its pid, so the pid can't have been reused. + */ +inline std::optional exited_without_reaping(pid_t pid) +{ + siginfo_t info{}; + if (::waitid(P_PID, static_cast(pid), &info, WEXITED | WNOHANG | WNOWAIT) == 0) { + if (info.si_pid != pid) { + return std::nullopt; // still running + } + if (info.si_code == CLD_EXITED) { + return ProcessExit{ .code = info.si_status }; + } + return ProcessExit{ .signal = info.si_status }; + } + // Not our child, or already reaped: all we can tell is whether it still exists. + if (errno == ECHILD && ::kill(pid, 0) != 0 && errno == ESRCH) { + return ProcessExit{}; + } + return std::nullopt; +} + +struct BlockingCallResult { + enum class Status { Ok, ServerExited, TooLarge }; + Status status = Status::Ok; + /** The response, when status is Ok. Release it on the client once read. */ + std::span response; + /** How the server ended, when status is ServerExited. */ + ProcessExit server_exit; +}; + +/** How often a blocking call checks that its server is still alive. Bounds how late a death is noticed, nothing else. + */ +inline constexpr uint64_t BLOCKING_CALL_POLL_NS = 1'000'000'000; + +/** + * One blocking request/response round trip, for a caller that cannot otherwise + * notice its server dying (e.g. a sync call holding Node's main thread, where the + * child's exit event can't run). There is no deadline: a live server is waited on + * however long it takes. Given the server's pid, the call checks between waits of + * `poll_ns` whether that process has exited, and if so ends with how it exited. + * Without a pid, it waits indefinitely. + */ +inline BlockingCallResult call_blocking(IpcClient& client, + const void* data, + size_t len, + std::optional server_pid, + uint64_t poll_ns = BLOCKING_CALL_POLL_NS) +{ + auto exited = [&]() -> std::optional { + return server_pid ? exited_without_reaping(*server_pid) : std::nullopt; + }; + for (;;) { + errno = 0; + if (client.send(data, len, poll_ns)) { + break; + } + if (errno == EMSGSIZE) { + return { .status = BlockingCallResult::Status::TooLarge }; + } + if (auto exit = exited()) { + return { .status = BlockingCallResult::Status::ServerExited, .server_exit = *exit }; + } + } + std::span response; + while ((response = client.receive(poll_ns)).data() == nullptr) { + if (auto exit = exited()) { + return { .status = BlockingCallResult::Status::ServerExited, .server_exit = *exit }; + } + } + return { .response = response }; +} + +} // namespace ipc diff --git a/ipc-runtime/cpp/ipc_runtime/mpsc_shm_client.hpp b/ipc-runtime/cpp/ipc_runtime/mpsc_shm_client.hpp index c718c357487..d50cd7a739b 100644 --- a/ipc-runtime/cpp/ipc_runtime/mpsc_shm_client.hpp +++ b/ipc-runtime/cpp/ipc_runtime/mpsc_shm_client.hpp @@ -6,6 +6,7 @@ #include "shm/spsc_shm.hpp" #include "shm_common.hpp" #include +#include #include #include #include @@ -89,6 +90,13 @@ class MpscShmClient : public IpcClient { return false; } + // A frame over half the ring can never be claimed, however long we wait; + // the server rejects the same bound on receive. + if (FRAME_ID_SIZE + len > producer_->capacity() / 2 - sizeof(uint32_t)) { + errno = EMSGSIZE; + return false; + } + // Claim space for length prefix + request id + data size_t total_size = sizeof(uint32_t) + FRAME_ID_SIZE + len; void* buf = producer_->claim(total_size, normalize_call_timeout(timeout_ns)); diff --git a/ipc-runtime/cpp/ipc_runtime/shm.test.cpp b/ipc-runtime/cpp/ipc_runtime/shm.test.cpp index 0d8b2682953..159b5fa6625 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm.test.cpp +++ b/ipc-runtime/cpp/ipc_runtime/shm.test.cpp @@ -1,3 +1,4 @@ +#include "ipc_runtime/blocking_call.hpp" #include "ipc_runtime/ipc_client.hpp" #include "ipc_runtime/ipc_server.hpp" #include "ipc_runtime/shm/mpsc_shm.hpp" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1075,4 +1077,135 @@ TEST(ShmTest, MpscSlotFreedOnDisconnectAndReclaimed) server->close(); } +/** + * Serves `name` over MPSC SHM in a forked child, handling requests with + * `handler`, and returns the child's pid once the server is listening. + */ +pid_t fork_shm_server(const std::string& name, + size_t ring_size, + const std::function(std::span)>& handler) +{ + int ready[2]; + if (::pipe(ready) != 0) { + return -1; + } + pid_t pid = fork(); + if (pid == 0) { + ::close(ready[0]); + auto server = IpcServer::create_mpsc_shm(name, 1, ring_size, ring_size); + if (!server->listen()) { + _exit(2); + } + char byte = 1; + [[maybe_unused]] ssize_t n = ::write(ready[1], &byte, 1); + server->run([&](int, std::span req) { return handler(req); }); + _exit(0); + } + ::close(ready[1]); + char byte = 0; + [[maybe_unused]] ssize_t n = ::read(ready[0], &byte, 1); + ::close(ready[0]); + return pid; +} + +constexpr uint64_t TEST_POLL_NS = 50'000'000; // 50ms + +double seconds_since(std::chrono::steady_clock::time_point start) +{ + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +// A server that exits without replying ends the call with its exit code, as +// soon as the next poll notices, rather than after some deadline. +TEST(BlockingCallTest, EndsWhenTheServerExitsWithoutReplying) +{ + std::string name = "bc_exit_" + std::to_string(getpid()); + pid_t pid = fork_shm_server(name, 4096, [](std::span) -> std::vector { _exit(7); }); + ASSERT_GT(pid, 0); + auto client = IpcClient::create_mpsc_shm(name); + ASSERT_TRUE(client->connect()); + + uint8_t byte = 1; + auto start = std::chrono::steady_clock::now(); + auto result = call_blocking(*client, &byte, 1, pid, TEST_POLL_NS); + EXPECT_LT(seconds_since(start), 5.0); + EXPECT_EQ(result.status, BlockingCallResult::Status::ServerExited); + EXPECT_EQ(result.server_exit.code, 7); + EXPECT_EQ(result.server_exit.signal, -1); + + // The exit was observed without reaping the child, so its owner still can. + int status = 0; + EXPECT_EQ(::waitpid(pid, &status, 0), pid); + client->close(); +} + +TEST(BlockingCallTest, ReportsTheSignalThatKilledTheServer) +{ + std::string name = "bc_kill_" + std::to_string(getpid()); + pid_t pid = fork_shm_server(name, 4096, [](std::span) -> std::vector { + ::raise(SIGKILL); + return {}; + }); + ASSERT_GT(pid, 0); + auto client = IpcClient::create_mpsc_shm(name); + ASSERT_TRUE(client->connect()); + + uint8_t byte = 1; + auto start = std::chrono::steady_clock::now(); + auto result = call_blocking(*client, &byte, 1, pid, TEST_POLL_NS); + EXPECT_LT(seconds_since(start), 5.0); + EXPECT_EQ(result.status, BlockingCallResult::Status::ServerExited); + EXPECT_EQ(result.server_exit.signal, SIGKILL); + + int status = 0; + EXPECT_EQ(::waitpid(pid, &status, 0), pid); + client->close(); +} + +// A live server is waited on however many polls its reply takes. +TEST(BlockingCallTest, WaitsOutASlowServerThatIsStillAlive) +{ + std::string name = "bc_slow_" + std::to_string(getpid()); + pid_t pid = fork_shm_server(name, 4096, [](std::span req) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 10 polls + return std::vector(req.begin(), req.end()); + }); + ASSERT_GT(pid, 0); + auto client = IpcClient::create_mpsc_shm(name); + ASSERT_TRUE(client->connect()); + + uint8_t byte = 42; + auto result = call_blocking(*client, &byte, 1, pid, TEST_POLL_NS); + ASSERT_EQ(result.status, BlockingCallResult::Status::Ok); + ASSERT_EQ(result.response.size(), 1U); + EXPECT_EQ(result.response[0], 42); + client->release(result.response.size()); + + client->close(); + ::kill(pid, SIGKILL); + ::waitpid(pid, nullptr, 0); +} + +// A request that can never fit the request ring fails at once, instead of +// waiting for space a live server will never free. +TEST(BlockingCallTest, RejectsARequestTooLargeForTheRing) +{ + std::string name = "bc_big_" + std::to_string(getpid()); + pid_t pid = fork_shm_server( + name, 4096, [](std::span req) { return std::vector(req.begin(), req.end()); }); + ASSERT_GT(pid, 0); + auto client = IpcClient::create_mpsc_shm(name); + ASSERT_TRUE(client->connect()); + + std::vector big(8192); + auto start = std::chrono::steady_clock::now(); + auto result = call_blocking(*client, big.data(), big.size(), pid, TEST_POLL_NS); + EXPECT_LT(seconds_since(start), 5.0); + EXPECT_EQ(result.status, BlockingCallResult::Status::TooLarge); + + client->close(); + ::kill(pid, SIGKILL); + ::waitpid(pid, nullptr, 0); +} + } // namespace diff --git a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp index ba5213add98..2a6ad7751f3 100644 --- a/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp +++ b/ipc-runtime/cpp/ipc_runtime/shm/mpsc_shm.hpp @@ -225,6 +225,9 @@ class MpscProducer { */ void publish(size_t n); + /** @brief Size of the producer's ring, which bounds the largest message it can carry. */ + uint64_t capacity() const { return ring_.capacity(); } + private: MpscProducer(SpscShm&& ring, int doorbell_fd, size_t doorbell_len, MpscDoorbell* doorbell, size_t producer_id); diff --git a/ipc-runtime/cpp/napi/msgpack_client_wrapper.cpp b/ipc-runtime/cpp/napi/msgpack_client_wrapper.cpp index 63bd8010283..248921cb001 100644 --- a/ipc-runtime/cpp/napi/msgpack_client_wrapper.cpp +++ b/ipc-runtime/cpp/napi/msgpack_client_wrapper.cpp @@ -1,9 +1,9 @@ #include "msgpack_client_wrapper.hpp" +#include "ipc_runtime/blocking_call.hpp" #include "ipc_runtime/ipc_client.hpp" #include "napi.h" -#include #include #include #include @@ -27,6 +27,12 @@ MsgpackClientWrapper::MsgpackClientWrapper(const Napi::CallbackInfo& info) client_id = static_cast(info[1].As().Uint32Value()); } + // Optional third arg: the server's pid, so a call can tell a dead server + // from a slow one. Without it, calls wait indefinitely. + if (info.Length() >= 3 && info[2].IsNumber()) { + server_pid_ = static_cast(info[2].As().Int32Value()); + } + // MPSC-SHM client — matches ipc::make_server which uses MPSC by default, // so the same shm_name can host multiple clients (each with a distinct slot). client_ = ipc::IpcClient::create_mpsc_shm(shm_name, client_id); @@ -61,34 +67,24 @@ Napi::Value MsgpackClientWrapper::call(const Napi::CallbackInfo& info) const uint8_t* input_data = input_buffer.Data(); size_t input_len = input_buffer.Length(); - // Retry on backpressure, but with an overall deadline: this is a blocking - // call on the Node main thread, and a dead/wedged server must surface as - // an error rather than hanging the process forever. - constexpr uint64_t TIMEOUT_NS = 1'000'000'000; // 1s per attempt - constexpr uint64_t CALL_DEADLINE_NS = 60'000'000'000; // 60s overall - - auto now_ns = [] { - return static_cast( - std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()) - .count()); - }; - const uint64_t start_ns = now_ns(); - - while (!client_->send(input_data, input_len, TIMEOUT_NS)) { - // request ring full, consumer behind — retry until the deadline - if (now_ns() - start_ns > CALL_DEADLINE_NS) { - throw Napi::Error::New(env, "IPC call timed out sending request (server unresponsive?)"); - } + // Node's main thread is held for the whole call, so the server's exit event + // can't run: call_blocking checks whether the server is alive itself. + auto result = ipc::call_blocking(*client_, input_data, input_len, server_pid_); + if (result.status == ipc::BlockingCallResult::Status::TooLarge) { + throw Napi::Error::New(env, "IPC request too large for the shared-memory ring"); } - - // data() == nullptr means timeout; a non-null empty span is a valid - // zero-length response. - std::span response; - while ((response = client_->receive(TIMEOUT_NS)).data() == nullptr) { - if (now_ns() - start_ns > CALL_DEADLINE_NS) { - throw Napi::Error::New(env, "IPC call timed out waiting for response (server unresponsive?)"); - } + if (result.status == ipc::BlockingCallResult::Status::ServerExited) { + auto err = Napi::Error::New(env, "IPC server process exited"); + err.Set("serverExited", Napi::Boolean::New(env, true)); + err.Set("exitCode", + result.server_exit.code >= 0 ? Napi::Value(Napi::Number::New(env, result.server_exit.code)) + : env.Null()); + err.Set("signal", + result.server_exit.signal >= 0 ? Napi::Value(Napi::Number::New(env, result.server_exit.signal)) + : env.Null()); + throw err; } + std::span response = result.response; auto js_buffer = Napi::Buffer::Copy(env, response.data(), response.size()); client_->release(response.size()); diff --git a/ipc-runtime/cpp/napi/msgpack_client_wrapper.hpp b/ipc-runtime/cpp/napi/msgpack_client_wrapper.hpp index e095e2de697..c3423f96356 100644 --- a/ipc-runtime/cpp/napi/msgpack_client_wrapper.hpp +++ b/ipc-runtime/cpp/napi/msgpack_client_wrapper.hpp @@ -3,6 +3,8 @@ #include "ipc_runtime/ipc_client.hpp" #include "napi.h" #include +#include +#include namespace ipc::napi { @@ -10,7 +12,9 @@ namespace ipc::napi { * @brief NAPI wrapper for synchronous msgpack calls over shared-memory IPC. * * Wraps an ipc::IpcClient (SHM transport) and exposes a blocking - * `call(Buffer) -> Buffer` to JavaScript. One round-trip per `call`. + * `call(Buffer) -> Buffer` to JavaScript. One round-trip per `call`, with no + * deadline. Given the server's pid, a call to a server that has died throws an + * error with `serverExited: true`, `exitCode` and `signal` (number or null). */ class MsgpackClientWrapper : public Napi::ObjectWrap { public: @@ -25,6 +29,7 @@ class MsgpackClientWrapper : public Napi::ObjectWrap { private: std::unique_ptr client_; bool connected_ = false; + std::optional server_pid_; }; } // namespace ipc::napi diff --git a/ipc-runtime/ts/src/native_loader.ts b/ipc-runtime/ts/src/native_loader.ts index cb2b3c96633..515f923e579 100644 --- a/ipc-runtime/ts/src/native_loader.ts +++ b/ipc-runtime/ts/src/native_loader.ts @@ -81,7 +81,11 @@ export function findIpcRuntimeNapi(customPath?: string): string | null { * the addon cannot be located or fails to dlopen. */ export function loadIpcRuntimeNapi(customPath?: string): { - MsgpackClient: new (shmName: string, clientId?: number) => any; + MsgpackClient: new ( + shmName: string, + clientId?: number, + serverPid?: number, + ) => any; MsgpackClientAsync: new (shmName: string, clientId?: number) => any; } { const addonPath = findIpcRuntimeNapi(customPath); diff --git a/ipc-runtime/ts/src/shm_client.ts b/ipc-runtime/ts/src/shm_client.ts index aee5b268048..83a2e77a942 100644 --- a/ipc-runtime/ts/src/shm_client.ts +++ b/ipc-runtime/ts/src/shm_client.ts @@ -162,6 +162,13 @@ export interface CreateNapiShmOptions { customAddonPath?: string; /** Applied to every rejected call's error. */ mapError?: IpcErrorMapper; + /** + * Sync client only: the server's pid. A sync call holds the main thread, so + * nothing else can notice the server dying; with the pid, a call to a dead + * server throws (with `serverExited`, `exitCode` and `signal` set) instead + * of waiting forever. Calls have no deadline either way. + */ + serverPid?: number; } /** @@ -176,11 +183,9 @@ export function createNapiShmSyncClient( ): NapiShmSyncClient { const napi = loadIpcRuntimeNapi(options.customAddonPath); return new NapiShmSyncClient( - // Omit the slot id when not given so the native side self-allocates a free - // slot (kAutoClientId) instead of aliasing every client onto slot 0. - options.clientId === undefined - ? new napi.MsgpackClient(shmName) - : new napi.MsgpackClient(shmName, options.clientId), + // An undefined slot id makes the native side self-allocate a free slot + // (kAutoClientId) instead of aliasing every client onto slot 0. + new napi.MsgpackClient(shmName, options.clientId, options.serverPid), ); } diff --git a/ipc-runtime/ts/src/spawned_backend.ts b/ipc-runtime/ts/src/spawned_backend.ts index bffd0cf3da8..45d9543a181 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -1,6 +1,6 @@ import { type ChildProcess, spawn } from "node:child_process"; import { open, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { constants as osConstants, tmpdir } from "node:os"; import { join } from "node:path"; import { threadId } from "node:worker_threads"; import { @@ -618,7 +618,7 @@ export class SpawnedProcessBackendSync implements IpcClientSync { try { const client = await Promise.race([ - connectShmSyncClient(options, ipcPath), + connectShmSyncClient(options, ipcPath, child.pid), childReadyFailure, ]); return new SpawnedProcessBackendSync( @@ -654,14 +654,20 @@ export class SpawnedProcessBackendSync implements IpcClientSync { 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) { + // The call holds the main thread, so the child's exit event hasn't run: + // the native client reports the death itself. Report it with the log + // path rather than as an opaque transport error. + const exit = + nativeServerExit(err) ?? + (this.child.exitCode !== null || this.child.signalCode !== null + ? { code: this.child.exitCode, signal: this.child.signalCode } + : undefined); + if (exit) { throw new IpcProcessExitedError( - `${this.options.binaryName} exited unexpectedly (code=${this.child.exitCode}, signal=${this.child.signalCode})` + + `${this.options.binaryName} exited unexpectedly (code=${exit.code}, signal=${exit.signal})` + (this.logPath !== undefined ? `; see logs: ${this.logPath}` : ""), - this.child.exitCode, - this.child.signalCode, + exit.code, + exit.signal, this.logPath, ); } @@ -686,10 +692,32 @@ export class SpawnedProcessBackendSync implements IpcClientSync { } } +/** The server exit a native sync shm call reported, if that is why it failed. */ +function nativeServerExit( + err: unknown, +): { code: number | null; signal: NodeJS.Signals | null } | undefined { + const e = err as { + serverExited?: boolean; + exitCode?: number | null; + signal?: number | null; + }; + if (!e || e.serverExited !== true) { + return undefined; + } + const signal = + e.signal == null + ? null + : ((Object.entries(osConstants.signals).find( + ([, n]) => n === e.signal, + )?.[0] as NodeJS.Signals | undefined) ?? null); + return { code: e.exitCode ?? null, signal }; +} + /** Retry attaching the sync shm client until the server has created its rings. */ async function connectShmSyncClient( options: SpawnedProcessBackendOptions, ipcPath: string, + serverPid: number | undefined, ): Promise { const deadline = Date.now() + (options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS); @@ -699,6 +727,7 @@ async function connectShmSyncClient( return createNapiShmSyncClient(ipcPath.replace(/\.shm$/, ""), { clientId: options.clientId, customAddonPath: options.napiPath, + serverPid, }); } catch (err) { lastError = err; From a916793f08ce37c74fbfc0d7d73faec5bfc2200f Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:19:37 +0000 Subject: [PATCH 19/19] chore(bb): 900s timeout for the ASAN root rollup Tampering test It takes ~250s on an idle machine, but timed out at 600s while the bb cross-object builds compiled alongside the tests: the run queue held 150-350 threads on 64 cores, and the test is pinned to all of them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Prk3bcqkopGENJUuUpT52q --- barretenberg/cpp/bootstrap.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 1c38af9a770..58478b1f6a7 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -368,9 +368,13 @@ function test_cmds_asan { ["ultra_honk_tests"]="MegaHonkTests/0.Basic" ["dsl_tests"]="HonkRecursionConstraintTestWithoutPredicate/2.Tampering" ) + # The root rollup Tampering test takes ~250s, but over 600s when it shares the machine with the cross builds. + declare -A asan_timeouts=( + ["dsl_tests"]=":TIMEOUT=900s" + ) for bin_name in "${!asan_tests[@]}"; do local filter=${asan_tests[$bin_name]} - echo -e "$prefix barretenberg/cpp/build-asan-fast/bin/$bin_name --gtest_filter=$filter" + echo -e "$prefix${asan_timeouts[$bin_name]:-} barretenberg/cpp/build-asan-fast/bin/$bin_name --gtest_filter=$filter" done }