diff --git a/barretenberg/.gitignore b/barretenberg/.gitignore index dca89bdb0516..050c2d1512dc 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/cpp/.gitignore b/barretenberg/cpp/.gitignore index 430b399a98c4..03af6faa5ebb 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/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 2a3fb4ae09e3..62aeb3ea129e 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -128,7 +128,6 @@ if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) add_subdirectory(barretenberg/world_state) # 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/wsdb) add_subdirectory(barretenberg/vm2_wsdb) add_subdirectory(barretenberg/cdb) @@ -234,7 +233,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) add_library( diff --git a/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/api/CMakeLists.txt index 9fcf0636252a..185425b29e6a 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 bc50a2749129..4bb7393fed32 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,299 +22,90 @@ 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; } - // 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)); + // The generated dispatch responds through a callback; handlers here + // complete synchronously, so capture the response and write it out. + std::vector response; + handler(buffer, [&response](std::vector r) { response = std::move(r); }); - // Serialize the response - msgpack::sbuffer response_buffer; - msgpack::pack(response_buffer, response); - - // 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 8f4a22cffa7a..5ed6fd7b6873 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 fa6b7858a1eb..e0f3d89f9ddb 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 afe3529ad653..eb7fb3e16999 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 8baaef5b0276..e7a90defb53a 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 000000000000..20dab049c505 --- /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 000000000000..64f1c2c36cbe --- /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]" + } + }, + "PedersenCommit": { + "request": { + "inputs": "fr[]", + "hash_index": "u32" + }, + "response": { + "point": "GrumpkinPoint" + } + }, + "PedersenHash": { + "request": { + "inputs": "fr[]", + "hash_index": "u32" + }, + "response": { + "hash": "fr" + } + }, + "PedersenHashBuffer": { + "request": { + "input": "bytes", + "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 000000000000..f7eb958f81b4 --- /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 74210e06aa01..0d9b39960dd2 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 0415f245592e..096dd732f744 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi.test.cpp @@ -45,7 +45,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 000000000000..d5f1c34ce8b0 --- /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 4a59f76cd339..000000000000 --- 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 1fecf8860695..000000000000 --- 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 000000000000..19b847adefbd --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp @@ -0,0 +1,570 @@ +/** + * @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_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_pedersen_hash_buffer(BBApiRequest& /*ctx*/, + wire::BbPedersenHashBuffer&& cmd, + Responder respond) +{ + crypto::GeneratorContext gctx; + gctx.offset = static_cast(cmd.hash_index); + auto hash = crypto::pedersen_hash::hash_buffer(cmd.input, 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 000000000000..1afd873b9cea --- /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_pedersen_commit(BBApiRequest& ctx, + wire::BbPedersenCommit&& cmd, + Responder respond); +void handle_pedersen_hash(BBApiRequest& ctx, + wire::BbPedersenHash&& cmd, + Responder respond); +void handle_pedersen_hash_buffer(BBApiRequest& ctx, + wire::BbPedersenHashBuffer&& 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 000000000000..f40bae2e2d75 --- /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 000000000000..08f4df834993 --- /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 000000000000..5ddaa3c3dfbb --- /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 000000000000..166ccdaeb470 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_wire_convert.hpp @@ -0,0 +1,286 @@ +#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 { + +// --------------------------------------------------------------------------- +// 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 dd74f8dcf759..a977a4f308dd 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp @@ -1,41 +1,39 @@ #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" -#ifndef NO_MULTITHREADING -#include -#endif +#include "barretenberg/bbapi/generated/bb_dispatch.hpp" +#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; +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) +WASM_EXPORT void ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len) { -#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 + static auto handler = bb::bbapi::make_bb_handler(global_request); + std::vector response; + handler(std::span(input, input_len), + [&response](std::vector r) { response = std::move(r); }); + // 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(); } - -} // namespace bb::bbapi - -// Use CBIND macro to export the bbapi function for WASM -CBIND_NOSCHEMA(bbapi, bb::bbapi::bbapi) diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp index 7b7878d4412a..1c9794db74a2 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 e38f71320b2c..4fc6a49c5979 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp @@ -1,62 +1,77 @@ -#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 that exceptions thrown during command execution are caught and converted to ErrorResponse -TEST(CBind, CatchesExceptionAndReturnsErrorResponse) +// 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) { - // 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) + // 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); - Command command = std::move(cmd); + uint8_t* out = nullptr; + size_t out_len = 0; + ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &out, &out_len); - // Call bbapi - exception should be caught and converted to ErrorResponse - CommandResponse response = bbapi(std::move(command)); + 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; +} - // 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; +} // namespace - // 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"; +#ifndef BB_NO_EXCEPTIONS - 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'; - } +// 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 + + EXPECT_EQ(ffi_response_type("BbSrsInitSrs", 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; + wire::BbPoseidon2Hash cmd; + cmd.inputs = { std::array{} }; // hash of a single zero field - // Call bbapi - should return success response - CommandResponse response = bbapi(std::move(command)); - - // 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 2103fa9f89f8..5ed1e9e6398e 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 47ece59c46a8..000000000000 --- 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 6a9b8afcf011..000000000000 --- 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 99e29c91e898..000000000000 --- 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 387f0efa27c0..000000000000 --- 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 26be91bde7c3..000000000000 --- 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 49d6c5da4df9..000000000000 --- 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 a14879efe4b2..000000000000 --- 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 4833d7029d4c..000000000000 --- 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 0684ebeea898..000000000000 --- 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 51922b14f9a1..000000000000 --- 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 f6b52b9c5524..000000000000 --- 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 22208e5a61c3..000000000000 --- 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 e3d2f4031164..000000000000 --- 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 e8c307134b46..000000000000 --- 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 384f7ea0dc10..000000000000 --- 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 df1d6257ed86..000000000000 --- 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 0587ab1f3eef..000000000000 --- 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 33a2479ea483..000000000000 --- 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 709deca3159c..000000000000 --- 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 0fc8140a0e1f..000000000000 --- 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 57c1a01e3d36..000000000000 --- 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 3fe15b6d43a0..000000000000 --- 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 42e60b540308..000000000000 --- 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 42ee599d9d19..000000000000 --- 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 62c8bf69cbd3..000000000000 --- 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 1d1dd941a56f..000000000000 --- 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/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt index 736f67b83f2b..639e243162f6 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt @@ -27,7 +27,7 @@ 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 lmdblib) +target_link_libraries(nodejs_module PRIVATE lmdblib) # On macOS, Node.js N-API symbols are provided by the runtime, not at link time if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp index 1a5a0d0dd396..f70b5e6a0918 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp @@ -1,15 +1,9 @@ #include "barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp" -#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, "LMDBStore"), bb::nodejs::lmdb_store::LMDBStoreWrapper::get_class(env)); - 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; } 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 5b46c505f5ed..000000000000 --- 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 580bde934132..000000000000 --- 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 b72114a00abf..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.cpp +++ /dev/null @@ -1,99 +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 - 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 e426376d9636..000000000000 --- 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/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 709713beeede..025ac2915bc9 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 7c86a5d588bc..c0b49c1de8fa 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 256d87f64d31..000000000000 --- 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 35a286abc103..000000000000 --- 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 456e703e016c..742d9fc8170c 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 fe1aab37fbf2..5ec39b9b5a36 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 177c295090f7..d9e3bbb97142 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 f637a2a82a56..72ded0456c7e 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 75eef08387c3..000000000000 --- 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 22a4243d92ef..000000000000 --- 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 2e3cd248061d..000000000000 --- 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 726ac4a9ad2f..000000000000 --- 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 000000000000..09587a458e82 --- /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 000000000000..b5b309834805 --- /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 000000000000..99643afb6ec2 --- /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 000000000000..ea0d531d903b --- /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 ffccd08c514d..949400e0f51e 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 6ad04b26ecd7..000000000000 --- 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 8c05fe8e50be..42c05dc8720c 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 02cf98907bd4..3936ae9d71be 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 5ab317a5d8bd..f0da98a91b16 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 9399d0021b0f..ba0a722c837f 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 af9ebe7df71f..7c88bc2f15a9 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 000000000000..0d4529771b2f --- /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 895d59dad63e..bd905c6953a0 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 59c9453bd6e0..2f7fa274213c 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 a07a969829e9..2e50525bd178 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/generate.sh b/barretenberg/ts/bb.js/scripts/generate.sh new file mode 100755 index 000000000000..5bb35ba4ca95 --- /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 ab612a201dc1..bd622e087e61 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 217d2e5ff0f1..44a6209fa0dd 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 }); } barretenbergSyncSingleton = await barretenbergSyncSingletonPromise; 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 213a2c2712aa..e9fd64c59ea4 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 c6c4aeab6161..e7a5a91393a9 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/node/index.ts b/barretenberg/ts/bb.js/src/bb_backends/node/index.ts index d02b4d2c76c5..b19b1ba43495 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) @@ -34,12 +34,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: @@ -80,12 +82,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 05ddea618a49..000000000000 --- 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 ef97be2e8899..c0ce6472a386 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 1f8eb77e4985..68608fba1ac9 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 c1c169968d65..9dbfc3ba3f4b 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 @@ -70,13 +70,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/, ); }); }); 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 c09bb7cfd224..bc8e2f506475 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,291 +1,50 @@ -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 { 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; - /** - * 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 constructor( - private process: ChildProcess, - socket: net.Socket, - private logger: (msg: string) => void, - ) { - this.socket = socket; - - this.process.on('error', err => { - this.failAllPending(new Error(`Native backend process error: ${err.message}`)); - }); - - this.process.on('exit', (code, signal) => { - const errorMsg = - 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.failAllPending(new Error(errorMsg)); - }); - - socket.on('data', (chunk: Buffer) => { - this.handleData(chunk); - }); - - socket.on('error', err => { - this.failAllPending(new Error(`Socket error: ${err.message}`)); - }); - - socket.on('end', () => { - this.failAllPending(new Error('Socket connection ended unexpectedly')); - }); - } + private constructor(private backend: SpawnedProcessBackend) {} - /** - * 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, logger?: (msg: string) => void, unref?: boolean, ): Promise { - // 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, - }); - - // 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) { - throw new Error(`Native backend process error: ${(err as Error).message}`); - } - - try { - const socket = await this.waitForSocketAndConnect(socketPath, proc); - return new BarretenbergNativeSocketAsyncBackend(proc, socket, logger ?? (() => {})); - } catch (err) { - proc.kill('SIGKILL'); - throw err; - } - } - - 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); + 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, }); - } - - 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; - } - } - - 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; - } - } - } + return new BarretenbergNativeSocketAsyncBackend(backend); } call(inputBuffer: Uint8Array): Promise { - if (!this.socket) { - return Promise.reject(new Error('Socket not connected')); - } - - 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) { - this.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); - this.socket!.write(lengthBuf); - this.socket!.write(inputBuffer); - }); + return this.backend.call(inputBuffer); } destroy(): Promise { - this.failAllPending(new Error('Backend connection closed')); - // Don't try to unlink socket - bb owns it and will clean it up - this.process.kill('SIGTERM'); - this.process.removeAllListeners(); - return Promise.resolve(); + 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 d481de87d4e3..5d08a5456686 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 0e4cc335f910..05a63d2519fe 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 1a4785bc1720..3c90963fd11c 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 9cd309aa9801..000000000000 --- 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 ed584e830654..000000000000 --- 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 4f7ceba46ab6..000000000000 --- 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 27e3aa91d08b..000000000000 --- 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 972a23a4ed6d..000000000000 --- 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 733fb86dc1a5..000000000000 --- 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 411b7172dc22..000000000000 --- 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 4a8fc4367750..571793f8c41e 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 cce5c066b393..b4089bf0fbb5 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 --experimental-transform-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 2f1bf139ced6..ccee440319c3 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 c0f3e4264fb5..3ff3949d12c6 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 98b1d6442fe3..876a533df3c5 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 100b0e1330d4..5315a65e9ac2 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 2666e1e3c176..77a4baadf70c 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 0e5d3d3d85eb..e09f09843fd8 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 127ca6f45ced..6c06d6c011a5 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 77256e5ecfb7..1a1459d1c649 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 87d78583fc1f..b0a6932c61ab 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 b2d6692c6aaa..bbd45541639d 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 b57760967e2c..25ddaa6b11f0 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 70981fdb4e93..4077acad15a2 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 000000000000..cce8b70fbd89 --- /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 000000000000..438089fa6263 --- /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 000000000000..ae6d234f8f50 --- /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 000000000000..3cd325779526 --- /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 000000000000..331cc0431a84 --- /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 4943e50ba609..d62741ae55a5 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 8c5d690bf708..c6d41ab37fb8 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 3adde5e9f049..8af262a736a8 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 e284425551a1..b24ddc3991af 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 c4cd25a5b302..de6cde60365e 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 b83ba3cc64b8..03a28355ea11 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 ebfc6cea6da1..3a095cfd47fc 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.ts b/ipc-runtime/ts/src/spawned_backend.ts index 4943eb288674..f11d378a22dc 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -4,8 +4,11 @@ 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 { + 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 +36,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 +72,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. */ @@ -235,9 +365,7 @@ export class SpawnedProcessBackend implements IpcClientAsync { /** 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 +376,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 +390,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 +408,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 +465,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 +476,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 +525,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 de67744cd419..6771954e702c 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 4c6173572223..2bdc09fdc640 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 e93af3f31444..7415271520cc 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"); +}