From e32e14b5230c172f9650a8c1827f403cdc1d7b3f Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:25:09 +0000 Subject: [PATCH 1/6] feat(bb.js): generated bb.js-api package with an in-process wasm transport bb.js reached bb's wasm through roughly 1,400 lines of hand-written plumbing (comlink workers, WASI stubs, a heap allocator, pako inflation, base64-embedded modules) sitting next to a generated client. This moves "how a client reaches a service module" into ipc-codegen/ipc-runtime, generates the bb API as its own package, and leaves bb.js a facade over it. ipc-runtime gains a generic wasm FFI backend for any WASI reactor implementing the contract (_ipc_ffi_entry plus its allocator pair): an async backend that runs the instance in a worker with wasi threads on further workers, a synchronous one that runs on the calling thread, node and browser entries chosen by export condition, streaming compilation with gzip support, and a WASI shim. Workers are spawned through factories holding the literal new URL(..., import.meta.url) that bundlers must see. ipc-codegen emits the FFI entry itself for C++ and Rust, with symbols carrying the service name so several services can link into one binary, and its TS --package gains a wasm transport. The generated package owns backend selection: the spawned process when the binary resolves, else the wasm module, with an explicit choice forcing one and no silent downgrade. A failed command raises a generated error class rather than an injected factory. bb's wasm is now a plain WASI reactor. It imported a logger, an abort hook and a thread count because barretenberg/wasi stubbed out the WASI calls the old host did not implement; ipc-runtime implements them, so the stubs go and the three functions are defined against stderr, the environment and proc_exit instead. bb.js keeps its public API. Its own uds/shm/wasm backend files, the base64 wasm embedding and the bundled bb binary are gone; bb resolves through bb.js-api's per-platform packages. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 10 +- barretenberg/acir_tests/package.json | 7 +- barretenberg/acir_tests/yarn.lock | 66 +- barretenberg/cpp/src/CMakeLists.txt | 9 +- barretenberg/cpp/src/barretenberg/bb/cli.cpp | 1 - .../cpp/src/barretenberg/bbapi/CMakeLists.txt | 12 +- .../bbapi/bb_curve_constants.json | 36 - .../cpp/src/barretenberg/bbapi/bb_schema.json | 4 + .../src/barretenberg/bbapi/bbapi_handlers.cpp | 41 + .../src/barretenberg/bbapi/bbapi_handlers.hpp | 1 + .../cpp/src/barretenberg/bbapi/c_bind.cpp | 54 -- .../cpp/src/barretenberg/bbapi/c_bind.hpp | 13 - .../bbapi/c_bind_exception.test.cpp | 14 +- .../src/barretenberg/bbapi/ffi_dispatcher.cpp | 32 + .../src/barretenberg/common/wasm_export.hpp | 7 - .../barretenberg/env/hardware_concurrency.hpp | 2 +- .../cpp/src/barretenberg/env/logstr.hpp | 6 +- .../barretenberg/env/throw_or_abort_impl.hpp | 6 +- .../cpp/src/barretenberg/wasi/wasi_stubs.cpp | 260 ------ .../cpp/src/barretenberg/wasi/wasm_env.cpp | 45 + barretenberg/ts/.gitignore | 4 +- barretenberg/ts/bb.js/.prettierignore | 4 - barretenberg/ts/bb.js/README.md | 33 +- barretenberg/ts/bb.js/bootstrap.sh | 36 +- barretenberg/ts/bb.js/eslint.config.js | 10 +- barretenberg/ts/bb.js/package.json | 14 +- .../ts/bb.js/scripts/browser_postprocess.sh | 8 - .../ts/bb.js/scripts/cjs_postprocess.sh | 15 +- barretenberg/ts/bb.js/scripts/copy_cross.sh | 33 - barretenberg/ts/bb.js/scripts/copy_native.sh | 17 - barretenberg/ts/bb.js/scripts/copy_wasm.sh | 24 - barretenberg/ts/bb.js/scripts/generate.sh | 21 - barretenberg/ts/bb.js/src/async_map/index.ts | 15 - .../ts/bb.js/src/backends/browser/index.ts | 59 ++ .../src/{bb_backends => backends}/index.ts | 11 +- .../ts/bb.js/src/backends/node/index.ts | 118 +++ .../node/native_shm.test.ts | 8 +- .../ts/bb.js/src/backends/wasm.test.ts | 72 ++ .../ts/bb.js/src/barretenberg/backend.ts | 4 +- .../chonk_pinned_inputs.test.ts | 0 .../ts/bb.js/src/barretenberg/errors.test.ts | 63 ++ .../ts/bb.js/src/barretenberg/index.ts | 49 +- .../bb.js/src/barretenberg/pedersen.test.ts | 2 +- .../src/barretenberg/poseidon.bench.test.ts | 8 - .../bb.js/src/barretenberg/poseidon.test.ts | 2 +- .../testing}/timer.ts | 0 .../barretenberg-threads.wasm.gz | 1 - .../barretenberg_wasm/barretenberg.wasm.gz | 1 - .../barretenberg_wasm_base/index.ts | 118 --- .../factory/browser/index.ts | 9 - .../factory/browser/main.worker.ts | 7 - .../factory/node/index.ts | 19 - .../factory/node/main.worker.ts | 11 - .../barretenberg_wasm_main/heap_allocator.ts | 72 -- .../barretenberg_wasm_main/index.ts | 264 ------ .../factory/browser/index.ts | 9 - .../factory/browser/thread.worker.ts | 7 - .../factory/node/index.ts | 19 - .../factory/node/thread.worker.ts | 13 - .../barretenberg_wasm_thread/index.ts | 49 -- .../browser/barretenberg-threads.ts | 3 - .../fetch_code/browser/barretenberg.ts | 3 - .../fetch_code/browser/index.ts | 34 - .../src/barretenberg_wasm/fetch_code/index.ts | 1 - .../fetch_code/node/index.ts | 33 - .../fetch_code/wasm-module.d.ts | 4 - .../helpers/browser/index.ts | 55 -- .../src/barretenberg_wasm/helpers/index.ts | 1 - .../barretenberg_wasm/helpers/node/index.ts | 63 -- .../helpers/node/node_endpoint.ts | 28 - .../bb.js/src/barretenberg_wasm/index.test.ts | 47 - .../ts/bb.js/src/barretenberg_wasm/index.ts | 21 - .../ts/bb.js/src/bb_backends/browser/index.ts | 49 -- .../bb.js/src/bb_backends/browser/platform.ts | 7 - .../ts/bb.js/src/bb_backends/errors.ts | 29 - .../ts/bb.js/src/bb_backends/interface.ts | 35 - .../ts/bb.js/src/bb_backends/node/index.ts | 109 --- .../bb.js/src/bb_backends/node/native_shm.ts | 61 -- .../src/bb_backends/node/native_shm_async.ts | 60 -- .../bb_backends/node/native_socket.test.ts | 230 ----- .../src/bb_backends/node/native_socket.ts | 52 -- .../ts/bb.js/src/bb_backends/node/platform.ts | 130 --- barretenberg/ts/bb.js/src/bb_backends/wasm.ts | 108 --- .../src/bbapi/exception_handling.test.ts | 52 -- barretenberg/ts/bb.js/src/bbapi_exception.ts | 13 - barretenberg/ts/bb.js/src/benchmark/index.ts | 27 - barretenberg/ts/bb.js/src/bin/index.ts | 3 +- barretenberg/ts/bb.js/src/crs/net_crs.ts | 2 +- .../src/{retry/index.ts => crs/retry.ts} | 0 .../ts/bb.js/src/curve_constants.test.ts | 55 ++ barretenberg/ts/bb.js/src/curve_constants.ts | 71 ++ barretenberg/ts/bb.js/src/index.html | 9 - barretenberg/ts/bb.js/src/index.ts | 39 +- .../ts/bb.js/src/{proof/index.ts => proof.ts} | 0 barretenberg/ts/bootstrap.sh | 137 ++- barretenberg/ts/package.json | 2 + barretenberg/ts/yarn.lock | 62 +- ipc-codegen/README.md | 33 +- ipc-codegen/SCHEMA_SPEC.md | 27 + ipc-codegen/bootstrap.sh | 4 + ipc-codegen/echo_example/cpp/CMakeLists.txt | 14 +- ipc-codegen/echo_example/cpp/bootstrap.sh | 3 +- ipc-codegen/echo_example/cpp/src/echo_ffi.cpp | 20 + .../echo_example/cpp/src/echo_handlers.cpp | 52 ++ .../echo_example/cpp/src/echo_handlers.hpp | 32 + .../echo_example/cpp/src/echo_server.cpp | 56 +- ipc-codegen/echo_example/cpp/src/ffi_test.cpp | 119 +++ ipc-codegen/echo_example/rust/Cargo.toml | 7 +- ipc-codegen/echo_example/rust/bootstrap.sh | 10 +- .../echo_example/rust/src/echo_server.rs | 41 +- ipc-codegen/echo_example/rust/src/handler.rs | 44 + ipc-codegen/echo_example/rust/src/lib.rs | 10 + .../echo_example/rust/tests/ffi_roundtrip.rs | 28 + .../echo_example/ts_package/.gitignore | 9 +- ipc-codegen/echo_example/ts_package/README.md | 36 +- .../echo_example/ts_package/bootstrap.sh | 14 +- .../ts_package/src/package_test.ts | 67 +- .../echo_example/zig/src/ffi_check.zig | 12 +- ipc-codegen/src/cpp_codegen.ts | 127 +++ ipc-codegen/src/generate.ts | 228 +++-- ipc-codegen/src/rust_codegen.ts | 229 ++++- ipc-codegen/src/typescript_codegen.ts | 41 +- ipc-codegen/src/typescript_package_codegen.ts | 827 +++++++++++++----- ipc-codegen/src/zig_codegen.ts | 43 + ipc-codegen/templates/rust/ffi_backend.rs | 128 --- ipc-codegen/templates/zig/ffi_backend.zig | 34 - ipc-runtime/README.md | 21 + ipc-runtime/ts/package.json | 30 +- .../ts/scripts/prepare_arch_packages.mjs | 73 ++ ipc-runtime/ts/src/backend_registry.ts | 54 ++ ipc-runtime/ts/src/index.ts | 15 + ipc-runtime/ts/src/service.ts | 237 +++++ ipc-runtime/ts/src/spawned_backend.ts | 46 +- ipc-runtime/ts/src/wasm/backend.ts | 452 ++++++++++ ipc-runtime/ts/src/wasm/browser/index.ts | 40 + .../ts/src/wasm/browser/main.worker.ts | 16 + ipc-runtime/ts/src/wasm/browser/platform.ts | 49 ++ .../ts/src/wasm/browser/thread.worker.ts | 5 + ipc-runtime/ts/src/wasm/entry.ts | 46 + ipc-runtime/ts/src/wasm/host.ts | 277 ++++++ ipc-runtime/ts/src/wasm/main_worker.ts | 77 ++ ipc-runtime/ts/src/wasm/module_source.ts | 125 +++ ipc-runtime/ts/src/wasm/node/index.ts | 33 + ipc-runtime/ts/src/wasm/node/main.worker.ts | 10 + ipc-runtime/ts/src/wasm/node/platform.ts | 57 ++ ipc-runtime/ts/src/wasm/node/thread.worker.ts | 5 + ipc-runtime/ts/src/wasm/platform.ts | 28 + ipc-runtime/ts/src/wasm/service.ts | 46 + ipc-runtime/ts/src/wasm/thread_worker.ts | 56 ++ ipc-runtime/ts/src/wasm/wasi_shim.ts | 170 ++++ ipc-runtime/ts/src/wasm_engine.test.ts | 377 ++++++++ ipc-runtime/ts/src/wasm_engine_stub.worker.ts | 2 + ipc-runtime/ts/src/wasm_module_source.test.ts | 54 ++ ipc-runtime/ts/tsconfig.json | 2 +- ipc-runtime/ts/yarn.lock | 2 + ...js-api-alongside-bb.js-aztec-package.patch | 48 + native-packages/wsdb/bootstrap.sh | 4 + native-packages/wsdb/yarn.lock | 2 + 158 files changed, 5098 insertions(+), 3146 deletions(-) delete mode 100644 barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json delete mode 100644 barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp create mode 100644 barretenberg/cpp/src/barretenberg/bbapi/ffi_dispatcher.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/wasi/wasi_stubs.cpp create mode 100644 barretenberg/cpp/src/barretenberg/wasi/wasm_env.cpp delete mode 100755 barretenberg/ts/bb.js/scripts/copy_cross.sh delete mode 100755 barretenberg/ts/bb.js/scripts/copy_native.sh delete mode 100755 barretenberg/ts/bb.js/scripts/copy_wasm.sh delete mode 100755 barretenberg/ts/bb.js/scripts/generate.sh delete mode 100644 barretenberg/ts/bb.js/src/async_map/index.ts create mode 100644 barretenberg/ts/bb.js/src/backends/browser/index.ts rename barretenberg/ts/bb.js/src/{bb_backends => backends}/index.ts (88%) create mode 100644 barretenberg/ts/bb.js/src/backends/node/index.ts rename barretenberg/ts/bb.js/src/{bb_backends => backends}/node/native_shm.test.ts (87%) create mode 100644 barretenberg/ts/bb.js/src/backends/wasm.test.ts rename barretenberg/ts/bb.js/src/{bbapi => barretenberg}/chonk_pinned_inputs.test.ts (100%) create mode 100644 barretenberg/ts/bb.js/src/barretenberg/errors.test.ts rename barretenberg/ts/bb.js/src/{benchmark => barretenberg/testing}/timer.ts (100%) delete mode 120000 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz delete mode 120000 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_base/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/heap_allocator.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/node/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/wasm-module.d.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/browser/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/index.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/node_endpoint.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/index.test.ts delete mode 100644 barretenberg/ts/bb.js/src/barretenberg_wasm/index.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/browser/index.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/errors.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/interface.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/index.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/node/platform.ts delete mode 100644 barretenberg/ts/bb.js/src/bb_backends/wasm.ts delete mode 100644 barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts delete mode 100644 barretenberg/ts/bb.js/src/bbapi_exception.ts delete mode 100644 barretenberg/ts/bb.js/src/benchmark/index.ts rename barretenberg/ts/bb.js/src/{retry/index.ts => crs/retry.ts} (100%) create mode 100644 barretenberg/ts/bb.js/src/curve_constants.test.ts create mode 100644 barretenberg/ts/bb.js/src/curve_constants.ts delete mode 100644 barretenberg/ts/bb.js/src/index.html rename barretenberg/ts/bb.js/src/{proof/index.ts => proof.ts} (100%) create mode 100644 ipc-codegen/echo_example/cpp/src/echo_ffi.cpp create mode 100644 ipc-codegen/echo_example/cpp/src/echo_handlers.cpp create mode 100644 ipc-codegen/echo_example/cpp/src/echo_handlers.hpp create mode 100644 ipc-codegen/echo_example/cpp/src/ffi_test.cpp create mode 100644 ipc-codegen/echo_example/rust/src/handler.rs create mode 100644 ipc-codegen/echo_example/rust/tests/ffi_roundtrip.rs delete mode 100644 ipc-codegen/templates/rust/ffi_backend.rs delete mode 100644 ipc-codegen/templates/zig/ffi_backend.zig create mode 100755 ipc-runtime/ts/scripts/prepare_arch_packages.mjs create mode 100644 ipc-runtime/ts/src/backend_registry.ts create mode 100644 ipc-runtime/ts/src/service.ts create mode 100644 ipc-runtime/ts/src/wasm/backend.ts create mode 100644 ipc-runtime/ts/src/wasm/browser/index.ts create mode 100644 ipc-runtime/ts/src/wasm/browser/main.worker.ts create mode 100644 ipc-runtime/ts/src/wasm/browser/platform.ts create mode 100644 ipc-runtime/ts/src/wasm/browser/thread.worker.ts create mode 100644 ipc-runtime/ts/src/wasm/entry.ts create mode 100644 ipc-runtime/ts/src/wasm/host.ts create mode 100644 ipc-runtime/ts/src/wasm/main_worker.ts create mode 100644 ipc-runtime/ts/src/wasm/module_source.ts create mode 100644 ipc-runtime/ts/src/wasm/node/index.ts create mode 100644 ipc-runtime/ts/src/wasm/node/main.worker.ts create mode 100644 ipc-runtime/ts/src/wasm/node/platform.ts create mode 100644 ipc-runtime/ts/src/wasm/node/thread.worker.ts create mode 100644 ipc-runtime/ts/src/wasm/platform.ts create mode 100644 ipc-runtime/ts/src/wasm/service.ts create mode 100644 ipc-runtime/ts/src/wasm/thread_worker.ts create mode 100644 ipc-runtime/ts/src/wasm/wasi_shim.ts create mode 100644 ipc-runtime/ts/src/wasm_engine.test.ts create mode 100644 ipc-runtime/ts/src/wasm_engine_stub.worker.ts create mode 100644 ipc-runtime/ts/src/wasm_module_source.test.ts create mode 100644 labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch diff --git a/Makefile b/Makefile index f54fb0b9bd3..eb1d841e6fa 100644 --- a/Makefile +++ b/Makefile @@ -255,7 +255,7 @@ bb-cpp-full: bb-cpp bb-cpp-gcc bb-cpp-fuzzing bb-cpp-windows bb-cpp-asan bb-cpp- bb-ts: bb-cpp-wasm bb-cpp-wasm-threads bb-cpp-native ipc-runtime $(call build,$@,barretenberg/ts,build_bb_js) -# Copies the cross-compiles into bb.js. +# Copies the cross-compiled bb binaries into bb.js-api, which bb.js runs through. bb-ts-cross-copy: bb-ts bb-cpp-cross $(call build,$@,barretenberg/ts,cross_copy_bb_js) @@ -266,10 +266,10 @@ bb-ts-cross-copy: bb-ts bb-cpp-cross bb-avm-sim: ipc-codegen ipc-runtime bb-cpp-native bb-ts $(call build,$@,barretenberg/ts,build_bb_avm_sim) -# Ordered after bb-cdb for the same reason bb-cdb is ordered after bb-avm-sim: -# all three regenerate the same barretenberg/ts workspaces and install into the -# same node_modules. -bb-avm-sim-cross-copy: bb-avm-sim bb-cdb bb-cpp-cross +# Ordered after bb-cdb (and after bb-ts-cross-copy, which builds bb.js-api's cross copies) +# for the same reason bb-cdb is ordered after bb-avm-sim: they all regenerate the same +# barretenberg/ts workspaces and install into the same node_modules. +bb-avm-sim-cross-copy: bb-avm-sim bb-cdb bb-ts-cross-copy bb-cpp-cross $(call build,$@,barretenberg/ts,cross_copy_bb_avm_sim) # Generated @aztec-foundation/cdb server bindings. Ordered after bb-avm-sim rather than run diff --git a/barretenberg/acir_tests/package.json b/barretenberg/acir_tests/package.json index d0cddb1f814..cd28493ec97 100644 --- a/barretenberg/acir_tests/package.json +++ b/barretenberg/acir_tests/package.json @@ -16,6 +16,11 @@ "basic-ftp": "^5.2.0", "node-forge": "^1.3.2", "serialize-javascript": "^7.0.3", - "@aztec-foundation/ipc-runtime": "portal:../../ipc-runtime/ts" + "@aztec-foundation/ipc-runtime": "portal:../../ipc-runtime/ts", + "@aztec-foundation/bb.js-api": "portal:../ts/bb.js-api", + "@aztec-foundation/bb.js-api-darwin-arm64": "portal:../ts/bb.js-api/packages/bb.js-api-darwin-arm64", + "@aztec-foundation/bb.js-api-darwin-x64": "portal:../ts/bb.js-api/packages/bb.js-api-darwin-x64", + "@aztec-foundation/bb.js-api-linux-arm64": "portal:../ts/bb.js-api/packages/bb.js-api-linux-arm64", + "@aztec-foundation/bb.js-api-linux-x64": "portal:../ts/bb.js-api/packages/bb.js-api-linux-x64" } } diff --git a/barretenberg/acir_tests/yarn.lock b/barretenberg/acir_tests/yarn.lock index 91e9dd9eb49..0116166b998 100644 --- a/barretenberg/acir_tests/yarn.lock +++ b/barretenberg/acir_tests/yarn.lock @@ -12,12 +12,65 @@ __metadata: languageName: node linkType: hard +"@aztec-foundation/bb.js-api-darwin-arm64@portal:../ts/bb.js-api/packages/bb.js-api-darwin-arm64::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-darwin-arm64@portal:../ts/bb.js-api/packages/bb.js-api-darwin-arm64::locator=%40aztec%2Facir-tests%40workspace%3A." + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: soft + +"@aztec-foundation/bb.js-api-darwin-x64@portal:../ts/bb.js-api/packages/bb.js-api-darwin-x64::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-darwin-x64@portal:../ts/bb.js-api/packages/bb.js-api-darwin-x64::locator=%40aztec%2Facir-tests%40workspace%3A." + conditions: os=darwin & cpu=x64 + languageName: node + linkType: soft + +"@aztec-foundation/bb.js-api-linux-arm64@portal:../ts/bb.js-api/packages/bb.js-api-linux-arm64::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-linux-arm64@portal:../ts/bb.js-api/packages/bb.js-api-linux-arm64::locator=%40aztec%2Facir-tests%40workspace%3A." + conditions: os=linux & cpu=arm64 + languageName: node + linkType: soft + +"@aztec-foundation/bb.js-api-linux-x64@portal:../ts/bb.js-api/packages/bb.js-api-linux-x64::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-linux-x64@portal:../ts/bb.js-api/packages/bb.js-api-linux-x64::locator=%40aztec%2Facir-tests%40workspace%3A." + conditions: os=linux & cpu=x64 + languageName: node + linkType: soft + +"@aztec-foundation/bb.js-api@portal:../ts/bb.js-api::locator=%40aztec%2Facir-tests%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api@portal:../ts/bb.js-api::locator=%40aztec%2Facir-tests%40workspace%3A." + dependencies: + "@aztec-foundation/bb.js-api-darwin-arm64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-darwin-x64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-linux-arm64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-linux-x64": "npm:0.1.0" + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" + msgpackr: "npm:^1.11.2" + tslib: "npm:^2.4.0" + dependenciesMeta: + "@aztec-foundation/bb.js-api-darwin-arm64": + optional: true + "@aztec-foundation/bb.js-api-darwin-x64": + optional: true + "@aztec-foundation/bb.js-api-linux-arm64": + optional: true + "@aztec-foundation/bb.js-api-linux-x64": + optional: true + bin: + bb: ./dest/bin.js + languageName: node + linkType: soft + "@aztec-foundation/bb.js@portal:../../ts/bb.js::locator=bbjs-test%40workspace%3Abbjs-test": version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@portal:../../ts/bb.js::locator=bbjs-test%40workspace%3Abbjs-test" dependencies: + "@aztec-foundation/bb.js-api": "workspace:^" "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" - comlink: "npm:^4.4.1" commander: "npm:^12.1.0" idb-keyval: "npm:^6.2.1" msgpackr: "npm:^1.11.2" @@ -33,8 +86,8 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@portal:../../ts/bb.js::locator=browser-test-app%40workspace%3Abrowser-test-app" dependencies: + "@aztec-foundation/bb.js-api": "workspace:^" "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" - comlink: "npm:^4.4.1" commander: "npm:^12.1.0" idb-keyval: "npm:^6.2.1" msgpackr: "npm:^1.11.2" @@ -49,6 +102,8 @@ __metadata: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Facir-tests%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Facir-tests%40workspace%3A." + bin: + ipc-runtime-prepare-arch-packages: ./scripts/prepare_arch_packages.mjs languageName: node linkType: soft @@ -1604,13 +1659,6 @@ __metadata: languageName: node linkType: hard -"comlink@npm:^4.4.1": - version: 4.4.2 - resolution: "comlink@npm:4.4.2" - checksum: 10c0/38aa1f455cf08e94aaa8fc494fd203cc0ef02ece6c21404b7931ce17567e8a72deacddab98aa5650cfd78332ff24c34610586f6fb27fd19dc77e753ed1980deb - languageName: node - linkType: hard - "command-exists@npm:^1.2.8": version: 1.2.9 resolution: "command-exists@npm:1.2.9" diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 11967cf84c2..ad492f8682b 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -114,7 +114,12 @@ add_subdirectory(barretenberg/transcript) add_subdirectory(barretenberg/translator_vm) add_subdirectory(barretenberg/ultra_honk) add_subdirectory(barretenberg/vm2_stub) -add_subdirectory(barretenberg/wasi) +# wasi implements the reactor's env against wasm builtins (__builtin_wasm_memory_size +# and friends), which a native compiler does not have. Only the WASM branch below +# consumes wasi_objects, so there is nothing to build here otherwise. +if(WASM) + add_subdirectory(barretenberg/wasi) +endif() if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) # Mobile builds exclude these modules that aren't needed on mobile @@ -222,7 +227,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 ipc_ffi_entry(). +# Uses the core object list without lmdb/world_state — FFI consumers only need bb_ipc_ffi_entry(). # Built with -fvisibility=hidden; only WASM_EXPORT symbols remain visible. if(NOT WASM) # world_state_reference (the in-memory reference merkle DB and its bb_wsref_* C ABI) rides diff --git a/barretenberg/cpp/src/barretenberg/bb/cli.cpp b/barretenberg/cpp/src/barretenberg/bb/cli.cpp index 8260770625f..80b5fe86a04 100644 --- a/barretenberg/cpp/src/barretenberg/bb/cli.cpp +++ b/barretenberg/cpp/src/barretenberg/bb/cli.cpp @@ -26,7 +26,6 @@ #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" #include "barretenberg/common/get_bytecode.hpp" #include "barretenberg/common/memory_profile.hpp" diff --git a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt index e7a90defb53..423d12aea02 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt @@ -9,6 +9,8 @@ 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_ffi.cpp + ${BB_GEN_DIR}/bb_ffi.hpp ${BB_GEN_DIR}/bb_ipc_server.hpp ${BB_GEN_DIR}/bb_types.hpp ${BB_GEN_DIR}/ipc_codegen/msgpack_adaptor.hpp @@ -20,6 +22,7 @@ if(NOT FUZZING) ${IPC_CODEGEN_DIR}/src/*.ts ${IPC_CODEGEN_DIR}/templates/cpp/*.hpp ) + # --ffi emits the in-process entry (bb_ipc_ffi_entry) bb.js's wasm backend and barretenberg-rs call. add_custom_command( OUTPUT ${BB_GEN_OUTPUTS} COMMAND node --experimental-strip-types --experimental-transform-types --no-warnings @@ -28,11 +31,12 @@ if(NOT FUZZING) --lang cpp --out ${BB_GEN_DIR} --server + --ffi --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" + COMMENT "Generating BB IPC wire types + server dispatch + FFI entry from bb_schema.json" VERBATIM ) add_custom_target(bb_ipc_generated DEPENDS ${BB_GEN_OUTPUTS}) @@ -48,6 +52,12 @@ barretenberg_module(bbapi common chonk dsl crypto_poseidon2 crypto_pedersen_comm if(NOT FUZZING) add_dependencies(bbapi_objects bb_ipc_generated) + # The FFI entry is generated at build time, so the module's configure-time source glob misses it + # on a clean tree; list it explicitly (skipping the duplicate once the glob does see it). + get_target_property(BBAPI_SOURCES bbapi_objects SOURCES) + if(NOT "${BB_GEN_DIR}/bb_ffi.cpp" IN_LIST BBAPI_SOURCES) + target_sources(bbapi_objects PRIVATE ${BB_GEN_DIR}/bb_ffi.cpp) + endif() endif() # bbapi_tests needs vm2_stub to resolve dsl's AVM recursion constraint references diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json b/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json deleted file mode 100644 index 20dab049c50..00000000000 --- a/barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "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 index 0a305b611f8..33f1c250fad 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json +++ b/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json @@ -641,6 +641,10 @@ "response": { "dummy": "u8" } + }, + "Warmup": { + "request": {}, + "response": {} } } } diff --git a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp index 5ffa23edd91..ea877a42053 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.cpp @@ -30,6 +30,8 @@ #include "barretenberg/crypto/poseidon2/poseidon2_permutation.hpp" #include "barretenberg/crypto/schnorr/schnorr.hpp" #include "barretenberg/crypto/sha256/sha256.hpp" +#include "barretenberg/ecc/scalar_multiplication/scalar_multiplication.hpp" +#include "barretenberg/polynomials/polynomial.hpp" #include "barretenberg/srs/factories/bn254_crs_data.hpp" #include "barretenberg/srs/factories/bn254_g1_chunk_hashes.hpp" #include "barretenberg/srs/global_crs.hpp" @@ -576,4 +578,43 @@ void handle_srs_init_grumpkin_srs(BBApiRequest& /*ctx*/, respond.ok({}); } +void handle_warmup(BBApiRequest& /*ctx*/, wire::BbWarmup&& /*cmd*/, Responder respond) +{ + // Run the prover's hot loops once — field arithmetic, batch inversion, Poseidon2 and a Pippenger + // MSM — so a tiering JIT (wasm) has optimized them before real work runs through them. + constexpr size_t NUM_POINTS = 1 << 14; + constexpr size_t NUM_HASHES = 1 << 10; + + // Successive doublings of the generator: sums of distinct subsets of {2^i G} are distinct, so the + // MSM's addition tree never meets two equal points, which the unsafe (SRS-shaped) path cannot + // handle. Small multiples of G would (G + 4G = 2G + 3G). + std::vector points(NUM_POINTS); + points[0] = g1::one; + for (size_t i = 1; i < NUM_POINTS; ++i) { + points[i] = points[i - 1].dbl(); + } + g1::element::batch_normalize(points.data(), NUM_POINTS); + std::vector affine_points(NUM_POINTS); + for (size_t i = 0; i < NUM_POINTS; ++i) { + affine_points[i] = g1::affine_element(points[i]); + } + Polynomial scalars = Polynomial::random(NUM_POINTS); + g1::element commitment = scalar_multiplication::pippenger_unsafe(scalars, affine_points); + + std::vector elements(NUM_POINTS); + for (size_t i = 0; i < NUM_POINTS; ++i) { + elements[i] = scalars[i].sqr() + fr(static_cast(i + 1)); + } + fr::batch_invert(elements.data(), NUM_POINTS); + + fr acc = elements[0]; + for (size_t i = 0; i < NUM_HASHES; ++i) { + acc = crypto::Poseidon2::hash({ acc, elements[i] }); + } + if (commitment.is_point_at_infinity() || acc.is_zero()) { + throw_or_abort("Warmup: degenerate result"); + } + 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 index 58b6d1d6e26..efb27949937 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/bbapi_handlers.hpp @@ -170,4 +170,5 @@ void handle_chonk_batch_verifier_stop(BBApiRequest& ctx, void handle_srs_init_grumpkin_srs(BBApiRequest& ctx, wire::BbSrsInitGrumpkinSrs&& cmd, Responder respond); +void handle_warmup(BBApiRequest& ctx, wire::BbWarmup&& cmd, Responder respond); } // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp deleted file mode 100644 index 056d3504c50..00000000000 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "c_bind.hpp" -#include "barretenberg/bbapi/bbapi_handlers.hpp" -#include "barretenberg/bbapi/bbapi_shared.hpp" -#include "barretenberg/bbapi/generated/bb_dispatch.hpp" -#include -#include -#include -#include -#include -#include - -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) -bb::bbapi::BBApiRequest global_request; -} // namespace - -/** - * @brief In-process FFI/wasm entrypoint: the ipc-codegen FFI backend contract. - * - * 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. - */ -WASM_EXPORT void ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len) -{ - static auto handler = bb::bbapi::make_bb_handler(global_request); - std::vector response; - // The dispatch answers a malformed envelope, an unknown command and a - // throwing handler with an error frame, but an unpack or convert failure - // still escapes it. There is no frame to carry an exception across an FFI - // boundary, so anything that gets this far has to become a response. -#ifndef BB_NO_EXCEPTIONS - try { -#endif - handler(std::span(input, input_len), - [&response](std::vector r) { response = std::move(r); }); -#ifndef BB_NO_EXCEPTIONS - } catch (const std::exception& e) { - response = bb::bbapi::detail::make_error(e.what()); - } catch (...) { - response = bb::bbapi::detail::make_error("bbapi: unknown exception"); - } -#endif - // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) - auto* out = static_cast(aligned_alloc(64, response.size())); - std::memcpy(out, response.data(), response.size()); - *output = out; - *output_len = response.size(); -} diff --git a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp b/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp deleted file mode 100644 index 1c9794db74a..00000000000 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "barretenberg/common/wasm_export.hpp" -#include -#include - -/** - * @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 3ed45750e9d..13a873e2a64 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp +++ b/barretenberg/cpp/src/barretenberg/bbapi/c_bind_exception.test.cpp @@ -1,4 +1,4 @@ -#include "barretenberg/bbapi/c_bind.hpp" +#include "barretenberg/bbapi/generated/bb_ffi.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" @@ -26,11 +26,10 @@ template std::string ffi_response_type(const char* name, const Cm uint8_t* out = nullptr; size_t out_len = 0; - ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &out, &out_len); + bb_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &out, &out_len); auto oh = msgpack::unpack(reinterpret_cast(out), out_len); - // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) - free(out); + bb_ipc_ffi_free(out); auto arr = oh.get().via.array; EXPECT_EQ(arr.size, 2U); auto type = arr.ptr[0].as(); @@ -86,6 +85,13 @@ TEST(CBind, UnknownCommandReturnsErrorResponse) EXPECT_EQ(ffi_response_type("NoSuchCommand", cmd), "BbErrorResponse"); } +// Warmup runs the prover's hot loops on self-made inputs; they must be shaped so that +// the unsafe MSM path never meets two equal points. +TEST(CBind, WarmupSucceeds) +{ + EXPECT_EQ(ffi_response_type("BbWarmup", wire::BbWarmup{}), "BbWarmupResponse"); +} + #else TEST(CBind, ExceptionsDisabled) { diff --git a/barretenberg/cpp/src/barretenberg/bbapi/ffi_dispatcher.cpp b/barretenberg/cpp/src/barretenberg/bbapi/ffi_dispatcher.cpp new file mode 100644 index 00000000000..f75b5ae832f --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/bbapi/ffi_dispatcher.cpp @@ -0,0 +1,32 @@ +/** + * @file ffi_dispatcher.cpp + * @brief Names the context bb's handlers run over, for the in-process FFI entry. + * + * The generated dispatch is a template over the context type (make_bb_handler), so the + * generator never learns what that type is. The serve loop gets it from main(), which constructs a + * BBApiRequest and passes it in. A library has no main(), so this is where the FFI build names it. + * It is the FFI build's entry point rather than boilerplate, and the barretenberg-rs crate does the + * same thing one line at a time in its own lib.rs. + */ +#include "barretenberg/bbapi/bbapi_handlers.hpp" +#include "barretenberg/bbapi/bbapi_shared.hpp" + +#include "barretenberg/bbapi/generated/bb_ffi.hpp" + +namespace bb::bbapi { + +/** + * @brief The dispatcher behind the generated in-process FFI entry (bb_ipc_ffi_entry, see bb_ffi.hpp). + * + * 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. + */ +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static BBApiRequest request; + static AsyncDispatchHandler handler = make_bb_handler(request); + return handler; +} + +} // namespace bb::bbapi diff --git a/barretenberg/cpp/src/barretenberg/common/wasm_export.hpp b/barretenberg/cpp/src/barretenberg/common/wasm_export.hpp index cab57a90e1f..2020b5ee86d 100644 --- a/barretenberg/cpp/src/barretenberg/common/wasm_export.hpp +++ b/barretenberg/cpp/src/barretenberg/common/wasm_export.hpp @@ -9,11 +9,4 @@ #define ASYNC_WASM_EXPORT extern "C" __attribute__((visibility("default"))) #endif -#ifdef __wasm__ -// Allow linker to not link this -#define WASM_IMPORT(name) extern "C" __attribute__((import_module("env"), import_name(name))) -#else -#define WASM_IMPORT(name) extern "C" -#endif - using uint8_vec_vec_in_buf = uint8_t const*; diff --git a/barretenberg/cpp/src/barretenberg/env/hardware_concurrency.hpp b/barretenberg/cpp/src/barretenberg/env/hardware_concurrency.hpp index 44124b452a1..afa50cd1a6e 100644 --- a/barretenberg/cpp/src/barretenberg/env/hardware_concurrency.hpp +++ b/barretenberg/cpp/src/barretenberg/env/hardware_concurrency.hpp @@ -2,4 +2,4 @@ #include "barretenberg/common/wasm_export.hpp" #include -WASM_IMPORT("env_hardware_concurrency") uint32_t env_hardware_concurrency(); \ No newline at end of file +extern "C" uint32_t env_hardware_concurrency(); \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/env/logstr.hpp b/barretenberg/cpp/src/barretenberg/env/logstr.hpp index 6503b88dcdd..8f6c930a8cb 100644 --- a/barretenberg/cpp/src/barretenberg/env/logstr.hpp +++ b/barretenberg/cpp/src/barretenberg/env/logstr.hpp @@ -1,10 +1,8 @@ -// To be provided by the environment. -// For a WASM build, this is provided by the JavaScript environment. -// For a native build, this is provided in this module. +// Defined by this module for native builds, and by barretenberg/wasi for the wasm reactor. #include "barretenberg/common/wasm_export.hpp" #include -WASM_IMPORT("logstr") void logstr(char const*); +extern "C" void logstr(char const*); // Returns the peak RSS in bytes for the current process, or 0 on failure / unsupported platform. std::size_t peak_rss_bytes(); diff --git a/barretenberg/cpp/src/barretenberg/env/throw_or_abort_impl.hpp b/barretenberg/cpp/src/barretenberg/env/throw_or_abort_impl.hpp index 000c950a0fd..f5587b3537b 100644 --- a/barretenberg/cpp/src/barretenberg/env/throw_or_abort_impl.hpp +++ b/barretenberg/cpp/src/barretenberg/env/throw_or_abort_impl.hpp @@ -1,6 +1,4 @@ -// To be provided by the environment. -// For a WASM build, this is provided by the JavaScript environment. -// For a native build, this is provided in this module. +// Defined by this module for native builds, and by barretenberg/wasi for the wasm reactor. #include "barretenberg/common/wasm_export.hpp" -WASM_IMPORT("throw_or_abort_impl") void throw_or_abort_impl [[noreturn]] (char const*); +extern "C" void throw_or_abort_impl [[noreturn]] (char const*); diff --git a/barretenberg/cpp/src/barretenberg/wasi/wasi_stubs.cpp b/barretenberg/cpp/src/barretenberg/wasi/wasi_stubs.cpp deleted file mode 100644 index 31258e97195..00000000000 --- a/barretenberg/cpp/src/barretenberg/wasi/wasi_stubs.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// If building WASM, we can stub out functions we know we don't need, to save the host -// environment from having to stub them itself. -#include -#include -#include -#include - -extern "C" { - -int32_t __imported_wasi_snapshot_preview1_sched_yield() -{ - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_poll_oneoff(int32_t, int32_t, int32_t, int32_t) -{ - info("poll_oneoff not implemented."); - abort(); -} - -// void __imported_wasi_snapshot_preview1_proc_exit(int32_t) -// { -// info("proc_exit not implemented."); -// abort(); -// } - -struct iovs_struct { - char* data; - size_t len; -}; - -int32_t __imported_wasi_snapshot_preview1_fd_write(int32_t fd, iovs_struct* iovs_ptr, size_t iovs_len, size_t* ret_ptr) -{ - if (fd != 1 && fd != 2) { - info("fd_write to unsupported file descriptor: ", fd); - abort(); - } - std::string str; - for (size_t i = 0; i < iovs_len; ++i) { - auto iovs = iovs_ptr[i]; - str += std::string(iovs.data, iovs.len); - } - logstr(str.c_str()); - *ret_ptr = str.length(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_seek(int32_t, int64_t, int32_t, int32_t) -{ - info("fd_seek not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_close(int32_t) -{ - info("fd_close not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_environ_get(int32_t environ_ptr, int32_t environ_buf_ptr) -{ - // No environment variables, so nothing to write. The pointers point to - // arrays that would hold the environ entries and the concatenated - // key=value strings respectively, but with count == 0 they are empty. - (void)environ_ptr; - (void)environ_buf_ptr; - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_environ_sizes_get(int32_t count_ptr, int32_t buf_size_ptr) -{ - // WASI requires writing the number of environment variables and the total - // buffer size needed to hold them. We have none of either. - *(int32_t*)(uintptr_t)count_ptr = 0; - *(int32_t*)(uintptr_t)buf_size_ptr = 0; - return 0; -} - -// int32_t __imported_wasi_snapshot_preview1_clock_time_get(int32_t, int64_t, int32_t) -// { -// info("clock_time_get not implemented."); -// abort(); -// return 0; -// } - -int32_t __imported_wasi_snapshot_preview1_fd_fdstat_get(int32_t fd, void* buf) -{ - // info("fd_fdstat_get not implemented."); - // abort(); - if (fd != 1 && fd != 2) { - info("fd_fdstat_get with unsupported file descriptor: ", fd); - abort(); - } - memset(buf, 0, 20); - *(uint8_t*)buf = (uint8_t)fd; - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_fdstat_set_flags(int32_t, int32_t) -{ - info("fd_fdstat_set_flags not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_filestat_get(int32_t, int32_t) -{ - info("fd_filestat_get not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_filestat_set_size(int32_t, int64_t) -{ - info("fd_filestat_set_size not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_create_directory(int32_t, int32_t, int32_t) -{ - info("path_create_directory not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_readdir(int32_t, int32_t, int32_t, int64_t, int32_t) -{ - info("fd_readdir not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_advise(int32_t, int64_t, int64_t, int32_t) -{ - info("fd_advise not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_allocate(int32_t, int64_t, int64_t) -{ - info("fd_allocate not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_datasync(int32_t) -{ - info("fd_datasync not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_sync(int32_t) -{ - info("fd_sync not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_renumber(int32_t, int32_t) -{ - info("fd_renumber not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_tell(int32_t, uint64_t*) -{ - info("fd_tell stubbed."); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_read(int32_t, int32_t, int32_t, int32_t) -{ - info("fd_read not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_open( - int32_t, int32_t, int32_t, int32_t, int32_t, int64_t, int64_t, int32_t, int32_t) -{ - info("path_open not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_fd_prestat_get(int32_t, int32_t) -{ - // info("fd_prestat_get not implemented."); - // abort(); - return 8; -} - -int32_t __imported_wasi_snapshot_preview1_fd_prestat_dir_name(int32_t, int32_t, int32_t) -{ - info("fd_prestat_dir_name not implemented."); - abort(); - return 28; -} - -int32_t __imported_wasi_snapshot_preview1_path_filestat_get(int32_t, int32_t, int32_t, int32_t, int32_t) -{ - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_filestat_set_times( - int32_t, int32_t, int32_t, int32_t, int64_t, int64_t, int32_t) -{ - info("path_filestat_set_times not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_link(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) -{ - info("path_link not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_readlink(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) -{ - info("path_readlink not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_remove_directory(int32_t, int32_t, int32_t) -{ - info("path_remove_directory not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_rename(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) -{ - info("path_rename not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_symlink(int32_t, int32_t, int32_t, int32_t, int32_t) -{ - info("path_symlink not implemented."); - abort(); - return 0; -} - -int32_t __imported_wasi_snapshot_preview1_path_unlink_file(int32_t, int32_t, int32_t) -{ - info("path_unlink_file not implemented."); - abort(); - return 0; -} -} diff --git a/barretenberg/cpp/src/barretenberg/wasi/wasm_env.cpp b/barretenberg/cpp/src/barretenberg/wasi/wasm_env.cpp new file mode 100644 index 00000000000..8f2e074fe37 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wasi/wasm_env.cpp @@ -0,0 +1,45 @@ +/** + * The three environment functions barretenberg declares in barretenberg/env, implemented for the + * wasm reactor. The native build gets them from the env module; this one gets them from here, so + * the module asks its host for nothing but WASI. + * + * Everything here reaches the host through wasi-libc, which is why barretenberg/wasi carries no + * WASI stubs of its own: a module that answers its own fd_write cannot have its output routed by + * whoever embeds it. + */ +#include "barretenberg/common/wasm_export.hpp" +#include +#include +#include + +extern "C" { + +// WASM_EXPORT ensures these symbols stay visible when compiling with -fvisibility=hidden. + +/** Logs to stderr with the module's linear memory size, the wasm counterpart of native peak RSS. */ +WASM_EXPORT void logstr(char const* msg) +{ + constexpr size_t PAGES_PER_MIB = 16; // 64 KiB pages + const size_t mib = __builtin_wasm_memory_size(0) / PAGES_PER_MIB; + std::cerr << msg << " (mem: " << mib << " MiB)\n"; +} + +/** + * Only reached when HARDWARE_CONCURRENCY is unset, which an embedder is expected to put in the + * module's WASI environment. Wasm has no way to ask how many cores the machine has. + */ +WASM_EXPORT uint32_t env_hardware_concurrency() +{ + return 1; +} + +/** + * The wasm build compiles without exceptions, so a throw cannot unwind: report and exit. The + * embedder sees proc_exit, which its WASI implementation turns back into an exception. + */ +WASM_EXPORT void throw_or_abort_impl [[noreturn]] (char const* err) +{ + std::cerr << "abort: " << err << "\n" << std::flush; + _Exit(1); +} +} diff --git a/barretenberg/ts/.gitignore b/barretenberg/ts/.gitignore index ba0a722c837..ecde99a3073 100644 --- a/barretenberg/ts/.gitignore +++ b/barretenberg/ts/.gitignore @@ -8,6 +8,7 @@ bb.js/build bb.js/.tsbuildinfo* bb.js/*.tsbuildinfo bb-avm-sim/ +bb.js-api/ cdb/ .tsbuildinfo* *.tsbuildinfo @@ -16,6 +17,3 @@ cdb/ package.tgz package packages/ - -# Generated files -bb.js/src/generated/ diff --git a/barretenberg/ts/bb.js/.prettierignore b/barretenberg/ts/bb.js/.prettierignore index 0d4529771b2..e69de29bb2d 100644 --- a/barretenberg/ts/bb.js/.prettierignore +++ b/barretenberg/ts/bb.js/.prettierignore @@ -1,4 +0,0 @@ -# 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/README.md b/barretenberg/ts/bb.js/README.md index 28cbb4d8cd8..22ac40ed096 100644 --- a/barretenberg/ts/bb.js/README.md +++ b/barretenberg/ts/bb.js/README.md @@ -44,6 +44,25 @@ If `1` is specified, fallback to non multi-threaded wasm that doesn't need share See `src/main.ts` for larger example of how to use. +### How bb is reached + +The typed API (`Barretenberg` extends it) and every way of reaching bb come from the +`@aztec-foundation/bb.js-api` package, generated from bb's schema by ipc-codegen; bb.js adds the facades, +its `BackendType` options and CRS handling. That package ships the bb binary as per-platform optional +dependencies (override with `bbPath` or `BB_BINARY_PATH`) and bb's wasm modules (single-thread and threads +builds), run in-process through `@aztec-foundation/ipc-runtime/wasm`: the module in a worker, wasi threads +on further workers. Pass `warmup: true` to run bb's `Warmup` command after initialization, which takes the +prover's hot loops through the engine's optimizing tier before the first real request. bb.js itself only +bundles bb's LMDB NAPI module (`findNapiBinary`). + +The wasm modules ship uncompressed and are fetched as ordinary assets, so the browser streams them +straight into `WebAssembly.compileStreaming` and can cache the compiled code between visits. That means +serving them with your host's own compression: most CDNs compress `application/wasm` by default, but nginx +does not unless `application/wasm` is added to `gzip_types`. Where you cannot compress on the wire, pass +`wasmPath` (or set `BB_WASM_PATH` in node) pointing at a gzipped copy — the loader recognises gzip, at the +cost of the compiled-code cache. `wasmPath` also takes any other build of the module, and is used verbatim: +unlike earlier versions, bb.js no longer rewrites the filename to pick a `-threads` variant. + ### Browser Context It's recommended to use a dynamic import. This allows the developer to pick the time at which the package (several MB @@ -53,6 +72,11 @@ in size) is loaded and keeps page load times responsive. const { Barretenberg, RawBuffer, Crs } = await import('@aztec-foundation/bb.js'); ``` +The worker scripts and the wasm modules are referenced with `new URL('...', import.meta.url)` (the workers as +`new Worker(new URL(...), { type: 'module' })`), which webpack 5, Vite and similar bundlers turn into chunks and +assets of your application. Vite users should exclude `@aztec-foundation/bb.js` and `@aztec-foundation/bb.js-api` +from `optimizeDeps`, so those references are resolved from the packages rather than from a pre-bundled copy. + ### Multithreading in browser Multithreading in bb.js requires [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) to be enabled. It is only enabled in browsers if COOP and COEP headers are set by the server. Read more [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements). @@ -82,10 +106,13 @@ You can enable these headers for specific pages that perform proof generation, b ## Debugging -Got an unhelpful stack trace in wasm? Run: +Got an unhelpful stack trace in wasm? Point bb.js at the unstripped module the wasm build leaves next to the +stripped one: ``` -BUILD_CPP=1 NO_STRIP=1 ./script/copy_wasm.sh +BB_WASM_PATH=$(git rev-parse --show-toplevel)/barretenberg/cpp/build-wasm-threads/bin/barretenberg-debug.wasm ``` -This will drop unstripped wasms into the dest folder. Run your test again to get a trace. +(the loader takes a `.wasm` or a `.wasm.gz` either way) + +Run your test again to get a trace. diff --git a/barretenberg/ts/bb.js/bootstrap.sh b/barretenberg/ts/bb.js/bootstrap.sh index bd905c6953a..f9b6012b8b8 100755 --- a/barretenberg/ts/bb.js/bootstrap.sh +++ b/barretenberg/ts/bb.js/bootstrap.sh @@ -13,10 +13,9 @@ hash=$(hash_str \ ${AVM_TRANSPILER:-1}) function prepare_project { - (cd .. && ./bootstrap.sh generate_packages) - # 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$") + # Generates the workspace packages (including @aztec-foundation/bb.js-api, which bb.js + # compiles against) and installs; same cache-key inputs as barretenberg/ts/bootstrap.sh. + (cd .. && ./bootstrap.sh build_bb_js_api_ts) } function formatting { @@ -27,17 +26,16 @@ function formatting { function build { echo_header "bb.js build" - prepare_project + # bb.js compiles against @aztec-foundation/bb.js-api and runs the wasm modules and bb binary + # that package stages, so stage them whether or not bb.js's own build is cached. + (cd .. && ./bootstrap.sh build_bb_js_api) yarn formatting if ! cache_download bb.js-$hash.tar.gz; then find . -exec touch -d "@0" {} + 2>/dev/null || true yarn clean - yarn generate - yarn build:wasm - yarn build:native parallel -v --line-buffered --tag 'denoise "yarn {}"' ::: build:esm build:cjs build:browser - cache_upload bb.js-$hash.tar.gz dest build + cache_upload bb.js-$hash.tar.gz dest fi # We copy snapshot dirs to dest so we can run tests from dest. @@ -57,16 +55,11 @@ function test_cmds { for test in **/*.test.js; do # Skip benchmarks here. [[ "$test" =~ \.bench\.test\.js$ ]] && continue - [[ "$test" == "bbapi/chonk_pinned_inputs.test.js" ]] && continue + [[ "$test" == "barretenberg/chonk_pinned_inputs.test.js" ]] && continue - local prefix=$hash - # Extra resource. - if [[ "$test" =~ ^examples/ ]]; then - prefix="$prefix:CPUS=16" - fi - echo "$prefix barretenberg/ts/bb.js/scripts/run_test.sh $test" + echo "$hash barretenberg/ts/bb.js/scripts/run_test.sh $test" done - echo "$hash:CPUS=8:MEM=32g:TIMEOUT=20m barretenberg/cpp/scripts/chonk_inputs.sh download && barretenberg/ts/bb.js/scripts/run_test.sh bbapi/chonk_pinned_inputs.test.js" + echo "$hash:CPUS=8:MEM=32g:TIMEOUT=20m barretenberg/cpp/scripts/chonk_inputs.sh download && barretenberg/ts/bb.js/scripts/run_test.sh barretenberg/chonk_pinned_inputs.test.js" } function bench_cmds { @@ -79,18 +72,9 @@ function test { } function release { - cross_copy - # The bundled binaries are restored from the build cache, which is keyed on source, not on - # the release: finalize them here so they carry this release's version like every other copy. - local f - for f in ./build/*/bb ./build/*/bb.exe; do [ -f "$f" ] && ../../cpp/bootstrap.sh finalize_bb_binary "$(realpath "$f")"; done retry "deploy_npm ${REF_NAME#v}" } -function cross_copy { - ./scripts/copy_cross.sh "$@" -} - case "$cmd" in "") build diff --git a/barretenberg/ts/bb.js/eslint.config.js b/barretenberg/ts/bb.js/eslint.config.js index 2f7fa274213..ec78cb46424 100644 --- a/barretenberg/ts/bb.js/eslint.config.js +++ b/barretenberg/ts/bb.js/eslint.config.js @@ -22,8 +22,6 @@ export default [ 'eslint.config.js', 'eslint.config.*.js', 'src/jest/*.mjs', - // Codegen output; see .prettierignore. - 'src/generated/**', ]), ...tseslint.config({ extends: [ @@ -92,13 +90,7 @@ export default [ curly: ['error', 'all'], camelcase: 'error', 'import-x/no-relative-packages': 'error', - 'import-x/no-unresolved': [ - 'error', - { - // Generated later in bootstrap; the tracked wasm symlinks are broken in a clean checkout until the C++ build runs. - ignore: ['generated', '\\.wasm\\.gz$'], - }, - ], + 'import-x/no-unresolved': 'error', 'import-x/no-extraneous-dependencies': 'error', // this unfortunately doesn't block `fit` and `fdescribe` 'no-only-tests/no-only-tests': ['error'], diff --git a/barretenberg/ts/bb.js/package.json b/barretenberg/ts/bb.js/package.json index 5086a901128..15e429c2d16 100644 --- a/barretenberg/ts/bb.js/package.json +++ b/barretenberg/ts/bb.js/package.json @@ -11,9 +11,6 @@ "require": "./dest/node-cjs/index.js", "browser": "./dest/browser/index.js", "default": "./dest/node/index.js" - }, - "./platform": { - "default": "./dest/node/bb_backends/node/platform.js" } }, "bin": { @@ -22,24 +19,19 @@ "files": [ "src/", "dest/", - "build/", "README.md" ], "scripts": { - "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", + "clean": "rm -rf ./dest .tsbuildinfo .tsbuildinfo.cjs", + "build": "yarn clean && yarn build:esm && yarn build:cjs && yarn build:browser", "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": "./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", "test:chonk-inputs": "yarn test chonk_pinned_inputs", "test:debug": "NODE_NO_WARNINGS=1 node --inspect-brk=0.0.0.0 --experimental-vm-modules ./node_modules/.bin/jest --no-cache --passWithNoTests --runInBand", - "simple_test": "NODE_NO_WARNINGS=1 node ./src/examples/simple.rawtest.ts", "deploy": "npm publish --access public" }, "jest": { @@ -70,8 +62,8 @@ "rootDir": "./src" }, "dependencies": { + "@aztec-foundation/bb.js-api": "workspace:^", "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime", - "comlink": "^4.4.1", "commander": "^12.1.0", "idb-keyval": "^6.2.1", "msgpackr": "^1.11.2", diff --git a/barretenberg/ts/bb.js/scripts/browser_postprocess.sh b/barretenberg/ts/bb.js/scripts/browser_postprocess.sh index ad3e1a091c1..82442186050 100755 --- a/barretenberg/ts/bb.js/scripts/browser_postprocess.sh +++ b/barretenberg/ts/bb.js/scripts/browser_postprocess.sh @@ -9,11 +9,3 @@ done # Replace all **/node/** imports and exports with **/browser/** find "$DIR" -type f -name "*.js" -exec sed -i 's/\(import\|export\)\(.*\)from\(.*\)\/node\//\1\2from\3\/browser\//g' {} + - -# Provide default wasm files as gziped base64 strings -for file in barretenberg barretenberg-threads; do - GZIP_FILE=${DIR}/barretenberg_wasm/$file.wasm.gz - BB_BASE64=$(cat ${GZIP_FILE} | base64 -w0) - printf "const barretenberg = \"data:application/gzip;base64,$BB_BASE64\"; \\nexport default barretenberg;" > $DIR/barretenberg_wasm/fetch_code/browser/$file.js - rm $GZIP_FILE -done diff --git a/barretenberg/ts/bb.js/scripts/cjs_postprocess.sh b/barretenberg/ts/bb.js/scripts/cjs_postprocess.sh index b167435ef72..def40b0aeef 100755 --- a/barretenberg/ts/bb.js/scripts/cjs_postprocess.sh +++ b/barretenberg/ts/bb.js/scripts/cjs_postprocess.sh @@ -5,12 +5,9 @@ cat >dest/node-cjs/package.json < { ... }))` only works if one's happy with each element handler - * being run concurrently. - * If one required sequential execution of async fn's, the only alternative was regular loops with mutable state vars. - * The equivalent with asyncMap: `await asyncMap(arr, async e => { ... })`. - */ -export async function asyncMap(arr: T[], fn: (e: T, i: number) => Promise): Promise { - const results: U[] = []; - for (let i = 0; i < arr.length; ++i) { - results.push(await fn(arr[i], i)); - } - return results; -} diff --git a/barretenberg/ts/bb.js/src/backends/browser/index.ts b/barretenberg/ts/bb.js/src/backends/browser/index.ts new file mode 100644 index 00000000000..3e7808a9cd0 --- /dev/null +++ b/barretenberg/ts/bb.js/src/backends/browser/index.ts @@ -0,0 +1,59 @@ +import { createBackend, createBackendSync, sharedMemoryAvailable } from '@aztec-foundation/bb.js-api'; +import type { IpcClientAsync, IpcClientSync } from '@aztec-foundation/ipc-runtime'; + +import { BackendOptions, BackendType } from '../index.js'; + +/** There is no bb binary in a browser; the node build's counterpart locates the packaged one. */ +export function findBbBinary(_customPath?: string): string | null { + throw new Error('findBbBinary is not available in a browser.'); +} + +/** + * Create backend of specific type (no fallback) + */ +export async function createAsyncBackend( + type: BackendType, + options: BackendOptions, + logger: (msg: string) => void, +): Promise { + switch (type) { + case BackendType.Wasm: + case BackendType.WasmWorker: { + const worker = type === BackendType.WasmWorker; + logger(`Using WASM backend (worker: ${worker})`); + return await createBackend({ + backend: 'wasm', + // bb.js's documented behaviour: a page served without COOP/COEP gets the single-threaded + // module, whatever thread count was asked for. + threads: sharedMemoryAvailable() ? options.threads : 1, + logger, + wasm: { module: options.wasmPath, memory: options.memory, worker }, + }); + } + + default: + throw new Error(`Unknown backend type: ${type}`); + } +} + +/** + * Create backend of specific type (no fallback) + */ +export async function createSyncBackend( + type: BackendType, + options: BackendOptions, + logger: (msg: string) => void, +): Promise { + switch (type) { + case BackendType.Wasm: + logger('Using WASM backend'); + return await createBackendSync({ + backend: 'wasm', + logger, + wasm: { module: options.wasmPath, memory: options.memory }, + }); + + default: + throw new Error(`Backend ${type} not supported for BarretenbergSync`); + } +} diff --git a/barretenberg/ts/bb.js/src/bb_backends/index.ts b/barretenberg/ts/bb.js/src/backends/index.ts similarity index 88% rename from barretenberg/ts/bb.js/src/bb_backends/index.ts rename to barretenberg/ts/bb.js/src/backends/index.ts index 9991aa2d3c8..4fe8a7e604e 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/index.ts +++ b/barretenberg/ts/bb.js/src/backends/index.ts @@ -25,9 +25,18 @@ export type BackendOptions = { /** @description Number of G1 points to download when initializing the CRS/SRS for WASM backends */ srsSize?: number; - /** @description Path to download WASM files */ + /** + * @description A bb wasm module to run instead of the one bundled with @aztec-foundation/bb.js-api: + * a file path (node) or URL of the (optionally gzipped) module. Defaults to BB_WASM_PATH in node. + */ wasmPath?: string; + /** + * @description After initializing a WASM backend, run bb's Warmup command: a pass over the + * prover's hot loops so the engine has optimized them before the first real request. + */ + warmup?: boolean; + /** @description Custom path to bb binary for native backend (overrides automatic detection) */ bbPath?: string; diff --git a/barretenberg/ts/bb.js/src/backends/node/index.ts b/barretenberg/ts/bb.js/src/backends/node/index.ts new file mode 100644 index 00000000000..b927ed21150 --- /dev/null +++ b/barretenberg/ts/bb.js/src/backends/node/index.ts @@ -0,0 +1,118 @@ +import { createBackend, createBackendSync } from '@aztec-foundation/bb.js-api'; +import type { IpcClientAsync, IpcClientSync } from '@aztec-foundation/ipc-runtime'; +import * as os from 'os'; + +import { BackendOptions, BackendType } from '../index.js'; + +export { findBbBinary } from '@aztec-foundation/bb.js-api'; + +// Shared-memory rings sized for bb's payloads (witnesses, proofs); the async backend pipelines, so +// it gets a response ring of the same size. +const SHM_RING_SIZE = 1024 * 1024 * 4; + +/** + * An idle bb never keeps the caller's process alive: it dies with its parent anyway, since + * ipc-runtime's C++ installs that watch and the generated serve() calls it. Starting up and + * calls in flight still hold the loop, so this costs nothing but the child's trailing log lines + * if the process exits mid-line. + */ +const BB_PROCESS_LIFETIME = { unref: true }; + +/** + * Create backend of specific type (no fallback). Everything here is bb's choice of options over + * @aztec-foundation/bb.js-api's backends: thread defaults, ring sizes, artifact overrides. + */ +export async function createAsyncBackend( + type: BackendType, + options: BackendOptions, + logger: (msg: string) => void, +): Promise { + const wasmPath = options.wasmPath ?? process.env.BB_WASM_PATH; + + switch (type) { + case BackendType.NativeUnixSocket: + logger('Using native Unix socket backend'); + return await createBackend({ + backend: 'process', + // If threads not set use num cpu cores, max 16. + threads: options.threads ?? Math.min(16, os.cpus().length), + logger: options.logger, + process: { binaryPath: options.bbPath, transport: 'uds', respawn: options.respawn, ...BB_PROCESS_LIFETIME }, + }); + + case BackendType.NativeSharedMemory: + logger('Using native shared memory async backend'); + return await createBackend({ + backend: 'process', + threads: options.threads ?? 16, + logger: options.logger, + process: { + binaryPath: options.bbPath, + transport: 'shm', + clientId: 0, + napiPath: options.napiPath, + extraArgs: ['--request-ring-size', `${SHM_RING_SIZE}`, '--response-ring-size', `${SHM_RING_SIZE}`], + ...BB_PROCESS_LIFETIME, + }, + }); + + case BackendType.Wasm: + case BackendType.WasmWorker: { + // WasmWorker hosts the module in a worker thread; Wasm runs it on the calling thread, where + // every call blocks until bb returns. + const worker = type === BackendType.WasmWorker; + logger(`Using WASM backend (worker: ${worker})`); + return await createBackend({ + backend: 'wasm', + threads: options.threads, + logger: options.logger, + unref: options.unref, + wasm: { module: wasmPath, memory: options.memory, worker }, + }); + } + + default: + throw new Error(`Unknown backend type: ${type}`); + } +} + +/** + * Create backend of specific type (no fallback) + */ +export async function createSyncBackend( + type: BackendType, + options: BackendOptions, + logger: (msg: string) => void, +): Promise { + const wasmPath = options.wasmPath ?? process.env.BB_WASM_PATH; + + switch (type) { + case BackendType.NativeSharedMemory: + logger('Using native shared memory backend'); + return await createBackendSync({ + backend: 'process', + // Sync callers do short, one-at-a-time requests: one thread, one request ring. + threads: options.threads ?? 1, + logger: options.logger, + process: { + binaryPath: options.bbPath, + transport: 'shm', + napiPath: options.napiPath, + extraArgs: ['--request-ring-size', `${SHM_RING_SIZE}`], + ...BB_PROCESS_LIFETIME, + }, + }); + + case BackendType.Wasm: + logger('Using WASM backend'); + return await createBackendSync({ + backend: 'wasm', + logger: options.logger, + unref: options.unref, + wasm: { module: wasmPath, memory: options.memory }, + }); + + default: + throw new Error(`Backend ${type} not supported for BarretenbergSync`); + } +} diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts b/barretenberg/ts/bb.js/src/backends/node/native_shm.test.ts similarity index 87% rename from barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts rename to barretenberg/ts/bb.js/src/backends/node/native_shm.test.ts index 9678a60aa32..8d4daf9e469 100644 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.test.ts +++ b/barretenberg/ts/bb.js/src/backends/node/native_shm.test.ts @@ -1,8 +1,8 @@ import { IpcProcessExitedError } from '@aztec-foundation/ipc-runtime'; import { readFileSync, readdirSync } from 'fs'; -import { BarretenbergNativeShmSyncBackend } from './native_shm.js'; -import { findBbBinary } from './platform.js'; +import { BackendType } from '../index.js'; +import { createSyncBackend, findBbBinary } from './index.js'; const bbPath = findBbBinary(); // Linux only: the test finds bb's pid through /proc. @@ -31,10 +31,10 @@ function bbChildPids(): number[] { return pids; } -describeIfBb('BarretenbergNativeShmSyncBackend', () => { +describeIfBb('native shared memory sync backend', () => { it('fails a call to a bb that has died with its exit, promptly rather than after a deadline', async () => { const before = new Set(bbChildPids()); - const backend = await BarretenbergNativeShmSyncBackend.new(bbPath!); + const backend = await createSyncBackend(BackendType.NativeSharedMemory, { bbPath: bbPath! }, () => {}); const [pid] = bbChildPids().filter(p => !before.has(p)); expect(pid).toBeDefined(); diff --git a/barretenberg/ts/bb.js/src/backends/wasm.test.ts b/barretenberg/ts/bb.js/src/backends/wasm.test.ts new file mode 100644 index 00000000000..821eaf106e7 --- /dev/null +++ b/barretenberg/ts/bb.js/src/backends/wasm.test.ts @@ -0,0 +1,72 @@ +import { createHash } from 'crypto'; + +import { BackendType, Barretenberg, BarretenbergSync } from '../index.js'; + +// The WASM backends come from @aztec-foundation/bb.js-api (the generic wasm FFI backend of +// ipc-runtime over bb's module); bb.js only selects and configures them. +describe('wasm backends', () => { + const input = Buffer.from('hello bb.js-api'); + const expected = createHash('blake2s256').update(input).digest(); + + it.each([BackendType.Wasm, BackendType.WasmWorker])( + '%s hashes with 4 threads', + async backend => { + const api = await Barretenberg.new({ backend, threads: 4, skipSrsInit: true }); + try { + const { hash } = await api.blake2s({ data: input }); + expect(Buffer.from(hash)).toEqual(expected); + } finally { + await api.destroy(); + } + }, + 60000, + ); + + it('runs bb Warmup on request and keeps answering', async () => { + const api = await Barretenberg.new({ + backend: BackendType.WasmWorker, + threads: 4, + skipSrsInit: true, + warmup: true, + }); + try { + const { hash } = await api.blake2s({ data: input }); + expect(Buffer.from(hash)).toEqual(expected); + } finally { + await api.destroy(); + } + }, 120000); + + it('sync backend runs single-threaded on the calling thread', async () => { + const api = await BarretenbergSync.new({ backend: BackendType.Wasm }); + try { + expect(Buffer.from(api.blake2s({ data: input }).hash)).toEqual(expected); + } finally { + api.destroy(); + } + }, 60000); + + it('surfaces bb errors as exceptions, with the message bb gave', () => { + // Buffers too small for the point count they claim: bb throws inside from_buffer, and under + // BB_NO_EXCEPTIONS (which wasm builds with) that becomes an abort the host turns into a throw. + const tooSmall = { numPoints: 100, pointsBuf: new Uint8Array(10), g2Point: new Uint8Array(10) }; + return BarretenbergSync.new({ backend: BackendType.Wasm }).then(api => { + try { + expect(() => api.srsInitSrs(tooSmall)).toThrow(/invalid points_buf size/); + } finally { + api.destroy(); + } + }); + }, 60000); + + it('surfaces bb errors as exceptions asynchronously', async () => { + const api = await Barretenberg.new({ backend: BackendType.WasmWorker, threads: 1, skipSrsInit: true }); + try { + await expect( + api.srsInitSrs({ numPoints: 100, pointsBuf: new Uint8Array(10), g2Point: new Uint8Array(10) }), + ).rejects.toThrow(/invalid points_buf size/); + } finally { + await api.destroy(); + } + }, 60000); +}); diff --git a/barretenberg/ts/bb.js/src/barretenberg/backend.ts b/barretenberg/ts/bb.js/src/barretenberg/backend.ts index bd622e087e6..1fb00b445b2 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/backend.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/backend.ts @@ -1,9 +1,9 @@ +import { ChonkProof, fromChonkProof, toChonkProof } from '@aztec-foundation/bb.js-api'; import { Decoder, Encoder } from 'msgpackr'; import { ungzip } from 'pako'; import { CircuitKind } from '../circuit_kind.js'; -import { ChonkProof, fromChonkProof, toChonkProof } from '../generated/api_types.js'; -import { ProofData, hexToUint8Array, uint8ArrayToHex } from '../proof/index.js'; +import { ProofData, hexToUint8Array, uint8ArrayToHex } from '../proof.js'; import type { Barretenberg } from './index.js'; export class AztecClientBackendError extends Error { diff --git a/barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts b/barretenberg/ts/bb.js/src/barretenberg/chonk_pinned_inputs.test.ts similarity index 100% rename from barretenberg/ts/bb.js/src/bbapi/chonk_pinned_inputs.test.ts rename to barretenberg/ts/bb.js/src/barretenberg/chonk_pinned_inputs.test.ts diff --git a/barretenberg/ts/bb.js/src/barretenberg/errors.test.ts b/barretenberg/ts/bb.js/src/barretenberg/errors.test.ts new file mode 100644 index 00000000000..5ce81b2c824 --- /dev/null +++ b/barretenberg/ts/bb.js/src/barretenberg/errors.test.ts @@ -0,0 +1,63 @@ +import { BbError } from '@aztec-foundation/bb.js-api'; + +import { BackendType, Barretenberg } from './index.js'; + +/** + * A failure bb reports with BBAPI_ERROR: an off-curve point, rejected before any work is done. + * The handler returns an error response, so the reply is an error frame on every backend. + */ +function reportedFailure(api: Barretenberg) { + const one = new Uint8Array(32); + one[31] = 1; + const five = new Uint8Array(32); + five[31] = 5; + return api.bn254G1Mul({ point: { x: one, y: one }, scalar: five }); +} + +/** + * A failure bb raises with throw_or_abort: a compressed SRS buffer that is not chunk-aligned. + * The handler throws rather than returning, which is the case the two backends diverge on. + */ +function thrownFailure(api: Barretenberg) { + return api.srsInitSrs({ pointsBuf: new Uint8Array(32), numPoints: 1, g2Point: new Uint8Array(128) }); +} + +describe('command errors', () => { + let native: Barretenberg; + let wasm: Barretenberg; + + beforeAll(async () => { + native = await Barretenberg.new({ backend: BackendType.NativeUnixSocket, threads: 1, skipSrsInit: true }); + wasm = await Barretenberg.new({ backend: BackendType.Wasm, threads: 1, skipSrsInit: true }); + }); + + afterAll(async () => { + await native?.destroy(); + await wasm?.destroy(); + }); + + it('reports a returned error as BbError on both backends', async () => { + for (const api of [native, wasm]) { + const err = await reportedFailure(api).catch(e => e); + expect(err).toBeInstanceOf(BbError); + expect(err.message).toMatch(/must be on the curve/); + } + }); + + it('reports a thrown error as BbError on native', async () => { + const err = await thrownFailure(native).catch(e => e); + expect(err).toBeInstanceOf(BbError); + expect(err.message).toMatch(/must be a positive multiple of/); + }); + + // bb's wasm build compiles with BB_NO_EXCEPTIONS, so the dispatcher's catch is compiled away and + // a throw cannot unwind: bb writes the reason to stderr and exits, and ipc-runtime turns that + // into an error carrying the text. The message survives; the type does not. Pinned so the + // divergence cannot change silently — catch Error to handle both. + it('reports a thrown error as a plain Error on wasm', async () => { + const err = await thrownFailure(wasm).catch(e => e); + expect(err).toBeInstanceOf(Error); + expect(err).not.toBeInstanceOf(BbError); + expect(err.message).toMatch(/must be a positive multiple of/); + }); +}); diff --git a/barretenberg/ts/bb.js/src/barretenberg/index.ts b/barretenberg/ts/bb.js/src/barretenberg/index.ts index 7b1b17c05ef..403ae1885ed 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/index.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/index.ts @@ -1,10 +1,9 @@ -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 { BBApiException } from '../bbapi_exception.js'; +import { AsyncApi, SyncApi } from '@aztec-foundation/bb.js-api'; +import type { IpcClientAsync } from '@aztec-foundation/ipc-runtime'; + +import { BackendOptions, BackendType } from '../backends/index.js'; +import { createAsyncBackend, createSyncBackend } from '../backends/node/index.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. @@ -21,7 +20,7 @@ export { type UltraHonkBackendOptions, type VerifierTarget, } from './backend.js'; -export * from '../bb_backends/index.js'; +export * from '../backends/index.js'; export type CircuitOptions = { /** @description Whether to produce SNARK friendly proofs */ @@ -35,8 +34,8 @@ export type CircuitOptions = { export class Barretenberg extends AsyncApi { private options: BackendOptions; - constructor(backend: IMsgpackBackendAsync, options: BackendOptions) { - super(backend, message => new BBApiException(message)); + constructor(backend: IpcClientAsync, options: BackendOptions) { + super(backend); this.options = options; } @@ -54,11 +53,8 @@ export class Barretenberg extends AsyncApi { if (options.backend) { // Explicit backend required - no fallback const backend = new Barretenberg(await createAsyncBackend(options.backend, options, logger), options); - if ( - !options.skipSrsInit && - (options.backend === BackendType.Wasm || options.backend === BackendType.WasmWorker) - ) { - await backend.initSRSChonk(options.srsSize); + if (options.backend === BackendType.Wasm || options.backend === BackendType.WasmWorker) { + await backend.initWasm(); } return backend; } @@ -69,21 +65,30 @@ export class Barretenberg extends AsyncApi { } catch (err: any) { logger(`Unix socket unavailable (${err.message}), falling back to WASM`); const backend = new Barretenberg(await createAsyncBackend(BackendType.Wasm, options, logger), options); - if (!options.skipSrsInit) { - await backend.initSRSChonk(options.srsSize); - } + await backend.initWasm(); return backend; } } else { logger(`In browser, using WASM over worker backend.`); const backend = new Barretenberg(await createAsyncBackend(BackendType.WasmWorker, options, logger), options); - if (!options.skipSrsInit) { - await backend.initSRSChonk(options.srsSize); - } + await backend.initWasm(); return backend; } } + /** + * A WASM backend starts with no SRS loaded (a native bb reads its own), and with its code + * unoptimized by the engine's baseline tier until it has run. + */ + private async initWasm(): Promise { + if (!this.options.skipSrsInit) { + await this.initSRSChonk(this.options.srsSize); + } + if (this.options.warmup) { + await this.warmup({}); + } + } + async initSRSChonk(srsSize = this.getDefaultSrsSize()): Promise { // crsPath can be undefined const crs = await Crs.new(srsSize, this.options.crsPath, this.options.logger); @@ -185,10 +190,6 @@ let barretenbergSyncSingletonPromise: Promise | undefined; let barretenbergSyncSingleton: BarretenbergSync | undefined; export class BarretenbergSync extends SyncApi { - constructor(backend: IMsgpackBackendSync) { - super(backend, message => new BBApiException(message)); - } - /** * Create a new BarretenbergSync instance. * diff --git a/barretenberg/ts/bb.js/src/barretenberg/pedersen.test.ts b/barretenberg/ts/bb.js/src/barretenberg/pedersen.test.ts index b8430c111eb..255608e6050 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/pedersen.test.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/pedersen.test.ts @@ -1,6 +1,6 @@ -import { Timer } from '../benchmark/timer.js'; import { BarretenbergSync } from './index.js'; import { Fr } from './testing/fields.js'; +import { Timer } from './testing/timer.js'; describe('pedersen sync', () => { let api: BarretenbergSync; diff --git a/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts b/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts index a246f95d631..e1ceb59f8ec 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/poseidon.bench.test.ts @@ -1,5 +1,3 @@ -import { BarretenbergWasmMain } from '../barretenberg_wasm/barretenberg_wasm_main/index.js'; -import { fetchModuleAndThreads } from '../barretenberg_wasm/index.js'; import { Barretenberg, BarretenbergSync } from '../index.js'; import { BackendType } from './index.js'; import { Fr } from './testing/fields.js'; @@ -25,14 +23,8 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => { let nativeSocketApi: Barretenberg | null = null; let nativeShmApi: Barretenberg | null = null; let nativeShmSyncApi: BarretenbergSync | null = null; - let wasm: BarretenbergWasmMain; beforeAll(async () => { - // Setup direct WASM access for baseline benchmark (always required) - wasm = new BarretenbergWasmMain(); - const { module } = await fetchModuleAndThreads(1); - await wasm.init(module, 1); - // Setup WASM API try { wasmApi = await Barretenberg.new({ backend: BackendType.Wasm, threads: 1, skipSrsInit: true }); diff --git a/barretenberg/ts/bb.js/src/barretenberg/poseidon.test.ts b/barretenberg/ts/bb.js/src/barretenberg/poseidon.test.ts index e34fe2f4a78..de060ee9b9e 100644 --- a/barretenberg/ts/bb.js/src/barretenberg/poseidon.test.ts +++ b/barretenberg/ts/bb.js/src/barretenberg/poseidon.test.ts @@ -1,6 +1,6 @@ -import { Timer } from '../benchmark/timer.js'; import { BarretenbergSync } from './index.js'; import { Fr } from './testing/fields.js'; +import { Timer } from './testing/timer.js'; describe('poseidon sync', () => { let api: BarretenbergSync; diff --git a/barretenberg/ts/bb.js/src/benchmark/timer.ts b/barretenberg/ts/bb.js/src/barretenberg/testing/timer.ts similarity index 100% rename from barretenberg/ts/bb.js/src/benchmark/timer.ts rename to barretenberg/ts/bb.js/src/barretenberg/testing/timer.ts 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 deleted file mode 120000 index e9fd64c59ea..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg-threads.wasm.gz +++ /dev/null @@ -1 +0,0 @@ -../../../../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 deleted file mode 120000 index e7a5a91393a..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg.wasm.gz +++ /dev/null @@ -1 +0,0 @@ -../../../../cpp/build-wasm/bin/barretenberg.wasm.gz \ No newline at end of file diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_base/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_base/index.ts deleted file mode 100644 index 9b810ebba70..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_base/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { randomBytes } from '../../random/index.js'; - -/** - * Base implementation of BarretenbergWasm. - * Contains code that is common to the "main thread" implementation and the "child thread" implementation. - */ -export class BarretenbergWasmBase { - protected memory!: WebAssembly.Memory; - protected instance!: WebAssembly.Instance; - protected logger: (msg: string) => void = () => {}; - - protected getImportObj(memory: WebAssembly.Memory) { - /* eslint-disable camelcase */ - const importObj = { - // We need to implement a part of the wasi api: - // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md - // We literally only need to support random_get, everything else is noop implementated in barretenberg.wasm. - wasi_snapshot_preview1: { - random_get: (out: any, length: number) => { - out = out >>> 0; - const randomData = randomBytes(length); - const mem = this.getMemory(); - mem.set(randomData, out); - }, - clock_time_get: (a1: number, a2: number, out: number) => { - out = out >>> 0; - const ts = BigInt(new Date().getTime()) * 1000000n; - const view = new DataView(this.getMemory().buffer); - view.setBigUint64(out, ts, true); - }, - proc_exit: () => { - this.logger('PANIC: proc_exit was called.'); - throw new Error(); - }, - }, - - // These are functions implementations for imports we've defined are needed. - // The native C++ build defines these in a module called "env". We must implement TypeScript versions here. - env: { - /** - * The 'info' call we use for logging in C++, calls this under the hood. - * The native code will just print to std:err (to avoid std::cout which is used for IPC). - * Here we just emit the log line for the client to decide what to do with. - */ - logstr: (addr: number) => { - const str = this.stringFromAddress(addr); - const m = this.getMemory(); - const str2 = `${str} (mem: ${(m.length / (1024 * 1024)).toFixed(2)}MiB)`; - this.logger(str2); - }, - - throw_or_abort_impl: (addr: number) => { - const str = this.stringFromAddress(addr); - throw new Error(str); - }, - - memory, - }, - }; - /* eslint-enable camelcase */ - - return importObj; - } - - public exports(): any { - return this.instance.exports; - } - - /** - * When returning values from the WASM, use >>> operator to convert signed representation to unsigned representation. - */ - public call(name: string, ...args: any) { - if (!this.exports()[name]) { - throw new Error(`WASM function ${name} not found.`); - } - try { - return this.exports()[name](...args) >>> 0; - } catch (err: any) { - const message = `WASM function ${name} aborted, error: ${err}`; - this.logger(message); - this.logger(err.stack); - throw err; - } - } - - public memSize() { - return this.getMemory().length; - } - - /** - * Returns a copy of the data, not a view. - */ - public getMemorySlice(start: number, end: number) { - return this.getMemory().subarray(start, end).slice(); - } - - public writeMemory(offset: number, arr: Uint8Array) { - const mem = this.getMemory(); - mem.set(arr, offset); - } - - public getMemory() { - return new Uint8Array(this.memory.buffer); - } - - // PRIVATE METHODS - - private stringFromAddress(addr: number) { - addr = addr >>> 0; - const m = this.getMemory(); - let i = addr; - while (m[i] !== 0) { - ++i; - } - const textDecoder = new TextDecoder('ascii'); - return textDecoder.decode(m.slice(addr, i)); - } -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts deleted file mode 100644 index 730d44031e4..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { readinessListener } from '../../../helpers/browser/index.js'; - -export async function createMainWorker() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const worker = new Worker(new URL('./main.worker.js', import.meta.url), { type: 'module' }); - await new Promise(resolve => readinessListener(worker, resolve)); - return worker; -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts deleted file mode 100644 index efa1613e99d..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expose } from 'comlink'; - -import { Ready } from '../../../helpers/browser/index.js'; -import { BarretenbergWasmMain } from '../../index.js'; - -expose(new BarretenbergWasmMain()); -postMessage(Ready); diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts deleted file mode 100644 index a9b21d903a2..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { Worker } from 'worker_threads'; - -function getCurrentDir() { - if (typeof __dirname !== 'undefined') { - return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return dirname(fileURLToPath(import.meta.url)); - } -} - -export function createMainWorker() { - const __dirname = getCurrentDir(); - const worker = new Worker(__dirname + `/main.worker.js`); - return Promise.resolve(worker); -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts deleted file mode 100644 index 4b3d1e934cb..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expose } from 'comlink'; -import { parentPort } from 'worker_threads'; - -import { nodeEndpoint } from '../../../helpers/node/node_endpoint.js'; -import { BarretenbergWasmMain } from '../../index.js'; - -if (!parentPort) { - throw new Error('No parentPort'); -} - -expose(new BarretenbergWasmMain(), nodeEndpoint(parentPort)); diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/heap_allocator.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/heap_allocator.ts deleted file mode 100644 index fec7282d9a1..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/heap_allocator.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { BarretenbergWasmMain } from './index.js'; - -/** - * Keeps track of heap allocations so they can be easily freed. - * The WASM memory layout has 1024 bytes of unused "scratch" space at the start (addresses 0-1023). - * We can leverage this for IO rather than making expensive bb_malloc bb_free calls. - * Heap allocations will be created for input/output args that don't fit into the scratch space. - * Input scratch grows UP from 0, output scratch grows DOWN from 1024, meeting in the middle. - * This maximizes space utilization while preventing overlap. - */ -export class HeapAllocator { - private allocs: number[] = []; - private inScratchPtr = 0; // Next input starts here, grows UP - private outScratchPtr = 1024; // Next output ends here, grows DOWN - - constructor(private wasm: BarretenbergWasmMain) {} - - getInputs(buffers: (Uint8Array | number)[]) { - return buffers.map(bufOrNum => { - if (typeof bufOrNum === 'object') { - const size = bufOrNum.length; - // Check if there's room in scratch space (inputs grow up, outputs grow down) - if (this.inScratchPtr + size <= this.outScratchPtr) { - const ptr = this.inScratchPtr; - this.inScratchPtr += size; // Grow UP - this.wasm.writeMemory(ptr, bufOrNum); - return ptr; - } else { - // Fall back to heap allocation - const ptr = this.wasm.call('bbmalloc', size); - this.wasm.writeMemory(ptr, bufOrNum); - this.allocs.push(ptr); - return ptr; - } - } else { - return bufOrNum; - } - }); - } - - getOutputPtrs(outLens: (number | undefined)[]) { - return outLens.map(len => { - // If the obj is variable length, we need a 4 byte ptr to write the serialized data address to. - // WARNING: 4 only works with WASM as it has 32 bit memory. - const size = len || 4; - - // Check if there's room in scratch space (inputs grow up, outputs grow down) - if (this.inScratchPtr + size <= this.outScratchPtr) { - this.outScratchPtr -= size; // Grow DOWN - return this.outScratchPtr; - } else { - // Fall back to heap allocation - const ptr = this.wasm.call('bbmalloc', size); - this.allocs.push(ptr); - return ptr; - } - }); - } - - addOutputPtr(ptr: number) { - // Only add to dealloc list if it's a heap allocation (not in scratch space 0-1023) - if (ptr >= 1024) { - this.allocs.push(ptr); - } - } - - freeAll() { - for (const ptr of this.allocs) { - this.wasm.call('bbfree', ptr); - } - } -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/index.ts deleted file mode 100644 index f14bc9092d5..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_main/index.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { Remote } from 'comlink'; -import type { Worker } from 'worker_threads'; - -import { BarretenbergWasmBase } from '../barretenberg_wasm_base/index.js'; -import { createThreadWorker } from '../barretenberg_wasm_thread/factory/node/index.js'; -import type { BarretenbergWasmThreadWorker } from '../barretenberg_wasm_thread/index.js'; -import { getNumCpu, getRemoteBarretenbergWasm, getSharedMemoryAvailable } from '../helpers/index.js'; -import { HeapAllocator } from './heap_allocator.js'; - -/** - * This is the "main thread" implementation of BarretenbergWasm. - * It spawns a bunch of "child thread" implementations. - * In a browser context, this still runs on a worker, as it will block waiting on child threads. - */ -export class BarretenbergWasmMain extends BarretenbergWasmBase { - static MAX_THREADS = 32; - private workers: Worker[] = []; - private remoteWasms: BarretenbergWasmThreadWorker[] = []; - private nextWorker = 0; - private nextThreadId = 1; - private useCustomLogger = false; - - // Pre-allocated scratch buffers for msgpack I/O to avoid malloc/free overhead - private msgpackInputScratch: number = 0; // 8MB input buffer - private msgpackOutputScratch: number = 0; // 8MB output buffer - private readonly MSGPACK_SCRATCH_SIZE = 1024 * 1024 * 8; // 8MB - - public getNumThreads() { - return this.workers.length + 1; - } - - /** - * Init as main thread. Spawn child threads. - */ - public async init( - module: WebAssembly.Module, - threads = Math.min(getNumCpu(), BarretenbergWasmMain.MAX_THREADS), - logger?: (msg: string) => void, - initial = 37, - maximum = this.getDefaultMaximumMemoryPages(), - unref = false, - ) { - // Track whether a custom logger was provided so workers know whether to postMessage logs - this.useCustomLogger = logger !== undefined; - this.logger = logger ?? (() => {}); - - const initialMb = (initial * 2 ** 16) / (1024 * 1024); - const maxMb = (maximum * 2 ** 16) / (1024 * 1024); - const shared = getSharedMemoryAvailable(); - - this.logger( - `Initializing bb wasm: initial memory ${initial} pages ${initialMb}MiB; ` + - `max memory: ${maximum} pages, ${maxMb}MiB; ` + - `threads: ${threads}; shared memory: ${shared}`, - ); - - this.memory = new WebAssembly.Memory({ initial, maximum, shared }); - - const instance = await WebAssembly.instantiate(module, this.getImportObj(this.memory)); - - this.instance = instance; - - // Init all global/static data. - this.call('_initialize'); - - // Allocate dedicated msgpack scratch buffers (never freed, reused for all msgpack calls) - this.msgpackInputScratch = this.call('bbmalloc', this.MSGPACK_SCRATCH_SIZE); - this.msgpackOutputScratch = this.call('bbmalloc', this.MSGPACK_SCRATCH_SIZE); - this.logger( - `Allocated msgpack scratch buffers: ` + - `input @ ${this.msgpackInputScratch}, output @ ${this.msgpackOutputScratch} (${this.MSGPACK_SCRATCH_SIZE} bytes each)`, - ); - - // Create worker threads. Create 1 less than requested, as main thread counts as a thread. - if (threads > 1) { - this.logger(`Creating ${threads} worker threads`); - this.workers = await Promise.all(Array.from({ length: threads - 1 }).map(createThreadWorker)); - - // Set up log message forwarding from workers to our logger (only if custom logger provided) - if (this.useCustomLogger) { - this.workers.forEach(worker => this.setupWorkerLogForwarding(worker)); - } - - this.remoteWasms = this.workers.map(getRemoteBarretenbergWasm); - await Promise.all(this.remoteWasms.map(w => w.initThread(module, this.memory, this.useCustomLogger))); - - if (unref) { - for (const worker of this.workers) { - worker.unref(); - } - } - } - } - - private getDefaultMaximumMemoryPages(): number { - // iOS browser is very aggressive with memory. Check if running in browser and on iOS. - // We at any rate expect the mobile iOS browser to kill us >=1GB, so we don't set a maximum higher than that. - // Use `self` instead of `window` so this check also works inside Web Workers. - if ( - typeof self !== 'undefined' && - typeof self.navigator !== 'undefined' && - /iPad|iPhone/.test(self.navigator.userAgent) - ) { - return 2 ** 14; - } - return 2 ** 16; - } - - /** - * Set up forwarding of log messages from worker threads to our logger. - * Workers post messages with { type: 'log', msg: string } which we intercept here. - */ - private setupWorkerLogForwarding(worker: Worker) { - const handler = (data: unknown) => { - if (data && typeof data === 'object' && 'type' in data && data.type === 'log' && 'msg' in data) { - this.logger(data.msg as string); - } - }; - - // Node Workers use 'on' method, browser Workers use 'addEventListener' - // The 'worker' variable is typed as Node's Worker, but at runtime in browser - // it will be a browser Worker (due to browser_postprocess.sh import rewriting) - if ('on' in worker && typeof worker.on === 'function') { - // Node.js worker_threads Worker - worker.on('message', handler); - } else if ('addEventListener' in worker) { - // Browser Web Worker - (worker as unknown as globalThis.Worker).addEventListener('message', (event: MessageEvent) => { - handler(event.data); - }); - } - } - - /** - * Called on main thread. Signals child threads to gracefully exit. - */ - public async destroy() { - await Promise.all(this.workers.map(w => w.terminate())); - } - - protected getImportObj(memory: WebAssembly.Memory) { - const baseImports = super.getImportObj(memory); - - /* eslint-disable camelcase */ - return { - ...baseImports, - wasi: { - 'thread-spawn': (arg: number) => { - arg = arg >>> 0; - const id = this.nextThreadId++; - const worker = this.nextWorker++ % this.remoteWasms.length; - // this.logger(`spawning thread ${id} on worker ${worker} with arg ${arg >>> 0}`); - this.remoteWasms[worker].call('wasi_thread_start', id, arg).catch(this.logger); - // this.remoteWasms[worker].postMessage({ msg: 'thread', data: { id, arg } }); - return id; - }, - }, - env: { - ...baseImports.env, - env_hardware_concurrency: () => { - // If there are no workers (we're already running as a worker, or the main thread requested no workers) - // then we return 1, which should cause any algos using threading to just not create a thread. - return this.remoteWasms.length + 1; - }, - }, - }; - /* eslint-enable camelcase */ - } - - callWasmExport(funcName: string, inArgs: (Uint8Array | number)[], outLens: (number | undefined)[]) { - const alloc = new HeapAllocator(this); - const inPtrs = alloc.getInputs(inArgs); - const outPtrs = alloc.getOutputPtrs(outLens); - this.call(funcName, ...inPtrs, ...outPtrs); - const outArgs = this.getOutputArgs(outLens, outPtrs, alloc); - alloc.freeAll(); - return outArgs; - } - - private getOutputArgs(outLens: (number | undefined)[], outPtrs: number[], alloc: HeapAllocator) { - return outLens.map((len, i) => { - if (len) { - return this.getMemorySlice(outPtrs[i], outPtrs[i] + len); - } - const slice = this.getMemorySlice(outPtrs[i], outPtrs[i] + 4); - const ptr = new DataView(slice.buffer, slice.byteOffset, slice.byteLength).getUint32(0, true); - - // Add our heap buffer to the dealloc list. - alloc.addOutputPtr(ptr); - - // The length will be found in the first 4 bytes of the buffer, big endian. See to_heap_buffer. - const lslice = this.getMemorySlice(ptr, ptr + 4); - const length = new DataView(lslice.buffer, lslice.byteOffset, lslice.byteLength).getUint32(0, false); - - return this.getMemorySlice(ptr + 4, ptr + 4 + length); - }); - } - - cbindCall(cbind: string, inputBuffer: Uint8Array): any { - const needsCustomInputBuffer = inputBuffer.length > this.MSGPACK_SCRATCH_SIZE; - let inputPtr: number; - - if (needsCustomInputBuffer) { - // Allocate temporary buffer for oversized input - inputPtr = this.call('bbmalloc', inputBuffer.length); - } else { - // Use pre-allocated scratch buffer - inputPtr = this.msgpackInputScratch; - } - - // Write input to buffer - this.writeMemory(inputPtr, inputBuffer); - - // Setup output scratch buffer with IN-OUT parameter pattern: - // Reserve 8 bytes for metadata (pointer + size), rest is scratch data space - const METADATA_SIZE = 8; - const outputPtrLocation = this.msgpackOutputScratch; - const outputSizeLocation = this.msgpackOutputScratch + 4; - const scratchDataPtr = this.msgpackOutputScratch + METADATA_SIZE; - const scratchDataSize = this.MSGPACK_SCRATCH_SIZE - METADATA_SIZE; - - // Get memory and create DataView for writing IN values - let mem = this.getMemory(); - let view = new DataView(mem.buffer); - - // Write IN values: provide scratch buffer pointer and size to C++ - view.setUint32(outputPtrLocation, scratchDataPtr, true); - view.setUint32(outputSizeLocation, scratchDataSize, true); - - // Call WASM - this.call(cbind, inputPtr, inputBuffer.length, outputPtrLocation, outputSizeLocation); - - // Free custom input buffer if allocated - if (needsCustomInputBuffer) { - this.call('bbfree', inputPtr); - } - - // Re-fetch memory after WASM call, as the buffer may have been detached if memory grew - mem = this.getMemory(); - view = new DataView(mem.buffer); - - // Read OUT values: C++ returns actual buffer pointer and size - const outputDataPtr = view.getUint32(outputPtrLocation, true); - const outputSize = view.getUint32(outputSizeLocation, true); - - // Check if C++ used scratch (pointer unchanged) or allocated (pointer changed) - const usedScratch = outputDataPtr === scratchDataPtr; - - // Copy output data from WASM memory - const encodedResult = this.getMemorySlice(outputDataPtr, outputDataPtr + outputSize); - - // Only free if C++ allocated beyond scratch - if (!usedScratch) { - this.call('bbfree', outputDataPtr); - } - - return encodedResult; - } -} - -/** - * The comlink type that asyncifies the BarretenbergWasmMain api. - */ -export type BarretenbergWasmMainWorker = Remote; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts deleted file mode 100644 index 4b65988cf27..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { readinessListener } from '../../../helpers/browser/index.js'; - -export async function createThreadWorker() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const worker = new Worker(new URL('./thread.worker.js', import.meta.url), { type: 'module' }); - await new Promise(resolve => readinessListener(worker, resolve)); - return worker; -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts deleted file mode 100644 index e914600ec6a..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expose } from 'comlink'; - -import { Ready } from '../../../helpers/browser/index.js'; -import { BarretenbergWasmThread } from '../../index.js'; - -expose(new BarretenbergWasmThread()); -postMessage(Ready); diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts deleted file mode 100644 index 03c7988c10d..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { Worker } from 'worker_threads'; - -function getCurrentDir() { - if (typeof __dirname !== 'undefined') { - return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return dirname(fileURLToPath(import.meta.url)); - } -} - -export function createThreadWorker() { - const __dirname = getCurrentDir(); - const worker = new Worker(__dirname + `/thread.worker.js`); - return Promise.resolve(worker); -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts deleted file mode 100644 index 21cad4523fe..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expose } from 'comlink'; -import { parentPort } from 'worker_threads'; - -import { nodeEndpoint } from '../../../helpers/node/node_endpoint.js'; -import { BarretenbergWasmThread } from '../../index.js'; - -if (!parentPort) { - throw new Error('No parentPort'); -} - -const endpoint = nodeEndpoint(parentPort); - -expose(new BarretenbergWasmThread(), endpoint); diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts deleted file mode 100644 index ff134479ffe..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Remote } from 'comlink'; - -import { BarretenbergWasmBase } from '../barretenberg_wasm_base/index.js'; -import { killSelf, threadLogger } from '../helpers/index.js'; - -export class BarretenbergWasmThread extends BarretenbergWasmBase { - /** - * Init as worker thread. - * @param useCustomLogger - If true, logs will be posted back to main thread for custom logger routing - */ - public async initThread(module: WebAssembly.Module, memory: WebAssembly.Memory, useCustomLogger = false) { - this.logger = threadLogger(useCustomLogger) || this.logger; - this.memory = memory; - this.instance = await WebAssembly.instantiate(module, this.getImportObj(this.memory)); - } - - public destroy() { - killSelf(); - } - - protected getImportObj(memory: WebAssembly.Memory) { - const baseImports = super.getImportObj(memory); - - /* eslint-disable camelcase */ - return { - ...baseImports, - wasi: { - 'thread-spawn': () => { - this.logger('PANIC: threads cannot spawn threads!'); - this.logger(new Error().stack!); - killSelf(); - }, - }, - - // These are functions implementations for imports we've defined are needed. - // The native C++ build defines these in a module called "env". We must implement TypeScript versions here. - env: { - ...baseImports.env, - env_hardware_concurrency: () => { - // We return 1, which should cause any algos using threading to just not create a thread. - return 1; - }, - }, - }; - /* eslint-enable camelcase */ - } -} - -export type BarretenbergWasmThreadWorker = Remote; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts deleted file mode 100644 index 23aa9ed841f..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts +++ /dev/null @@ -1,3 +0,0 @@ -import barretenbergThreadsModule from '../../barretenberg-threads.wasm.gz'; - -export default barretenbergThreadsModule; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts deleted file mode 100644 index c75591e5c6e..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts +++ /dev/null @@ -1,3 +0,0 @@ -import barretenbergModule from '../../barretenberg.wasm.gz'; - -export default barretenbergModule; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/index.ts deleted file mode 100644 index 4b227fcf15e..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/browser/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ungzip } from 'pako'; - -// Annoyingly the wasm declares if it's memory is shared or not. So now we need two wasms if we want to be -// able to fallback on "non shared memory" situations. -export async function fetchCode(multithreaded: boolean, wasmPath?: string) { - let url: string; - if (wasmPath) { - const suffix = multithreaded ? '-threads' : ''; - const filePath = wasmPath.split('/').slice(0, -1).join('/'); - const fileNameWithExtensions = wasmPath.split('/').pop(); - const [fileName, ...extensions] = fileNameWithExtensions!.split('.'); - url = `${filePath}/${fileName}${suffix}.${extensions.join('.')}`; - } else { - url = multithreaded - ? (await import('./barretenberg-threads.js')).default - : (await import('./barretenberg.js')).default; - } - const res = await fetch(url); - // Default bb wasm is compressed, but user could point it to a non-compressed version - const maybeCompressedData = await res.arrayBuffer(); - const buffer = new Uint8Array(maybeCompressedData); - const isGzip = - // Check magic number - buffer[0] === 0x1f && - buffer[1] === 0x8b && - // Check compression method: - buffer[2] === 0x08; - if (isGzip) { - const decompressedData = ungzip(buffer); - return decompressedData.buffer as unknown as Uint8Array; - } else { - return buffer; - } -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/index.ts deleted file mode 100644 index 950c3e00670..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './node/index.js'; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/node/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/node/index.ts deleted file mode 100644 index 375fc8c4988..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/node/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { readFile } from 'fs/promises'; -import { ungzip } from 'pako'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -function getCurrentDir() { - if (typeof __dirname !== 'undefined') { - return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return dirname(fileURLToPath(import.meta.url)); - } -} - -export async function fetchCode(multithreaded: boolean, wasmPath?: string) { - const path = wasmPath ?? getCurrentDir() + '/../../barretenberg-threads.wasm.gz'; - // Default bb wasm is compressed, but user could point it to a non-compressed version - const maybeCompressedData = await readFile(path); - const buffer = new Uint8Array(maybeCompressedData); - const isGzip = - // Check magic number - buffer[0] === 0x1f && - buffer[1] === 0x8b && - // Check compression method: - buffer[2] === 0x08; - if (isGzip) { - const decompressedData = ungzip(buffer); - return decompressedData.buffer as unknown as Uint8Array; - } else { - return buffer; - } -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/wasm-module.d.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/wasm-module.d.ts deleted file mode 100644 index b5f0a8c6ba0..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/fetch_code/wasm-module.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.wasm.gz' { - const content: string; - export default content; -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/browser/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/browser/index.ts deleted file mode 100644 index 6040e2dd147..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/browser/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { wrap } from 'comlink'; - -export function getSharedMemoryAvailable() { - const globalScope = typeof window !== 'undefined' ? window : globalThis; - return typeof SharedArrayBuffer !== 'undefined' && globalScope.crossOriginIsolated; -} - -export function getRemoteBarretenbergWasm(worker: Worker) { - return wrap(worker); -} - -export function getNumCpu() { - return navigator.hardwareConcurrency; -} - -export function threadLogger(useCustomLogger: boolean): ((msg: string) => void) | undefined { - if (useCustomLogger) { - // Post log messages back to main thread for routing through user-provided logger - return (msg: string) => { - postMessage({ type: 'log', msg }); - }; - } - // Use console.log directly when no custom logger is provided - // eslint-disable-next-line no-console -- Console logging is the worker fallback when no logger is provided. - return console.log; -} - -export function killSelf() { - self.close(); -} - -export function getAvailableThreads(logger: (msg: string) => void): number { - if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) { - return navigator.hardwareConcurrency; - } else { - logger(`Could not detect environment to query number of threads. Falling back to one thread.`); - return 1; - } -} - -// Solution to async initialization of workers, taken from -// https://github.com/GoogleChromeLabs/comlink/issues/635#issuecomment-1598913044 - -/** The message expected by the `readinessListener`. */ -export const Ready = { ready: true }; - -/** Listen for the readiness message from the Worker and call the `callback` once. */ -export function readinessListener(worker: Worker, callback: () => void) { - worker.addEventListener('message', function ready(event: MessageEvent) { - if (!!event.data && event.data.ready === true) { - worker.removeEventListener('message', ready); - callback(); - } - }); -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/index.ts deleted file mode 100644 index 950c3e00670..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './node/index.js'; diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/index.ts deleted file mode 100644 index 2e8ed464865..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { wrap } from 'comlink'; -import { writeSync } from 'fs'; -import os from 'os'; -import { Worker, parentPort } from 'worker_threads'; - -import { nodeEndpoint } from './node_endpoint.js'; - -export function getSharedMemoryAvailable() { - return true; -} - -/** - * Comlink allows you to produce a Proxy to the worker, enabling you to call methods as if it were a normal class. - * Note we give it the type information it needs so the returned Proxy object looks like that type. - * Node has a different implementation, needing this nodeEndpoint wrapper, hence this function exists here. - */ -export function getRemoteBarretenbergWasm(worker: Worker) { - return wrap(nodeEndpoint(worker)); -} - -/** - * Returns number of cpus as reported by the system, unless overriden by HARDWARE_CONCURRENCY env var. - */ -export function getNumCpu() { - return +process.env.HARDWARE_CONCURRENCY! || os.cpus().length; -} - -/** - * Returns a logger function for worker threads. - * When a custom logger is provided, posts messages back to the main thread. - * Otherwise, writes directly to stdout. - */ -export function threadLogger(useCustomLogger: boolean): ((msg: string) => void) | undefined { - if (useCustomLogger) { - return (msg: string) => { - if (parentPort) { - parentPort.postMessage({ type: 'log', msg }); - } - }; - } - // Write directly to stdout when no custom logger is provided - return (msg: string) => { - writeSync(1, msg + '\n'); - }; -} - -export function killSelf(): never { - // Extordinarily hard process termination. Due to how parent threads block on child threads etc, even process.exit - // doesn't seem to be able to abort the process. The following does. - process.kill(process.pid); - throw new Error(); -} - -export function getAvailableThreads(logger: (msg: string) => void): number { - try { - return os.cpus().length; - } catch (e: any) { - logger( - `Could not detect environment to query number of threads. Falling back to one thread. Error: ${e.message ?? e}`, - ); - return 1; - } -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/node_endpoint.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/node_endpoint.ts deleted file mode 100644 index 63d5301b2ea..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/helpers/node/node_endpoint.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NodeEndpoint } from 'comlink/dist/esm/node-adapter.js'; - -export function nodeEndpoint(nep: NodeEndpoint) { - const listeners = new WeakMap(); - return { - postMessage: nep.postMessage.bind(nep), - addEventListener: (_: any, eh: any) => { - const l = (data: any) => { - if ('handleEvent' in eh) { - eh.handleEvent({ data }); - } else { - eh({ data }); - } - }; - nep.on('message', l); - listeners.set(eh, l); - }, - removeEventListener: (_: any, eh: any) => { - const l = listeners.get(eh); - if (!l) { - return; - } - nep.off('message', l); - listeners.delete(eh); - }, - start: nep.start && nep.start.bind(nep), - }; -} diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/index.test.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/index.test.ts deleted file mode 100644 index 6b26b90d819..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Worker } from 'worker_threads'; - -import { createMainWorker } from '../barretenberg_wasm/barretenberg_wasm_main/factory/node/index.js'; -import { BarretenbergWasmMainWorker } from '../barretenberg_wasm/barretenberg_wasm_main/index.js'; -import { getRemoteBarretenbergWasm } from '../barretenberg_wasm/helpers/index.js'; -import { fetchModuleAndThreads } from '../barretenberg_wasm/index.js'; - -describe('barretenberg wasm', () => { - let wasm: BarretenbergWasmMainWorker; - let worker: Worker; - - beforeAll(async () => { - worker = await createMainWorker(); - wasm = getRemoteBarretenbergWasm(worker); - const { module, threads } = await fetchModuleAndThreads(2); - await wasm.init(module, threads); - }, 20000); - - afterAll(async () => { - await wasm.destroy(); - await worker.terminate(); - }); - - it('should new malloc, transfer and slice mem', async () => { - const length = 1024; - const ptr = await wasm.call('bbmalloc', length); - const buf = Buffer.alloc(length, 128); - await wasm.writeMemory(ptr, Uint8Array.from(buf)); - const result = Buffer.from(await wasm.getMemorySlice(ptr, ptr + length)); - await wasm.call('bbfree', ptr); - expect(result).toStrictEqual(buf); - }); - - it('test abort', async () => { - await expect(() => wasm.call('test_abort')).rejects.toThrow(); - }); - - it('should new malloc, transfer and slice mem', async () => { - const length = 1024; - const ptr = await wasm.call('bbmalloc', length); - const buf = Buffer.alloc(length, 128); - await wasm.writeMemory(ptr, Uint8Array.from(buf)); - const result = Buffer.from(await wasm.getMemorySlice(ptr, ptr + length)); - await wasm.call('bbfree', ptr); - expect(result).toStrictEqual(buf); - }); -}); diff --git a/barretenberg/ts/bb.js/src/barretenberg_wasm/index.ts b/barretenberg/ts/bb.js/src/barretenberg_wasm/index.ts deleted file mode 100644 index 070cbe9d1cd..00000000000 --- a/barretenberg/ts/bb.js/src/barretenberg_wasm/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { fetchCode } from './fetch_code/index.js'; -import { getAvailableThreads, getSharedMemoryAvailable } from './helpers/node/index.js'; - -export async function fetchModuleAndThreads( - desiredThreads = 32, - wasmPath?: string, - logger: (msg: string) => void = () => {}, -) { - const shared = getSharedMemoryAvailable(); - - const availableThreads = shared ? getAvailableThreads(logger) : 1; - // We limit the number of threads to 32 as we do not benefit from greater numbers. - const limitedThreads = Math.min(desiredThreads, availableThreads, 32); - - logger(`Fetching bb wasm from ${wasmPath ?? 'default location'}`); - const code = await fetchCode(shared, wasmPath); - logger(`Compiling bb wasm of ${code.byteLength} bytes`); - const module = await WebAssembly.compile(code); - logger('Compilation of bb wasm complete'); - return { module, threads: limitedThreads }; -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/browser/index.ts b/barretenberg/ts/bb.js/src/bb_backends/browser/index.ts deleted file mode 100644 index 730d6594270..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/browser/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BackendOptions, BackendType } from '../index.js'; -import type { IMsgpackBackendAsync, IMsgpackBackendSync } from '../interface.js'; -import { BarretenbergWasmAsyncBackend, BarretenbergWasmSyncBackend } from '../wasm.js'; - -/** - * Create backend of specific type (no fallback) - */ -export async function createAsyncBackend( - type: BackendType, - options: BackendOptions, - logger: (msg: string) => void, -): Promise { - switch (type) { - case BackendType.Wasm: - case BackendType.WasmWorker: { - const useWorker = type === BackendType.WasmWorker; - logger(`Using WASM backend (worker: ${useWorker})`); - return await BarretenbergWasmAsyncBackend.new({ - threads: options.threads, - wasmPath: options.wasmPath, - logger, - memory: options.memory, - useWorker, - }); - } - - default: - throw new Error(`Unknown backend type: ${type}`); - } -} - -/** - * Create backend of specific type (no fallback) - */ -export async function createSyncBackend( - type: BackendType, - options: BackendOptions, - logger: (msg: string) => void, -): Promise { - switch (type) { - case BackendType.Wasm: { - logger('Using WASM backend'); - return await BarretenbergWasmSyncBackend.new(options.wasmPath, logger); - } - - default: - throw new Error(`Backend ${type} not supported for BarretenbergSync`); - } -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts b/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts deleted file mode 100644 index 10f21920d6c..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/browser/platform.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function findPackageRoot(): string | null { - throw new Error('Not implemented in browser environment.'); -} - -export function findBbBinary(_customPath?: string): string | null { - throw new Error('Not implemented in browser environment.'); -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/errors.ts b/barretenberg/ts/bb.js/src/bb_backends/errors.ts deleted file mode 100644 index 1c5bf976b9d..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * The cross-layer contract for a failed call is the bare `retry` property, not this class. - * - * An error with `retry === true` failed for environmental reasons — the bb process died, its - * connection broke, the machine was too loaded to start one — and the operation may be retried. - * `retry === false`, or no such property, means retrying cannot help: the command itself is the - * problem, or the backend was destroyed by its owner. - * - * Consumers feature-detect the property rather than importing this class, so the convention - * survives package boundaries: - * - * if (err instanceof Error && (err as Error & { retry?: unknown }).retry === true) { ... } - * - * This is deliberately the same convention the ipc-runtime transports use, so a caller written - * against one works unchanged against the other. - */ -export class BackendUnavailableError extends Error { - readonly retry = true; - - constructor(message: string, options?: { cause?: unknown }) { - super(message, options); - this.name = 'BackendUnavailableError'; - } -} - -/** Whether a failure was environmental, and so may be retried. */ -export function isRetryable(err: unknown): boolean { - return err instanceof Error && (err as Error & { retry?: unknown }).retry === true; -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/interface.ts b/barretenberg/ts/bb.js/src/bb_backends/interface.ts deleted file mode 100644 index fe2e54a84f9..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/interface.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Generic interface for msgpack backend implementations. - * Both WASM and native binary backends implement this interface. - */ -export interface IMsgpackBackend { - /** - * Execute a msgpack command and return the msgpack response. - * @param inputBuffer The msgpack-encoded input buffer - * @returns The msgpack-encoded response buffer (sync or async) - */ - call(inputBuffer: Uint8Array): Uint8Array | Promise; - - /** - * Clean up resources. - */ - destroy(): void | Promise; -} - -/** - * Synchronous variant of IMsgpackBackend. - * Used by BarretenbergSync and SyncApi. - */ -export interface IMsgpackBackendSync extends IMsgpackBackend { - call(inputBuffer: Uint8Array): Uint8Array; - destroy(): void; -} - -/** - * Asynchronous variant of IMsgpackBackend. - * Used by Barretenberg and AsyncApi. - */ -export interface IMsgpackBackendAsync extends IMsgpackBackend { - call(inputBuffer: Uint8Array): Promise; - destroy(): Promise; -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/index.ts b/barretenberg/ts/bb.js/src/bb_backends/node/index.ts deleted file mode 100644 index 670fd1ad965..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { BackendOptions, BackendType } from '../index.js'; -import type { IMsgpackBackendAsync, IMsgpackBackendSync } from '../interface.js'; -import { BarretenbergWasmAsyncBackend, BarretenbergWasmSyncBackend } from '../wasm.js'; -import { BarretenbergNativeShmSyncBackend } from './native_shm.js'; -import { BarretenbergNativeShmAsyncBackend } from './native_shm_async.js'; -import { BarretenbergNativeSocketAsyncBackend } from './native_socket.js'; -import { findBbBinary } from './platform.js'; - -/** - * Create backend of specific type (no fallback) - */ -export async function createAsyncBackend( - type: BackendType, - options: BackendOptions, - logger: (msg: string) => void, -): Promise { - options = { - ...options, - wasmPath: options.wasmPath ?? process.env.BB_WASM_PATH, - }; - - switch (type) { - case BackendType.NativeUnixSocket: { - const bbPath = findBbBinary(options.bbPath); - if (!bbPath) { - throw new Error('Native backend requires bb binary.'); - } - logger(`Using native Unix socket backend: ${bbPath}`); - return await BarretenbergNativeSocketAsyncBackend.new( - bbPath, - options.threads, - options.logger, - options.unref, - options.respawn, - ); - } - - case BackendType.NativeSharedMemory: { - const bbPath = findBbBinary(options.bbPath); - if (!bbPath) { - throw new Error('Native backend requires bb binary.'); - } - logger(`Using native shared memory async backend: ${bbPath}`); - return await BarretenbergNativeShmAsyncBackend.new( - bbPath, - options.napiPath, - options.threads, - options.logger, - options.unref, - ); - } - - case BackendType.Wasm: - case BackendType.WasmWorker: { - const useWorker = type === BackendType.WasmWorker; - logger(`Using WASM backend (worker: ${useWorker})`); - return await BarretenbergWasmAsyncBackend.new({ - threads: options.threads, - wasmPath: options.wasmPath, - logger: options.logger, - memory: options.memory, - useWorker, - unref: options.unref, - }); - } - - default: - throw new Error(`Unknown backend type: ${type}`); - } -} - -/** - * Create backend of specific type (no fallback) - */ -export async function createSyncBackend( - type: BackendType, - options: BackendOptions, - logger: (msg: string) => void, -): Promise { - options = { - ...options, - wasmPath: options.wasmPath ?? process.env.BB_WASM_PATH, - }; - - switch (type) { - case BackendType.NativeSharedMemory: { - const bbPath = findBbBinary(options.bbPath); - if (!bbPath) { - throw new Error('Native backend requires bb binary.'); - } - logger(`Using native shared memory backend: ${bbPath}`); - return await BarretenbergNativeShmSyncBackend.new( - bbPath, - options.napiPath, - options.threads, - options.logger, - options.unref, - ); - } - - case BackendType.Wasm: { - logger('Using WASM backend'); - return await BarretenbergWasmSyncBackend.new(options.wasmPath, logger); - } - - default: - throw new Error(`Backend ${type} not supported for BarretenbergSync`); - } -} 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 deleted file mode 100644 index c0ce6472a38..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { SpawnedProcessBackendSync } from '@aztec-foundation/ipc-runtime'; - -import { IMsgpackBackendSync } from '../interface.js'; - -// 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 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 constructor(private backend: SpawnedProcessBackendSync) {} - - /** - * Create and initialize a shared memory backend. - * @param bbBinaryPath Path to bb 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, - threads?: number, - logger?: (msg: string) => void, - unref?: boolean, - ): Promise { - // 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' }, - }); - return new BarretenbergNativeShmSyncBackend(backend); - } - - call(inputBuffer: Uint8Array): Uint8Array { - return this.backend.call(inputBuffer); - } - - destroy(): void { - 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 deleted file mode 100644 index 68608fba1ac..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_shm_async.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { SpawnedProcessBackend } from '@aztec-foundation/ipc-runtime'; - -import { IMsgpackBackendAsync } from '../interface.js'; - -// 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 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 constructor(private backend: SpawnedProcessBackend) {} - - /** - * Create and initialize an async shared memory backend. - * @param bbBinaryPath Path to bb binary - * @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, - threads?: number, - logger?: (msg: string) => void, - unref?: boolean, - ): Promise { - 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' }, - }); - return new BarretenbergNativeShmAsyncBackend(backend); - } - - call(inputBuffer: Uint8Array): Promise { - return this.backend.call(inputBuffer); - } - - destroy(): Promise { - 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 deleted file mode 100644 index 9a25511e45a..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { jest } from '@jest/globals'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -import { isRetryable } from '../errors.js'; -import { BarretenbergNativeSocketAsyncBackend } from './native_socket.js'; - -jest.setTimeout(30_000); - -// Echo server speaking the bb msgpack socket protocol (4-byte LE length prefix), started after -// an optional delay to simulate bb's startup time on a loaded machine. -const ECHO_SERVER_JS = ` -const net = require('net'); -const socketPath = process.argv[2]; -const server = net.createServer(sock => { - let buf = Buffer.alloc(0); - sock.on('data', d => { - buf = Buffer.concat([buf, d]); - while (buf.length >= 4) { - const len = buf.readUInt32LE(0); - if (buf.length < 4 + len) break; - const payload = buf.subarray(4, 4 + len); - const out = Buffer.alloc(4); - out.writeUInt32LE(payload.length, 0); - sock.write(out); - sock.write(payload); - buf = buf.subarray(4 + len); - } - }); -}); -server.listen(socketPath); -`; - -// A fake bb binary: a bash script that optionally sleeps, then runs the echo server on the -// socket path bb receives via `msgpack run --input ` ($4). -function writeFakeBb(startupDelaySecs: number): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-')); - const serverJs = path.join(dir, 'echo_server.cjs'); - fs.writeFileSync(serverJs, ECHO_SERVER_JS); - const file = path.join(dir, 'bb'); - const sleep = startupDelaySecs > 0 ? `sleep ${startupDelaySecs}\n` : ''; - fs.writeFileSync(file, `#!/bin/bash\n${sleep}exec node ${serverJs} "$4"\n`, { mode: 0o755 }); - return file; -} - -// A fake bb that writes its pid next to the script before serving, so a replacement process can -// be told apart from the one it replaced. -function writeFakeBbRecordingPid(): { path: string; pids: () => number[] } { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-')); - const serverJs = path.join(dir, 'echo_server.cjs'); - fs.writeFileSync(serverJs, ECHO_SERVER_JS); - const pidLog = path.join(dir, 'pids'); - const file = path.join(dir, 'bb'); - fs.writeFileSync(file, `#!/bin/bash\necho $$ >> ${pidLog}\nexec node ${serverJs} "$4"\n`, { mode: 0o755 }); - return { - path: file, - pids: () => fs.readFileSync(pidLog, 'utf-8').split('\n').filter(Boolean).map(Number), - }; -} - -function writeFakeBbScript(script: string): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-')); - const file = path.join(dir, 'bb'); - fs.writeFileSync(file, script, { mode: 0o755 }); - return file; -} - -describe('BarretenbergNativeSocketAsyncBackend', () => { - it('connects and echoes when bb starts promptly', async () => { - const fakeBb = writeFakeBb(0); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fakeBb); - const response = await backend.call(new Uint8Array([1, 2, 3, 4])); - expect(response).toEqual(new Uint8Array([1, 2, 3, 4])); - await backend.destroy(); - }); - - it('connects even when bb takes longer than 5s to create its socket', async () => { - const fakeBb = writeFakeBb(7); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fakeBb); - const response = await backend.call(new Uint8Array([42])); - expect(response).toEqual(new Uint8Array([42])); - await backend.destroy(); - }); - - 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 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( - /Failed to spawn bb/, - ); - }); - - describe('when bb dies', () => { - it('fails the call as retryable, and keeps failing without respawn', async () => { - const fake = writeFakeBbRecordingPid(); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fake.path); - expect(await backend.call(new Uint8Array([1]))).toEqual(new Uint8Array([1])); - - process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !isProcessAlive(fake.pids()[0])); - - const err = await backend.call(new Uint8Array([2])).catch(e => e); - expect(isRetryable(err)).toBe(true); - // No replacement: the same retryable failure, and no second process. - expect(isRetryable(await backend.call(new Uint8Array([3])).catch(e => e))).toBe(true); - expect(fake.pids()).toHaveLength(1); - await backend.destroy(); - }); - - it('serves the next call from a replacement process when respawn is on', async () => { - const fake = writeFakeBbRecordingPid(); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fake.path, undefined, undefined, undefined, true); - expect(await backend.call(new Uint8Array([1]))).toEqual(new Uint8Array([1])); - - process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !isProcessAlive(fake.pids()[0])); - - expect(await backend.call(new Uint8Array([2]))).toEqual(new Uint8Array([2])); - const pids = fake.pids(); - expect(pids).toHaveLength(2); - expect(pids[1]).not.toEqual(pids[0]); - await backend.destroy(); - }); - - it('starts one replacement however many calls find the connection down', async () => { - const fake = writeFakeBbRecordingPid(); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fake.path, undefined, undefined, undefined, true); - await backend.call(new Uint8Array([1])); - - process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !isProcessAlive(fake.pids()[0])); - - const results = await Promise.all([1, 2, 3, 4].map(n => backend.call(new Uint8Array([n])))); - expect(results).toEqual([1, 2, 3, 4].map(n => new Uint8Array([n]))); - expect(fake.pids()).toHaveLength(2); - await backend.destroy(); - }); - - it('does not leave a replacement running when destroyed while it starts', async () => { - const fake = writeFakeBbRecordingPid(); - const backend = await BarretenbergNativeSocketAsyncBackend.new(fake.path, undefined, undefined, undefined, true); - await backend.call(new Uint8Array([1])); - - process.kill(fake.pids()[0], 'SIGKILL'); - await waitUntil(() => !isProcessAlive(fake.pids()[0])); - - const call = backend.call(new Uint8Array([2])).catch(e => e); - await backend.destroy(); - expect(String(await call)).toMatch(/destroy/); - - // Whether or not the replacement got as far as starting, nothing may be left running. - await waitUntil(() => fake.pids().every(pid => !isProcessAlive(pid))); - }); - - it('fails retryably when the replacement cannot start either', async () => { - // The scenario the option exists for, gone wrong: bb is killed, and its replacement dies - // under the same pressure before it can connect. The caller must still be told to retry. - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-')); - const serverJs = path.join(dir, 'echo_server.cjs'); - fs.writeFileSync(serverJs, ECHO_SERVER_JS); - const pidLog = path.join(dir, 'pids'); - const bb = path.join(dir, 'bb'); - // Serves on the first start; every later start exits before creating its socket. - fs.writeFileSync( - bb, - `#!/bin/bash\nif [ -e ${pidLog} ]; then exit 9; fi\necho $$ >> ${pidLog}\nexec node ${serverJs} "$4"\n`, - { mode: 0o755 }, - ); - - const backend = await BarretenbergNativeSocketAsyncBackend.new(bb, undefined, undefined, undefined, true); - await backend.call(new Uint8Array([1])); - - const pid = Number(fs.readFileSync(pidLog, 'utf-8').trim()); - process.kill(pid, 'SIGKILL'); - await waitUntil(() => !isProcessAlive(pid)); - - const err = await backend.call(new Uint8Array([2])).catch(e => e); - expect(isRetryable(err)).toBe(true); - expect(String(err)).toMatch(/exited before IPC connection was ready/); - await backend.destroy(); - }); - - it('leaves no bb running when the connection breaks but the process does not exit', async () => { - // A server that hangs up on the first request and keeps running, as bb does: its client is - // disconnected, its serve loop is not. - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-bb-')); - const serverJs = path.join(dir, 'hangup_server.cjs'); - fs.writeFileSync( - serverJs, - `const net = require('net');\nnet.createServer(s => s.on('data', () => s.end())).listen(process.argv[2]);\nsetInterval(() => {}, 1000);\n`, - ); - const pidLog = path.join(dir, 'pids'); - const bb = path.join(dir, 'bb'); - fs.writeFileSync(bb, `#!/bin/bash\necho $$ >> ${pidLog}\nexec node ${serverJs} "$4"\n`, { mode: 0o755 }); - - const backend = await BarretenbergNativeSocketAsyncBackend.new(bb); - const pid = Number(fs.readFileSync(pidLog, 'utf-8').trim()); - await expect(backend.call(new Uint8Array([1]))).rejects.toThrow(); - - await waitUntil(() => !isProcessAlive(pid)); - await backend.destroy(); - }); - }); -}); - -/** Poll until `predicate` holds, so a test never depends on when an event lands. */ -async function waitUntil(predicate: () => boolean, timeoutMs = 10_000): Promise { - const deadline = Date.now() + timeoutMs; - while (!predicate()) { - if (Date.now() > deadline) { - throw new Error('timed out waiting for condition'); - } - await new Promise(resolve => setTimeout(resolve, 10)); - } -} - -function isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} 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 deleted file mode 100644 index d6a890c8088..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/native_socket.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SpawnedProcessBackend } from '@aztec-foundation/ipc-runtime'; -import * as os from 'os'; - -import { IMsgpackBackendAsync } from '../interface.js'; - -/** - * 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. - * - * 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 constructor(private backend: SpawnedProcessBackend) {} - - static async new( - bbBinaryPath: string, - threads?: number, - logger?: (msg: string) => void, - unref?: boolean, - respawn?: boolean, - ): Promise { - // If threads not set use num cpu cores, max 16. - const hwc = threads ? threads.toString() : Math.min(16, os.cpus().length).toString(); - const backend = await SpawnedProcessBackend.spawn({ - binaryPath: bbBinaryPath, - binaryName: 'bb', - instancePrefix: 'bb', - ipcPathArgs: ['msgpack', 'run', '--input', '{path}'], - transport: 'uds', - logger, - env: { HARDWARE_CONCURRENCY: hwc }, - unref: true, - unrefStdio: unref, - respawn, - }); - return new BarretenbergNativeSocketAsyncBackend(backend); - } - - call(inputBuffer: Uint8Array): Promise { - return this.backend.call(inputBuffer); - } - - destroy(): Promise { - return this.backend.destroy(); - } -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts b/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts deleted file mode 100644 index 966f8d82a93..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/node/platform.ts +++ /dev/null @@ -1,130 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { fileURLToPath } from 'url'; - -function getCurrentDir() { - if (typeof __dirname !== 'undefined') { - return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return path.dirname(fileURLToPath(import.meta.url)); - } -} - -/** - * Find package root by climbing directory tree until package.json is found. - * @param startDir Starting directory to search from - * @returns Absolute path to package root, or null if not found - */ -export function findPackageRoot(): string | null { - let currentDir = getCurrentDir(); - const root = path.parse(currentDir).root; - - while (currentDir !== root) { - const packageJsonPath = path.join(currentDir, 'package.json'); - if (fs.existsSync(packageJsonPath)) { - // Check if this is the actual package root by verifying it has a 'build' directory - // This ensures we skip intermediate package.json files (e.g., in dest/node-cjs/) - const buildDir = path.join(currentDir, 'build'); - if (fs.existsSync(buildDir)) { - return currentDir; - } - } - currentDir = path.dirname(currentDir); - } - - return null; -} - -/** - * Supported platform/architecture combinations. - */ -export type Platform = 'x86_64-linux' | 'x86_64-darwin' | 'aarch64-linux' | 'aarch64-darwin'; - -/** - * Map from Platform to build directory name. - */ -const PLATFORM_TO_BUILD_DIR: Record = { - 'x86_64-linux': 'amd64-linux', - 'x86_64-darwin': 'amd64-macos', - 'aarch64-linux': 'arm64-linux', - 'aarch64-darwin': 'arm64-macos', -}; - -/** - * Detect the current platform and architecture. - * @returns Platform identifier or null if unsupported - */ -export function detectPlatform(): Platform | null { - const arch = process.arch; // 'x64' | 'arm64' | ... - const platform = process.platform; // 'linux' | 'darwin' | 'win32' | ... - - if (arch === 'x64' && platform === 'linux') { - return 'x86_64-linux'; - } - if (arch === 'x64' && platform === 'darwin') { - return 'x86_64-darwin'; - } - if (arch === 'arm64' && platform === 'linux') { - return 'aarch64-linux'; - } - if (arch === 'arm64' && platform === 'darwin') { - return 'aarch64-darwin'; - } - - return null; -} - -/** - * Find the bb binary for the native backend. - * @param customPath Optional custom path to bb binary (overrides automatic detection) - * @returns Absolute path to bb binary, or null if not found - * - * Search order: - * 1. If customPath is provided and exists, return it - * 2. If BB_BINARY_PATH is set and exists, return it - * 3. Otherwise search in /build//bb - */ -export function findBbBinary(customPath?: string): string | null { - // Check custom path first if provided - if (customPath) { - if (fs.existsSync(customPath)) { - return path.resolve(customPath); - } - // Custom path provided but doesn't exist - return null - return null; - } - - const envPath = process.env.BB_BINARY_PATH; - if (envPath) { - if (fs.existsSync(envPath)) { - return path.resolve(envPath); - } - return null; - } - - // Automatic detection - const platform = detectPlatform(); - if (!platform) { - return null; - } - - const buildDir = PLATFORM_TO_BUILD_DIR[platform]; - - // Get package root by climbing directory tree to find package.json - const packageRoot = findPackageRoot(); - - if (!packageRoot) { - return null; - } - - // Check in build//bb - const bbPath = path.join(packageRoot, 'build', buildDir, 'bb'); - - if (fs.existsSync(bbPath)) { - return bbPath; - } - - return null; -} diff --git a/barretenberg/ts/bb.js/src/bb_backends/wasm.ts b/barretenberg/ts/bb.js/src/bb_backends/wasm.ts deleted file mode 100644 index 5d08a545668..00000000000 --- a/barretenberg/ts/bb.js/src/bb_backends/wasm.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { proxy } from 'comlink'; - -import { createMainWorker } from '../barretenberg_wasm/barretenberg_wasm_main/factory/node/index.js'; -import { BarretenbergWasmMain, BarretenbergWasmMainWorker } from '../barretenberg_wasm/barretenberg_wasm_main/index.js'; -import { getRemoteBarretenbergWasm } from '../barretenberg_wasm/helpers/index.js'; -import { fetchModuleAndThreads } from '../barretenberg_wasm/index.js'; -import { IMsgpackBackendAsync, IMsgpackBackendSync } from './interface.js'; - -/** - * Synchronous WASM backend that wraps BarretenbergWasmMain. - * Encapsulates all WASM initialization and memory management. - */ -export class BarretenbergWasmSyncBackend implements IMsgpackBackendSync { - private constructor(private wasm: BarretenbergWasmMain) {} - - /** - * Create and initialize a synchronous WASM backend. - * @param wasmPath Optional path to WASM files - * @param logger Optional logging function - */ - static async new(wasmPath?: string, logger?: (msg: string) => void): Promise { - const wasm = new BarretenbergWasmMain(); - const { module, threads } = await fetchModuleAndThreads(1, wasmPath, logger); - await wasm.init(module, threads, logger); - return new BarretenbergWasmSyncBackend(wasm); - } - - call(inputBuffer: Uint8Array): Uint8Array { - return this.wasm.cbindCall('ipc_ffi_entry', inputBuffer); - } - - destroy(): void { - // BarretenbergWasmMain has async destroy, but for sync API we call it without awaiting - // This is consistent with the synchronous semantics expected by the caller - void this.wasm.destroy(); - } -} - -/** - * Asynchronous WASM backend that supports both direct WASM and worker-based modes. - * - * Worker mode (default): Runs WASM on a worker thread to avoid blocking the main thread. Used in browsers. - * Direct mode: Runs WASM directly on the calling thread. Used by node.js for better performance. - */ -export class BarretenbergWasmAsyncBackend implements IMsgpackBackendAsync { - private constructor( - private wasm: BarretenbergWasmMain | BarretenbergWasmMainWorker, - private worker?: any, - ) {} - - /** - * Create and initialize an asynchronous WASM backend. - * @param options.threads Number of threads (defaults to hardware max, up to 32 for parallel proving) - * @param options.wasmPath Optional path to WASM files - * @param options.logger Optional logging function - * @param options.memory Optional initial and maximum memory configuration - * @param options.useWorker Run on worker thread (default: true for browser safety) - * @param options.unref Unref worker handles so they don't prevent process exit - */ - static async new( - options: { - threads?: number; - wasmPath?: string; - logger?: (msg: string) => void; - memory?: { initial?: number; maximum?: number }; - useWorker?: boolean; - unref?: boolean; - } = {}, - ): Promise { - // Default to worker mode for browser safety - const useWorker = options.useWorker ?? true; - - if (useWorker) { - // Worker-based mode: runs on worker thread (browser-safe) - const worker = await createMainWorker(); - const wasm = getRemoteBarretenbergWasm(worker); - const { module, threads } = await fetchModuleAndThreads(options.threads, options.wasmPath, options.logger); - await wasm.init( - module, - threads, - proxy(options.logger ?? (() => {})), - options.memory?.initial, - options.memory?.maximum, - ); - if (options.unref) { - worker.unref(); - } - return new BarretenbergWasmAsyncBackend(wasm, worker); - } else { - // Direct mode: runs on calling thread (faster but blocks thread) - const wasm = new BarretenbergWasmMain(); - const { module, threads } = await fetchModuleAndThreads(options.threads, options.wasmPath, options.logger); - await wasm.init(module, threads, options.logger, options.memory?.initial, options.memory?.maximum, options.unref); - return new BarretenbergWasmAsyncBackend(wasm); - } - } - - call(inputBuffer: Uint8Array): Promise { - return Promise.resolve(this.wasm.cbindCall('ipc_ffi_entry', inputBuffer)); - } - - async destroy(): Promise { - await this.wasm.destroy(); - if (this.worker) { - await this.worker.terminate(); - } - } -} diff --git a/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts b/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts deleted file mode 100644 index 3c90963fd11..00000000000 --- a/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BarretenbergWasmSyncBackend } from '../bb_backends/wasm.js'; -import { SyncApi } from '../generated/sync.js'; - -describe('BBApi Exception Handling from bb.js', () => { - let backend: BarretenbergWasmSyncBackend; - let api: SyncApi; - - beforeAll(async () => { - backend = await BarretenbergWasmSyncBackend.new(); - api = new SyncApi(backend); - }, 60000); - - afterAll(() => { - backend.destroy(); - }); - - it('should catch CRS initialization exceptions from WASM', () => { - // Create an SrsInitSrs command with invalid data that will cause an exception in C++ - // We pass buffers that are too small, which will cause validation to fail - const invalidCommand = { - numPoints: 100, // Request 100 points (requires 6400 bytes) - pointsBuf: new Uint8Array(10), // Only 10 bytes - will cause exception - g2Point: new Uint8Array(10), // Only 10 bytes (needs 128) - will cause exception - }; - - // In WASM builds, throw_or_abort calls abort directly which throws a generic Error - // In native builds with exceptions, our try-catch in bbapi converts it to ErrorResponse - // This test verifies that errors are catchable from bb.js (even if as generic Error in WASM) - expect(() => { - api.srsInitSrs(invalidCommand); - }).toThrow(); - }); - - it('should return error message from caught exception', () => { - const invalidCommand = { - numPoints: 100, - pointsBuf: new Uint8Array(10), - g2Point: new Uint8Array(10), - }; - - try { - api.srsInitSrs(invalidCommand); - fail('Expected exception to be thrown'); - } catch (error) { - // Error is catchable and contains a useful message - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toBeTruthy(); - expect((error as Error).message.length).toBeGreaterThan(0); - expect((error as Error).message).toContain('invalid points_buf size'); - } - }); -}); diff --git a/barretenberg/ts/bb.js/src/bbapi_exception.ts b/barretenberg/ts/bb.js/src/bbapi_exception.ts deleted file mode 100644 index 47f3fac43aa..00000000000 --- a/barretenberg/ts/bb.js/src/bbapi_exception.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Exception thrown when barretenberg API operations fail - */ -export class BBApiException extends Error { - constructor(message: string) { - super(message); - this.name = 'BBApiException'; - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, BBApiException); - } - } -} diff --git a/barretenberg/ts/bb.js/src/benchmark/index.ts b/barretenberg/ts/bb.js/src/benchmark/index.ts deleted file mode 100644 index 607fd705b5b..00000000000 --- a/barretenberg/ts/bb.js/src/benchmark/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as fs from 'fs'; - -export * from './timer.js'; - -const bfd = (() => { - const bfdStr = process.env.BENCHMARK_FD; - const bfd = bfdStr ? parseInt(bfdStr) : -1; - if (bfd >= 0 && !fs.fstatSync(bfd)) { - throw new Error('fd is not open. Did you redirect in your shell?'); - } - return bfd; -})(); - -export function writeBenchmark(name: string, value: T, labels: Record = {}) { - if (bfd === -1) { - return; - } - const data = { - timestamp: new Date().toISOString(), - name, - type: typeof value, - value, - ...labels, - }; - const jsonl = JSON.stringify(data) + '\n'; - fs.writeSync(bfd, jsonl); -} diff --git a/barretenberg/ts/bb.js/src/bin/index.ts b/barretenberg/ts/bb.js/src/bin/index.ts index f4064ef5b53..5c9795af993 100644 --- a/barretenberg/ts/bb.js/src/bin/index.ts +++ b/barretenberg/ts/bb.js/src/bin/index.ts @@ -1,8 +1,7 @@ #!/usr/bin/env node +import { findBbBinary } from '@aztec-foundation/bb.js-api'; import { spawnSync } from 'node:child_process'; -import { findBbBinary } from '../bb_backends/node/platform.js'; - const bin = findBbBinary(); if (!bin) { diff --git a/barretenberg/ts/bb.js/src/crs/net_crs.ts b/barretenberg/ts/bb.js/src/crs/net_crs.ts index cbe85f95fe9..2b098f2e720 100644 --- a/barretenberg/ts/bb.js/src/crs/net_crs.ts +++ b/barretenberg/ts/bb.js/src/crs/net_crs.ts @@ -1,4 +1,4 @@ -import { makeBackoff, retry } from '../retry/index.js'; +import { makeBackoff, retry } from './retry.js'; // Primary CRS host (Cloudflare R2) const CRS_PRIMARY_HOST = 'https://crs.aztec-cdn.foundation'; diff --git a/barretenberg/ts/bb.js/src/retry/index.ts b/barretenberg/ts/bb.js/src/crs/retry.ts similarity index 100% rename from barretenberg/ts/bb.js/src/retry/index.ts rename to barretenberg/ts/bb.js/src/crs/retry.ts diff --git a/barretenberg/ts/bb.js/src/curve_constants.test.ts b/barretenberg/ts/bb.js/src/curve_constants.test.ts new file mode 100644 index 00000000000..ab42f0e8248 --- /dev/null +++ b/barretenberg/ts/bb.js/src/curve_constants.test.ts @@ -0,0 +1,55 @@ +import { findBbBinary } from '@aztec-foundation/bb.js-api'; +import { execFileSync } from 'child_process'; +import { Decoder } from 'msgpackr'; + +import { + BN254_FQ_MODULUS, + BN254_FR_MODULUS, + BN254_G1_GENERATOR, + BN254_G2_GENERATOR, + GRUMPKIN_FQ_MODULUS, + GRUMPKIN_FR_MODULUS, + GRUMPKIN_G1_GENERATOR, + SECP256K1_FQ_MODULUS, + SECP256K1_FR_MODULUS, + SECP256K1_G1_GENERATOR, + SECP256R1_FQ_MODULUS, + SECP256R1_FR_MODULUS, + SECP256R1_G1_GENERATOR, +} from './curve_constants.js'; + +// The constants are written out in curve_constants.ts; bb computes the same values from the curve +// definitions it is compiled against. If the two ever disagree, one of them is wrong. +describe('curve constants', () => { + const bb = findBbBinary(); + const itWithBb = bb ? it : it.skip; + + itWithBb('match the curves bb is built against', () => { + const emitted = execFileSync(bb!, ['msgpack', 'curve_constants'], { + maxBuffer: 1024 * 1024, + }); + const actual = new Decoder({ useRecords: false }).decode(emitted) as Record; + + const hex = (b: Uint8Array) => Buffer.from(b).toString('hex'); + const point = (p: { x: Uint8Array; y: Uint8Array }) => [hex(p.x), hex(p.y)]; + const pointFq2 = (p: { x: readonly Uint8Array[]; y: readonly Uint8Array[] }) => [p.x.map(hex), p.y.map(hex)]; + const asModulus = (b: Uint8Array) => BigInt(`0x${hex(b)}`); + + expect(asModulus(actual.bn254_fr_modulus)).toEqual(BN254_FR_MODULUS); + expect(asModulus(actual.bn254_fq_modulus)).toEqual(BN254_FQ_MODULUS); + expect(point(actual.bn254_g1_generator)).toEqual(point(BN254_G1_GENERATOR)); + expect(pointFq2(actual.bn254_g2_generator)).toEqual(pointFq2(BN254_G2_GENERATOR)); + + expect(asModulus(actual.grumpkin_fr_modulus)).toEqual(GRUMPKIN_FR_MODULUS); + expect(asModulus(actual.grumpkin_fq_modulus)).toEqual(GRUMPKIN_FQ_MODULUS); + expect(point(actual.grumpkin_g1_generator)).toEqual(point(GRUMPKIN_G1_GENERATOR)); + + expect(asModulus(actual.secp256k1_fr_modulus)).toEqual(SECP256K1_FR_MODULUS); + expect(asModulus(actual.secp256k1_fq_modulus)).toEqual(SECP256K1_FQ_MODULUS); + expect(point(actual.secp256k1_g1_generator)).toEqual(point(SECP256K1_G1_GENERATOR)); + + expect(asModulus(actual.secp256r1_fr_modulus)).toEqual(SECP256R1_FR_MODULUS); + expect(asModulus(actual.secp256r1_fq_modulus)).toEqual(SECP256R1_FQ_MODULUS); + expect(point(actual.secp256r1_g1_generator)).toEqual(point(SECP256R1_G1_GENERATOR)); + }); +}); diff --git a/barretenberg/ts/bb.js/src/curve_constants.ts b/barretenberg/ts/bb.js/src/curve_constants.ts new file mode 100644 index 00000000000..1ded9f27975 --- /dev/null +++ b/barretenberg/ts/bb.js/src/curve_constants.ts @@ -0,0 +1,71 @@ +/** + * Field moduli and generator points for the curves bb works over, for callers doing their own + * field arithmetic rather than asking bb. + * + * These are properties of the curves, not of any bb build, so they are written out here rather + * than fetched or generated. `curve_constants.test.ts` checks them against `bb msgpack + * curve_constants`, which computes the same values from the curve definitions bb is compiled + * against, so a divergence is caught rather than assumed away. + * + * Coordinates are 32-byte big-endian, matching how bb serialises a field element. BN254's G2 is + * over Fq2, so each of its coordinates is a pair. + */ + +function modulus(hex: string): bigint { + return BigInt(`0x${hex}`); +} + +function coord(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return bytes; +} + +/** A point with 32-byte big-endian coordinates. */ +export type CurvePoint = { readonly x: Uint8Array; readonly y: Uint8Array }; +/** A point over a quadratic extension field, each coordinate a pair. */ +export type CurvePointFq2 = { + readonly x: readonly [Uint8Array, Uint8Array]; + readonly y: readonly [Uint8Array, Uint8Array]; +}; + +export const BN254_FR_MODULUS = modulus('30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001'); +export const BN254_FQ_MODULUS = modulus('30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47'); +export const BN254_G1_GENERATOR: CurvePoint = { + x: coord('0000000000000000000000000000000000000000000000000000000000000001'), + y: coord('0000000000000000000000000000000000000000000000000000000000000002'), +}; +export const BN254_G2_GENERATOR: CurvePointFq2 = { + x: [ + coord('1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed'), + coord('198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2'), + ], + y: [ + coord('12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa'), + coord('090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b'), + ], +}; + +// Grumpkin is BN254 with the two fields swapped: its scalar field is BN254's base field. +export const GRUMPKIN_FR_MODULUS = modulus('30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47'); +export const GRUMPKIN_FQ_MODULUS = modulus('30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001'); +export const GRUMPKIN_G1_GENERATOR: CurvePoint = { + x: coord('0000000000000000000000000000000000000000000000000000000000000001'), + y: coord('0000000000000002cf135e7506a45d632d270d45f1181294833fc48d823f272c'), +}; + +export const SECP256K1_FR_MODULUS = modulus('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); +export const SECP256K1_FQ_MODULUS = modulus('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); +export const SECP256K1_G1_GENERATOR: CurvePoint = { + x: coord('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + y: coord('483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8'), +}; + +export const SECP256R1_FR_MODULUS = modulus('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'); +export const SECP256R1_FQ_MODULUS = modulus('ffffffff00000001000000000000000000000000ffffffffffffffffffffffff'); +export const SECP256R1_G1_GENERATOR: CurvePoint = { + x: coord('6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296'), + y: coord('4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'), +}; diff --git a/barretenberg/ts/bb.js/src/index.html b/barretenberg/ts/bb.js/src/index.html deleted file mode 100644 index 82293342f4e..00000000000 --- a/barretenberg/ts/bb.js/src/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - My App - - - - - diff --git a/barretenberg/ts/bb.js/src/index.ts b/barretenberg/ts/bb.js/src/index.ts index 77e7aa41794..2f6494675f1 100644 --- a/barretenberg/ts/bb.js/src/index.ts +++ b/barretenberg/ts/bb.js/src/index.ts @@ -16,8 +16,19 @@ export { } from './barretenberg/index.js'; export { randomBytes } from './random/index.js'; -export { splitHonkProof, reconstructHonkProof, deflattenFields, type ProofData } from './proof/index.js'; -export { BBApiException } from './bbapi_exception.js'; +export { splitHonkProof, reconstructHonkProof, deflattenFields, type ProofData } from './proof.js'; +/** + * `BbError` is thrown when bb answers a command with an error rather than a result. It comes from + * the generated API, so the async and sync classes throw the same one. + * + * One failure does not raise it: a handler that reports by throwing (throw_or_abort) rather than by + * returning an error response gives a plain Error on the wasm backends, because bb's wasm build + * compiles with BB_NO_EXCEPTIONS and the throw reaches the host's hook instead of the dispatcher's + * catch. The message is the same either way, so catch Error to handle both. + * + * BBApiException is the name the previous API used, kept so existing callers keep compiling. + */ +export { BbError, BbError as BBApiException } from '@aztec-foundation/bb.js-api'; // Export Point types for use in foundation and other packages export type { @@ -28,9 +39,9 @@ export type { GrumpkinPoint, Secp256k1Point, Secp256r1Point, -} from './generated/api_types.js'; +} from '@aztec-foundation/bb.js-api'; -export { toChonkProof } from './generated/api_types.js'; +export { toChonkProof } from '@aztec-foundation/bb.js-api'; /** * @deprecated Fq2 coordinates are typed per curve now (see Bn254G2Point). @@ -40,21 +51,7 @@ export type Field2 = [Uint8Array, Uint8Array]; export { CircuitKind } from './circuit_kind.js'; -// Export curve constants for use in foundation -export { - BN254_FQ_MODULUS, - BN254_FR_MODULUS, - BN254_G1_GENERATOR, - BN254_G2_GENERATOR, - GRUMPKIN_FR_MODULUS, - GRUMPKIN_FQ_MODULUS, - GRUMPKIN_G1_GENERATOR, - SECP256K1_FR_MODULUS, - SECP256K1_FQ_MODULUS, - SECP256K1_G1_GENERATOR, - SECP256R1_FR_MODULUS, - SECP256R1_FQ_MODULUS, - SECP256R1_G1_GENERATOR, -} from './generated/curve_constants.js'; +// Curve constants, for callers doing their own field arithmetic. +export * from './curve_constants.js'; -export { findBbBinary } from './bb_backends/node/platform.js'; +export { findBbBinary } from './backends/node/index.js'; diff --git a/barretenberg/ts/bb.js/src/proof/index.ts b/barretenberg/ts/bb.js/src/proof.ts similarity index 100% rename from barretenberg/ts/bb.js/src/proof/index.ts rename to barretenberg/ts/bb.js/src/proof.ts diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index 97a4c764659..45dc22e314e 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -5,6 +5,7 @@ ROOT=$(git rev-parse --show-toplevel) BB_AVM_SIM_BINARY=bb-avm-sim BB_AVM_SIM_PACKAGE=@aztec-foundation/bb-avm-sim CDB_PACKAGE=@aztec-foundation/cdb +BB_JS_API_PACKAGE=@aztec-foundation/bb.js-api hash=$(hash_str \ $(bb.js/bootstrap.sh hash) \ @@ -25,8 +26,6 @@ function generate_bb_avm_sim_package { "$ROOT/ipc-codegen/src/generate.ts" \ --schema "$ROOT/barretenberg/cpp/src/barretenberg/avm/avm_schema.json" \ --lang ts \ - --client \ - --out "$ROOT/barretenberg/ts/bb-avm-sim/src/generated" \ --package "$ROOT/barretenberg/ts/bb-avm-sim" \ --package-name "$BB_AVM_SIM_PACKAGE" \ --binary-name "$BB_AVM_SIM_BINARY" \ @@ -44,17 +43,100 @@ function generate_cdb_package { --schema "$ROOT/barretenberg/cpp/src/barretenberg/cdb/cdb_schema.json" \ --lang ts \ --server \ - --out "$ROOT/barretenberg/ts/cdb/src/generated" \ --package "$ROOT/barretenberg/ts/cdb" \ --package-name "$CDB_PACKAGE" } -# Both bb-avm-sim and cdb are gitignored workspaces declared in package.json, so -# `yarn install --immutable` fails against the committed lockfile unless both exist. +# The bb API as a generated client package: typed AsyncApi/SyncApi over bb spawned as a +# process (uds, shm) or run in-process as the wasm module (node and browsers). bb.js is +# a consumer of this package, keeping only its facades and CRS handling. +function generate_bb_js_api_package { + local 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 \ + --package "$ROOT/barretenberg/ts/bb.js-api" \ + --package-name "$BB_JS_API_PACKAGE" \ + --binary-name bb \ + --binary-env-var BB_BINARY_PATH \ + --strip-method-prefix \ + --strip-type-prefix \ + --package-transports uds,shm,wasm \ + --package-ipc-path-args 'msgpack,run,--input,{path}' \ + --package-wasm-module barretenberg.wasm \ + --package-wasm-threads-module barretenberg-threads.wasm +} + +# bb-avm-sim, cdb and bb.js-api are gitignored workspaces declared in package.json, so +# `yarn install --immutable` fails against the committed lockfile unless all exist. # Generate them together before installing, whichever one we're about to build. function generate_packages { generate_bb_avm_sim_package generate_cdb_package + generate_bb_js_api_package +} + +# The wasm builds the bb.js-api package ships: threads (node, cross-origin isolated +# browsers) and single-thread (browsers without SharedArrayBuffer). +# +# Shipped uncompressed. npm tarballs are gzipped either way, so this costs nothing on install, +# and it is the form a host's own compression and the browser's compiled-code cache both want: +# only a real application/wasm response can be streamed straight into WebAssembly.compileStreaming +# and cached. A consumer serving from a host that does not compress can point bb.js's wasmPath (or +# BB_WASM_PATH) at a compressed copy instead; the loader recognises gzip. +function copy_bb_js_api_wasm { + # Replace rather than add to: the package publishes everything under wasm/, so a module left + # from an earlier build would ship alongside the current one. + rm -rf bb.js-api/wasm + mkdir -p bb.js-api/wasm + cp "$ROOT/barretenberg/cpp/build-wasm-threads/bin/barretenberg.wasm" bb.js-api/wasm/barretenberg-threads.wasm + cp "$ROOT/barretenberg/cpp/build-wasm/bin/barretenberg.wasm" bb.js-api/wasm/barretenberg.wasm +} + +function copy_bb_js_api_native { + local target_dir="bb.js-api/build/$(arch)-$(os)" + mkdir -p "$target_dir" + cp "$ROOT/barretenberg/cpp/build/bin/bb" "$target_dir/bb" +} + +function copy_bb_js_api_cross { + if [ -n "${1:-}" ]; then + local cross_arch="$1" + mkdir -p "bb.js-api/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/bb" "bb.js-api/build/$cross_arch/bb" + elif semver check "${REF_NAME:-}" && [ "$(arch)" == "amd64" ]; then + for cross_arch in arm64-linux amd64-macos arm64-macos; do + mkdir -p "bb.js-api/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/bb" "bb.js-api/build/$cross_arch/bb" + done + else + echo "This task is expected to be run with an explicit arch or in an x86 release context." + fi +} + +function prepare_bb_js_api_arch_packages { + yarn workspace "$BB_JS_API_PACKAGE" run prepare_arch_packages "$@" +} + +# Generate + compile the package bb.js compiles against, without the wasm/binary artifacts +# (enough for type-checking, formatting and lint). Not cached: it is a few seconds of tsc. +function build_bb_js_api_ts { + generate_packages + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$BB_JS_API_PACKAGE" build +} + +# The full package: compiled TS plus the wasm modules and this machine's bb binary. bb.js +# runs these at test time, so it stages them even when its own build is cached. +function build_bb_js_api { + echo_header "bb.js-api package build" + build_bb_js_api_ts + copy_bb_js_api_wasm + copy_bb_js_api_native + prepare_bb_js_api_arch_packages "$(arch)-$(os)=build/$(arch)-$(os)/bb" } function copy_bb_avm_sim_native { @@ -81,7 +163,7 @@ function copy_bb_avm_sim_cross { } function prepare_bb_avm_sim_arch_packages { - (cd bb-avm-sim && ./scripts/prepare_arch_packages.sh "$@") + yarn workspace "$BB_AVM_SIM_PACKAGE" run prepare_arch_packages "$@" } function build_bb_js { @@ -160,8 +242,9 @@ function test { (cd bb.js && ./bootstrap.sh test) } +# bb.js ships no native artifacts of its own; the bb binary it runs through comes from bb.js-api. function cross_copy_bb_js { - (cd bb.js && ./bootstrap.sh cross_copy "$@") + cross_copy_bb_js_api "$@" } function cross_copy_bb_avm_sim { @@ -178,6 +261,12 @@ function cross_copy { function get_projects { echo "$PWD/bb.js" + if [ -d bb.js-api ]; then + for package_dir in bb.js-api/packages/*; do + [ -d "$package_dir" ] && echo "$PWD/$package_dir" + done + echo "$PWD/bb.js-api" + fi if [ -d bb-avm-sim ]; then for package_dir in bb-avm-sim/packages/*; do [ -d "$package_dir" ] && echo "$PWD/$package_dir" @@ -209,7 +298,38 @@ function release_cdb { (cd cdb && retry "deploy_npm ${REF_NAME#v}") } +function cross_copy_bb_js_api { + generate_packages + copy_bb_js_api_cross "$@" + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$BB_JS_API_PACKAGE" build + prepare_bb_js_api_arch_packages +} + +# bb.js depends on bb.js-api, so it is published first (with its arch packages, the one published +# home of the bb binary). +function release_bb_js_api { + generate_packages + copy_bb_js_api_wasm + copy_bb_js_api_native + copy_bb_js_api_cross + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$BB_JS_API_PACKAGE" build + prepare_bb_js_api_arch_packages + # The binaries come from builds keyed on source, not on the release: finalize them so they + # carry this release's version like every other copy. + local f + for f in bb.js-api/packages/*/bb; do + [ -f "$f" ] && ../cpp/bootstrap.sh finalize_bb_binary "$(realpath "$f")" + done + for package_dir in bb.js-api/packages/*; do + (cd "$package_dir" && retry "deploy_npm ${REF_NAME#v}") + done + (cd bb.js-api && retry "deploy_npm ${REF_NAME#v}") +} + function release { + release_bb_js_api (cd bb.js && ./bootstrap.sh release) release_bb_avm_sim release_cdb @@ -217,7 +337,8 @@ function release { } export -f generate_bb_avm_sim_package copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages -export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim release release_cdb +export -f generate_bb_js_api_package copy_bb_js_api_wasm copy_bb_js_api_native copy_bb_js_api_cross prepare_bb_js_api_arch_packages build_bb_js_api_ts build_bb_js_api +export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim cross_copy_bb_js_api release release_cdb release_bb_js_api case "$cmd" in "") diff --git a/barretenberg/ts/package.json b/barretenberg/ts/package.json index 552ffc5c233..ab6588c9187 100644 --- a/barretenberg/ts/package.json +++ b/barretenberg/ts/package.json @@ -8,6 +8,8 @@ }, "workspaces": [ "bb.js", + "bb.js-api", + "bb.js-api/packages/*", "bb-avm-sim", "bb-avm-sim/packages/*", "cdb" diff --git a/barretenberg/ts/yarn.lock b/barretenberg/ts/yarn.lock index eace48622f5..ef513e4233d 100644 --- a/barretenberg/ts/yarn.lock +++ b/barretenberg/ts/yarn.lock @@ -56,10 +56,62 @@ __metadata: languageName: unknown linkType: soft +"@aztec-foundation/bb.js-api-darwin-arm64@npm:0.1.0, @aztec-foundation/bb.js-api-darwin-arm64@workspace:bb.js-api/packages/bb.js-api-darwin-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-darwin-arm64@workspace:bb.js-api/packages/bb.js-api-darwin-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/bb.js-api-darwin-x64@npm:0.1.0, @aztec-foundation/bb.js-api-darwin-x64@workspace:bb.js-api/packages/bb.js-api-darwin-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-darwin-x64@workspace:bb.js-api/packages/bb.js-api-darwin-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/bb.js-api-linux-arm64@npm:0.1.0, @aztec-foundation/bb.js-api-linux-arm64@workspace:bb.js-api/packages/bb.js-api-linux-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-linux-arm64@workspace:bb.js-api/packages/bb.js-api-linux-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/bb.js-api-linux-x64@npm:0.1.0, @aztec-foundation/bb.js-api-linux-x64@workspace:bb.js-api/packages/bb.js-api-linux-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api-linux-x64@workspace:bb.js-api/packages/bb.js-api-linux-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/bb.js-api@workspace:^, @aztec-foundation/bb.js-api@workspace:bb.js-api": + version: 0.0.0-use.local + resolution: "@aztec-foundation/bb.js-api@workspace:bb.js-api" + dependencies: + "@aztec-foundation/bb.js-api-darwin-arm64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-darwin-x64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-linux-arm64": "npm:0.1.0" + "@aztec-foundation/bb.js-api-linux-x64": "npm:0.1.0" + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" + "@types/node": "npm:^22.15.17" + msgpackr: "npm:^1.11.2" + tslib: "npm:^2.4.0" + typescript: "npm:^5.3.3" + dependenciesMeta: + "@aztec-foundation/bb.js-api-darwin-arm64": + optional: true + "@aztec-foundation/bb.js-api-darwin-x64": + optional: true + "@aztec-foundation/bb.js-api-linux-arm64": + optional: true + "@aztec-foundation/bb.js-api-linux-x64": + optional: true + bin: + bb: ./dest/bin.js + languageName: unknown + linkType: soft + "@aztec-foundation/bb.js@workspace:bb.js": version: 0.0.0-use.local resolution: "@aztec-foundation/bb.js@workspace:bb.js" dependencies: + "@aztec-foundation/bb.js-api": "workspace:^" "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" "@jest/globals": "npm:^30.0.0" "@swc/core": "npm:^1.10.1" @@ -72,7 +124,6 @@ __metadata: "@types/proper-lockfile": "npm:^4.1.4" "@types/source-map-support": "npm:^0.5.6" "@typescript/native-preview": "npm:7.0.0-dev.20251126.1" - comlink: "npm:^4.4.1" commander: "npm:^12.1.0" eslint: "npm:^9.26.0" eslint-config-prettier: "npm:^10.1.5" @@ -113,6 +164,8 @@ __metadata: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Fbarretenberg-ts-packages%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec%2Fbarretenberg-ts-packages%40workspace%3A." + bin: + ipc-runtime-prepare-arch-packages: ./scripts/prepare_arch_packages.mjs languageName: node linkType: soft @@ -2712,13 +2765,6 @@ __metadata: languageName: node linkType: hard -"comlink@npm:^4.4.1": - version: 4.4.2 - resolution: "comlink@npm:4.4.2" - checksum: 10/ecee53b5b4536b3aa3f7636c383f831e68fbc013def77665cc7fad873d72cfa23b994e1ec4b49e83e4e909c1089a03acae03a523e33a5e5ed938cdb613456434 - languageName: node - linkType: hard - "commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" diff --git a/ipc-codegen/README.md b/ipc-codegen/README.md index c306cc18eba..f7bf7aee536 100644 --- a/ipc-codegen/README.md +++ b/ipc-codegen/README.md @@ -72,8 +72,8 @@ ipc-codegen/ naming.ts # snake_case / PascalCase helpers templates/ # static templates copied alongside generated code cpp/ipc_codegen/*.hpp # C++ support headers copied into generated output - rust/{backend,error,ffi_backend}.rs - zig/{backend,ffi_backend}.zig + rust/{backend,error}.rs + zig/backend.zig echo_example/ # 4-language echo service (cross-lang test harness) SCHEMA_SPEC.md # wire protocol and schema-format reference ``` @@ -98,7 +98,7 @@ node --experimental-strip-types --no-warnings \ |---|---| | `--schema ` | Path to the schema (JSON or JSONC; friendly or legacy positional form — see `SCHEMA_SPEC.md`). | | `--lang ` | Target language. | -| `--out ` | Output directory. Generated files are (re)written every run; static templates are copied alongside and re-copied only if missing (so handwritten edits to templated scaffolding are preserved). | +| `--out ` | Output directory. Generated files are (re)written every run; static templates are copied alongside and re-copied only if missing (so handwritten edits to templated scaffolding are preserved). Implied by `--package` (`/src/generated`, where the package shell imports the bindings from). | ### Role flags @@ -106,9 +106,12 @@ node --experimental-strip-types --no-warnings \ |---|---| | `--server` | Emit server dispatch (matches request name to handler, deserialises, calls handler, serialises response). Pair it with an `ipc::IpcServer` from ipc-runtime. | | `--client` | Emit a typed client class/struct with one method per command. Pair it with an `ipc::IpcClient` (C++) or the equivalent Rust/Zig/TS binding. | -| `--package ` | TS only. Emit a complete package shell. Which of the two shells you get depends on the role flags. With `--client` (or with neither role flag): a wrapper around the generated async client that launches a native service binary, connects over UDS or SHM, and resolves the binary from an override path, environment variable, installed arch package, or local `build//` directory. With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. Passing `--server --client` selects the client shell. | +| `--package ` | TS only. Emit a complete client package: the generated API over every backend the package has (`--package-transports`), with a `Service.create(options)` that picks one by default (the spawned process when the binary resolves, else the wasm module), can be forced to one, or takes a backend object of the consumer's own; one entry per host (node, `browser`, `react-native`) so each host sees only the backends that exist there; the binary resolved from an override path, an environment variable, or the installed per-platform arch package (this package's optional dependencies). With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. | | `--uds` | Rust/Zig only. Copies the `Backend` trait template (and `error.rs` for Rust) into `` so consumers can plug ipc-runtime — or any custom transport — behind the generated client. The flag name is historical: the trait is transport-agnostic. | -| `--ffi` | Rust/Zig only. Adds the `ffi_backend` template (a thin wrapper exposing the generated client over a C ABI for embedding in other languages). | +| `--ffi` | In-process FFI, both directions of the contract in `SCHEMA_SPEC.md` ("FFI entry"). With `--client` (Rust/Zig): generates `ffi_backend`, a client backend that calls a linked library's `_ipc_ffi_entry`. With `--server` (C++/Rust): emits the entry itself — `_ffi.{hpp,cpp}` defining `_ipc_ffi_entry`/`_alloc`/`_free` over the generated dispatch (the service defines `ipc_ffi_dispatcher()`), or `_ffi.rs` with an `export__ffi!` macro doing the same over a `Handler + Default` type. The service prefix on the symbols lets several services be linked into one binary. A wasm reactor built from these is what the TS `wasm` transport runs. | +| `--package-transports ` | TS `--package` only. Comma-separated transports the package offers: `uds`, `shm` (spawned process) and `wasm` (the service's wasm module, run in-process through `@aztec-foundation/ipc-runtime/wasm`; adds a browser entry). | +| `--package-wasm-module `, `--package-wasm-threads-module ` | TS `wasm` transport. Basenames of the single-thread and threads builds the owning project copies into the package's `wasm/` directory; at least one is required. | +| `--package-wasm-host-imports ` | TS `wasm` transport. A TS module copied to `src/wasm_host_imports.ts` exporting `hostImports`, for a module whose platform layer imports functions beyond WASI (bb imports a logger, an abort hook and its thread count). Default: none. | ### Naming flags @@ -133,7 +136,6 @@ flags below are only for legacy positional schemas, which have no `service`. | Flag | Purpose | |---|---| -| `--curve-constants` | TS only. Also emit `curve_constants.ts` with bn254/grumpkin/secp moduli & generators for schemas that need curve constants. | | `--skeleton ` | One-shot scaffolding: writes a `_handlers.{ts,rs,zig,cpp}` stub, `main`, and a build file into `` if they don't already exist. Skipped on subsequent runs. | | `--package-name ` | TS package mode only. Package name to write into the generated `package.json`. | | `--binary-name ` | Client package shell only; ignored by the server shell. Native service binary name to launch. | @@ -147,18 +149,17 @@ Paths below are illustrative — consumers commit their own schema next to the C++ server that owns the wire format and supply absolute or relative paths on the command line. -### TypeScript client, with curve constants +### TypeScript client ```sh src/generate.ts \ --schema /path/to/myservice_schema.jsonc \ --lang ts \ --out /path/to/output/generated \ - --client \ - --curve-constants + --client ``` -Produces `api_types.ts`, `async.ts`, `sync.ts`, `curve_constants.ts`. The TS +Produces `api_types.ts`, `async.ts`, `sync.ts`. The TS client uses `@aztec-foundation/ipc-runtime`'s `UdsIpcClient` or `NapiShmSyncClient` for transport — no template copy. @@ -168,7 +169,6 @@ transport — no template copy. src/generate.ts \ --schema /path/to/myservice_schema.jsonc \ --lang ts \ - --out /path/to/myservice/src/generated \ --client \ --package /path/to/myservice \ --package-name @aztec/myservice \ @@ -176,11 +176,14 @@ src/generate.ts \ --package-transports uds,shm ``` -Produces the generated TS client under `src/generated/` plus a package shell +Produces the generated TS client under `src/generated/` (the package implies +`--out`) plus a package shell (`package.json`, `tsconfig.json`, `src/index.ts`, `src/platform.ts`, and -`scripts/prepare_arch_packages.sh`). The package exports a -`MyServiceService.spawn(...)` helper that launches the native binary and wraps -the generated async client. `scripts/prepare_arch_packages.sh` turns +`scripts/prepare_arch_packages.sh`). The package exports +`MyServiceService.create(...)`, which wraps the generated async client around +the backend chosen for the host (a spawned process by default here; `spawn` +and `wasm` force one), plus `createBackend` for facades that wrap the API +themselves. `scripts/prepare_arch_packages.sh` turns `build//` directories into per-architecture npm packages matching the binary resolution path. diff --git a/ipc-codegen/SCHEMA_SPEC.md b/ipc-codegen/SCHEMA_SPEC.md index d4f1ddabe05..269ee61e380 100644 --- a/ipc-codegen/SCHEMA_SPEC.md +++ b/ipc-codegen/SCHEMA_SPEC.md @@ -212,6 +212,33 @@ msgpack uses the smallest encoding that fits the value, not the declared type: a `u64` of `5` encodes as a single positive-fixint byte. Decoders MUST accept any integer encoding width for any integer field. +### FFI entry + +A service linked into its caller — a native library, or a wasm reactor — takes +requests through one exported C symbol instead of a transport: + +```c +void _ipc_ffi_entry(const uint8_t* input, size_t input_len, + uint8_t** output, size_t* output_len); +void* _ipc_ffi_alloc(size_t size); +void _ipc_ffi_free(void* ptr); +``` + +`` is the schema's `service` in snake_case (`Bb` → `bb_ipc_ffi_entry`; +a positional schema uses the generator's type prefix, or the bare names when +there is none). The prefix is what lets several services be linked into one +binary. `input` is exactly the request payload above — no framing envelope, +since an in-process call has no transport — and `*output` receives the response +payload in a buffer from `_ipc_ffi_alloc()` that the caller releases with +`_ipc_ffi_free()` (both malloc/free-compatible). Input buffers the caller +hands over are allocated the same way when the caller lives outside the +module's memory (wasm). The entry is not thread-safe; callers serialize their +calls. `--server --ffi` generates it; the client side is the generated Rust/Zig +`ffi_backend` natively and `@aztec-foundation/ipc-runtime/wasm` for a wasm +module, which also expects a wasi reactor (`_initialize`, WASI imports, +wasi-threads `thread-spawn` for a threads build) and finds the entry by its +`_ipc_ffi_entry` suffix when not told its name. + ## Schema versioning A SHA-256 hash of the schema can be computed and embedded in generated code for diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh index af3c6065231..368ab33de70 100755 --- a/ipc-codegen/bootstrap.sh +++ b/ipc-codegen/bootstrap.sh @@ -62,8 +62,12 @@ function test_cmds { echo "$prefix $script golden ts" echo "$prefix $script golden cpp" echo "$prefix $script golden zig" + # The generated C++ FFI entry, driven in-process (the Rust equivalent runs as + # a cargo test during build()). + echo "$prefix ipc-codegen/echo_example/cpp/build/bin/ffi_test" echo "$prefix ipc-codegen/echo_example/ts_package/test.sh uds" echo "$prefix ipc-codegen/echo_example/ts_package/test.sh shm" + echo "$prefix ipc-codegen/echo_example/ts_package/test.sh wasm" # Matrix: one command per (server, client) pair over UDS. for server in "${matrix_langs[@]}"; do diff --git a/ipc-codegen/echo_example/cpp/CMakeLists.txt b/ipc-codegen/echo_example/cpp/CMakeLists.txt index ab156f9b048..fabe0bd0cde 100644 --- a/ipc-codegen/echo_example/cpp/CMakeLists.txt +++ b/ipc-codegen/echo_example/cpp/CMakeLists.txt @@ -39,7 +39,7 @@ target_compile_definitions(echo_common INTERFACE MSGPACK_USE_STD_VARIANT_ADAPTOR ) -add_executable(echo_server src/echo_server.cpp) +add_executable(echo_server src/echo_server.cpp src/echo_handlers.cpp) target_link_libraries(echo_server PRIVATE echo_common ipc_runtime) add_executable(echo_client @@ -50,3 +50,15 @@ target_link_libraries(echo_client PRIVATE echo_common ipc_runtime) add_executable(golden_test src/golden_test.cpp) target_link_libraries(golden_test PRIVATE echo_common) + +# The service as an in-process library: the generated FFI entry over the same +# handlers the socket server uses. +add_library(echo_ffi STATIC + src/echo_ffi.cpp + src/echo_handlers.cpp + src/generated/echo_ffi.cpp +) +target_link_libraries(echo_ffi PUBLIC echo_common) + +add_executable(ffi_test src/ffi_test.cpp) +target_link_libraries(ffi_test PRIVATE echo_ffi) diff --git a/ipc-codegen/echo_example/cpp/bootstrap.sh b/ipc-codegen/echo_example/cpp/bootstrap.sh index acba8e0db11..abf3b0ae37e 100755 --- a/ipc-codegen/echo_example/cpp/bootstrap.sh +++ b/ipc-codegen/echo_example/cpp/bootstrap.sh @@ -10,8 +10,9 @@ $NODE "$CODEGEN/src/generate.ts" \ --lang cpp \ --server \ --client \ + --ffi \ --out "$DIR/src/generated" \ --cpp-namespace echo cmake -S "$DIR" -B "$DIR/build" -cmake --build "$DIR/build" --target echo_server echo_client golden_test +cmake --build "$DIR/build" --target echo_server echo_client golden_test ffi_test diff --git a/ipc-codegen/echo_example/cpp/src/echo_ffi.cpp b/ipc-codegen/echo_example/cpp/src/echo_ffi.cpp new file mode 100644 index 00000000000..cff28b2e363 --- /dev/null +++ b/ipc-codegen/echo_example/cpp/src/echo_ffi.cpp @@ -0,0 +1,20 @@ +// Names the context the echo handlers run over, for the in-process FFI entry. +// +// The generated dispatch is a template over the context type +// (make_echo_handler), so the generator never learns what that type is. +// The server gets it from main(), which constructs an EchoCtx and passes it to +// echo::serve(). A library has no main(), so this is where the FFI build names +// it: the same job lib.rs does for the Rust example with export_echo_ffi!. +#include "echo_handlers.hpp" + +#include "generated/echo_ffi.hpp" + +namespace echo { + +AsyncDispatchHandler &ipc_ffi_dispatcher() { + static EchoCtx ctx; + static AsyncDispatchHandler handler = make_echo_handler(ctx); + return handler; +} + +} // namespace echo diff --git a/ipc-codegen/echo_example/cpp/src/echo_handlers.cpp b/ipc-codegen/echo_example/cpp/src/echo_handlers.cpp new file mode 100644 index 00000000000..32ba0562a1d --- /dev/null +++ b/ipc-codegen/echo_example/cpp/src/echo_handlers.cpp @@ -0,0 +1,52 @@ +// Echo service handlers — echo input fields back in the response. Handlers are +// asynchronous: they produce their result via respond.ok(...) (synchronously +// here; a real service could defer to a thread pool and respond later). +#include "echo_handlers.hpp" + +#include +#include + +namespace echo { + +template <> +void handle_bytes(EchoCtx & /*ctx*/, wire::EchoBytes &&cmd, + Responder respond) { + respond.ok({.data = std::move(cmd.data)}); +} + +template <> +void handle_fields(EchoCtx & /*ctx*/, wire::EchoFields &&cmd, + Responder respond) { + respond.ok({.a = cmd.a, .b = cmd.b, .name = std::move(cmd.name)}); +} + +template <> +void handle_nested(EchoCtx & /*ctx*/, wire::EchoNested &&cmd, + Responder respond) { + respond.ok({.inner = std::move(cmd.inner)}); +} + +template <> +void handle_aliases(EchoCtx & /*ctx*/, wire::EchoAliases &&cmd, + Responder respond) { + respond.ok({.treeId = cmd.treeId, + .hash = cmd.hash, + .maybeHash = cmd.maybeHash, + .hashes = std::move(cmd.hashes)}); +} + +template <> +void handle_blobs(EchoCtx & /*ctx*/, wire::EchoBlobs &&cmd, + Responder respond) { + respond.ok( + {.maybeData = std::move(cmd.maybeData), .parts = std::move(cmd.parts)}); +} + +template <> +void handle_fail(EchoCtx & /*ctx*/, wire::EchoFail &&cmd, + Responder /*respond*/) { + // Throwing is turned into an error frame by the generated dispatch. + throw std::runtime_error(cmd.message); +} + +} // namespace echo diff --git a/ipc-codegen/echo_example/cpp/src/echo_handlers.hpp b/ipc-codegen/echo_example/cpp/src/echo_handlers.hpp new file mode 100644 index 00000000000..21016159c7c --- /dev/null +++ b/ipc-codegen/echo_example/cpp/src/echo_handlers.hpp @@ -0,0 +1,32 @@ +// Echo service handlers: the template specializations the header-only generated +// dispatch calls, shared by the socket server (echo_server.cpp) and the +// in-process FFI entry (echo_ffi.cpp). Declared here so every translation unit +// that instantiates make_echo_handler() sees them first. +#pragma once + +#include "generated/echo_dispatch.hpp" + +namespace echo { + +struct EchoCtx {}; // empty context for the echo service + +template <> +void handle_bytes(EchoCtx &ctx, wire::EchoBytes &&cmd, + Responder respond); +template <> +void handle_fields(EchoCtx &ctx, wire::EchoFields &&cmd, + Responder respond); +template <> +void handle_nested(EchoCtx &ctx, wire::EchoNested &&cmd, + Responder respond); +template <> +void handle_aliases(EchoCtx &ctx, wire::EchoAliases &&cmd, + Responder respond); +template <> +void handle_blobs(EchoCtx &ctx, wire::EchoBlobs &&cmd, + Responder respond); +template <> +void handle_fail(EchoCtx &ctx, wire::EchoFail &&cmd, + Responder respond); + +} // namespace echo diff --git a/ipc-codegen/echo_example/cpp/src/echo_server.cpp b/ipc-codegen/echo_example/cpp/src/echo_server.cpp index f272d6e01fd..12814cdb7bc 100644 --- a/ipc-codegen/echo_example/cpp/src/echo_server.cpp +++ b/ipc-codegen/echo_example/cpp/src/echo_server.cpp @@ -1,63 +1,13 @@ -// Echo IPC server (C++) — provides handler specializations for the -// header-only generated dispatch. +// Echo IPC server (C++) over the header-only generated dispatch and +// ipc-runtime's socket/shared-memory server. // Usage: echo_server --socket /tmp/echo.sock +#include "echo_handlers.hpp" #include "generated/echo_ipc_server.hpp" #include -#include #include -namespace echo { - -struct EchoCtx {}; // empty context for the echo service - -// Template specializations — echo input fields back in response. Handlers are -// asynchronous: they produce their result via respond.ok(...) (synchronously -// here; a real service could defer to a thread pool and respond later). -template <> -void handle_bytes(EchoCtx & /*ctx*/, wire::EchoBytes &&cmd, - Responder respond) { - respond.ok({.data = std::move(cmd.data)}); -} - -template <> -void handle_fields(EchoCtx & /*ctx*/, wire::EchoFields &&cmd, - Responder respond) { - respond.ok({.a = cmd.a, .b = cmd.b, .name = std::move(cmd.name)}); -} - -template <> -void handle_nested(EchoCtx & /*ctx*/, wire::EchoNested &&cmd, - Responder respond) { - respond.ok({.inner = std::move(cmd.inner)}); -} - -template <> -void handle_aliases(EchoCtx & /*ctx*/, wire::EchoAliases &&cmd, - Responder respond) { - respond.ok({.treeId = cmd.treeId, - .hash = cmd.hash, - .maybeHash = cmd.maybeHash, - .hashes = std::move(cmd.hashes)}); -} - -template <> -void handle_blobs(EchoCtx & /*ctx*/, wire::EchoBlobs &&cmd, - Responder respond) { - respond.ok( - {.maybeData = std::move(cmd.maybeData), .parts = std::move(cmd.parts)}); -} - -template <> -void handle_fail(EchoCtx & /*ctx*/, wire::EchoFail &&cmd, - Responder /*respond*/) { - // Throwing is turned into an error frame by the generated dispatch. - throw std::runtime_error(cmd.message); -} - -} // namespace echo - int main(int argc, char **argv) { const char *socket_path = nullptr; for (int i = 1; i < argc - 1; i++) { diff --git a/ipc-codegen/echo_example/cpp/src/ffi_test.cpp b/ipc-codegen/echo_example/cpp/src/ffi_test.cpp new file mode 100644 index 00000000000..522be0daa74 --- /dev/null +++ b/ipc-codegen/echo_example/cpp/src/ffi_test.cpp @@ -0,0 +1,119 @@ +// In-process FFI conformance test (C++): drives the generated +// echo_ipc_ffi_entry with wire requests and checks the responses — a round +// trip, the error frame for a failing command, and the error frame for +// malformed input. +// +// Usage: ffi_test + +#include "generated/echo_ffi.hpp" + +#include +#include +#include +#include +#include + +namespace { + +int g_fail = 0; + +void check(bool ok, const std::string &label) { + std::cerr << (ok ? " PASS: " : " FAIL: ") << label << "\n"; + if (!ok) + g_fail++; +} + +// [[name, payload]] — a named-union pair inside the one-element argument array. +template +std::vector pack_request(const char *name, const Cmd &cmd) { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(name)); + pk.pack(cmd); + return std::vector(buf.data(), buf.data() + buf.size()); +} + +// Hand the request over the way a foreign caller does: in a buffer from +// echo_ipc_ffi_alloc, receiving the response in one it frees with +// echo_ipc_ffi_free. +std::vector call(const std::vector &request) { + auto *in = static_cast(echo_ipc_ffi_alloc(request.size())); + std::copy(request.begin(), request.end(), in); + uint8_t *out = nullptr; + size_t out_len = 0; + echo_ipc_ffi_entry(in, request.size(), &out, &out_len); + echo_ipc_ffi_free(in); + std::vector response(out, out + out_len); + echo_ipc_ffi_free(out); + return response; +} + +struct Response { + std::string type; + msgpack::object_handle handle; + msgpack::object payload; +}; + +Response decode(const std::vector &bytes) { + Response r; + r.handle = msgpack::unpack(reinterpret_cast(bytes.data()), + bytes.size()); + auto obj = r.handle.get(); + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 2) { + throw std::runtime_error("response is not a [name, payload] pair"); + } + r.type = obj.via.array.ptr[0].as(); + r.payload = obj.via.array.ptr[1]; + return r; +} + +std::string error_message(const msgpack::object &payload) { + std::map fields; + payload.convert(fields); + return fields["message"]; +} + +} // namespace + +int main() { + { + echo::wire::EchoBytes cmd{.data = {0xde, 0xad, 0xbe, 0xef, 0x42}}; + auto response = decode(call(pack_request("EchoBytes", cmd))); + echo::wire::EchoBytesResponse decoded; + response.payload.convert(decoded); + check(response.type == "EchoBytesResponse" && decoded.data == cmd.data, + "EchoBytes round trip"); + } + { + echo::wire::EchoFields cmd{.a = 42, .b = 999999, .name = "hello ffi"}; + auto response = decode(call(pack_request("EchoFields", cmd))); + echo::wire::EchoFieldsResponse decoded; + response.payload.convert(decoded); + check(response.type == "EchoFieldsResponse" && decoded.a == 42 && + decoded.b == 999999 && decoded.name == "hello ffi", + "EchoFields round trip"); + } + { + echo::wire::EchoFail cmd{.message = "boom"}; + auto response = decode(call(pack_request("EchoFail", cmd))); + check(response.type == "EchoErrorResponse" && + error_message(response.payload) == "boom", + "EchoFail becomes an error frame carrying the message"); + } + { + auto response = decode(call(pack_request("NoSuchCommand", 0))); + check(response.type == "EchoErrorResponse" && + error_message(response.payload).find("unknown command") != + std::string::npos, + "unknown command becomes an error frame"); + } + + if (g_fail > 0) { + std::cerr << "ffi_test: " << g_fail << " failure(s)\n"; + return 1; + } + std::cerr << "ffi_test: all passed\n"; + return 0; +} diff --git a/ipc-codegen/echo_example/rust/Cargo.toml b/ipc-codegen/echo_example/rust/Cargo.toml index b951bba882d..f1c8c0af18d 100644 --- a/ipc-codegen/echo_example/rust/Cargo.toml +++ b/ipc-codegen/echo_example/rust/Cargo.toml @@ -3,6 +3,9 @@ name = "echo-wire-compat" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["rlib", "cdylib"] + [[bin]] name = "echo_server" path = "src/echo_server.rs" @@ -14,8 +17,8 @@ path = "src/echo_client.rs" [features] default = ["ipc-runtime"] ipc-runtime = ["dep:ipc-runtime"] -# Compile-checks the generated FFI backend (no real FFI library is linked; -# the extern symbol is only required at link time of a consumer binary). +# The generated FFI entry (export_echo_ffi!) and FFI client backend; the two +# meet in-process in tests/ffi_roundtrip.rs. ffi = ["dep:libc"] [dependencies] diff --git a/ipc-codegen/echo_example/rust/bootstrap.sh b/ipc-codegen/echo_example/rust/bootstrap.sh index 635d588d2a7..9afa7d260b3 100755 --- a/ipc-codegen/echo_example/rust/bootstrap.sh +++ b/ipc-codegen/echo_example/rust/bootstrap.sh @@ -15,5 +15,11 @@ $NODE "$CODEGEN/src/generate.ts" \ --out "$DIR/src/generated" (cd "$DIR" && cargo build --locked --quiet) -# Compile-check the generated FFI backend (not linked into the binaries). -(cd "$DIR" && cargo check --locked --quiet --features ffi) +# The generated FFI entry and the FFI client backend, linked into one crate: the +# client calls the service in-process (tests/ffi_roundtrip.rs). +(cd "$DIR" && cargo test --locked --quiet --features ffi) +# The same FFI entry as a wasi reactor, which is what the TS package's wasm +# transport runs. A cdylib on wasm32-wasip1 needs no linker flags of its own: +# the exported echo_ipc_ffi_* symbols are the whole contract. +(cd "$DIR" && cargo build --locked --quiet --lib --release \ + --target wasm32-wasip1 --no-default-features --features ffi) diff --git a/ipc-codegen/echo_example/rust/src/echo_server.rs b/ipc-codegen/echo_example/rust/src/echo_server.rs index ebf4e68fcbb..6a136c3fd2d 100644 --- a/ipc-codegen/echo_example/rust/src/echo_server.rs +++ b/ipc-codegen/echo_example/rust/src/echo_server.rs @@ -1,49 +1,10 @@ //! Echo IPC server — uses GENERATED dispatch + types + ipc-runtime transport. //! Usage: echo_server --socket /tmp/echo.sock -use echo_wire_compat::generated::echo_server::{Handler, Responder}; -use echo_wire_compat::generated::echo_types::*; +use echo_wire_compat::handler::EchoHandler; use ipc_runtime::IpcServer; use std::cell::RefCell; -struct EchoHandler; - -// Handlers are asynchronous: they produce their result via respond.ok(...) / -// respond.error(...) (synchronously here; an async transport could defer and -// respond later from another thread). -impl Handler for EchoHandler { - fn bytes(&mut self, cmd: EchoBytes, respond: Responder) { - respond.ok(EchoBytesResponse { data: cmd.data }); - } - fn fields(&mut self, cmd: EchoFields, respond: Responder) { - respond.ok(EchoFieldsResponse { - a: cmd.a, - b: cmd.b, - name: cmd.name, - }); - } - fn nested(&mut self, cmd: EchoNested, respond: Responder) { - respond.ok(EchoNestedResponse { inner: cmd.inner }); - } - fn aliases(&mut self, cmd: EchoAliases, respond: Responder) { - respond.ok(EchoAliasesResponse { - tree_id: cmd.tree_id, - hash: cmd.hash, - maybe_hash: cmd.maybe_hash, - hashes: cmd.hashes, - }); - } - fn blobs(&mut self, cmd: EchoBlobs, respond: Responder) { - respond.ok(EchoBlobsResponse { - maybe_data: cmd.maybe_data, - parts: cmd.parts, - }); - } - fn fail(&mut self, cmd: EchoFail, respond: Responder) { - respond.error(cmd.message); - } -} - fn main() { let args: Vec = std::env::args().collect(); let socket_path = args diff --git a/ipc-codegen/echo_example/rust/src/handler.rs b/ipc-codegen/echo_example/rust/src/handler.rs new file mode 100644 index 00000000000..83c1738e17c --- /dev/null +++ b/ipc-codegen/echo_example/rust/src/handler.rs @@ -0,0 +1,44 @@ +//! The echo service's handler, shared by the socket server binary and the +//! in-process FFI entry (`export_echo_ffi!` in lib.rs). + +use crate::generated::echo_server::{Handler, Responder}; +use crate::generated::echo_types::*; + +#[derive(Default)] +pub struct EchoHandler; + +// Handlers are asynchronous: they produce their result via respond.ok(...) / +// respond.error(...) (synchronously here; an async transport could defer and +// respond later from another thread). +impl Handler for EchoHandler { + fn bytes(&mut self, cmd: EchoBytes, respond: Responder) { + respond.ok(EchoBytesResponse { data: cmd.data }); + } + fn fields(&mut self, cmd: EchoFields, respond: Responder) { + respond.ok(EchoFieldsResponse { + a: cmd.a, + b: cmd.b, + name: cmd.name, + }); + } + fn nested(&mut self, cmd: EchoNested, respond: Responder) { + respond.ok(EchoNestedResponse { inner: cmd.inner }); + } + fn aliases(&mut self, cmd: EchoAliases, respond: Responder) { + respond.ok(EchoAliasesResponse { + tree_id: cmd.tree_id, + hash: cmd.hash, + maybe_hash: cmd.maybe_hash, + hashes: cmd.hashes, + }); + } + fn blobs(&mut self, cmd: EchoBlobs, respond: Responder) { + respond.ok(EchoBlobsResponse { + maybe_data: cmd.maybe_data, + parts: cmd.parts, + }); + } + fn fail(&mut self, cmd: EchoFail, respond: Responder) { + respond.error(cmd.message); + } +} diff --git a/ipc-codegen/echo_example/rust/src/lib.rs b/ipc-codegen/echo_example/rust/src/lib.rs index 8f7c1acd17e..d5b167479b7 100644 --- a/ipc-codegen/echo_example/rust/src/lib.rs +++ b/ipc-codegen/echo_example/rust/src/lib.rs @@ -4,6 +4,8 @@ pub mod generated { pub mod backend; pub mod echo_client; + #[cfg(feature = "ffi")] + pub mod echo_ffi; pub mod echo_server; pub mod echo_types; pub mod error; @@ -11,8 +13,16 @@ pub mod generated { pub mod ffi_backend; } +pub mod handler; + // Re-export under the names that generated server/client code expects // (they use `crate::types_gen`, `crate::error`, `crate::backend`) pub use generated::backend; pub use generated::echo_types as types_gen; pub use generated::error; + +// The service as an in-process library: the generated FFI entry over the same +// handler the socket server uses. With `ffi_backend` in the same crate, the +// generated client can call it without any process (see tests/ffi_roundtrip.rs). +#[cfg(feature = "ffi")] +crate::export_echo_ffi!(crate::generated::echo_ffi, crate::handler::EchoHandler); diff --git a/ipc-codegen/echo_example/rust/tests/ffi_roundtrip.rs b/ipc-codegen/echo_example/rust/tests/ffi_roundtrip.rs new file mode 100644 index 00000000000..4464ed73c39 --- /dev/null +++ b/ipc-codegen/echo_example/rust/tests/ffi_roundtrip.rs @@ -0,0 +1,28 @@ +//! In-process round trip: the generated FFI client backend calling this crate's +//! own generated FFI entry (`export_echo_ffi!` in lib.rs), no process involved. +#![cfg(feature = "ffi")] + +use echo_wire_compat::generated::echo_client::EchoApi; +use echo_wire_compat::generated::ffi_backend::FfiBackend; + +#[test] +fn ffi_roundtrip() { + let mut api = EchoApi::new(FfiBackend::new().expect("FfiBackend")); + + let data = vec![0xde, 0xad, 0xbe, 0xef, 0x42]; + let resp = api.bytes(&data).expect("EchoBytes"); + assert_eq!(resp.data, data); + + let resp = api + .fields(42, 999_999, "hello ffi".to_string()) + .expect("EchoFields"); + assert_eq!( + (resp.a, resp.b, resp.name.as_str()), + (42, 999_999, "hello ffi") + ); + + assert!( + api.fail("boom".to_string()).is_err(), + "EchoFail must surface as an error" + ); +} diff --git a/ipc-codegen/echo_example/ts_package/.gitignore b/ipc-codegen/echo_example/ts_package/.gitignore index 79f99d29e23..fb38dae611c 100644 --- a/ipc-codegen/echo_example/ts_package/.gitignore +++ b/ipc-codegen/echo_example/ts_package/.gitignore @@ -7,6 +7,13 @@ package.json tsconfig.json src/generated/ src/index.ts +src/react-native.ts src/platform.ts +src/process.ts src/bin.ts -scripts/prepare_arch_packages.sh +src/browser.ts +src/wasm.ts +src/wasm/ +src/wasm_host_imports.ts +wasm/ +.ipc-codegen-manifest diff --git a/ipc-codegen/echo_example/ts_package/README.md b/ipc-codegen/echo_example/ts_package/README.md index 6e04a94a70c..9c2aa9b1fdc 100644 --- a/ipc-codegen/echo_example/ts_package/README.md +++ b/ipc-codegen/echo_example/ts_package/README.md @@ -1,11 +1,13 @@ # @aztec/echo-ipc -Generated TypeScript IPC package for the Echo service. +Generated TypeScript package for the Echo service: the typed API +(`AsyncApi`/`SyncApi`, one method per command) over whichever backend reaches +the service on the current host. ```ts import { EchoService } from '@aztec/echo-ipc'; -const service = await EchoService.spawn({ transport: 'uds' }); +const service = await EchoService.create(); try { const response = await service.bytes({ data: new Uint8Array([1, 2, 3]) }); } finally { @@ -13,12 +15,36 @@ try { } ``` -The package resolves `echo_server` from `ECHO_SERVER_PATH`, -an explicit `binaryPath`, or an installed/prepared arch package. +`create` picks the process when the binary resolves, otherwise the wasm module. `options.backend` forces one, with no fallback: + +- `'process'`: spawns the `echo_server` binary (node) and talks to it over uds or shm. The binary is resolved from `ECHO_SERVER_PATH`, an explicit `process.binaryPath`, or the installed arch package (one of this package's optional dependencies). +- `'wasm'`: runs the service's wasm module in-process (node and browsers) through `@aztec-foundation/ipc-runtime/wasm`: the main instance in a worker, wasi threads on further workers where a shared memory is available (node, or a browser page served with COOP/COEP headers), otherwise the single-thread module. The worker scripts and the module are referenced with `new URL(..., import.meta.url)`, so bundlers emit them as chunks and assets of the application, and only the module actually chosen is ever fetched (Vite users: exclude the package from `optimizeDeps`). The module ships uncompressed, which is what lets the browser stream it into `WebAssembly.compileStreaming` and cache the compiled code between visits; serve it with your host's own compression. Where that is not possible, `wasm.module` takes a compressed copy — or bytes, a `Response`, or an already compiled `Module`. +- an object: anything with `call(bytes)`/`destroy()`, for a transport of your own (a bridge to a natively linked library, for instance). + +`threads` sets the service's parallelism for any backend +(a process reads it from `HARDWARE_CONCURRENCY`/`RAYON_NUM_THREADS`; wasm runs that many +worker threads, and asking for more than one where no shared memory exists is an error rather than +a silent downgrade). `EchoServiceSync.create` is the synchronous form (shared memory for a process, else the single-threaded wasm module on the calling thread). +`createBackend`/`createBackendSync` expose the same policy for code that wraps +the generated API itself. + +## Which thread the work runs on + +The caller chooses. An asynchronous wasm backend runs the module in a worker by +default, so a long call never blocks the caller; `{ wasm: { worker: false } }` +runs it on the calling thread instead, blocking until it returns. The +synchronous backend always runs on the calling thread, which is what short work +such as hashing wants, including on a browser's main thread. A spawned process +is off the caller's thread either way. + +## Entries per host + +The package resolves to a different entry per host through export conditions: +node (`default`) has every backend above; browsers (`browser`) have the wasm module only; React Native (`react-native`) has no built-in backend, because Hermes has no WebAssembly or workers — a native backend package registers one with `registerBackend`, or the app passes `options.backend`. ## Build -The package shell (package.json, tsconfig, src/index.ts, scripts/) is +The package shell (package.json, tsconfig, `src/*.ts`, scripts/) is generated; build through the owning project's `./bootstrap.sh`, which regenerates and then runs `npm install --omit=optional && npm run build`. diff --git a/ipc-codegen/echo_example/ts_package/bootstrap.sh b/ipc-codegen/echo_example/ts_package/bootstrap.sh index 9a747160e4f..191af34d07f 100755 --- a/ipc-codegen/echo_example/ts_package/bootstrap.sh +++ b/ipc-codegen/echo_example/ts_package/bootstrap.sh @@ -9,15 +9,16 @@ NODE="node --experimental-strip-types --no-warnings" $NODE "$CODEGEN/src/generate.ts" \ --schema "$DIR/../schema/schema.jsonc" \ --lang ts \ - --client \ - --out "$DIR/src/generated" \ --package "$DIR" \ --package-name "@aztec/echo-ipc" \ --binary-name echo_server \ - --package-transports uds,shm \ + --package-transports uds,shm,wasm \ + --package-wasm-module echo.wasm \ --ipc-runtime-dependency "file:../../../ipc-runtime/ts" -(cd "$DIR/../cpp" && ./bootstrap.sh) +# Both the spawned binary and the wasm reactor come from the Rust crate, so the package +# offers one implementation over three transports. +(cd "$DIR/../rust" && ./bootstrap.sh) # ipc-runtime is built by the Makefile (ipc-codegen depends on it) so its ts/dest # and NAPI addon are ready for the file: link below; don't reinstall the shared # ipc-runtime/ts here — concurrent build units doing so corrupt its node_modules. @@ -25,8 +26,9 @@ $NODE "$CODEGEN/src/generate.ts" \ platform_dir="$( node -e "const arch = { x64: 'amd64', arm64: 'arm64' }[process.arch] ?? process.arch; const os = { linux: 'linux', darwin: 'macos' }[process.platform] ?? process.platform; console.log(arch + '-' + os);" )" -mkdir -p "$DIR/build/$platform_dir" -cp "$DIR/../cpp/build/bin/echo_server" "$DIR/build/$platform_dir/echo_server" +mkdir -p "$DIR/build/$platform_dir" "$DIR/wasm" +cp "$DIR/../rust/target/debug/echo_server" "$DIR/build/$platform_dir/echo_server" +cp "$DIR/../rust/target/wasm32-wasip1/release/echo_wire_compat.wasm" "$DIR/wasm/echo.wasm" rm -rf "$DIR/node_modules" (cd "$DIR" && npm install --omit=optional --no-package-lock --quiet) diff --git a/ipc-codegen/echo_example/ts_package/src/package_test.ts b/ipc-codegen/echo_example/ts_package/src/package_test.ts index 9365da43104..ab69aa78fe6 100644 --- a/ipc-codegen/echo_example/ts_package/src/package_test.ts +++ b/ipc-codegen/echo_example/ts_package/src/package_test.ts @@ -1,12 +1,11 @@ -import { EchoService, SyncApi, type EchoTransport } from "./index.js"; +import { EchoService, EchoServiceSync, SyncApi } from "./index.js"; import { createNapiShmSyncClient } from "@aztec-foundation/ipc-runtime"; const args = process.argv.slice(2); const transportArg = args[args.indexOf("--transport") + 1] ?? "uds"; -if (transportArg !== "uds" && transportArg !== "shm") { +if (transportArg !== "uds" && transportArg !== "shm" && transportArg !== "wasm") { throw new Error(`Unknown --transport '${transportArg}'`); } -const transport = transportArg as EchoTransport; function testHash(base: number): Uint8Array { return Uint8Array.from({ length: 32 }, (_v, i) => base + i); @@ -28,6 +27,62 @@ function assertBytes(actual: Uint8Array, expected: Uint8Array, label: string) { ); } +// The wasm transport runs the service's own wasi reactor in-process, with no binary at all. +// Both the worker-hosted and calling-thread forms, and the synchronous one. +if (transportArg === "wasm") { + for (const worker of [true, false]) { + const wasmService = await EchoService.create({ + backend: "wasm", + wasm: { worker }, + }); + try { + const data = Uint8Array.from([0xde, 0xad, 0xbe, 0xef]); + assertBytes((await wasmService.bytes({ data })).data, data, `wasm(worker=${worker}).bytes`); + const fields = await wasmService.fields({ a: 1, b: 2, name: "wasm" }); + assertEqual(fields.name, "wasm", `wasm(worker=${worker}).fields.name`); + await wasmService + .fail({ message: "boom" }) + .then(() => { + throw new Error(`wasm(worker=${worker}): fail should reject`); + }) + .catch((e: Error) => { + if (!e.message.includes("boom")) throw e; + }); + } finally { + await wasmService.destroy(); + } + } + + const sync = await EchoServiceSync.create({ backend: "wasm" }); + try { + assertBytes( + sync.bytes({ data: Uint8Array.from([1, 2, 3]) }).data, + Uint8Array.from([1, 2, 3]), + "wasm sync bytes.data", + ); + } finally { + sync.destroy(); + } + console.error("echo ts package: wasm OK"); + process.exit(0); +} + +const transport = transportArg; + +// The default policy: the echo binary resolves, so create() spawns it. +{ + const created = await EchoService.create(); + try { + if (!created.process) { + throw new Error("create(): expected a spawned process backend"); + } + const data = Uint8Array.from([7, 8, 9]); + assertBytes((await created.bytes({ data })).data, data, "create().bytes"); + } finally { + await created.destroy(); + } +} + const service = await EchoService.spawn({ transport }); try { const data = Uint8Array.from([0xde, 0xad, 0xbe, 0xef, 0x42]); @@ -50,7 +105,11 @@ try { const nested = await service.nested({ inner }); assertEqual(nested.inner.flag, true, "nested.inner.flag"); assertEqual(nested.inner.values.length, 2, "nested.inner.values.length"); - assertBytes(nested.inner.values[0]!, inner.values[0]!, "nested.inner.values[0]"); + assertBytes( + nested.inner.values[0]!, + inner.values[0]!, + "nested.inner.values[0]", + ); const hash = testHash(0x10); const second = testHash(0x40); diff --git a/ipc-codegen/echo_example/zig/src/ffi_check.zig b/ipc-codegen/echo_example/zig/src/ffi_check.zig index 9d89b86e07c..5fccd90fe76 100644 --- a/ipc-codegen/echo_example/zig/src/ffi_check.zig +++ b/ipc-codegen/echo_example/zig/src/ffi_check.zig @@ -1,16 +1,20 @@ -//! Compile coverage for the generated FFI backend. The real FFI symbol is -//! provided by whatever native library a consumer links; a stub satisfies -//! the linker here so the backend's code is fully analyzed and built. +//! Compile coverage for the generated FFI backend. The real FFI symbols are +//! provided by whatever native library a consumer links; stubs satisfy the +//! linker here so the backend's code is fully analyzed and built. const std = @import("std"); const ffi = @import("generated/ffi_backend.zig"); -export fn ipc_ffi_entry(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void { +export fn echo_ipc_ffi_entry(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void { _ = input; _ = input_len; output.* = undefined; output_len.* = 0; } +export fn echo_ipc_ffi_free(ptr: [*]u8) void { + _ = ptr; +} + pub fn main() void { comptime { std.testing.refAllDeclsRecursive(ffi); diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index 3e110aef413..91452ab251c 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -783,6 +783,133 @@ void serve(const std::string& input_path, Ctx& ctx) } } // namespace ${ns} +`; + } + + /** + * Header of the in-process FFI entry (the ipc-codegen FFI backend contract): the exported C + * symbols, and the one hook the service defines to hand over its dispatcher. + */ + /** + * Exported symbol names carry the service (`bb_ipc_ffi_entry`), so libraries of several services + * can be linked into one binary. A schema with no service/prefix keeps the bare names. + */ + private ffiSymbol(name: string): string { + const prefix = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; + return `${prefix}${name}`; + } + + generateFfiHeader(): string { + const { namespace: ns, prefix } = this.opts; + const dispatchHeader = `${toSnakeCase(prefix)}_dispatch.hpp`; + const entry = this.ffiSymbol("ipc_ffi_entry"); + const alloc = this.ffiSymbol("ipc_ffi_alloc"); + const free = this.ffiSymbol("ipc_ffi_free"); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// In-process FFI entry for ${prefix}: the ipc-codegen FFI backend contract, with the symbols +// prefixed by the service so several services can be linked into one binary. +#pragma once + +#include "${dispatchHeader}" + +#include +#include + +// Default visibility so the symbols survive -fvisibility=hidden in a static library and are +// exported from a wasm reactor linked with --export-dynamic. +#ifndef IPC_FFI_EXPORT +#define IPC_FFI_EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +namespace ${ns} { + +// Defined by the service, because make_${toSnakeCase(prefix)}_handler() is a template over the +// context type its handlers run over and the schema cannot name that type. A serve loop takes the +// context from main(); a library has none, so the service defines this instead: +// +// namespace ${ns} { +// AsyncDispatchHandler& ipc_ffi_dispatcher() +// { +// static MyCtx ctx; +// static AsyncDispatchHandler handler = make_${toSnakeCase(prefix)}_handler(ctx); +// return handler; +// } +// } +// +// One context for the process, built on first use, so stateful command sequences share state the +// way they would over one transport connection. +AsyncDispatchHandler& ipc_ffi_dispatcher(); + +} // namespace ${ns} + +// Execute one msgpack command payload — the same bytes a transport client puts inside a frame, +// without the length/id envelope — and leave the response payload in a buffer from +// ${alloc}(), which the caller releases with ${free}(). +IPC_FFI_EXPORT void ${entry}(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len); + +// malloc/free-compatible allocation for buffers crossing the FFI boundary in either direction. +IPC_FFI_EXPORT void* ${alloc}(size_t size); +IPC_FFI_EXPORT void ${free}(void* ptr); +`; + } + + /** Definitions of the FFI entry symbols declared by generateFfiHeader(). */ + generateFfiSource(): string { + const { namespace: ns, prefix } = this.opts; + const ffiHeader = `${toSnakeCase(prefix)}_ffi.hpp`; + const entry = this.ffiSymbol("ipc_ffi_entry"); + const alloc = this.ffiSymbol("ipc_ffi_alloc"); + const free = this.ffiSymbol("ipc_ffi_free"); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// In-process FFI entry for ${prefix}: the ipc-codegen FFI backend contract. +#include "${ffiHeader}" + +#include +#include +#include +#include +#include +#include + +IPC_FFI_EXPORT void* ${alloc}(size_t size) +{ + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) + return std::malloc(size == 0 ? 1 : size); +} + +IPC_FFI_EXPORT void ${free}(void* ptr) +{ + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) + std::free(ptr); +} + +IPC_FFI_EXPORT void ${entry}(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len) +{ + std::vector response; + // The dispatch answers a malformed envelope, an unknown command and a throwing + // handler with an error frame, but an unpack or convert failure still escapes + // it. An exception cannot cross an FFI boundary, so anything reaching here has + // to become a response. +#ifdef BB_NO_EXCEPTIONS + ${ns}::ipc_ffi_dispatcher()(std::span(input, input_len), + [&response](std::vector frame) { response = std::move(frame); }); +#else + try { + ${ns}::ipc_ffi_dispatcher()(std::span(input, input_len), + [&response](std::vector frame) { response = std::move(frame); }); + } catch (const std::exception& e) { + response = ${ns}::detail::make_error(e.what()); + } catch (...) { + response = ${ns}::detail::make_error("${prefix}: unknown exception"); + } +#endif + auto* out = static_cast(${alloc}(response.size())); + std::memcpy(out, response.data(), response.size()); + *output = out; + *output_len = response.size(); +} `; } } diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index 3ff3949d12c..80554093496 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -23,9 +23,10 @@ import { mkdirSync, cpSync, rmSync, + rmdirSync, } from "fs"; import { execSync } from "child_process"; -import { basename, dirname, join, resolve } from "path"; +import { basename, dirname, join, relative, resolve } from "path"; import { fileURLToPath } from "url"; import { SchemaVisitor, @@ -48,6 +49,9 @@ import { toSnakeCase } from "./naming.ts"; // @ts-ignore const __dirname = dirname(fileURLToPath(import.meta.url)); +/** Records what the last run produced, so this one can remove what it no longer emits. */ +const MANIFEST = ".ipc-codegen-manifest"; + // --------------------------------------------------------------------------- // Argument parsing // --------------------------------------------------------------------------- @@ -65,13 +69,14 @@ interface Args { binaryEnvVar: string; packageTransports: string; packageIpcPathArgs: string; + packageWasmModule: string; + packageWasmThreadsModule: string; ipcRuntimeDependency: string; cppNamespace: string; cppWireNamespace: string; cppIncludeDir: string; uds: boolean; ffi: boolean; - curveConstants: string; stripMethodPrefix: boolean; stripTypePrefix: boolean; } @@ -82,7 +87,8 @@ function usage(): never { Required: --schema JSON schema file --lang Target language (ts, rust, zig, cpp) - --out Output directory + --out Output directory for the generated bindings (implied by + --package: /src/generated) Optional: --server Generate server dispatch @@ -94,9 +100,16 @@ Optional: --package-name TS package name for --package --binary-name Native service binary name for --package --binary-env-var Env var overriding the binary path for --package - --package-transports Comma-separated transports for --package (uds,shm) + --package-transports Comma-separated transports for --package (uds,shm,wasm) --package-ipc-path-args Comma-separated binary args for IPC path; use {path} + --package-wasm-module + wasm transport: the single-thread module, shipped in the + package's wasm/ directory. Ship it uncompressed: only a + real application/wasm response can be streamed into + WebAssembly.compileStreaming and cached by the browser + --package-wasm-threads-module + wasm transport: the threads module, shipped in wasm/ --ipc-runtime-dependency package.json dependency spec for @aztec-foundation/ipc-runtime --prefix Type prefix (auto-detected when >= 2 commands share one) @@ -106,11 +119,13 @@ Optional: 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) + --ffi In-process FFI. With --client (rust, zig): copy the FFI + client backend template. With --server (rust, cpp): emit + the exported FFI entry (ipc_ffi_entry) over the dispatch --cpp-namespace C++ namespace (e.g. my::ns) --cpp-wire-namespace Wire types sub-namespace (default: wire) --cpp-include-dir Include path for generated dir (e.g. myservice/generated) - --curve-constants Generate TS curve constants from JSON at `); +`); process.exit(1); } @@ -128,13 +143,14 @@ function parseArgs(argv: string[]): Args { binaryEnvVar: "", packageTransports: "uds", packageIpcPathArgs: "--socket,{path}", + packageWasmModule: "", + packageWasmThreadsModule: "", ipcRuntimeDependency: "@aztec-foundation/ipc-runtime", cppNamespace: "", cppWireNamespace: "wire", cppIncludeDir: "", uds: false, ffi: false, - curveConstants: "", stripMethodPrefix: false, stripTypePrefix: false, }; @@ -186,6 +202,12 @@ function parseArgs(argv: string[]): Args { case "--package-ipc-path-args": args.packageIpcPathArgs = takeValue(); break; + case "--package-wasm-module": + args.packageWasmModule = takeValue(); + break; + case "--package-wasm-threads-module": + args.packageWasmThreadsModule = takeValue(); + break; case "--ipc-runtime-dependency": args.ipcRuntimeDependency = takeValue(); break; @@ -204,9 +226,6 @@ function parseArgs(argv: string[]): Args { case "--ffi": args.ffi = true; break; - case "--curve-constants": - args.curveConstants = takeValue(); - break; case "--strip-method-prefix": args.stripMethodPrefix = true; break; @@ -219,6 +238,18 @@ function parseArgs(argv: string[]): Args { } } + if (args.packageDir) { + // The package shell imports the bindings from ./generated, so that is where they go. + const packageOut = join(resolve(args.packageDir), "src", "generated"); + if (!args.out) { + args.out = packageOut; + } else if (resolve(args.out) !== packageOut) { + console.error( + `--out must be /src/generated (${packageOut}) when --package is given; omit it`, + ); + process.exit(1); + } + } if (!args.schema || !args.lang || !args.out) { usage(); } @@ -226,13 +257,26 @@ function parseArgs(argv: string[]): Args { console.error(`--package is only supported for --lang ts`); process.exit(1); } - if ((args.uds || args.ffi) && args.lang !== "rust" && args.lang !== "zig") { + if (args.uds && args.lang !== "rust" && args.lang !== "zig") { console.error( - `--uds/--ffi copy backend templates and only apply to rust and zig; ` + + `--uds copies backend templates and only applies to rust and zig; ` + `ts and cpp consume transports from ipc-runtime directly`, ); process.exit(1); } + if (args.ffi && !["rust", "zig", "cpp"].includes(args.lang)) { + console.error( + `--ffi applies to rust, zig and cpp; a ts package reaches an FFI module ` + + `through the wasm transport (--package-transports wasm)`, + ); + process.exit(1); + } + if (args.ffi && args.lang === "cpp" && !args.server) { + console.error( + `--ffi for cpp emits the server-side FFI entry; pass --server`, + ); + process.exit(1); + } return args; } @@ -300,6 +344,49 @@ function detectPrefix(compiled: CompiledSchema): string { // Template copying // --------------------------------------------------------------------------- +/** + * Every path this run produced. Generated output is disposable and the set of files changes as + * the generator does, so the next run uses this to delete what it no longer emits — otherwise a + * checkout keeps compiling a file that is no longer generated from anything. + */ +const written: string[] = []; + +/** + * Remove anything an earlier run produced under `root` that this one did not, then record what + * this one did. Paths are stored relative to `root` so the tree can move. + */ +function pruneStale(root: string) { + const manifestPath = join(root, MANIFEST); + const current = written.map((p) => relative(root, p)).sort(); + let previous: string[] = []; + try { + previous = readFileSync(manifestPath, "utf-8").split("\n").filter(Boolean); + } catch { + // No manifest: either the first run here, or output from before manifests existed. Either + // way there is nothing we can safely claim to own, so only record. + } + const emptied = new Set(); + for (const stale of previous.filter((p) => !current.includes(p))) { + const path = join(root, stale); + rmSync(path, { recursive: true, force: true }); + console.log(` ${path} (removed, no longer generated)`); + for (let dir = dirname(path); dir !== root; dir = dirname(dir)) { + emptied.add(dir); + } + } + // Deepest first, so a directory holding only now-empty directories goes too. rmdir on a + // directory that still holds something fails, which is exactly the test we want. + for (const dir of [...emptied].sort((a, b) => b.length - a.length)) { + try { + rmdirSync(dir); + console.log(` ${dir} (removed, now empty)`); + } catch { + // Still holds something generated, or something we did not write. Leave it. + } + } + writeFileSync(manifestPath, current.join("\n") + "\n"); +} + function copyTemplate(lang: string, filename: string, outDir: string) { const templatePath = join(__dirname, "..", "templates", lang, filename); const destPath = join(outDir, filename); @@ -307,6 +394,7 @@ function copyTemplate(lang: string, filename: string, outDir: string) { const tmpPath = `${destPath}.${process.pid}.tmp`; writeFileSync(tmpPath, readFileSync(templatePath, "utf-8")); renameSync(tmpPath, destPath); + written.push(destPath); console.log(` ${destPath} (template)`); } @@ -315,6 +403,7 @@ function copyTemplateDir(lang: string, dirname: string, outDir: string) { const destPath = join(outDir, dirname); rmSync(destPath, { recursive: true, force: true }); cpSync(templatePath, destPath, { recursive: true }); + written.push(destPath); console.log(` ${destPath} (template)`); } @@ -361,6 +450,7 @@ function generate(args: Args) { const tmpPath = `${path}.${process.pid}.tmp`; writeFileSync(tmpPath, content); renameSync(tmpPath, path); + written.push(path); console.log(` ${path}`); return path; } @@ -388,9 +478,6 @@ function generate(args: Args) { // No transport template copy — consumers import IpcClient from // '@aztec-foundation/ipc-runtime' (or hand in a compatible byte backend). } - if (args.curveConstants) { - generateCurveConstants(absOut, resolve(args.curveConstants)); - } if (args.packageDir) { const packageDir = resolve(args.packageDir); const packageName = @@ -405,6 +492,7 @@ function generate(args: Args) { const tmpPath = `${path}.${process.pid}.tmp`; writeFileSync(tmpPath, content); renameSync(tmpPath, path); + written.push(path); if (opts?.executable) { try { execSync(`chmod +x ${path}`); @@ -429,37 +517,57 @@ function generate(args: Args) { } else { const binaryName = args.binaryName || toSnakeCase(prefix).replace(/_/g, "-"); + const transports = args.packageTransports + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + const wasm = transports.includes("wasm"); + if ( + wasm && + !args.packageWasmModule && + !args.packageWasmThreadsModule + ) { + console.error( + `--package-transports wasm needs --package-wasm-module and/or --package-wasm-threads-module`, + ); + process.exit(1); + } const packageGen = new TypeScriptPackageCodegen({ prefix, packageName, binaryName, binaryEnvVar: args.binaryEnvVar || defaultBinaryEnvVar(binaryName), ipcRuntimeDependency: args.ipcRuntimeDependency, - transports: args.packageTransports - .split(",") - .map((t) => t.trim()) - .filter(Boolean), + transports, ipcPathArgs: args.packageIpcPathArgs .split(",") .map((arg) => arg.trim()) .filter(Boolean), + wasmModule: args.packageWasmModule || undefined, + wasmThreadsModule: args.packageWasmThreadsModule || undefined, }); writePackage("package.json", packageGen.generatePackageJson()); writePackage("tsconfig.json", packageGen.generateTsconfig()); writePackage("README.md", packageGen.generateReadme()); writePackage("src/index.ts", packageGen.generateIndex()); + writePackage( + "src/react-native.ts", + packageGen.generateReactNativeIndex(), + ); writePackage("src/platform.ts", packageGen.generatePlatform()); + if (transports.some((t) => t !== "wasm")) { + writePackage("src/process.ts", packageGen.generateProcess()); + } if (binaryName) { writePackage("src/bin.ts", packageGen.generateBin()); } + if (wasm) { + writePackage("src/browser.ts", packageGen.generateBrowserIndex()); + writePackage("src/wasm.ts", packageGen.generateWasm()); + } for (const manifest of packageGen.generateArchPackageManifests()) { writePackage(manifest.path, manifest.content); } - writePackage( - "scripts/prepare_arch_packages.sh", - packageGen.generatePrepareArchPackagesScript(), - { executable: true }, - ); } } break; @@ -479,6 +587,9 @@ function generate(args: Args) { `${toSnakeCase(prefix)}_server.rs`, gen.generateServer(compiled), ); + if (args.ffi) { + writeFile(`${toSnakeCase(prefix)}_ffi.rs`, gen.generateFfi()); + } } if (args.client) { writeFile( @@ -494,7 +605,8 @@ function generate(args: Args) { copyTemplate("rust", "error.rs", absOut); } if (args.ffi) { - copyTemplate("rust", "ffi_backend.rs", absOut); + // Generated rather than a template: it links the service-prefixed symbols. + writeFile("ffi_backend.rs", gen.generateFfiBackend()); } break; } @@ -530,7 +642,8 @@ function generate(args: Args) { copyTemplate("zig", "backend.zig", absOut); } if (args.ffi) { - copyTemplate("zig", "ffi_backend.zig", absOut); + // Generated rather than a template: it links the service-prefixed symbols. + writeFile("ffi_backend.zig", gen.generateFfiBackend()); } break; } @@ -565,6 +678,20 @@ function generate(args: Args) { gen.generateServerHeader(), ), ); + if (args.ffi) { + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_ffi.hpp`, + gen.generateFfiHeader(), + ), + ); + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_ffi.cpp`, + gen.generateFfiSource(), + ), + ); + } } if (args.client) { cppFiles.push( @@ -591,55 +718,12 @@ function generate(args: Args) { process.exit(1); } + // Generated output for one service lives under the package when there is one, and under --out + // otherwise; either way that directory is the generator's to keep tidy. + pruneStale(args.packageDir ? resolve(args.packageDir) : absOut); console.log("Done."); } -// --------------------------------------------------------------------------- -// Curve constants -// --------------------------------------------------------------------------- - -function hexToBigInt(hex: string): bigint { - return BigInt("0x" + hex); -} - -function hexToByteList(hex: string): string { - const bytes: number[] = []; - for (let i = 0; i < hex.length; i += 2) - bytes.push(parseInt(hex.substring(i, i + 2), 16)); - return `new Uint8Array([${bytes.join(", ")}])`; -} - -function serializeCoordinate(coord: string | string[]): string { - return Array.isArray(coord) - ? `[${coord.map((c) => hexToByteList(c)).join(", ")}]` - : hexToByteList(coord); -} - -function generateCurveConstants(outputDir: string, constantsPath: string) { - const constants = JSON.parse(readFileSync(constantsPath, "utf-8")); - const content = `// AUTOGENERATED FILE - DO NOT EDIT -export const BN254_FR_MODULUS = ${hexToBigInt(constants.bn254_fr_modulus)}n; -export const BN254_FQ_MODULUS = ${hexToBigInt(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; -export const GRUMPKIN_FR_MODULUS = ${hexToBigInt(constants.grumpkin_fr_modulus)}n; -export const GRUMPKIN_FQ_MODULUS = ${hexToBigInt(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; -export const SECP256K1_FR_MODULUS = ${hexToBigInt(constants.secp256k1_fr_modulus)}n; -export const SECP256K1_FQ_MODULUS = ${hexToBigInt(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; -export const SECP256R1_FR_MODULUS = ${hexToBigInt(constants.secp256r1_fr_modulus)}n; -export const SECP256R1_FQ_MODULUS = ${hexToBigInt(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; -`; - mkdirSync(outputDir, { recursive: true }); - const path = join(outputDir, "curve_constants.ts"); - const tmpPath = `${path}.${process.pid}.tmp`; - writeFileSync(tmpPath, content); - renameSync(tmpPath, path); - console.log(` ${path}`); -} - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts index 876a533df3c..305546b1bd3 100644 --- a/ipc-codegen/src/rust_codegen.ts +++ b/ipc-codegen/src/rust_codegen.ts @@ -162,9 +162,7 @@ export class RustCodegen { // which the C++ side rejects. private needsSerdeFixedBytes(type: Type): boolean { return ( - type.kind === "array" && - type.size! <= 32 && - this.isU8(type.element!) + type.kind === "array" && type.size! <= 32 && this.isU8(type.element!) ); } @@ -917,6 +915,231 @@ pub fn handle_request(handler: &mut dyn Handler, request_bytes: &[u8]) -> Vec Result { + 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 is valid for input.len() bytes; output_ptr/output_len are valid stack slots. + unsafe { + ipc_ffi_entry(input.as_ptr(), input.len(), &mut output_ptr, &mut output_len); + } + + if output_ptr.is_null() { + return Err(IpcError::Backend("FFI entry returned null pointer".to_string())); + } + + if output_len == 0 { + // SAFETY: the entry allocated output_ptr; it is freed exactly once. + unsafe { ipc_ffi_free(output_ptr) }; + return Err(IpcError::Backend("FFI entry returned empty response".to_string())); + } + + // SAFETY: output_ptr is valid for output_len bytes until freed below. + let output = unsafe { std::slice::from_raw_parts(output_ptr, output_len).to_vec() }; + + // SAFETY: the entry allocated output_ptr; it is freed exactly once. + unsafe { ipc_ffi_free(output_ptr) }; + + Ok(output) + } + + fn destroy(&mut self) -> Result<()> { + 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") + } +} +`; + } + + generateFfi(): string { + const { prefix } = this.opts; + const snake = toSnakeCase(prefix || "ipc"); + const macroName = `export_${snake}_ffi`; + const entry = this.ffiSymbol("ipc_ffi_entry"); + const alloc = this.ffiSymbol("ipc_ffi_alloc"); + const free = this.ffiSymbol("ipc_ffi_free"); + + return `//! AUTOGENERATED - DO NOT EDIT +//! In-process FFI entry for ${prefix || "service"}: the ipc-codegen FFI backend contract, with the +//! symbols prefixed by the service so several services can be linked into one binary. +//! +//! \`${macroName}!\` defines the exported C-ABI symbols \`${entry}\`, \`${alloc}\` and +//! \`${free}\` over one handler, built with \`Default\` on the first call and kept for the life +//! of the process, so stateful command sequences share state the way they would on one transport +//! connection. Invoke it once at the crate root of a \`cdylib\`/\`staticlib\`, or of a wasm reactor: +//! +//! \`\`\`ignore +//! ${macroName}!(crate::generated::${snake}_ffi, MyHandler); +//! \`\`\` +//! +//! Needs the \`libc\` crate for the malloc-compatible buffers. + +use super::${snake}_server::{handle_request, Handler}; + +/// One FFI round trip: decode and dispatch \`input\`, leaving the response payload in a buffer +/// from [\`ffi_alloc\`] that the caller releases with [\`ffi_free\`]. +/// +/// # Safety +/// \`input\` must be valid for \`input_len\` bytes; \`output\` and \`output_len\` must be valid for writes. +pub unsafe fn ffi_call( + handler: &mut dyn Handler, + input: *const u8, + input_len: usize, + output: *mut *mut u8, + output_len: *mut usize, +) { + let request: &[u8] = if input_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(input, input_len) } + }; + let response = handle_request(handler, request); + let buf = ffi_alloc(response.len()); + unsafe { + std::ptr::copy_nonoverlapping(response.as_ptr(), buf, response.len()); + *output = buf; + *output_len = response.len(); + } +} + +/// malloc-compatible allocation for buffers crossing the FFI boundary in either direction. +pub fn ffi_alloc(size: usize) -> *mut u8 { + // A zero-size request still yields a distinct, freeable pointer. + unsafe { libc::malloc(size.max(1)) as *mut u8 } +} + +/// Release a buffer from [\`ffi_alloc\`]. +/// +/// # Safety +/// \`ptr\` must have come from [\`ffi_alloc\`] and not have been freed already. +pub unsafe fn ffi_free(ptr: *mut u8) { + unsafe { libc::free(ptr as *mut libc::c_void) }; +} + +/// Define the exported C-ABI FFI symbols over \`$handler\` (a \`Handler + Default + Send\` type). +/// \`$ffi\` is the path of this module, which the expansion calls back into. +#[macro_export] +macro_rules! ${macroName} { + ($ffi:path, $handler:ty) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn ${entry}( + input: *const u8, + input_len: usize, + output: *mut *mut u8, + output_len: *mut usize, + ) { + use $ffi as ffi; + static HANDLER: ::std::sync::Mutex> = ::std::sync::Mutex::new(None); + let mut guard = HANDLER.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + let handler = guard.get_or_insert_with(<$handler as ::std::default::Default>::default); + unsafe { ffi::ffi_call(handler, input, input_len, output, output_len) }; + } + + #[unsafe(no_mangle)] + pub extern "C" fn ${alloc}(size: usize) -> *mut u8 { + use $ffi as ffi; + ffi::ffi_alloc(size) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn ${free}(ptr: *mut u8) { + use $ffi as ffi; + unsafe { ffi::ffi_free(ptr) }; + } + }; +} `; } } diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts index 5315a65e9ac..386331f178f 100644 --- a/ipc-codegen/src/typescript_codegen.ts +++ b/ipc-codegen/src/typescript_codegen.ts @@ -41,7 +41,10 @@ export class TypeScriptCodegen { /** Prefix to strip from generated type and converter names (e.g. "Bb" -> BbCircuitProve becomes CircuitProve) */ private typePrefix: string = ""; - constructor(options?: { stripMethodPrefix?: string; stripTypePrefix?: string }) { + constructor(options?: { + stripMethodPrefix?: string; + stripTypePrefix?: string; + }) { if (options?.stripMethodPrefix) { this.methodPrefix = options.stripMethodPrefix; } @@ -376,6 +379,11 @@ ${conversions} return this.generateConverter("from", type, value); } + /** Error class name for a service's error response, e.g. `BbErrorResponse` -> `BbError`. */ + private errorClassName(errorTypeName: string): string { + return errorTypeName.replace(/Response$/, ""); + } + // Generate types file (api_types.ts) generateTypes(schema: CompiledSchema, schemaHash?: string): string { const allStructs = dedupeStructsByName([ @@ -490,6 +498,18 @@ ${toFunctions} ${fromFunctions} +/** + * Thrown when the service answers a command with ${schema.errorTypeName} rather than a result. + * The message is the service's own, so the type is what separates "the service said no" from a + * fault in the client. Declared here so the async and sync APIs throw the same class. + */ +export class ${this.errorClassName(schema.errorTypeName)} extends Error { + constructor(message: string) { + super(message); + this.name = '${this.errorClassName(schema.errorTypeName)}'; + } +} + // Base API interfaces export interface AsyncApiBase { ${asyncApiMethods} @@ -513,7 +533,7 @@ ${syncApiMethods} const msgpackCommand = from${cmdType}(command); return msgpackCall(this.backend, [["${command.name}", msgpackCommand]]).then(([variantName, result]: [string, any]) => { if (variantName === '${this.errorTypeName}') { - throw this.createError(result.message || 'Unknown error from server'); + throw new ${this.errorClassName(this.errorTypeName)}(result.message || 'Unknown error from server'); } if (variantName !== '${command.responseType}') { throw new Error(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); @@ -532,7 +552,7 @@ ${syncApiMethods} const msgpackCommand = from${cmdType}(command); const [variantName, result] = msgpackCall(this.backend, [["${command.name}", msgpackCommand]]); if (variantName === '${this.errorTypeName}') { - throw this.createError(result.message || 'Unknown error from server'); + throw new ${this.errorClassName(this.errorTypeName)}(result.message || 'Unknown error from server'); } if (variantName !== '${command.responseType}') { throw new Error(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); @@ -559,8 +579,6 @@ export interface IpcClientAsync { destroy(): Promise; } -export type IpcErrorFactory = (message: string) => Error; - async function msgpackCall(backend: IpcClientAsync, input: any[]) { const inputBuffer = new Encoder({ useRecords: false, variableMapSize: true }).pack(input); const encodedResult = await backend.call(inputBuffer); @@ -568,10 +586,7 @@ async function msgpackCall(backend: IpcClientAsync, input: any[]) { } export class AsyncApi implements AsyncApiBase { - constructor( - protected backend: IpcClientAsync, - protected createError: IpcErrorFactory = message => new Error(message), - ) {} + constructor(protected backend: IpcClientAsync) {} ${methods} @@ -600,8 +615,6 @@ export interface IpcClientSync { destroy(): void; } -export type IpcErrorFactory = (message: string) => Error; - function msgpackCall(backend: IpcClientSync, input: any[]) { const inputBuffer = new Encoder({ useRecords: false, variableMapSize: true }).pack(input); const encodedResult = backend.call(inputBuffer); @@ -609,10 +622,7 @@ function msgpackCall(backend: IpcClientSync, input: any[]) { } export class SyncApi implements SyncApiBase { - constructor( - protected backend: IpcClientSync, - protected createError: IpcErrorFactory = message => new Error(message), - ) {} + constructor(protected backend: IpcClientSync) {} ${methods} @@ -641,6 +651,7 @@ ${methods} } types.add(baseInterface); + types.add(this.errorClassName(schema.errorTypeName)); const sortedTypes = Array.from(types).sort(); return `import { ${sortedTypes.join(", ")} } from './api_types.js';`; diff --git a/ipc-codegen/src/typescript_package_codegen.ts b/ipc-codegen/src/typescript_package_codegen.ts index 038934b86ea..4c10c125ab3 100644 --- a/ipc-codegen/src/typescript_package_codegen.ts +++ b/ipc-codegen/src/typescript_package_codegen.ts @@ -8,20 +8,16 @@ export interface TypeScriptPackageOptions { ipcRuntimeDependency: string; ipcPathArgs: string[]; transports: string[]; + /** wasm transport: basename of the single-thread module shipped in the package's wasm/ directory. */ + wasmModule?: string; + /** wasm transport: basename of the threads module shipped in the package's wasm/ directory. */ + wasmThreadsModule?: string; } function className(prefix: string): string { return `${prefix}Service`; } -function transportType(prefix: string): string { - return `${prefix}Transport`; -} - -function optionsType(prefix: string): string { - return `${prefix}ServiceOptions`; -} - function binaryFinderName(prefix: string): string { return `find${prefix}Binary`; } @@ -123,7 +119,7 @@ export class TypeScriptServerPackageCodegen { files: ["dest/", this.opts.schemaFileName, "README.md"], scripts: { clean: "rm -rf dest .tsbuildinfo", - build: "tsc -p tsconfig.json", + build: "rm -rf dest .tsbuildinfo && tsc -p tsconfig.json", }, dependencies: { msgpackr: "^1.11.2", @@ -178,6 +174,12 @@ runs \`yarn build\`. } } +/** + * Client package shell: one TS package that owns how the service is reached. Its entries + * (node, browser, react-native — selected by export condition) offer the backends that exist on + * that host and a `create` that picks one by default, can be forced to one, or takes a backend + * object of the consumer's own. + */ export class TypeScriptPackageCodegen { private opts: TypeScriptPackageOptions; @@ -185,13 +187,42 @@ export class TypeScriptPackageCodegen { this.opts = opts; } + private get wasm(): boolean { + return this.opts.transports.includes("wasm"); + } + + private get processTransports(): string[] { + return this.opts.transports.filter((t) => t !== "wasm"); + } + + private get process(): boolean { + return this.processTransports.length > 0; + } + + private get shm(): boolean { + return this.processTransports.includes("shm"); + } + + private generatedExports(): string { + return `export * from './generated/api_types.js'; +export { AsyncApi } from './generated/async.js'; +export { SyncApi } from './generated/sync.js'; +`; + } + generatePackageJson(): string { const archPackages = archPackageNames(this.opts.packageName); const scripts: Record = { clean: "rm -rf dest .tsbuildinfo", - build: "tsc -p tsconfig.json", - prepare_arch_packages: "./scripts/prepare_arch_packages.sh", + // Clean first: tsc would leave behind the output of a source file the generator no + // longer emits, and these packages are a couple of seconds to compile. + build: "rm -rf dest .tsbuildinfo && tsc -p tsconfig.json", + prepare_arch_packages: "ipc-runtime-prepare-arch-packages", }; + const entry = (name: string) => ({ + types: `./dest/${name}.d.ts`, + default: `./dest/${name}.js`, + }); const pkg = { name: this.opts.packageName, @@ -204,13 +235,25 @@ export class TypeScriptPackageCodegen { ...(this.opts.binaryName ? { bin: { [this.opts.binaryName]: "./dest/bin.js" } } : {}), + // main/types for consumers resolving without export conditions (a CommonJS build's + // TypeScript resolution); `exports` is authoritative everywhere else. + main: "./dest/index.js", + types: "./dest/index.d.ts", + // Metro reads this field; bundlers with export conditions use the entry below. + "react-native": "./dest/react-native.js", exports: { + // One entry per host, each with its own types: a browser consumer never sees the + // process backend, a React Native one sees neither process nor wasm. ".": { + ...(this.wasm ? { browser: entry("browser") } : {}), + "react-native": entry("react-native"), types: "./dest/index.d.ts", default: "./dest/index.js", }, + ...(this.wasm ? { "./browser": entry("browser") } : {}), + "./react-native": entry("react-native"), }, - files: ["dest/", "README.md"], + files: ["dest/", ...(this.wasm ? ["wasm/"] : []), "README.md"], scripts, dependencies: { "@aztec-foundation/ipc-runtime": this.opts.ipcRuntimeDependency, @@ -232,26 +275,11 @@ export class TypeScriptPackageCodegen { } generateBin(): string { - const findBinary = binaryFinderName(this.opts.prefix); return `#!/usr/bin/env node -import { spawnSync } from 'node:child_process'; -import { ${findBinary} } from './platform.js'; - -const binaryPath = ${findBinary}(); -if (!binaryPath) { - console.error( - "${this.opts.binaryName}: native binary not found. Install the matching " + - "'${this.opts.packageName}-' package, set ${this.opts.binaryEnvVar}, or pass its path.", - ); - process.exit(1); -} +import { runServiceBinary } from '@aztec-foundation/ipc-runtime'; +import { BINARY } from './platform.js'; -const result = spawnSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' }); -if (result.error) { - console.error(result.error.message); - process.exit(1); -} -process.exit(result.status ?? 1); +runServiceBinary(BINARY, '${this.opts.packageName}', process.argv.slice(2)); `; } @@ -281,236 +309,593 @@ process.exit(result.status ?? 1); return generatePackageTsconfig(); } - generateIndex(): string { - const prefix = this.opts.prefix; - const serviceClass = className(prefix); - const serviceOptions = optionsType(prefix); - const serviceTransport = transportType(prefix); - const findBinary = binaryFinderName(prefix); - const supportsShm = this.opts.transports.includes("shm"); - const transports = this.opts.transports.map((t) => `'${t}'`).join(" | "); - const ipcPathArgs = JSON.stringify(this.opts.ipcPathArgs); - const defaultTransport = this.opts.transports.includes("uds") + /** The spawned-process backend (node only): options, environment, and the two spawn functions. */ + /** The spawned-process backend (node only): this package's options over ipc-runtime's spawner. */ + generateProcess(): string { + const { prefix, binaryName } = this.opts; + const transports = this.processTransports.map((t) => `'${t}'`).join(" | "); + const defaultTransport = this.processTransports.includes("uds") ? "uds" - : this.opts.transports[0]!; + : this.processTransports[0]!; + const syncImports = this.shm + ? "\n type SpawnedProcessBackendSync,\n spawnServiceBackendSync," + : ""; + const syncSpawn = this.shm + ? ` +/** The synchronous form: shared memory is the one transport with a synchronous client. */ +export function spawnProcessBackendSync(options: ${prefix}ProcessOptions = {}): Promise { + return spawnServiceBackendSync(BINARY, options); +} +` + : ""; - return `import { IpcSpawnError, SpawnedProcessBackend } from '@aztec-foundation/ipc-runtime'; -import { AsyncApi, type IpcErrorFactory } from './generated/async.js'; -import { ${findBinary} } from './platform.js'; + return `import { + type ServiceProcessOptions, + type SpawnedProcessBackend,${syncImports} + spawnServiceBackend, +} from '@aztec-foundation/ipc-runtime'; +import { BINARY } from './platform.js'; -export * from './generated/api_types.js'; -export { AsyncApi } from './generated/async.js'; -export { SyncApi } from './generated/sync.js'; +export type ${prefix}Transport = ${transports}; -export type ${serviceTransport} = ${transports}; +/** + * Options for running '${binaryName}' as a spawned process (node only). See ServiceProcessOptions + * in ipc-runtime for the rest: binaryPath, threads, logger, env, extraArgs, respawn and unref. + */ +export interface ${prefix}ProcessOptions extends ServiceProcessOptions { + transport?: ${prefix}Transport; +} -export interface ${serviceOptions} { - binaryPath?: string; - transport?: ${serviceTransport}; - logger?: (msg: string) => void; - connectTimeoutMs?: number; - env?: NodeJS.ProcessEnv; - extraArgs?: string[]; - createError?: IpcErrorFactory; +/** The name this package's first version used for the spawn options. */ +export type ${prefix}ServiceOptions = ${prefix}ProcessOptions; + +/** + * Spawn '${binaryName}' and connect to it over ${this.processTransports.join(" or ")}. Process + * lifecycle — connectivity, death detection, optional respawn, teardown — is owned by the backend + * and never leaks onto the caller's API. + */ +export function spawnProcessBackend(options: ${prefix}ProcessOptions = {}): Promise { + return spawnServiceBackend(BINARY, '${defaultTransport}', options); +} +${syncSpawn}`; + } + + /** Shared shape of the create options: a backend name, a backend object, or nothing (defaults). */ + private createOptionTypes(backends: string[]): string { + const { prefix } = this.opts; + const process = this.process; + const wasm = this.wasm; + const backendType = backends.join(" | ") || "never"; + const defaults = [ + process && + `the spawned process when the '${this.opts.binaryName}' binary resolves`, + wasm && "the wasm module", + ] + .filter(Boolean) + .join(", otherwise "); + return `export type ${prefix}Backend = ${backendType}; + +/** Options for ${className(prefix)}.create / createBackend. */ +export interface ${prefix}CreateOptions { /** - * Respawn the server on the next call after it dies, instead of failing all - * subsequent calls. Only enable for stateless servers: a respawned process - * remembers nothing, so any server-side session state held by callers would - * silently dangle. + * Unset: ${defaults}. A name forces that backend, with no fallback. An object (anything with + * call()/destroy(), e.g. a bridge to a natively linked library) is used as is. */ - respawn?: boolean; -${supportsShm ? " napiPath?: string;\n clientId?: number;\n" : ""}} + backend?: ${prefix}Backend | IpcClientAsync; + /** Threads the service may use: a process reads them from HARDWARE_CONCURRENCY/RAYON_NUM_THREADS; wasm runs that many. */ + threads?: number; + logger?: (msg: string) => void; + /** Let node exit while the backend is alive (a process that watches its parent, or the wasm workers). */ + unref?: boolean; +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>;\n` : ""}} + +/** Options for ${className(prefix)}Sync.create / createBackendSync. */ +export interface ${prefix}CreateSyncOptions { + /** As for the asynchronous form; the synchronous wasm module runs on the calling thread, single-threaded. */ + backend?: ${prefix}Backend | IpcClientSync; + /** Threads a spawned process may use (the synchronous wasm module always has one). */ + threads?: number; + logger?: (msg: string) => void; + unref?: boolean; +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>;\n` : ""}} +`; + } -/** - * Spawns and talks to a '${this.opts.binaryName}' server process. Process - * lifecycle — connectivity, death detection, optional respawn, teardown — is - * owned by the backend (see SpawnedProcessBackend in ipc-runtime); it never - * leaks onto this API. Failed calls carry a 'retry' property set to true when - * the failure was environmental and the operation may be retried. + /** The service classes over createBackend/createBackendSync, with the direct constructors this entry offers. */ + private serviceClasses(opts: { process: boolean; wasm: boolean }): string { + const { prefix } = this.opts; + const svc = className(prefix); + const { process, wasm } = opts; + return `/** + * The ${prefix} service: the generated API over whichever backend \`create\` chose, forced, or was + * given.${process ? " Process lifecycle stays inside the backend and never leaks onto this API." : ""} */ -export class ${serviceClass} extends AsyncApi { - private constructor(private spawnedBackend: SpawnedProcessBackend, createError?: IpcErrorFactory) { - super(spawnedBackend, createError); +export class ${svc} extends AsyncApi { + private constructor(backend: IpcClientAsync) { + super(backend); } - static async spawn(options: ${serviceOptions} = {}): Promise<${serviceClass}> { - const binaryPath = ${findBinary}(options.binaryPath); - if (!binaryPath) { - throw new IpcSpawnError('${this.opts.binaryName} binary not found', /*retry=*/ false); - } - const backend = await SpawnedProcessBackend.spawn({ - binaryPath, - binaryName: '${this.opts.binaryName}', - instancePrefix: '${toSnakeCase(prefix)}', - ipcPathArgs: ${ipcPathArgs}, - transport: options.transport ?? '${defaultTransport}', - logger: options.logger, - connectTimeoutMs: options.connectTimeoutMs, - env: options.env, - extraArgs: options.extraArgs, - respawn: options.respawn, -${supportsShm ? " clientId: options.clientId,\n napiPath: options.napiPath,\n" : ""} }); - return new ${serviceClass}(backend, options.createError); + static async create(options: ${prefix}CreateOptions = {}): Promise<${svc}> { + return new ${svc}(await createBackend(options)); + } +${ + process + ? ` + /** The service as a spawned '${this.opts.binaryName}' process (no fallback). */ + static async spawn(options: ${prefix}ProcessOptions = {}): Promise<${svc}> { + return new ${svc}(await spawnProcessBackend(options)); + } +` + : "" +}${ + wasm + ? ` + /** The service over the in-process wasm module (no fallback). */ + static async wasm(options: ${prefix}WasmOptions = {}): Promise<${svc}> { + return new ${svc}(await createWasmBackend(options)); + } +` + : "" + }${ + process + ? ` + /** The spawned process behind this service, when that is what backs it. */ + get process(): SpawnedProcessBackend | undefined { + return this.backend instanceof SpawnedProcessBackend ? this.backend : undefined; } getIpcPath(): string { - return this.spawnedBackend.getIpcPath(); + return this.requireProcess().getIpcPath(); } sendProcessSignal(signal: NodeJS.Signals): void { - this.spawnedBackend.sendProcessSignal(signal); + this.requireProcess().sendProcessSignal(signal); } -} + + private requireProcess(): SpawnedProcessBackend { + const process = this.process; + if (!process) { + throw new Error('${svc}: not backed by a spawned process'); + } + return process; + } +` + : "" + }} + +/** The synchronous ${prefix} service: every call blocks the calling thread until the service answers. */ +export class ${svc}Sync extends SyncApi { + private constructor(backend: IpcClientSync) { + super(backend); + } + + static async create(options: ${prefix}CreateSyncOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(await createBackendSync(options)); + } +${ + process && this.shm + ? ` + /** The service as a spawned '${this.opts.binaryName}' process over shared memory (no fallback). */ + static async spawn(options: ${prefix}ProcessOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(await spawnProcessBackendSync(options)); + } +` + : "" +}${ + wasm + ? ` + /** The service over the single-threaded in-process wasm module (no fallback). */ + static async wasm(options: ${prefix}WasmOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(await createWasmBackendSync(options)); + } +` + : "" + }} `; } - generatePlatform(): string { - const packageName = this.opts.packageName; - const findBinary = binaryFinderName(this.opts.prefix); - const envVar = this.opts.binaryEnvVar; - const stem = packageStem(packageName); - const archPackages = archPackageNames(packageName); + /** Node entry: every backend the package has, and the default policy over them. */ + generateIndex(): string { + const { prefix, binaryName, packageName } = this.opts; + const findBinary = binaryFinderName(prefix); + const process = this.process; + const wasm = this.wasm; + const shm = this.shm; + const backends = [process && "'process'", wasm && "'wasm'"].filter( + Boolean, + ) as string[]; + + return `import { + type IpcClientAsync, + type IpcClientSync,${process ? "\n SpawnedProcessBackend," : ""} + pickServiceBackend, +} from '@aztec-foundation/ipc-runtime'; +import { AsyncApi } from './generated/async.js'; +import { SyncApi } from './generated/sync.js'; +${process ? `import { type ${prefix}ProcessOptions, spawnProcessBackend${shm ? ", spawnProcessBackendSync" : ""} } from './process.js';\n` : ""}import { ${findBinary} } from './platform.js'; +${wasm ? `import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js';\n` : ""} +${this.generatedExports()}${process ? "export * from './process.js';\n" : ""}${wasm ? "export * from './wasm.js';\n" : ""}export { ${findBinary} } from './platform.js'; + +${this.createOptionTypes(backends)} +/** + * The backend \`${className(prefix)}.create\` would use for \`options\`, for facades that wrap the + * generated API themselves. Unset backend: ${process ? `the process when the binary resolves${wasm ? ", falling back to wasm if it cannot be spawned" : ""}` : "the wasm module"}. + */ +export async function createBackend(options: ${prefix}CreateOptions = {}): Promise { + const common = { threads: options.threads, logger: options.logger, unref: options.unref }; + return pickServiceBackend(options.backend, { + label: '${packageName}', + logger: options.logger, +${ + process + ? ` process: { + available: () => ${findBinary}(options.process?.binaryPath) !== null, + create: () => spawnProcessBackend({ ...common, ...options.process }), + }, +` + : "" +}${ + wasm + ? ` wasm: { create: () => createWasmBackend({ ...common, ...options.wasm }) }, +` + : "" + } }); +} - return `import { createRequire } from 'node:module'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; +/** The synchronous counterpart of createBackend${shm ? ": the process over shared memory" : ""}${shm && wasm ? ", else " : ""}${wasm ? "the single-threaded wasm module" : ""}. */ +export async function createBackendSync(options: ${prefix}CreateSyncOptions = {}): Promise { + const common = { threads: options.threads, logger: options.logger, unref: options.unref }; + return pickServiceBackend(options.backend, { + label: '${packageName}', + logger: options.logger, +${ + shm + ? ` process: { + available: () => ${findBinary}(options.process?.binaryPath) !== null, + create: () => spawnProcessBackendSync({ ...common, ...options.process }), + }, +` + : "" +}${ + wasm + ? ` wasm: { create: () => createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }) }, +` + : "" + } }); +} -export type Platform = 'x86_64-linux' | 'x86_64-darwin' | 'aarch64-linux' | 'aarch64-darwin'; +${this.serviceClasses({ process, wasm })}`; + } -const PLATFORM_TO_PACKAGE: Record = { - 'x86_64-linux': '${archPackages["linux-x64"]}', - 'x86_64-darwin': '${archPackages["darwin-x64"]}', - 'aarch64-linux': '${archPackages["linux-arm64"]}', - 'aarch64-darwin': '${archPackages["darwin-arm64"]}', -}; + /** Browser entry: the service runs in-process as a wasm module; there is no process to spawn. */ + generateBrowserIndex(): string { + const { prefix, packageName } = this.opts; + + return `import type { IpcClientAsync, IpcClientSync } from '@aztec-foundation/ipc-runtime'; +import { AsyncApi } from './generated/async.js'; +import { SyncApi } from './generated/sync.js'; +import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js'; + +${this.generatedExports()}export * from './wasm.js'; + +export type ${prefix}Backend = 'wasm'; + +/** Options for ${className(prefix)}.create / createBackend (browser: the wasm module, or a backend object). */ +export interface ${prefix}CreateOptions { + backend?: ${prefix}Backend | IpcClientAsync; + /** Worker threads to run with; more than one needs a cross-origin isolated page (COOP/COEP). */ + threads?: number; + logger?: (msg: string) => void; + unref?: boolean; + wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>; +} -function currentDir(): string { - return path.dirname(fileURLToPath(import.meta.url)); +/** Options for ${className(prefix)}Sync.create / createBackendSync. */ +export interface ${prefix}CreateSyncOptions { + backend?: ${prefix}Backend | IpcClientSync; + logger?: (msg: string) => void; + unref?: boolean; + wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>; } -function detectPlatform(): Platform | null { - if (process.arch === 'x64' && process.platform === 'linux') return 'x86_64-linux'; - if (process.arch === 'x64' && process.platform === 'darwin') return 'x86_64-darwin'; - if (process.arch === 'arm64' && process.platform === 'linux') return 'aarch64-linux'; - if (process.arch === 'arm64' && process.platform === 'darwin') return 'aarch64-darwin'; - return null; +export async function createBackend(options: ${prefix}CreateOptions = {}): Promise { + if (typeof options.backend === 'object') { + return options.backend; + } + if (options.backend !== undefined && options.backend !== 'wasm') { + throw new Error(\`${packageName}: no such backend in a browser: \${String(options.backend)}\`); + } + return createWasmBackend({ threads: options.threads, logger: options.logger, unref: options.unref, ...options.wasm }); } -function findArchPackageDir(platform: Platform): string | null { - const packageName = PLATFORM_TO_PACKAGE[platform]; - try { - const require = createRequire(import.meta.url); - return path.dirname(require.resolve(packageName + '/package.json')); - } catch { - const siblingPackageDir = path.join(currentDir(), '..', 'packages', packageName.split('/').pop()!); - return fs.existsSync(path.join(siblingPackageDir, 'package.json')) ? siblingPackageDir : null; +export async function createBackendSync(options: ${prefix}CreateSyncOptions = {}): Promise { + if (typeof options.backend === 'object') { + return options.backend; } + if (options.backend !== undefined && options.backend !== 'wasm') { + throw new Error(\`${packageName}: no such synchronous backend in a browser: \${String(options.backend)}\`); + } + return createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }); } -export function ${findBinary}(customPath?: string): string | null { - if (customPath) { - return fs.existsSync(customPath) ? path.resolve(customPath) : null; +${this.serviceClasses({ process: false, wasm: true })}`; + } + + /** + * React Native entry: Hermes has no WebAssembly or workers and Metro cannot bundle worker URLs, so + * this entry ships no backend of its own. A native backend package (a JSI/TurboModule bridge to + * the service's linked library) registers itself; otherwise the consumer passes a backend object. + */ + /** + * React Native entry: Hermes has no WebAssembly or workers and Metro cannot bundle worker URLs, + * so this entry ships no backend of its own. A native backend package (a JSI/TurboModule bridge + * to the service's linked library) registers one with ipc-runtime; otherwise the consumer passes + * a backend object. + */ + generateReactNativeIndex(): string { + const { prefix, packageName } = this.opts; + const svc = className(prefix); + + return `import type { IpcClientAsync, IpcClientSync } from '@aztec-foundation/ipc-runtime/registry'; +import { registerBackend as register, registeredBackend } from '@aztec-foundation/ipc-runtime/registry'; +import { AsyncApi } from './generated/async.js'; +import { SyncApi } from './generated/sync.js'; + +${this.generatedExports()}export type { RegisteredBackends } from '@aztec-foundation/ipc-runtime/registry'; + +/** Make these the backends for ${prefix} in this app; a native backend package calls it on import. */ +export function registerBackend(factories: Parameters[1]): void { + register('${prefix}', factories); +} + +export interface ${prefix}CreateOptions { + /** A backend object (anything with call()/destroy()); default: the one a native package registered. */ + backend?: IpcClientAsync; +} + +export interface ${prefix}CreateSyncOptions { + backend?: IpcClientSync; +} + +export async function createBackend(options: ${prefix}CreateOptions = {}): Promise { + return options.backend ?? (await registeredBackend('${prefix}', 'async', '${packageName}')()); +} + +export async function createBackendSync(options: ${prefix}CreateSyncOptions = {}): Promise { + return options.backend ?? (await registeredBackend('${prefix}', 'sync', '${packageName}')()); +} + +export class ${svc} extends AsyncApi { + private constructor(backend: IpcClientAsync) { + super(backend); } - const envPath = process.env.${envVar}; - if (envPath) { - return fs.existsSync(envPath) ? path.resolve(envPath) : null; + static async create(options: ${prefix}CreateOptions = {}): Promise<${svc}> { + return new ${svc}(await createBackend(options)); } +} - const platform = detectPlatform(); - if (!platform) { - return null; +export class ${svc}Sync extends SyncApi { + private constructor(backend: IpcClientSync) { + super(backend); } - const archDir = findArchPackageDir(platform); - if (archDir) { - const candidate = path.join(archDir, '${this.opts.binaryName}'); - if (fs.existsSync(candidate)) { - return candidate; - } + static async create(options: ${prefix}CreateSyncOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(await createBackendSync(options)); } +} +`; + } + + /** Platform-neutral part of the wasm transport: options, module selection, backend construction. */ + generateWasm(): string { + const { prefix, packageName } = this.opts; + const moduleUrl = (name: string | undefined) => + name ? `new URL('../wasm/${name}', import.meta.url)` : "undefined"; + // The module's FFI symbols carry the service name (see the FFI entry in SCHEMA_SPEC.md). + const sym = prefix ? `${toSnakeCase(prefix)}_` : ""; + const ffiExports = ` entry: '${sym}ipc_ffi_entry',`; + + return `import { + type WasmFfiBackend, + type WasmFfiBackendSync, + type WasmModuleSource, + chooseWasmModule, + createWasmFfiBackend, + createWasmFfiBackendSync, + platform, + resolveWasmThreads, +} from '@aztec-foundation/ipc-runtime/wasm'; + +export { sharedMemoryAvailable } from '@aztec-foundation/ipc-runtime/wasm'; + +/** Options for running the ${this.opts.binaryName} wasm module in-process. */ +export interface ${prefix}WasmOptions { + /** + * Threads to run with (1 = no worker threads; selects the single-thread module). Default: the + * platform's parallelism where a shared memory is available, else 1. More than one thread where + * no shared memory is available is an error, not a silent downgrade. + */ + threads?: number; + /** Linear memory bounds in 64 KiB pages. */ + memory?: { initial?: number; maximum?: number }; + /** + * The module to run instead of the package's own: a URL or path, raw or gzipped bytes, a fetch + * Response, or an already compiled WebAssembly.Module. The package's own module is uncompressed, + * so that a browser can stream it into compileStreaming and cache the compiled code; point this + * at a gzipped copy when serving from a host that applies no compression of its own. + */ + module?: WasmModuleSource; + /** + * Where a call runs. Default (true) is a dedicated worker, so a long call never blocks the + * caller — the right choice for proving, and the only safe one on a browser's main thread. + * False runs the module on the calling thread and every call blocks it until the module + * returns, which is cheaper per call and fine for short work off the main thread. For short + * work *on* the main thread, prefer the synchronous backend, which always runs there. + */ + worker?: boolean; + /** WASI environ for the module. */ + env?: Record; + logger?: (msg: string) => void; + /** Let node exit while the module's workers are alive. */ + unref?: boolean; +} + +// This package's own modules, as URLs relative to its own files: only the package can express +// these, which is why the choosing lives here and the deciding does not. +const MODULES = { + threads: ${moduleUrl(this.opts.wasmThreadsModule)}, + single: ${moduleUrl(this.opts.wasmModule)}, +}; + +/** The module to run for a thread count: the threads build above one thread, else single. */ +export function defaultWasmModule(threads: number): URL { + return chooseWasmModule(MODULES, '${packageName}', threads); +} + +/** The thread count to run with: the host's parallelism by default, a request checked against it. */ +export function resolveThreads(threads?: number): number { + return resolveWasmThreads(platform, '${packageName}', threads); +} - return null; +/** + * The ${this.opts.binaryName} wasm module in-process: the main instance in a worker by default, wasi + * threads on further workers. The workers are ipc-runtime's own, spawned with the literal + * expression bundlers detect, so they ship as worker chunks of the consuming application. + */ +export async function createWasmBackend(options: ${prefix}WasmOptions = {}): Promise { + const threads = resolveThreads(options.threads); + const backend = await createWasmFfiBackend({ + module: options.module ?? defaultWasmModule(threads), + threads, + memory: options.memory, + env: options.env, + logger: options.logger, + worker: options.worker, +${ffiExports} + }); + if (options.unref) { + backend.unref(); + } + return backend; } -export const ARCH_PACKAGE_STEM = '${stem}'; +/** + * The module on the calling thread, single-threaded: every call blocks until it returns, with no + * worker and no message passing. This is the form for short synchronous work such as hashing, + * including on a browser's main thread. + */ +export async function createWasmBackendSync(options: ${prefix}WasmOptions = {}): Promise { + const backend = await createWasmFfiBackendSync({ + module: options.module ?? defaultWasmModule(1), + threads: 1, + memory: options.memory, + env: options.env, + logger: options.logger, +${ffiExports} + }); + if (options.unref) { + backend.unref(); + } + return backend; +} `; } - generatePrepareArchPackagesScript(): string { - return `#!/usr/bin/env bash -set -euo pipefail - -cd "$(dirname "$0")/.." - -declare -A PLATFORMS=( -${ARCH_PACKAGES.map(({ buildDir, suffix, os, cpu }) => ` ["${buildDir}"]="${suffix} ${os} ${cpu}"`).join("\n")} -) - -version=$(node -p "require('./package.json').version") - -declare -A BINARIES=() -for arg in "$@"; do - case "$arg" in - *=*) - key="\${arg%%=*}" - value="\${arg#*=}" - BINARIES["$key"]="$value" - ;; - *) - echo "Usage: npm run prepare_arch_packages -- [= ...]" >&2 - echo "Platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64" >&2 - exit 1 - ;; - esac -done - -for build_dir in "\${!PLATFORMS[@]}"; do - read -r suffix os cpu <<< "\${PLATFORMS[$build_dir]}" - pkg_name="${this.opts.packageName}-\${suffix}" - out_dir="packages/${packageStem(this.opts.packageName)}-\${suffix}" - binary_path="\${BINARIES[$suffix]:-\${BINARIES[$build_dir]:-}}" - - if [ -z "$binary_path" ]; then - binary_path="build/\${build_dir}/${this.opts.binaryName}" - fi - - if [ ! -f "$binary_path" ]; then - echo "Skipping \${pkg_name}: no binary at \${binary_path}" - continue - fi - - rm -rf "\${out_dir}" - mkdir -p "\${out_dir}" - cp "$binary_path" "\${out_dir}/${this.opts.binaryName}" - chmod +x "\${out_dir}/${this.opts.binaryName}" 2>/dev/null || true - - cat > "\${out_dir}/package.json" < ` '${key}': '${name}',`) + .join("\n"); + + return `import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { type ServiceBinary, findServiceBinary } from '@aztec-foundation/ipc-runtime'; + +/** This package's native binary, for ipc-runtime's resolver and process backends. */ +export const BINARY: ServiceBinary = { + name: '${this.opts.binaryName}', + envVar: '${this.opts.binaryEnvVar}', + archPackages: { +${byPlatform} + }, + ipcPathArgs: ${JSON.stringify(this.opts.ipcPathArgs)}, + instancePrefix: '${toSnakeCase(this.opts.prefix)}', + packageDir: path.join(path.dirname(fileURLToPath(import.meta.url)), '..'), +}; + +/** + * The '${this.opts.binaryName}' binary to run: an explicit path if given, else + * \`${this.opts.binaryEnvVar}\`, else the installed arch package for this platform. Null when none + * of those yields an existing file. + */ +export function ${findBinary}(customPath?: string): string | null { + return findServiceBinary(BINARY, customPath); +} + +export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; `; } + + generateReadme(): string { + const svc = className(this.opts.prefix); + const process = this.process; + const wasm = this.wasm; + const backends = [ + process && + `- \`'process'\`: spawns the \`${this.opts.binaryName}\` binary (node) and talks to it over ${this.processTransports.join(" or ")}. The binary is resolved from \`${this.opts.binaryEnvVar}\`, an explicit \`process.binaryPath\`, or the installed arch package (one of this package's optional dependencies).`, + wasm && + `- \`'wasm'\`: runs the service's wasm module in-process (node and browsers) through \`@aztec-foundation/ipc-runtime/wasm\`: the main instance in a worker, wasi threads on further workers where a shared memory is available (node, or a browser page served with COOP/COEP headers), otherwise the single-thread module. The worker scripts and the module are referenced with \`new URL(..., import.meta.url)\`, so bundlers emit them as chunks and assets of the application, and only the module actually chosen is ever fetched (Vite users: exclude the package from \`optimizeDeps\`). The module ships uncompressed, which is what lets the browser stream it into \`WebAssembly.compileStreaming\` and cache the compiled code between visits; serve it with your host's own compression. Where that is not possible, \`wasm.module\` takes a compressed copy — or bytes, a \`Response\`, or an already compiled \`Module\`.`, + `- an object: anything with \`call(bytes)\`/\`destroy()\`, for a transport of your own (a bridge to a natively linked library, for instance).`, + ] + .filter(Boolean) + .join("\n"); + const defaultPolicy = [ + process && `the process when the binary resolves`, + wasm && `the wasm module`, + ] + .filter(Boolean) + .join(", otherwise "); + const threadsNote = wasm + ? "(a process reads it from `HARDWARE_CONCURRENCY`/`RAYON_NUM_THREADS`; wasm runs that many\nworker threads, and asking for more than one where no shared memory exists is an error rather than\na silent downgrade)" + : "(the process reads it from `HARDWARE_CONCURRENCY`/`RAYON_NUM_THREADS`)"; + const syncForm = [ + this.shm && "shared memory for a process", + wasm && "the single-threaded wasm module on the calling thread", + ] + .filter(Boolean) + .join(", else "); + const hosts = [ + "node (`default`) has every backend above", + wasm + ? "browsers (`browser`) have the wasm module only" + : "browsers have no backend of their own (this service has no wasm module)", + "React Native (`react-native`) has no built-in backend, because Hermes has no WebAssembly or workers — a native backend package registers one with `registerBackend`, or the app passes `options.backend`", + ].join("; "); + return `# ${this.opts.packageName} -Generated TypeScript IPC package for the ${this.opts.prefix} service. +Generated TypeScript package for the ${this.opts.prefix} service: the typed API +(\`AsyncApi\`/\`SyncApi\`, one method per command) over whichever backend reaches +the service on the current host. \`\`\`ts -import { ${className(this.opts.prefix)} } from '${this.opts.packageName}'; +import { ${svc} } from '${this.opts.packageName}'; -const service = await ${className(this.opts.prefix)}.spawn({ transport: 'uds' }); +const service = await ${svc}.create(); try { const response = await service.bytes({ data: new Uint8Array([1, 2, 3]) }); } finally { @@ -518,12 +903,36 @@ try { } \`\`\` -The package resolves \`${this.opts.binaryName}\` from \`${this.opts.binaryEnvVar}\`, -an explicit \`binaryPath\`, or an installed/prepared arch package. +\`create\` picks ${defaultPolicy}. \`options.backend\` forces one, with no fallback: + +${backends} + +\`threads\` sets the service's parallelism for any backend +${threadsNote}.${syncForm ? ` \`${svc}Sync.create\` is the synchronous form (${syncForm}).` : ""} +\`createBackend\`/\`createBackendSync\` expose the same policy for code that wraps +the generated API itself. +${ + wasm + ? ` +## Which thread the work runs on + +The caller chooses. An asynchronous wasm backend runs the module in a worker by +default, so a long call never blocks the caller; \`{ wasm: { worker: false } }\` +runs it on the calling thread instead, blocking until it returns. The +synchronous backend always runs on the calling thread, which is what short work +such as hashing wants, including on a browser's main thread. A spawned process +is off the caller's thread either way. +` + : "" +} +## Entries per host + +The package resolves to a different entry per host through export conditions: +${hosts}. ## Build -The package shell (package.json, tsconfig, src/index.ts, scripts/) is +The package shell (package.json, tsconfig, \`src/*.ts\`, scripts/) is generated; build through the owning project's \`./bootstrap.sh\`, which regenerates and then runs \`npm install --omit=optional && npm run build\`. diff --git a/ipc-codegen/src/zig_codegen.ts b/ipc-codegen/src/zig_codegen.ts index 52f83308356..720bd5ca691 100644 --- a/ipc-codegen/src/zig_codegen.ts +++ b/ipc-codegen/src/zig_codegen.ts @@ -43,6 +43,49 @@ export class ZigCodegen { }; } + /** + * Client-side FFI backend over a linked library exporting this service's FFI entry. The symbols + * carry the service (`bb_ipc_ffi_entry`) so several services can be linked into one binary. + */ + generateFfiBackend(): string { + const sym = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; + return `//! AUTOGENERATED - DO NOT EDIT +//! FFI client backend for ${this.opts.prefix || "service"}: calls the service's in-process FFI +//! entry directly (\`${sym}ipc_ffi_entry\`), with no process and no IPC. Link the library +//! exporting it in your build.zig. +//! +//! Satisfies the backend interface: call(request) -> response, destroy(). +const std = @import("std"); + +extern fn ${sym}ipc_ffi_entry(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void; +extern fn ${sym}ipc_ffi_free(ptr: [*]u8) void; + +/// Allocator contract: callers free returned slices with this allocator +/// (the generated client uses std.heap.page_allocator), so the FFI buffer is +/// copied into it and released with the service's own free — freeing it with +/// a Zig allocator is undefined behaviour. +const alloc = std.heap.page_allocator; + +pub const FfiBackend = struct { + /// Send a msgpack command and receive the response via FFI. + pub fn call(self: *FfiBackend, request: []const u8) ![]u8 { + _ = self; + var out_ptr: [*]u8 = undefined; + var out_len: usize = 0; + ${sym}ipc_ffi_entry(request.ptr, request.len, &out_ptr, &out_len); + defer ${sym}ipc_ffi_free(out_ptr); + const response = try alloc.alloc(u8, out_len); + @memcpy(response, out_ptr[0..out_len]); + return response; + } + + pub fn destroy(self: *FfiBackend) void { + _ = self; + } +}; +`; + } + private primitiveType(type: Type): string { switch (type.primitive) { case "bool": diff --git a/ipc-codegen/templates/rust/ffi_backend.rs b/ipc-codegen/templates/rust/ffi_backend.rs deleted file mode 100644 index 5137ea99c6f..00000000000 --- a/ipc-codegen/templates/rust/ffi_backend.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! FFI backend scaffold for direct library linking. -//! -//! Calls a C symbol with msgpack bytes — no IPC overhead. Link against a -//! native library that exports `ipc_ffi_entry`, and add the appropriate -//! `-L` / `-l` directives to your `build.rs`. -//! -//! # Requirements -//! -//! 1. A native library exporting an extern-C function with this signature: -//! ```text -//! void ipc_ffi_entry( -//! const uint8_t* input, size_t input_len, -//! uint8_t** output_out, size_t* output_len_out); -//! ``` -//! `*output_out` must be a `malloc`'d buffer the caller is responsible for freeing. -//! 2. Library search path configured (via `.cargo/config.toml`, `RUSTFLAGS`, or -//! `cargo:rustc-link-search` in `build.rs`). -//! -//! # Example -//! -//! ```ignore -//! use my_service_client::{ServiceApi, FfiBackend}; -//! -//! let backend = FfiBackend::new()?; -//! let mut api = ServiceApi::new(backend); -//! let response = api.some_command(args)?; -//! ``` - -use super::backend::Backend; -use super::error::{IpcError, Result}; -use std::ptr; - -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 ipc_ffi_entry( - input_in: *const u8, - input_len_in: usize, - output_out: *mut *mut u8, - output_len_out: *mut usize, - ); -} - -/// FFI backend that calls a native library directly via its C ABI. -/// -/// Most performant backend (no process spawn, no IPC overhead) but requires -/// linking against the native library at build time. -/// -/// # Thread Safety -/// -/// This backend is **not** thread-safe by default. 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. - pub fn new() -> Result { - 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 - // - the FFI entrypoint allocates output using malloc, which we free below - unsafe { - ipc_ffi_entry( - input.as_ptr(), - input.len(), - &mut output_ptr, - &mut output_len, - ); - } - - if output_ptr.is_null() { - return Err(IpcError::Backend( - "FFI entry returned null pointer".to_string(), - )); - } - - if output_len == 0 { - unsafe { - libc::free(output_ptr as *mut libc::c_void); - } - return Err(IpcError::Backend( - "FFI entry 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() }; - - // SAFETY: output_ptr was allocated by the FFI entrypoint using malloc - unsafe { - libc::free(output_ptr as *mut libc::c_void); - } - - Ok(output) - } - - fn destroy(&mut self) -> Result<()> { - 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") - } -} diff --git a/ipc-codegen/templates/zig/ffi_backend.zig b/ipc-codegen/templates/zig/ffi_backend.zig deleted file mode 100644 index 93e16eb915e..00000000000 --- a/ipc-codegen/templates/zig/ffi_backend.zig +++ /dev/null @@ -1,34 +0,0 @@ -/// FFI backend scaffold for direct library linking. -/// -/// Calls a C symbol with msgpack bytes — no IPC overhead. Link against a -/// native library that exports `ipc_ffi_entry`, and adjust the link -/// configuration in your build.zig to pull that library in. -/// -/// Satisfies the backend interface: call(request) -> response, destroy(). -const std = @import("std"); - -extern fn ipc_ffi_entry(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void; - -/// Allocator contract: callers free returned slices with this allocator -/// (the generated client uses std.heap.page_allocator), so the malloc'd FFI -/// buffer is copied into it and freed with the C allocator here — freeing a -/// malloc'd pointer with a Zig allocator is undefined behaviour. -const alloc = std.heap.page_allocator; - -pub const FfiBackend = struct { - /// Send a msgpack command and receive the response via FFI. - pub fn call(self: *FfiBackend, request: []const u8) ![]u8 { - _ = self; - var out_ptr: [*]u8 = undefined; - var out_len: usize = 0; - ipc_ffi_entry(request.ptr, request.len, &out_ptr, &out_len); - defer std.c.free(out_ptr); - const response = try alloc.alloc(u8, out_len); - @memcpy(response, out_ptr[0..out_len]); - return response; - } - - pub fn destroy(self: *FfiBackend) void { - _ = self; - } -}; diff --git a/ipc-runtime/README.md b/ipc-runtime/README.md index 2d3360d3620..b516da3208c 100644 --- a/ipc-runtime/README.md +++ b/ipc-runtime/README.md @@ -215,6 +215,27 @@ Two transport-specific clients: `UdsIpcServer` is provided for in-process tests; production servers are in C++. +The `@aztec-foundation/ipc-runtime/wasm` entry (node and `browser` export +conditions) runs a service compiled to a wasi reactor in-process, through the +FFI entry of the ipc-codegen contract (`_ipc_ffi_entry` / `_alloc` / +`_free`, found by suffix when not named; see `ipc-codegen/SCHEMA_SPEC.md`): + +| Export | Role | +|-------------------------------|------------------------------------------------------------------------------------------| +| `WasmFfiBackend` | async `IpcClientAsync`; main instance in a worker by default, wasi threads on further workers | +| `WasmFfiBackendSync` | sync `IpcClientSync`; one thread on the calling thread | +| `createWasmFfiBackend(Sync)` | the above bound to this platform's worker scripts | +| `runMainWorker`, `runThreadWorker` | bodies for a package's own worker scripts (needed when the module has `hostImports`) | +| `compileWasmModule` | `Module` from a URL, `Response`, raw or gzipped bytes; streaming compilation for URLs | + +The module is loaded through `WebAssembly.compileStreaming` where possible, so +browsers that cache compiled wasm start from optimized code on a repeat visit. +Memory is created to the module's declared shape (a threads build gets a +shared memory even for one thread), WASI is served by a small shim (clock, +random, stdout/stderr to the logger, environ), and modules whose platform +layer imports functions of its own (bb imports a logger, an abort hook and its +thread count) supply them through `hostImports`. + ### Zig (`zig/`) `Server.fromPath(path)` / `Client.fromPath(path)` over the same C ABI; the diff --git a/ipc-runtime/ts/package.json b/ipc-runtime/ts/package.json index d23a1accc8d..ada5c1c31b4 100644 --- a/ipc-runtime/ts/package.json +++ b/ipc-runtime/ts/package.json @@ -9,6 +9,28 @@ ".": { "types": "./dest/index.d.ts", "import": "./dest/index.js" + }, + "./wasm": { + "browser": { + "types": "./dest/wasm/browser/index.d.ts", + "default": "./dest/wasm/browser/index.js" + }, + "default": { + "types": "./dest/wasm/node/index.d.ts", + "default": "./dest/wasm/node/index.js" + } + }, + "./wasm/node": { + "types": "./dest/wasm/node/index.d.ts", + "default": "./dest/wasm/node/index.js" + }, + "./wasm/browser": { + "types": "./dest/wasm/browser/index.d.ts", + "default": "./dest/wasm/browser/index.js" + }, + "./registry": { + "types": "./dest/backend_registry.d.ts", + "default": "./dest/backend_registry.js" } }, "scripts": { @@ -17,12 +39,16 @@ "test": "tsc -p tsconfig.json && node --test dest/*.test.js" }, "files": [ + "build", "dest", - "src", - "build" + "scripts", + "src" ], "devDependencies": { "@types/node": "^22", "typescript": "^5.6.3" + }, + "bin": { + "ipc-runtime-prepare-arch-packages": "./scripts/prepare_arch_packages.mjs" } } diff --git a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs new file mode 100755 index 00000000000..2cf6e38695c --- /dev/null +++ b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// Stage a generated client package's per-platform binary packages: the optional dependencies its +// binary resolver looks for. Run from the package root; the package name, binary name and version +// come from its own package.json, so this is the same script for every service. +// +// Usage: prepare_arch_packages [= ...] +// platform: linux-x64 | linux-arm64 | darwin-x64 | darwin-arm64, or the build directory name +// (amd64-linux, arm64-linux, amd64-macos, arm64-macos) +// Without an argument for a platform, build// is used when it exists. +import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const PLATFORMS = [ + { buildDir: 'amd64-linux', suffix: 'linux-x64', os: 'linux', cpu: 'x64' }, + { buildDir: 'arm64-linux', suffix: 'linux-arm64', os: 'linux', cpu: 'arm64' }, + { buildDir: 'amd64-macos', suffix: 'darwin-x64', os: 'darwin', cpu: 'x64' }, + { buildDir: 'arm64-macos', suffix: 'darwin-arm64', os: 'darwin', cpu: 'arm64' }, +]; + +const pkg = JSON.parse(readFileSync('package.json', 'utf8')); +const binaryName = Object.keys(pkg.bin ?? {})[0]; +if (!binaryName) { + console.error(`prepare_arch_packages: ${pkg.name} declares no bin, so it ships no binary`); + process.exit(1); +} +// The scoped name's last segment, which is what the per-platform directories are named after. +const stem = pkg.name.split('/').pop(); + +const overrides = new Map(); +for (const arg of process.argv.slice(2)) { + const eq = arg.indexOf('='); + if (eq < 0) { + console.error('Usage: prepare_arch_packages [= ...]'); + console.error('Platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64'); + process.exit(1); + } + overrides.set(arg.slice(0, eq), arg.slice(eq + 1)); +} + +for (const { buildDir, suffix, os, cpu } of PLATFORMS) { + const name = `${pkg.name}-${suffix}`; + const outDir = join('packages', `${stem}-${suffix}`); + const binaryPath = + overrides.get(suffix) ?? overrides.get(buildDir) ?? join('build', buildDir, binaryName); + + if (!existsSync(binaryPath)) { + console.log(`Skipping ${name}: no binary at ${binaryPath}`); + continue; + } + + rmSync(outDir, { recursive: true, force: true }); + mkdirSync(outDir, { recursive: true }); + copyFileSync(binaryPath, join(outDir, binaryName)); + chmodSync(join(outDir, binaryName), 0o755); + writeFileSync( + join(outDir, 'package.json'), + JSON.stringify( + { + name, + version: pkg.version, + description: `Native binary for ${pkg.name} (${suffix})`, + license: 'MIT', + os: [os], + cpu: [cpu], + files: [binaryName], + preferUnplugged: true, + }, + null, + 2, + ) + '\n', + ); + console.log(`Staged ${name} from ${binaryPath}`); +} diff --git a/ipc-runtime/ts/src/backend_registry.ts b/ipc-runtime/ts/src/backend_registry.ts new file mode 100644 index 00000000000..38243aa77e7 --- /dev/null +++ b/ipc-runtime/ts/src/backend_registry.ts @@ -0,0 +1,54 @@ +import type { IpcClientAsync, IpcClientSync } from "./types.js"; + +export type { IpcClientAsync, IpcClientSync }; + +/** + * Backends a native package registers for a service, so an app that installs it needs no wiring. + * This is how a host with no backend of its own is served — React Native, where Hermes has no + * WebAssembly and no workers, and the service is reached through a linked library instead. + */ +export interface RegisteredBackends { + async?: () => Promise | IpcClientAsync; + sync?: () => Promise | IpcClientSync; +} + +// A well-known global rather than an import in either direction, so a native backend package and +// the generated client package need not depend on each other, and two copies of this module in +// one app still agree. +const REGISTRY_KEY = Symbol.for("@aztec-foundation/ipc-runtime/backends"); + +function registry(): Map { + const global = globalThis as unknown as Record< + symbol, + Map | undefined + >; + return (global[REGISTRY_KEY] ??= new Map()); +} + +/** Make `factories` the backends for `service`; a native backend package calls this when imported. */ +export function registerBackend( + service: string, + factories: RegisteredBackends, +): void { + registry().set(service, factories); +} + +/** + * The registered backend for a service, or a message naming what to install. `hint` is the + * package a caller should be told about. + */ +export function registeredBackend( + service: string, + kind: K, + hint: string, +): NonNullable { + const factory = registry().get(service)?.[kind]; + if (!factory) { + throw new Error( + `${hint}: no ${kind === "sync" ? "synchronous " : ""}backend registered for ${service}. ` + + "Install a native backend package for it (registering itself when imported), or pass one " + + "as options.backend.", + ); + } + return factory as NonNullable; +} diff --git a/ipc-runtime/ts/src/index.ts b/ipc-runtime/ts/src/index.ts index 3a095cfd47f..85712c580a9 100644 --- a/ipc-runtime/ts/src/index.ts +++ b/ipc-runtime/ts/src/index.ts @@ -33,3 +33,18 @@ export { loadIpcRuntimeNapi, type Platform, } from "./native_loader.js"; +export { + type ServiceBinary, + type ServiceProcessOptions, + findServiceBinary, + pickServiceBackend, + runServiceBinary, + serviceProcessEnv, + spawnServiceBackend, + spawnServiceBackendSync, +} from "./service.js"; +export { + type RegisteredBackends, + registerBackend, + registeredBackend, +} from "./backend_registry.js"; diff --git a/ipc-runtime/ts/src/service.ts b/ipc-runtime/ts/src/service.ts new file mode 100644 index 00000000000..071d2447b7c --- /dev/null +++ b/ipc-runtime/ts/src/service.ts @@ -0,0 +1,237 @@ +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { + SpawnedProcessBackend, + SpawnedProcessBackendSync, +} from "./spawned_backend.js"; +import { IpcSpawnError } from "./errors.js"; +import type { IpcClientAsync, IpcClientSync } from "./types.js"; + +/** + * How a generated client package reaches its service as a spawned process. Everything here is + * fixed at generation time from the schema and the flags: the package supplies it, this module + * does the work, so the same logic is not regenerated into every package. + */ +export interface ServiceBinary { + /** Binary name, used for the arch-package lookup, log labels and errors. */ + name: string; + /** Environment variable that overrides the binary's path. */ + envVar: string; + /** npm package holding the binary, per platform (`process.arch`-`process.platform`). */ + archPackages: Record; + /** Argv template; each '{path}' is replaced with the backend's ipc path. */ + ipcPathArgs: string[]; + /** Prefix for the per-instance ipc path. */ + instancePrefix: string; + /** Directory of the package that owns the arch packages, for the sibling fallback. */ + packageDir: string; +} + +/** Options a caller may give when the service runs as a spawned process. */ +export interface ServiceProcessOptions { + binaryPath?: string; + transport?: "uds" | "shm"; + /** Threads the service may use, exported to it as HARDWARE_CONCURRENCY and RAYON_NUM_THREADS. */ + threads?: number; + logger?: (msg: string) => void; + connectTimeoutMs?: number; + env?: NodeJS.ProcessEnv; + extraArgs?: string[]; + respawn?: boolean; + /** When true, an idle backend does not keep the process alive; see SpawnedProcessBackendOptions. */ + unref?: boolean; + clientId?: number; + napiPath?: string; +} + +function platformKey(): string { + return `${process.arch}-${process.platform}`; +} + +function archPackageDir(binary: ServiceBinary): string | null { + const packageName = binary.archPackages[platformKey()]; + if (!packageName) { + return null; + } + try { + const require = createRequire(import.meta.url); + return path.dirname(require.resolve(`${packageName}/package.json`)); + } catch { + // Not installed as a dependency: fall back to a copy prepared inside the owning package, + // which is how a repository checkout runs before anything is published. + const sibling = path.join( + binary.packageDir, + "packages", + packageName.split("/").pop()!, + ); + return fs.existsSync(path.join(sibling, "package.json")) ? sibling : null; + } +} + +/** + * The binary to run: `customPath` if given, else the package's environment variable, else the + * installed arch package for this platform. Null when none of those yields an existing file. + */ +export function findServiceBinary( + binary: ServiceBinary, + customPath?: string, +): string | null { + const explicit = customPath ?? process.env[binary.envVar]; + if (explicit) { + return fs.existsSync(explicit) ? path.resolve(explicit) : null; + } + const dir = archPackageDir(binary); + if (dir) { + const candidate = path.join(dir, binary.name); + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +/** The child's environment: the caller's, plus the thread count under both names services read. */ +export function serviceProcessEnv( + options: Pick, +): NodeJS.ProcessEnv | undefined { + if (options.threads === undefined) { + return options.env; + } + const threads = String(options.threads); + return { + HARDWARE_CONCURRENCY: threads, + RAYON_NUM_THREADS: threads, + ...options.env, + }; +} + +function resolveOrThrow( + binary: ServiceBinary, + binaryPath: string | undefined, +): string { + const resolved = findServiceBinary(binary, binaryPath); + if (!resolved) { + throw new IpcSpawnError( + `${binary.name} binary not found`, + /*retry=*/ false, + ); + } + return resolved; +} + +/** + * Spawn the service and connect to it. Process lifecycle — connectivity, death detection, + * optional respawn, teardown — is owned by the backend and never leaks onto the caller's API. + */ +export function spawnServiceBackend( + binary: ServiceBinary, + defaultTransport: "uds" | "shm", + options: ServiceProcessOptions = {}, +): Promise { + return SpawnedProcessBackend.spawn({ + binaryPath: resolveOrThrow(binary, options.binaryPath), + binaryName: binary.name, + instancePrefix: binary.instancePrefix, + ipcPathArgs: binary.ipcPathArgs, + transport: options.transport ?? defaultTransport, + logger: options.logger, + connectTimeoutMs: options.connectTimeoutMs, + env: serviceProcessEnv(options), + extraArgs: options.extraArgs, + respawn: options.respawn, + unref: options.unref, + clientId: options.clientId, + napiPath: options.napiPath, + }); +} + +/** The synchronous form: shared memory is the one transport with a synchronous client. */ +export function spawnServiceBackendSync( + binary: ServiceBinary, + options: ServiceProcessOptions = {}, +): Promise { + if (options.transport !== undefined && options.transport !== "shm") { + throw new Error( + `${binary.name}: the synchronous backend needs the shm transport`, + ); + } + return SpawnedProcessBackendSync.spawn({ + binaryPath: resolveOrThrow(binary, options.binaryPath), + binaryName: binary.name, + instancePrefix: `${binary.instancePrefix}-sync`, + ipcPathArgs: binary.ipcPathArgs, + transport: "shm", + logger: options.logger, + connectTimeoutMs: options.connectTimeoutMs, + env: serviceProcessEnv(options), + extraArgs: options.extraArgs, + unref: options.unref, + clientId: options.clientId ?? 0, + napiPath: options.napiPath, + }); +} + +/** Run the service binary as a CLI, forwarding argv and exit status. Backs a package's `bin`. */ +export function runServiceBinary( + binary: ServiceBinary, + packageName: string, + argv: string[], +): never { + const binaryPath = findServiceBinary(binary); + if (!binaryPath) { + console.error( + `${binary.name}: native binary not found. Install the matching ` + + `'${packageName}-' package, set ${binary.envVar}, or pass its path.`, + ); + process.exit(1); + } + const result = spawnSync(binaryPath, argv, { stdio: "inherit" }); + if (result.error) { + console.error(result.error.message); + process.exit(1); + } + process.exit(result.status ?? 1); +} + +/** + * Which backend to use, given what this host can offer. Unset: the process when its binary + * resolves, else wasm. A name forces one, with no fallback to the other. + */ +export async function pickServiceBackend< + T extends IpcClientAsync | IpcClientSync, +>( + wanted: "process" | "wasm" | T | undefined, + choices: { + label: string; + process?: { available: () => boolean; create: () => Promise }; + wasm?: { create: () => Promise }; + logger?: (msg: string) => void; + }, +): Promise { + if (typeof wanted === "object") { + return wanted; + } + const { process: proc, wasm } = choices; + if ( + proc && + (wanted === "process" || + (wanted === undefined && (!wasm || proc.available()))) + ) { + try { + return await proc.create(); + } catch (err) { + if (wanted === "process" || !wasm) { + throw err; + } + choices.logger?.( + `${choices.label} process unavailable (${(err as Error).message}); falling back to wasm`, + ); + } + } + if (wasm && (wanted === undefined || wanted === "wasm")) { + return wasm.create(); + } + throw new Error(`${choices.label}: no such backend here: ${String(wanted)}`); +} diff --git a/ipc-runtime/ts/src/spawned_backend.ts b/ipc-runtime/ts/src/spawned_backend.ts index 45d9543a181..c96f6eaf65c 100644 --- a/ipc-runtime/ts/src/spawned_backend.ts +++ b/ipc-runtime/ts/src/spawned_backend.ts @@ -42,19 +42,15 @@ export interface SpawnedProcessBackendOptions { */ 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. + * When true, an idle backend does not keep the caller's process alive: you never have to + * destroy() it for node to exit. Work in progress still holds the loop — the connect while + * the server starts, and each call until its response arrives — so nothing exits early. + * + * The one thing given up is trailing output: the child's stdout/stderr pipes, which exist + * only when `logger` is set, stop holding the loop too, so a process that exits while the + * child is mid-line loses it. */ 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. */ @@ -112,6 +108,20 @@ async function removeStaleIpcPath( * live logger when there is one and to `logFd` otherwise. Shared by the async * and sync backends so process setup has exactly one implementation. */ +/** + * Stop the child and its output pipes holding the caller's event loop. Called once the backend is + * connected, never before: until then the child is the only thing keeping the process alive while + * the server starts, and unref'ing it lets node exit mid-startup. After it, each transport refs + * itself for the duration of a call (see UdsIpcClient's idle unref and the shm client's TSFN + * acquire/release), so only a genuinely idle backend is invisible to the loop. + */ +function unrefWhenIdle(child: ChildProcess): void { + child.unref(); + // 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?.(); +} + function spawnServerProcess( options: SpawnedProcessBackendOptions, ipcPath: string, @@ -144,14 +154,6 @@ function spawnServerProcess( ), ); } - 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; } @@ -467,6 +469,9 @@ export class SpawnedProcessBackend implements IpcClientAsync { await this.cleanupIpcPath(); throw this.asSpawnError(err); } + if (options.unref) { + unrefWhenIdle(child); + } return incarnation as Incarnation; } @@ -621,6 +626,9 @@ export class SpawnedProcessBackendSync implements IpcClientSync { connectShmSyncClient(options, ipcPath, child.pid), childReadyFailure, ]); + if (options.unref) { + unrefWhenIdle(child); + } return new SpawnedProcessBackendSync( options, child, diff --git a/ipc-runtime/ts/src/wasm/backend.ts b/ipc-runtime/ts/src/wasm/backend.ts new file mode 100644 index 00000000000..9c7031cf6c6 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/backend.ts @@ -0,0 +1,452 @@ +import type { IpcClientAsync, IpcClientSync } from "../types.js"; +import { WasmInstanceHost } from "./host.js"; +import { type WasmModuleSource, compileWasmModule } from "./module_source.js"; +import type { WasmPlatform, WorkerHandle } from "./platform.js"; + +/** Options shared by every way of running an FFI-contract module. */ +export interface WasmFfiOptions { + /** The module: a compiled `Module`, bytes (gzipped or not), a `Response`, or a URL/string. */ + module: WasmModuleSource; + /** Threads to run with (1 = no worker threads). Default: the platform's parallelism, capped at 32. */ + threads?: number; + /** Linear memory bounds in 64 KiB pages; `shared` defaults to `threads > 1`. */ + memory?: { initial?: number; maximum?: number; shared?: boolean }; + /** WASI environ for the module. `HARDWARE_CONCURRENCY` and `RAYON_NUM_THREADS` default to `threads`. */ + env?: Record; + logger?: (msg: string) => void; + /** FFI entry export; default: the module's one `_ipc_ffi_entry` export. */ + entry?: string; +} + +/** + * What a platform entry (`index.node.ts` / `index.browser.ts`) binds for the backend. Workers are + * created through factories rather than URLs: bundlers only bundle a worker's dependency graph when + * they see the literal `new Worker(new URL('./x.js', import.meta.url), { type: 'module' })`, so + * that expression has to live in the code that owns the worker script. + */ +export interface WasmFfiBinding { + platform: WasmPlatform; + /** Spawn a worker hosting one module instance per wasi thread. */ + createThreadWorker: () => WorkerHandle; + /** Spawn the worker hosting the main instance when `worker: true`. */ + createMainWorker: () => WorkerHandle; +} + +export interface WasmFfiBackendOptions extends WasmFfiOptions { + /** + * Run the main instance in a dedicated worker (default true), so a long call never blocks the + * caller's event loop and, in browsers, the thread pool is spawned from a worker. With `false` + * the main instance runs on the calling thread and every call blocks it until it returns. + */ + worker?: boolean; + /** Override the worker factories the platform entry binds. */ + createThreadWorker?: () => WorkerHandle; + createMainWorker?: () => WorkerHandle; +} + +/** Above this, more wasi threads stop paying for themselves; the engine clamps to it. */ +export const MAX_THREADS = 32; +const DEFAULT_INITIAL_PAGES = 64; +const DEFAULT_MAXIMUM_PAGES = 65536; +const FIRST_THREAD_ID = 2; + +type Pending = { + resolve: (out: Uint8Array) => void; + reject: (err: Error) => void; +}; + +function awaitReady(worker: WorkerHandle, what: string): Promise { + return new Promise((resolve, reject) => { + worker.onMessage((msg) => { + if (msg?.type === "ready") { + resolve(); + } else if (msg?.type === "init-error") { + reject(new Error(`${what}: ${msg.message}`)); + } + }); + worker.onError((err) => + reject(err instanceof Error ? err : new Error(`${what}: ${String(err)}`)), + ); + }); +} + +/** + * One worker per wasi thread, created when the module spawns it and dropped when it exits. + * + * A worker runs a thread to completion inside `wasi_thread_start`, so it can serve exactly one + * thread at a time; giving each its own worker is both the simplest arrangement and the only + * correct one. The module decides how many threads it wants — typically a pool of its own, sized + * from the thread count it was told — and this follows. + */ +class Threads { + private readonly workers = new Map(); + private nextTid = FIRST_THREAD_ID; + private unrefed = false; + + constructor( + private readonly createWorker: () => WorkerHandle, + private readonly init: Record, + private readonly logger: (msg: string) => void, + ) {} + + /** + * wasi-threads `thread-spawn`: start the module's thread entry on a new worker and return its + * tid. The import is synchronous, so the worker boots while the caller carries on; its `init` + * and `start` messages are queued in order and the worker serializes them. + */ + spawn(startArg: number): number { + const tid = this.nextTid++; + let worker: WorkerHandle; + try { + worker = this.createWorker(); + } catch (e) { + this.logger(`wasm thread ${tid} could not be created: ${String(e)}`); + return -1; + } + this.workers.set(tid, worker); + worker.onMessage((msg) => { + if (msg?.type === "log") { + this.logger(msg.message); + } else if (msg?.type === "thread-exit" || msg?.type === "init-error") { + if (msg.type === "init-error") { + this.logger(`wasm thread ${tid} failed to start: ${msg.message}`); + } + this.workers.delete(tid); + void worker.terminate(); + } + }); + worker.onError((err) => { + this.logger(`wasm thread ${tid}: ${String(err)}`); + this.workers.delete(tid); + }); + worker.postMessage({ type: "init", ...this.init }); + worker.postMessage({ type: "start", tid, startArg }); + if (this.unrefed) { + worker.unref(); + } + return tid; + } + + unref(): void { + this.unrefed = true; + for (const w of this.workers.values()) { + w.unref(); + } + } + + async destroy(): Promise { + const workers = [...this.workers.values()]; + this.workers.clear(); + await Promise.all(workers.map((w) => w.terminate())); + } +} + +/** + * The module running on the current thread: memory, main instance, and the workers serving its + * `thread-spawn` requests. `call` is synchronous and blocks until the module returns. + */ +export class WasmFfiEngine { + private constructor( + /** The module instance, for reaching exports of its own beyond the FFI entry. */ + readonly host: WasmInstanceHost, + private readonly threadWorkers: Threads | undefined, + readonly memory: WebAssembly.Memory, + readonly threads: number, + ) {} + + static async create( + opts: WasmFfiOptions, + binding: WasmFfiBinding, + ): Promise { + const { platform } = binding; + const logger = opts.logger ?? (() => {}); + const module = await compileWasmModule(opts.module, platform); + const wantThreads = Math.max( + 1, + Math.min(opts.threads ?? platform.hardwareConcurrency(), MAX_THREADS), + ); + const wantShared = + opts.memory?.shared ?? + (wantThreads > 1 && platform.sharedMemoryAvailable()); + const memory = createMemory( + module, + opts.memory?.initial ?? DEFAULT_INITIAL_PAGES, + opts.memory?.maximum ?? + platform.maximumMemoryPages?.() ?? + DEFAULT_MAXIMUM_PAGES, + wantShared, + ); + // The module's declaration wins over the request: a threads build gets a shared memory even + // for one thread, and workers need a shared memory, so the thread count follows the memory. + // A module that defines its own memory instead of importing one cannot be threaded at all — + // every instance would get a memory of its own, with nothing shared between them. + const importsMemory = WebAssembly.Module.imports(module).some( + (i) => i.kind === "memory", + ); + const shared = memory.buffer instanceof SharedArrayBuffer; + const threads = + importsMemory && shared && platform.sharedMemoryAvailable() + ? wantThreads + : 1; + const env = { + HARDWARE_CONCURRENCY: String(threads), + RAYON_NUM_THREADS: String(threads), + ...(opts.env ?? {}), + }; + // Threads are created when the module asks for them, so nothing is spawned here — and + // nothing at all if the module never spawns a thread. + const threadWorkers = + threads > 1 + ? new Threads( + binding.createThreadWorker, + { + module, + memory, + env, + entry: opts.entry, + }, + logger, + ) + : undefined; + + const host = await WasmInstanceHost.instantiate({ + module, + memory, + env, + logger, + entry: opts.entry, + threads, + spawnThread: (startArg) => threadWorkers?.spawn(startArg) ?? -1, + }); + // host.memory is the one the module actually uses, which is its own when it exports one. + logger( + `wasm: ${threads} thread(s), memory ${host.memory.buffer.byteLength >> 16} pages initial, ` + + `shared=${host.memory.buffer instanceof SharedArrayBuffer}, ` + + `${importsMemory ? "imported" : "owned by the module"}`, + ); + return new WasmFfiEngine(host, threadWorkers, host.memory, threads); + } + + call(input: Uint8Array): Uint8Array { + return this.host.call(input); + } + + unref(): void { + this.threadWorkers?.unref(); + } + + async destroy(): Promise { + await this.threadWorkers?.destroy(); + } +} + +/** + * The module's declared maximum is not visible from JS: instantiating with a larger `maximum` + * fails with a LinkError naming it, so probe downward until the import links. + */ +function createMemory( + module: WebAssembly.Module, + initial: number, + maximum: number, + shared: boolean, +): WebAssembly.Memory { + const imported = WebAssembly.Module.imports(module).find( + (i) => i.kind === "memory", + ); + let max = Math.max(initial, maximum); + let flippedShared = false; + for (;;) { + let memory: WebAssembly.Memory; + try { + memory = new WebAssembly.Memory({ initial, maximum: max, shared }); + } catch (e) { + // The engine could not reserve that much address space (e.g. mobile browsers). + if (max > initial) { + max = Math.max(initial, Math.floor(max / 2)); + continue; + } + throw e; + } + if (!imported) { + return memory; + } + try { + // A dry instantiation only to validate the memory import. V8 checks that every import + // module exists before it validates any single import, so stub every function import; + // the only errors left are then about the memory itself. + const stubs: Record> = {}; + for (const imp of WebAssembly.Module.imports(module)) { + stubs[imp.module] ??= {}; + if (imp.kind === "memory") { + stubs[imp.module][imp.name] = memory; + } else if (imp.kind === "function") { + stubs[imp.module][imp.name] = () => 0; + } + } + new WebAssembly.Instance(module, stubs); + return memory; + } catch (e) { + const message = e instanceof WebAssembly.LinkError ? String(e) : ""; + if (message.includes("maximum") && max > initial) { + max = Math.max(initial, Math.floor(max / 2)); + continue; + } + // A threads build declares its memory shared even when it will run with one thread (and a + // single-thread build declares it unshared): follow the module's declaration. + if (message.includes("shared") && !flippedShared) { + flippedShared = true; + shared = !shared; + continue; + } + return memory; + } + } +} + +/** + * Asynchronous FFI backend over a wasm module. Implements the runtime's `IpcClientAsync`, so the + * generated `AsyncApi` sits on it exactly as it sits on a spawned process. + */ +export class WasmFfiBackend implements IpcClientAsync { + private seq = 0; + private readonly pending = new Map(); + + private constructor( + private readonly engine: WasmFfiEngine | undefined, + private readonly worker: WorkerHandle | undefined, + ) {} + + static async create( + opts: WasmFfiBackendOptions, + binding: WasmFfiBinding, + ): Promise { + const bound: WasmFfiBinding = { + platform: binding.platform, + createThreadWorker: opts.createThreadWorker ?? binding.createThreadWorker, + createMainWorker: opts.createMainWorker ?? binding.createMainWorker, + }; + if (opts.worker === false) { + return new WasmFfiBackend( + await WasmFfiEngine.create(opts, bound), + undefined, + ); + } + // Compile here so the (cached, streaming) compilation happens once and the compiled Module is + // what crosses to the worker, rather than each side compiling the same bytes. + const module = await compileWasmModule(opts.module, bound.platform); + const worker = bound.createMainWorker(); + const backend = new WasmFfiBackend(undefined, worker); + const ready = awaitReady(worker, "wasm main worker"); + worker.onMessage((msg) => backend.onWorkerMessage(msg, opts.logger)); + worker.postMessage({ + type: "init", + module, + options: { + threads: opts.threads, + memory: opts.memory, + env: opts.env, + entry: opts.entry, + }, + }); + await ready; + return backend; + } + + private onWorkerMessage(msg: any, logger?: (msg: string) => void): void { + if (msg?.type === "log") { + logger?.(msg.message); + return; + } + if (msg?.type !== "result" && msg?.type !== "error") { + return; + } + const pending = this.pending.get(msg.id); + if (!pending) { + return; + } + this.pending.delete(msg.id); + if (msg.type === "result") { + pending.resolve(new Uint8Array(msg.output)); + } else { + pending.reject(new Error(msg.message)); + } + } + + call(input: Uint8Array): Promise { + if (this.engine) { + try { + return Promise.resolve(this.engine.call(input)); + } catch (e) { + return Promise.reject(e); + } + } + const id = ++this.seq; + // A private copy so the request can be transferred rather than cloned. Not `slice()`: on a + // node Buffer that aliases the caller's (pooled) memory, which the transfer would detach. + const copy = new Uint8Array(input.byteLength); + copy.set(input); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.worker!.postMessage({ type: "call", id, input: copy }, [ + copy.buffer, + ]); + }); + } + + /** Let the host process exit even while the module's workers are alive (node). */ + unref(): void { + this.engine?.unref(); + this.worker?.unref(); + } + + async destroy(): Promise { + if (this.engine) { + await this.engine.destroy(); + return; + } + const worker = this.worker!; + const done = new Promise((resolve) => { + worker.onMessage((msg) => { + if (msg?.type === "destroyed") { + resolve(); + } + }); + }); + worker.postMessage({ type: "destroy" }); + await Promise.race([done, new Promise((r) => setTimeout(r, 2000))]); + await worker.terminate(); + for (const p of this.pending.values()) { + p.reject(new Error("wasm backend destroyed")); + } + this.pending.clear(); + } +} + +/** + * Synchronous FFI backend: the module runs on the calling thread and every call blocks until it + * returns. Defaults to one thread, since a blocked caller cannot service anything else. + */ +export class WasmFfiBackendSync implements IpcClientSync { + private constructor(private readonly engine: WasmFfiEngine) {} + + static async create( + opts: WasmFfiOptions, + binding: WasmFfiBinding, + ): Promise { + return new WasmFfiBackendSync( + await WasmFfiEngine.create({ threads: 1, ...opts }, binding), + ); + } + + call(input: Uint8Array): Uint8Array { + return this.engine.call(input); + } + + unref(): void { + this.engine.unref(); + } + + destroy(): void { + void this.engine.destroy(); + } +} diff --git a/ipc-runtime/ts/src/wasm/browser/index.ts b/ipc-runtime/ts/src/wasm/browser/index.ts new file mode 100644 index 00000000000..d05518d22e1 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/browser/index.ts @@ -0,0 +1,40 @@ +// Browser entry of the wasm FFI backend (`@aztec-foundation/ipc-runtime/wasm`, `browser` condition). +import { type WasmFfiBinding, bindEntry } from "../entry.js"; +import { + browserPlatform, + browserWorkerHandle, + browserWorkerSide, +} from "./platform.js"; + +export * from "../entry.js"; +export { + browserPlatform as platform, + browserWorkerHandle as workerHandle, + browserWorkerSide as workerSide, +}; + +/** + * The worker scripts spawned for the main instance and each wasi thread, with the literal + * expression bundlers detect so they are emitted as worker chunks of the consuming application. + */ +export const binding: WasmFfiBinding = { + platform: browserPlatform, + createThreadWorker: () => + browserWorkerHandle( + new Worker(new URL("./thread.worker.js", import.meta.url), { + type: "module", + }), + ), + createMainWorker: () => + browserWorkerHandle( + new Worker(new URL("./main.worker.js", import.meta.url), { + type: "module", + }), + ), +}; + +export const { + createWasmFfiBackend, + createWasmFfiBackendSync, + sharedMemoryAvailable, +} = bindEntry(binding); diff --git a/ipc-runtime/ts/src/wasm/browser/main.worker.ts b/ipc-runtime/ts/src/wasm/browser/main.worker.ts new file mode 100644 index 00000000000..7819315d7fc --- /dev/null +++ b/ipc-runtime/ts/src/wasm/browser/main.worker.ts @@ -0,0 +1,16 @@ +// Default main-instance worker for browsers: hosts the module and its thread pool off the page's thread. +import { runMainWorker } from "../main_worker.js"; +import { + browserPlatform, + browserWorkerHandle, + browserWorkerSide, +} from "./platform.js"; + +runMainWorker(browserWorkerSide(), browserPlatform, { + createThreadWorker: () => + browserWorkerHandle( + new Worker(new URL("./thread.worker.js", import.meta.url), { + type: "module", + }), + ), +}); diff --git a/ipc-runtime/ts/src/wasm/browser/platform.ts b/ipc-runtime/ts/src/wasm/browser/platform.ts new file mode 100644 index 00000000000..c79f04eb034 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/browser/platform.ts @@ -0,0 +1,49 @@ +import type { WasmPlatform, WorkerHandle, WorkerSide } from "../platform.js"; + +/** The parent's handle on a browser `Worker`. */ +export function browserWorkerHandle(worker: Worker): WorkerHandle { + return { + postMessage: (message, transfer) => + worker.postMessage(message, transfer ?? []), + onMessage: (handler) => + worker.addEventListener("message", (e) => handler(e.data)), + onError: (handler) => worker.addEventListener("error", (e) => handler(e)), + terminate: async () => worker.terminate(), + unref: () => {}, + }; +} + +export const browserPlatform: WasmPlatform = { + // For scripts served as-is. Bundled code must spawn its workers with the literal + // `new Worker(new URL('./x.js', import.meta.url), { type: 'module' })` and wrap them with + // `browserWorkerHandle` (see `WasmFfiBinding`). + createWorker: (url: URL) => + browserWorkerHandle(new Worker(url, { type: "module" })), + hardwareConcurrency: () => navigator.hardwareConcurrency || 1, + sharedMemoryAvailable: () => + typeof SharedArrayBuffer !== "undefined" && + (globalThis as { crossOriginIsolated?: boolean }).crossOriginIsolated === + true, + // iOS browsers kill a page well before it reaches the 4 GiB a wasm memory may declare; asking + // for a 1 GiB maximum keeps the reservation within what they grant. + maximumMemoryPages: () => + typeof navigator !== "undefined" && /iPad|iPhone/.test(navigator.userAgent) + ? 2 ** 14 + : 2 ** 16, +}; + +/** The worker's side of a browser `Worker` channel. */ +export function browserWorkerSide(): WorkerSide { + const scope = globalThis as unknown as { + addEventListener(type: "message", handler: (e: MessageEvent) => void): void; + postMessage(message: unknown, transfer?: Transferable[]): void; + close(): void; + }; + return { + onMessage: (handler) => + scope.addEventListener("message", (e) => handler(e.data)), + postMessage: (message, transfer) => + scope.postMessage(message, transfer ?? []), + close: () => scope.close(), + }; +} diff --git a/ipc-runtime/ts/src/wasm/browser/thread.worker.ts b/ipc-runtime/ts/src/wasm/browser/thread.worker.ts new file mode 100644 index 00000000000..ebd4133bf92 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/browser/thread.worker.ts @@ -0,0 +1,5 @@ +// Default wasi-threads worker for browsers: one module instance per thread, no module-specific imports. +import { browserWorkerSide } from "./platform.js"; +import { runThreadWorker } from "../thread_worker.js"; + +runThreadWorker(browserWorkerSide()); diff --git a/ipc-runtime/ts/src/wasm/entry.ts b/ipc-runtime/ts/src/wasm/entry.ts new file mode 100644 index 00000000000..53758ab4e72 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/entry.ts @@ -0,0 +1,46 @@ +// What both platform entries (`index.node.ts`, `index.browser.ts`) share. They differ only in the +// platform they bind and in how they spawn a worker, so everything else lives here. +import { + type WasmFfiBackendOptions, + type WasmFfiBinding, + type WasmFfiOptions, + WasmFfiBackend, + WasmFfiBackendSync, +} from "./backend.js"; + +export type { WasmModuleSource } from "./module_source.js"; +export type { WasmPlatform, WorkerHandle, WorkerSide } from "./platform.js"; +export type { WasmFfiBackendOptions, WasmFfiBinding, WasmFfiOptions }; +export { + MAX_THREADS, + WasmFfiBackend, + WasmFfiBackendSync, + WasmFfiEngine, +} from "./backend.js"; +export { compileWasmModule } from "./module_source.js"; +export { WasmExitError } from "./wasi_shim.js"; +export { runMainWorker } from "./main_worker.js"; +export { runThreadWorker } from "./thread_worker.js"; +export { chooseWasmModule, resolveWasmThreads } from "./service.js"; +export { + type RegisteredBackends, + registerBackend, + registeredBackend, +} from "../backend_registry.js"; + +export interface WasmEntry { + createWasmFfiBackend(opts: WasmFfiBackendOptions): Promise; + createWasmFfiBackendSync(opts: WasmFfiOptions): Promise; + /** Whether this platform can run a module's threads build (a shared memory is available). */ + sharedMemoryAvailable(): boolean; +} + +/** The entry points a platform offers, over the workers and platform it binds. */ +export function bindEntry(binding: WasmFfiBinding): WasmEntry { + return { + createWasmFfiBackend: (opts) => WasmFfiBackend.create(opts, binding), + createWasmFfiBackendSync: (opts) => + WasmFfiBackendSync.create(opts, binding), + sharedMemoryAvailable: () => binding.platform.sharedMemoryAvailable(), + }; +} diff --git a/ipc-runtime/ts/src/wasm/host.ts b/ipc-runtime/ts/src/wasm/host.ts new file mode 100644 index 00000000000..fe0b4a5e79e --- /dev/null +++ b/ipc-runtime/ts/src/wasm/host.ts @@ -0,0 +1,277 @@ +import { WASI_NAMESPACE, createWasiImports } from "./wasi_shim.js"; + +export interface InstanceOptions { + module: WebAssembly.Module; + memory: WebAssembly.Memory; + /** WASI environ for the module (e.g. `HARDWARE_CONCURRENCY`). */ + env?: Record; + logger?: (msg: string) => void; + /** + * wasi-threads `thread-spawn`: start the module's thread entry on another instance and return + * its tid, or a negative errno when no thread can be started. + */ + spawnThread?: (startArg: number) => number; + /** + * Export taking `(input, input_len, output_out, output_len_out)`. Default: the module's one + * export named `_ipc_ffi_entry` (or bare `ipc_ffi_entry`). + */ + entry?: string; + /** Call the reactor's `_initialize` after instantiation (main instances only). Default true. */ + runInitialize?: boolean; + /** Threads this instance may use, for the WASI environ the module reads. Default 1. */ + threads?: number; +} + +/** Every FFI entry export ends with this; the generated ones are prefixed by their service. */ +export const ENTRY_SUFFIX = "ipc_ffi_entry"; + +/** Allocator pairs tried after the entry's own `_ipc_ffi_alloc`/`_free`. */ +export const FALLBACK_ALLOCATOR_EXPORTS: Array<[string, string]> = [ + ["malloc", "free"], + ["bbmalloc", "bbfree"], +]; + +type WasmFn = (...args: number[]) => number; + +/** The response pointer and length the entry writes back, ahead of the request in the scratch. */ +const SLOTS_BYTES = 8; +/** Starting size of the request scratch; it grows to fit and never shrinks. */ +const MIN_SCRATCH_BYTES = 64 * 1024; + +/** The entry export to use and the service prefix it carries (`bb_` for `bb_ipc_ffi_entry`). */ +function findEntry( + exports: Record, + wanted?: string, +): { entry: string; prefix: string } { + const prefixOf = (name: string) => + name.endsWith(ENTRY_SUFFIX) ? name.slice(0, -ENTRY_SUFFIX.length) : ""; + if (wanted) { + if (typeof exports[wanted] !== "function") { + throw new Error(`wasm module does not export ${wanted}`); + } + return { entry: wanted, prefix: prefixOf(wanted) }; + } + const candidates = Object.keys(exports).filter( + (name) => + typeof exports[name] === "function" && + (name === ENTRY_SUFFIX || name.endsWith(`_${ENTRY_SUFFIX}`)), + ); + if (candidates.length === 0) { + throw new Error(`wasm module exports no FFI entry (*_${ENTRY_SUFFIX})`); + } + if (candidates.length > 1) { + throw new Error( + `wasm module exports several FFI entries (${candidates.join(", ")}); pass \`entry\``, + ); + } + return { entry: candidates[0], prefix: prefixOf(candidates[0]) }; +} + +/** + * One instance of an FFI-contract module: imports resolved (WASI shim, wasi-threads, module + * specific host imports), `_initialize` run, and the `_ipc_ffi_entry` call protocol + * implemented over the module's own allocator. + */ +/** + * The tail of what the module wrote to stderr during the current call. A module compiled without + * exceptions cannot return a message when it gives up: it writes one out and exits. Keeping the + * text is what turns the bare `proc_exit` or trap the caller would otherwise see into an error + * that says what went wrong. + */ +class StderrTail { + private static readonly MAX_LINES = 8; + private lines: string[] = []; + + record(line: string): void { + this.lines.push(line); + if (this.lines.length > StderrTail.MAX_LINES) { + this.lines.shift(); + } + } + + reset(): void { + this.lines.length = 0; + } + + text(): string { + return this.lines.join("\n").trim(); + } +} + +export class WasmInstanceHost { + private constructor( + readonly instance: WebAssembly.Instance, + readonly memory: WebAssembly.Memory, + private readonly entry: WasmFn, + private readonly alloc: WasmFn, + private readonly free: WasmFn, + readonly logger: (msg: string) => void, + private readonly stderr: StderrTail, + ) {} + + static async instantiate(opts: InstanceOptions): Promise { + // A module either imports its memory, and is handed the one the caller made, or defines and + // exports its own — the default for a Rust cdylib. Which it is is only settled below, after + // instantiation, so everything reading memory goes through this. + let memory = opts.memory; + const logger = opts.logger ?? (() => {}); + const stderr = new StderrTail(); + const imports: Record> = { + env: { memory }, + [WASI_NAMESPACE]: createWasiImports(opts.module, () => memory, { + env: opts.env, + onStderr: (line) => { + stderr.record(line); + logger(line); + }, + onStdout: logger, + }), + wasi: { + "thread-spawn": (startArg: number) => + opts.spawnThread ? opts.spawnThread(startArg >>> 0) : -1, + }, + }; + + const missing: string[] = []; + for (const imp of WebAssembly.Module.imports(opts.module)) { + if (imports[imp.module]?.[imp.name] !== undefined) { + continue; + } + missing.push(`${imp.module}.${imp.name} (${imp.kind})`); + } + if (missing.length > 0) { + throw new Error( + `wasm module imports what this host does not provide; it must be a WASI reactor: ${missing.join(", ")}`, + ); + } + + const instance = await WebAssembly.instantiate(opts.module, imports); + const exports = instance.exports as Record; + if (exports.memory instanceof WebAssembly.Memory) { + // The module defined its own; the one passed in (if any) is not what it reads and writes. + memory = exports.memory; + } + if ( + opts.runInitialize !== false && + typeof exports._initialize === "function" + ) { + (exports._initialize as () => void)(); + } + const { entry, prefix } = findEntry(exports, opts.entry); + const allocatorExports: Array<[string, string]> = [ + [`${prefix}ipc_ffi_alloc`, `${prefix}ipc_ffi_free`], + ...FALLBACK_ALLOCATOR_EXPORTS, + ]; + const pair = allocatorExports.find( + ([a, f]) => + typeof exports[a] === "function" && typeof exports[f] === "function", + ); + if (!pair) { + throw new Error( + `wasm module exports no allocator pair (looked for ${allocatorExports + .map(([a, f]) => `${a}/${f}`) + .join(", ")})`, + ); + } + return new WasmInstanceHost( + instance, + memory, + exports[entry] as WasmFn, + exports[pair[0]] as WasmFn, + exports[pair[1]] as WasmFn, + logger, + stderr, + ); + } + + /** Call an arbitrary numeric export (used by thread workers for `wasi_thread_start`). */ + callExport(name: string, ...args: number[]): number { + const fn = ( + this.instance.exports as Record + )[name]; + if (typeof fn !== "function") { + throw new Error(`wasm module does not export ${name}`); + } + return (fn as WasmFn)(...args); + } + + /** Views over module memory, rebuilt only when a call grows it and detaches the old buffer. */ + private viewed?: ArrayBufferLike; + private bytes!: Uint8Array; + private words!: DataView; + + private refreshViews(): void { + if (this.viewed !== this.memory.buffer) { + this.viewed = this.memory.buffer; + this.bytes = new Uint8Array(this.memory.buffer); + this.words = new DataView(this.memory.buffer); + } + } + + /** + * A buffer held across calls for the request and the two response slots, so a round trip costs + * no allocator traffic of its own. It only ever grows, and calls are serialized (the entry is + * synchronous and single-threaded), so one buffer is enough. + */ + private scratch = 0; + private scratchCapacity = 0; + + private reserveScratch(size: number): number { + if (size <= this.scratchCapacity) { + return this.scratch; + } + const capacity = Math.max( + size, + this.scratchCapacity * 2, + MIN_SCRATCH_BYTES, + ); + const scratch = this.alloc(capacity) >>> 0; + if (scratch === 0) { + throw new Error( + `wasm module could not allocate a ${capacity} byte request buffer`, + ); + } + if (this.scratch !== 0) { + this.free(this.scratch); + } + this.scratch = scratch; + this.scratchCapacity = capacity; + return scratch; + } + + /** + * One FFI round trip: request bytes in, a copy of the response bytes out. The request is placed + * in module memory, and the response is read from where the module left it and released with the + * module's own free, as the FFI contract requires. + */ + call(input: Uint8Array): Uint8Array { + const slots = this.reserveScratch(SLOTS_BYTES + input.length); + const inPtr = slots + SLOTS_BYTES; + let outPtr = 0; + this.stderr.reset(); + try { + this.refreshViews(); + this.bytes.set(input, inPtr); + this.words.setUint32(slots, 0, true); + this.words.setUint32(slots + 4, 0, true); + try { + this.entry(inPtr, input.length, slots, slots + 4); + } catch (cause) { + const reported = this.stderr.text(); + if (!reported) { + throw cause; + } + throw new Error(reported, { cause }); + } + // The call may have grown memory, detaching the buffer the views were built over. + this.refreshViews(); + outPtr = this.words.getUint32(slots, true); + const outLen = this.words.getUint32(slots + 4, true); + return this.bytes.slice(outPtr, outPtr + outLen); + } finally { + if (outPtr !== 0) { + this.free(outPtr); + } + } + } +} diff --git a/ipc-runtime/ts/src/wasm/main_worker.ts b/ipc-runtime/ts/src/wasm/main_worker.ts new file mode 100644 index 00000000000..470a4dcefa0 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/main_worker.ts @@ -0,0 +1,77 @@ +import { type WasmFfiBinding, WasmFfiEngine } from "./backend.js"; +import type { WasmPlatform, WorkerHandle, WorkerSide } from "./platform.js"; + +export interface MainWorkerOptions { + /** Spawns the thread pool's workers (see `WasmFfiBinding` for why this is a factory). */ + createThreadWorker: () => WorkerHandle; +} + +/** + * Body of the worker hosting a module's main instance: create the engine on `init`, serve `call` + * requests, tear down on `destroy`. Calls run to completion on this worker's thread; the parent's + * event loop stays free. + */ +export function runMainWorker( + side: WorkerSide, + platform: WasmPlatform, + opts: MainWorkerOptions, +): void { + let engine: WasmFfiEngine | undefined; + const log = (message: string) => side.postMessage({ type: "log", message }); + side.onMessage(async (msg) => { + switch (msg?.type) { + case "init": { + try { + const o = msg.options ?? {}; + const binding: WasmFfiBinding = { + platform, + createThreadWorker: opts.createThreadWorker, + // Unused here: this worker is the main worker. + createMainWorker: opts.createThreadWorker, + }; + engine = await WasmFfiEngine.create( + { + module: msg.module, + threads: o.threads, + memory: o.memory, + env: o.env, + entry: o.entry, + logger: log, + }, + binding, + ); + side.postMessage({ type: "ready" }); + } catch (e) { + side.postMessage({ + type: "init-error", + message: e instanceof Error ? (e.stack ?? e.message) : String(e), + }); + } + break; + } + case "call": { + try { + const output = engine!.call(new Uint8Array(msg.input)); + side.postMessage({ type: "result", id: msg.id, output }, [ + output.buffer as ArrayBuffer, + ]); + } catch (e) { + side.postMessage({ + type: "error", + id: msg.id, + message: e instanceof Error ? e.message : String(e), + }); + } + break; + } + case "destroy": { + await engine?.destroy(); + side.postMessage({ type: "destroyed" }); + side.close(); + break; + } + default: + break; + } + }); +} diff --git a/ipc-runtime/ts/src/wasm/module_source.ts b/ipc-runtime/ts/src/wasm/module_source.ts new file mode 100644 index 00000000000..447e65136de --- /dev/null +++ b/ipc-runtime/ts/src/wasm/module_source.ts @@ -0,0 +1,125 @@ +/** + * Compile a wasm module from wherever a consumer keeps it: an already compiled `Module`, raw or + * gzipped bytes, a fetch `Response`, or a URL (`http(s):`, `data:`, `blob:`, `file:`). + * + * URLs and responses go through `WebAssembly.compileStreaming`, so compilation overlaps the + * download and, in browsers that cache compiled wasm (Chrome, Firefox), a repeat visit starts + * from the optimized code of the previous one. Gzip is recognised from the response headers, the + * URL, or the magic bytes and inflated with the platform's `DecompressionStream`, so a `.wasm.gz` + * asset or a `data:application/gzip;base64,…` URL needs no JavaScript inflater. + */ +export type WasmModuleSource = + | WebAssembly.Module + | Uint8Array + | ArrayBuffer + | Response + | URL + | string; + +export interface ModuleLoadPlatform { + /** Read a `file:` URL (node). Browsers fetch everything. */ + readFile?(url: URL): Promise; + /** Turn a plain filesystem path into a URL (node). */ + resolvePath?(path: string): URL; +} + +const GZIP_MAGIC = [0x1f, 0x8b, 0x08]; +const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d]; + +function startsWith(bytes: Uint8Array, magic: number[]): boolean { + return magic.every((b, i) => bytes[i] === b); +} + +async function gunzip(bytes: Uint8Array): Promise { + const stream = new Blob([bytes as BlobPart]) + .stream() + .pipeThrough(new DecompressionStream("gzip")); + return new Uint8Array(await new Response(stream).arrayBuffer()); +} + +async function compileBytes(bytes: Uint8Array): Promise { + if (startsWith(bytes, GZIP_MAGIC)) { + bytes = await gunzip(bytes); + } + if (!startsWith(bytes, WASM_MAGIC)) { + throw new Error("not a wasm module: bad magic bytes"); + } + // Our bytes never live on a SharedArrayBuffer; the cast only satisfies BufferSource's typing. + return WebAssembly.compile(bytes as unknown as BufferSource); +} + +async function compileStreamingOrBuffer( + response: Response, +): Promise { + if (typeof WebAssembly.compileStreaming === "function") { + try { + return await WebAssembly.compileStreaming(response.clone()); + } catch { + // Some hosts reject streaming compilation of synthetic responses; buffer instead. + } + } + return compileBytes(new Uint8Array(await response.arrayBuffer())); +} + +async function compileResponse( + response: Response, + label: string, +): Promise { + if (!response.ok) { + throw new Error(`fetching wasm module ${label}: HTTP ${response.status}`); + } + const type = (response.headers.get("content-type") ?? "").toLowerCase(); + const gzipped = type.includes("gzip") || /\.gz(\?|#|$)/.test(label); + if (gzipped && response.body) { + const inflated = new Response( + response.body.pipeThrough(new DecompressionStream("gzip")), + { headers: { "content-type": "application/wasm" } }, + ); + return compileStreamingOrBuffer(inflated); + } + if (type.includes("application/wasm")) { + return compileStreamingOrBuffer(response); + } + // Unknown or missing type (octet-stream, data: URLs): sniff the bytes. + return compileBytes(new Uint8Array(await response.arrayBuffer())); +} + +export async function compileWasmModule( + source: WasmModuleSource, + platform: ModuleLoadPlatform = {}, +): Promise { + if (source instanceof WebAssembly.Module) { + return source; + } + if (source instanceof Uint8Array) { + return compileBytes(source); + } + if (source instanceof ArrayBuffer) { + return compileBytes(new Uint8Array(source)); + } + if (source instanceof Response) { + return compileResponse(source, source.url); + } + let url: URL; + if (source instanceof URL) { + url = source; + } else { + try { + url = new URL(source); + } catch { + if (!platform.resolvePath) { + throw new Error(`wasm module source is not a URL: ${source}`); + } + url = platform.resolvePath(source); + } + } + if (url.protocol === "file:") { + if (!platform.readFile) { + throw new Error( + `file: wasm module sources need a platform with readFile: ${url.href}`, + ); + } + return compileBytes(await platform.readFile(url)); + } + return compileResponse(await fetch(url), url.href); +} diff --git a/ipc-runtime/ts/src/wasm/node/index.ts b/ipc-runtime/ts/src/wasm/node/index.ts new file mode 100644 index 00000000000..04d1a09273e --- /dev/null +++ b/ipc-runtime/ts/src/wasm/node/index.ts @@ -0,0 +1,33 @@ +// Node entry of the wasm FFI backend (`@aztec-foundation/ipc-runtime/wasm`, `default` condition). +import { type WasmFfiBinding, bindEntry } from "../entry.js"; +import { + nodePlatform, + nodeWorkerHandle, + nodeWorkerSide, +} from "./platform.js"; + +export * from "../entry.js"; +export { + nodePlatform as platform, + nodeWorkerHandle as workerHandle, + nodeWorkerSide as workerSide, +}; + +/** The worker scripts the node entry spawns for the main instance and each wasi thread. */ +export const binding: WasmFfiBinding = { + platform: nodePlatform, + createThreadWorker: () => + nodePlatform.createWorker( + new URL("./thread.worker.js", import.meta.url), + ), + createMainWorker: () => + nodePlatform.createWorker( + new URL("./main.worker.js", import.meta.url), + ), +}; + +export const { + createWasmFfiBackend, + createWasmFfiBackendSync, + sharedMemoryAvailable, +} = bindEntry(binding); diff --git a/ipc-runtime/ts/src/wasm/node/main.worker.ts b/ipc-runtime/ts/src/wasm/node/main.worker.ts new file mode 100644 index 00000000000..e83f1814b1d --- /dev/null +++ b/ipc-runtime/ts/src/wasm/node/main.worker.ts @@ -0,0 +1,10 @@ +// Default main-instance worker for node: hosts the module and its thread pool off the caller's thread. +import { runMainWorker } from "../main_worker.js"; +import { nodePlatform, nodeWorkerSide } from "./platform.js"; + +runMainWorker(nodeWorkerSide(), nodePlatform, { + createThreadWorker: () => + nodePlatform.createWorker( + new URL("./thread.worker.node.js", import.meta.url), + ), +}); diff --git a/ipc-runtime/ts/src/wasm/node/platform.ts b/ipc-runtime/ts/src/wasm/node/platform.ts new file mode 100644 index 00000000000..8e2c7556c49 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/node/platform.ts @@ -0,0 +1,57 @@ +import { readFile } from "node:fs/promises"; +import { availableParallelism } from "node:os"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { Worker, type MessagePort, parentPort } from "node:worker_threads"; +import type { WasmPlatform, WorkerHandle, WorkerSide } from "../platform.js"; + +/** The parent's handle on a `worker_threads` worker. */ +export function nodeWorkerHandle(worker: Worker): WorkerHandle { + return { + postMessage: (message, transfer) => + worker.postMessage( + message, + transfer as unknown as readonly import("node:worker_threads").TransferListItem[], + ), + onMessage: (handler) => { + worker.on("message", handler); + }, + onError: (handler) => { + worker.on("error", handler); + }, + terminate: async () => { + await worker.terminate(); + }, + unref: () => worker.unref(), + }; +} + +export const nodePlatform: WasmPlatform = { + readFile: async (url) => new Uint8Array(await readFile(fileURLToPath(url))), + resolvePath: (path) => pathToFileURL(path), + createWorker: (url: URL) => nodeWorkerHandle(new Worker(url)), + hardwareConcurrency: () => + Number(process.env.HARDWARE_CONCURRENCY) || availableParallelism(), + sharedMemoryAvailable: () => true, +}; + +/** The worker's side of a node `worker_threads` channel. */ +export function nodeWorkerSide( + port: MessagePort | null = parentPort, +): WorkerSide { + if (!port) { + throw new Error( + "nodeWorkerSide: not running inside a worker (no parentPort)", + ); + } + return { + onMessage: (handler) => { + port.on("message", handler); + }, + postMessage: (message, transfer) => + port.postMessage( + message, + transfer as unknown as readonly import("node:worker_threads").TransferListItem[], + ), + close: () => port.close(), + }; +} diff --git a/ipc-runtime/ts/src/wasm/node/thread.worker.ts b/ipc-runtime/ts/src/wasm/node/thread.worker.ts new file mode 100644 index 00000000000..d1fa09a1691 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/node/thread.worker.ts @@ -0,0 +1,5 @@ +// Default wasi-threads worker for node: one module instance per thread, no module-specific imports. +import { nodeWorkerSide } from "./platform.js"; +import { runThreadWorker } from "../thread_worker.js"; + +runThreadWorker(nodeWorkerSide()); diff --git a/ipc-runtime/ts/src/wasm/platform.ts b/ipc-runtime/ts/src/wasm/platform.ts new file mode 100644 index 00000000000..590e3aa2f2d --- /dev/null +++ b/ipc-runtime/ts/src/wasm/platform.ts @@ -0,0 +1,28 @@ +import type { ModuleLoadPlatform } from "./module_source.js"; + +/** The parent's handle on a spawned worker (node `worker_threads` or a browser `Worker`). */ +export interface WorkerHandle { + postMessage(message: unknown, transfer?: ArrayBuffer[]): void; + onMessage(handler: (message: any) => void): void; + onError(handler: (error: unknown) => void): void; + terminate(): Promise; + /** Let the host process exit even while this worker is alive (node only; no-op elsewhere). */ + unref(): void; +} + +/** What the wasm backend needs from its environment; one implementation per platform. */ +export interface WasmPlatform extends ModuleLoadPlatform { + createWorker(url: URL): WorkerHandle; + hardwareConcurrency(): number; + /** Threads need a shared memory; browsers only allow one under COOP/COEP. */ + sharedMemoryAvailable(): boolean; + /** Upper bound on linear memory (64 KiB pages) to ask for when the caller sets none. */ + maximumMemoryPages?(): number; +} + +/** The worker's view of its parent (node `parentPort` or a browser worker global). */ +export interface WorkerSide { + onMessage(handler: (message: any) => void): void; + postMessage(message: unknown, transfer?: ArrayBuffer[]): void; + close(): void; +} diff --git a/ipc-runtime/ts/src/wasm/service.ts b/ipc-runtime/ts/src/wasm/service.ts new file mode 100644 index 00000000000..fe96c81e8c8 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/service.ts @@ -0,0 +1,46 @@ +import { MAX_THREADS } from "./backend.js"; +import type { WasmPlatform } from "./platform.js"; + +/** + * How many threads to run a module with: the platform's parallelism when the caller named none, + * else the number asked for, checked against what this host can actually give. More than one + * thread needs a shared memory, which browsers only allow on a cross-origin isolated page, and + * asking for it where it cannot be had is an error rather than a silent downgrade. + */ +export function resolveWasmThreads( + platform: WasmPlatform, + label: string, + threads?: number, +): number { + if (threads === undefined) { + return platform.sharedMemoryAvailable() + ? Math.min(platform.hardwareConcurrency(), MAX_THREADS) + : 1; + } + if (threads > 1 && !platform.sharedMemoryAvailable()) { + throw new Error( + `${label}: ${threads} threads requested but no shared memory is available here ` + + "(browsers need a cross-origin isolated page: COOP/COEP headers); pass threads: 1", + ); + } + return threads; +} + +/** + * Which of a package's own modules to run: the threads build for more than one thread, otherwise + * the single-thread build. Each stands in for the other when a package ships only one. + */ +export function chooseWasmModule( + modules: { single?: URL; threads?: URL }, + label: string, + threads: number, +): URL { + const module = + threads > 1 + ? (modules.threads ?? modules.single) + : (modules.single ?? modules.threads); + if (!module) { + throw new Error(`${label}: no wasm module ships with this package`); + } + return module; +} diff --git a/ipc-runtime/ts/src/wasm/thread_worker.ts b/ipc-runtime/ts/src/wasm/thread_worker.ts new file mode 100644 index 00000000000..2a5e6de7595 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/thread_worker.ts @@ -0,0 +1,56 @@ +import { WasmInstanceHost } from "./host.js"; +import type { WorkerSide } from "./platform.js"; + +/** + * Body of a wasi-threads worker: instantiate the module over the shared memory on `init`, then run + * the module's thread entry on `start`. A thread runs to completion inside `wasi_thread_start`, so + * this worker serves that one thread and is done; the parent creates one worker per thread the + * module spawns. + */ +export function runThreadWorker(side: WorkerSide): void { + // `init` and `start` arrive back to back, and instantiation is asynchronous, so `start` waits on + // this rather than on the message order. + let instantiated: Promise | undefined; + const log = (message: string) => side.postMessage({ type: "log", message }); + + side.onMessage(async (msg) => { + try { + switch (msg?.type) { + case "init": + instantiated = WasmInstanceHost.instantiate({ + module: msg.module, + memory: msg.memory, + env: msg.env, + entry: msg.entry, + threads: 1, + runInitialize: false, + // Only the main instance spawns threads; a request from a thread is refused. + spawnThread: () => -1, + logger: log, + }); + await instantiated; + side.postMessage({ type: "ready" }); + break; + case "start": { + if (!instantiated) { + throw new Error("start before init"); + } + const host = await instantiated; + host.callExport("wasi_thread_start", msg.tid, msg.startArg); + side.postMessage({ type: "thread-exit", tid: msg.tid }); + break; + } + default: + break; + } + } catch (e) { + const message = e instanceof Error ? (e.stack ?? e.message) : String(e); + if (msg?.type === "init") { + side.postMessage({ type: "init-error", message }); + } else { + log(`wasm thread worker: ${message}`); + side.postMessage({ type: "thread-exit", tid: msg?.tid }); + } + } + }); +} diff --git a/ipc-runtime/ts/src/wasm/wasi_shim.ts b/ipc-runtime/ts/src/wasm/wasi_shim.ts new file mode 100644 index 00000000000..2043ed4b8c7 --- /dev/null +++ b/ipc-runtime/ts/src/wasm/wasi_shim.ts @@ -0,0 +1,170 @@ +/** + * The slice of WASI preview1 an in-process service needs: clocks, randomness, environ/args, + * stdout/stderr, exit. Every other WASI function the module imports is answered with `ENOSYS`, + * so linking never fails on a syscall the module carries but never calls (file I/O in a module + * that gets its inputs over the FFI entry, for instance). + */ +export const WASI_NAMESPACE = "wasi_snapshot_preview1"; + +const ERRNO_SUCCESS = 0; +const ERRNO_BADF = 8; +const ERRNO_NOSYS = 52; + +/** Raised when the module calls `proc_exit`; the process must not actually exit. */ +export class WasmExitError extends Error { + constructor(public readonly code: number) { + super(`wasm module called proc_exit(${code})`); + this.name = "WasmExitError"; + } +} + +export interface WasiShimOptions { + /** Environment visible to the module through `environ_get` (e.g. `HARDWARE_CONCURRENCY`). */ + env?: Record; + /** Program arguments visible through `args_get`. */ + args?: string[]; + /** Receives complete lines written to fd 1 / fd 2. */ + onStdout?: (line: string) => void; + onStderr?: (line: string) => void; +} + +function encodeNulTerminated(values: string[]): Uint8Array[] { + const encoder = new TextEncoder(); + return values.map((v) => encoder.encode(`${v}\0`)); +} + +/** + * Build the WASI import object for `module`. `memory` is read lazily on every call because the + * module's memory may grow (and its `buffer` be detached) between calls. + */ +export function createWasiImports( + module: WebAssembly.Module, + memory: () => WebAssembly.Memory, + opts: WasiShimOptions = {}, +): Record number> { + const view = () => new DataView(memory().buffer); + const bytes = () => new Uint8Array(memory().buffer); + const decoder = new TextDecoder(); + const envEntries = encodeNulTerminated( + Object.entries(opts.env ?? {}).map(([k, v]) => `${k}=${v}`), + ); + const argEntries = encodeNulTerminated(opts.args ?? []); + const pending: Record = {}; + + const sizesGet = ( + entries: Uint8Array[], + countOut: number, + sizeOut: number, + ) => { + view().setUint32(countOut, entries.length, true); + view().setUint32( + sizeOut, + entries.reduce((n, e) => n + e.length, 0), + true, + ); + return ERRNO_SUCCESS; + }; + const stringsGet = ( + entries: Uint8Array[], + ptrsOut: number, + bufOut: number, + ) => { + let p = bufOut; + entries.forEach((e, i) => { + view().setUint32(ptrsOut + 4 * i, p, true); + bytes().set(e, p); + p += e.length; + }); + return ERRNO_SUCCESS; + }; + + const implemented: Record number> = { + args_sizes_get: (countOut, sizeOut) => + sizesGet(argEntries, countOut, sizeOut), + args_get: (ptrsOut, bufOut) => stringsGet(argEntries, ptrsOut, bufOut), + environ_sizes_get: (countOut, sizeOut) => + sizesGet(envEntries, countOut, sizeOut), + environ_get: (ptrsOut, bufOut) => stringsGet(envEntries, ptrsOut, bufOut), + clock_res_get: (_id, out) => { + view().setBigUint64(out, 1000n, true); + return ERRNO_SUCCESS; + }, + clock_time_get: (_id, _precision, out) => { + const ns = BigInt( + Math.round((performance.timeOrigin + performance.now()) * 1e6), + ); + view().setBigUint64(out, ns, true); + return ERRNO_SUCCESS; + }, + random_get: (ptr, len) => { + // getRandomValues refuses views over shared memory and caps a call at 64 KiB: fill a + // private buffer in chunks and copy it in. + const dst = bytes(); + for (let off = 0; off < len; off += 65536) { + const chunk = new Uint8Array(Math.min(65536, len - off)); + crypto.getRandomValues(chunk); + dst.set(chunk, ptr + off); + } + return ERRNO_SUCCESS; + }, + fd_write: (fd, iovs, iovsLen, nwrittenOut) => { + let total = 0; + let text = ""; + for (let i = 0; i < iovsLen; i++) { + const ptr = view().getUint32(iovs + i * 8, true); + const len = view().getUint32(iovs + i * 8 + 4, true); + // slice, not subarray: with threads the wasm memory is a SharedArrayBuffer, + // and TextDecoder refuses a view onto shared memory ("The provided + // ArrayBufferView value must not be shared"). slice copies out first. + text += decoder.decode(bytes().slice(ptr, ptr + len)); + total += len; + } + const sink = + fd === 1 ? opts.onStdout : fd === 2 ? opts.onStderr : undefined; + if (sink) { + const lines = ((pending[fd] ?? "") + text).split("\n"); + pending[fd] = lines.pop() ?? ""; + for (const line of lines) { + sink(line); + } + } + view().setUint32(nwrittenOut, total, true); + return ERRNO_SUCCESS; + }, + fd_fdstat_get: (fd, out) => { + if (fd > 2) { + return ERRNO_BADF; + } + bytes().fill(0, out, out + 24); + view().setUint8(out, 2); // filetype: character device + return ERRNO_SUCCESS; + }, + fd_close: () => ERRNO_BADF, + // wasi-libc walks descriptors from 3 looking for preopened directories and aborts on any + // error but this one, so a module with no preopens must be told BADF, not NOSYS. + fd_prestat_get: () => ERRNO_BADF, + fd_prestat_dir_name: () => ERRNO_BADF, + sched_yield: () => ERRNO_SUCCESS, + proc_exit: (code) => { + // Nothing more will be written, so a line still waiting for its terminator is final output. + // A module that gives up part way through a message would otherwise lose it entirely. + for (const [fd, text] of Object.entries(pending)) { + const sink = Number(fd) === 1 ? opts.onStdout : opts.onStderr; + if (text && sink) { + sink(text); + } + delete pending[Number(fd)]; + } + throw new WasmExitError(code); + }, + }; + + const imports: Record number> = {}; + for (const imp of WebAssembly.Module.imports(module)) { + if (imp.module !== WASI_NAMESPACE || imp.kind !== "function") { + continue; + } + imports[imp.name] = implemented[imp.name] ?? (() => ERRNO_NOSYS); + } + return imports; +} diff --git a/ipc-runtime/ts/src/wasm_engine.test.ts b/ipc-runtime/ts/src/wasm_engine.test.ts new file mode 100644 index 00000000000..fc9c39c9a04 --- /dev/null +++ b/ipc-runtime/ts/src/wasm_engine.test.ts @@ -0,0 +1,377 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { WasmFfiEngine } from "./wasm/backend.js"; +import type { WasmFfiBinding, WorkerHandle } from "./wasm/node/index.js"; +import { nodePlatform } from "./wasm/node/platform.js"; + +/** + * A module small enough to write out by hand, so the engine can be pinned without a real service. + * It imports a memory and wasi-threads, and implements the FFI contract with a bump allocator and + * an entry that echoes the request back as the response. + */ +function buildModule(opts: { + shared: boolean; + min: number; + max: number; + /** Define and export a memory instead of importing one, the way a Rust cdylib does. */ + ownMemory?: boolean; +}): WebAssembly.Module { + const leb = (n: number) => { + const out = []; + do { + const byte = n & 0x7f; + n >>>= 7; + out.push(n === 0 ? byte : byte | 0x80); + } while (n !== 0); + return out; + }; + const str = (s: string) => [s.length, ...[...s].map((c) => c.charCodeAt(0))]; + const section = (id: number, body: number[]) => [ + id, + ...leb(body.length), + ...body, + ]; + const I32 = 0x7f; + // Function indices: 0 is the imported thread-spawn, then the four defined below. + const [SPAWN, ENTRY, ALLOC, FREE] = [1, 2, 3, 4]; + + const types = section(1, [ + 3, + 0x60, + 1, + I32, + 1, + I32, // (i32) -> i32 alloc, spawn_thread + 0x60, + 4, + I32, + I32, + I32, + I32, + 0, // (i32 x4) -> () entry + 0x60, + 1, + I32, + 0, // (i32) -> () free + ]); + const limits = [opts.shared ? 0x03 : 0x01, ...leb(opts.min), ...leb(opts.max)]; + const imports = section(2, [ + ...(opts.ownMemory + ? [1] + : [2, ...str("env"), ...str("memory"), 0x02, ...limits]), + ...str("wasi"), + ...str("thread-spawn"), + 0x00, + 0, + ]); + const memories = opts.ownMemory ? section(5, [1, ...limits]) : []; + const functions = section(3, [4, 0, 1, 0, 2]); + // The bump allocator's next free offset, past the page the entry never touches. + const globals = section(6, [1, I32, 0x01, 0x41, ...leb(4096), 0x0b]); + const exports = section(7, [ + opts.ownMemory ? 5 : 4, + ...(opts.ownMemory ? [...str("memory"), 0x02, 0] : []), + ...str("spawn_thread"), + 0x00, + SPAWN, + ...str("ipc_ffi_entry"), + 0x00, + ENTRY, + ...str("ipc_ffi_alloc"), + 0x00, + ALLOC, + ...str("ipc_ffi_free"), + 0x00, + FREE, + ]); + const body = (locals: number[], code: number[]) => { + const bytes = [...locals, ...code, 0x0b]; + return [...leb(bytes.length), ...bytes]; + }; + const STORE = [0x36, 0x02, 0x00]; // i32.store, natural alignment, no offset + const code = section(10, [ + 4, + // spawn_thread(arg) = thread-spawn(arg) + ...body([0], [0x20, 0, 0x10, 0]), + // entry(in, len, outPtr, outLenPtr): out = alloc(len); *outPtr = out; *outLenPtr = len; + // copy the request into it, so a caller sees its own bytes come back. + ...body( + [1, 1, I32], + [ + 0x20, + 1, + 0x10, + ALLOC, + 0x21, + 4, // local 4 = alloc(len) + 0x20, + 2, + 0x20, + 4, + ...STORE, + 0x20, + 3, + 0x20, + 1, + ...STORE, + 0x20, + 4, + 0x20, + 0, + 0x20, + 1, + 0xfc, + 0x0a, + 0x00, + 0x00, // memory.copy(out, in, len) + ], + ), + // alloc(n): return the bump pointer, then advance it. + ...body([0], [0x23, 0, 0x23, 0, 0x20, 0, 0x6a, 0x24, 0]), + // free(p): nothing to do. + ...body([0], []), + ]); + + return new WebAssembly.Module( + new Uint8Array([ + 0x00, + 0x61, + 0x73, + 0x6d, + 0x01, + 0x00, + 0x00, + 0x00, + ...types, + ...imports, + ...functions, + ...memories, + ...globals, + ...exports, + ...code, + ]), + ); +} + +/** A binding that counts the workers it is asked for, so on-demand creation is observable. */ +function countingBinding(): WasmFfiBinding & { created: () => number } { + let created = 0; + return { + platform: nodePlatform, + created: () => created, + createThreadWorker: () => { + created++; + // The thread never starts: this module has no `wasi_thread_start`, and the test is about + // when a worker is asked for, not what runs on it. + return nodePlatform.createWorker( + new URL("./wasm_engine_stub.worker.js", import.meta.url), + ); + }, + createMainWorker: (): WorkerHandle => { + throw new Error("the engine under test runs in-process"); + }, + }; +} + +const threadedModule = () => buildModule({ shared: true, min: 4, max: 64 }); + +test("creates no thread workers until the module spawns one, then one per thread", async () => { + const binding = countingBinding(); + const engine = await WasmFfiEngine.create( + { + module: threadedModule(), + threads: 4, + memory: { initial: 4, maximum: 64 }, + }, + binding, + ); + assert.equal(engine.threads, 4); + assert.equal(binding.created(), 0, "nothing spawned before the module asks"); + + const spawn = (arg: number) => engine.host.callExport("spawn_thread", arg); + assert.equal(spawn(0), 2, "first tid"); + assert.equal(binding.created(), 1); + assert.equal(spawn(0), 3, "tids keep counting up"); + assert.equal(spawn(0), 4); + assert.equal(binding.created(), 3, "one worker per spawned thread, no pool"); + + await engine.destroy(); +}); + +test("refuses to spawn when the engine runs single-threaded", async () => { + const binding = countingBinding(); + const engine = await WasmFfiEngine.create( + { + module: threadedModule(), + threads: 1, + memory: { initial: 4, maximum: 64 }, + }, + binding, + ); + assert.equal( + engine.host.callExport("spawn_thread", 0), + -1, + "wasi-threads errno, not a tid", + ); + assert.equal(binding.created(), 0); + await engine.destroy(); +}); + +test("follows the module's memory declaration rather than the request", async () => { + // A module built without threads declares an unshared memory; asking for several threads cannot + // change that, so the engine drops to one thread rather than failing to link. + const engine = await WasmFfiEngine.create( + { + module: buildModule({ shared: false, min: 4, max: 64 }), + threads: 4, + memory: { initial: 4, maximum: 64 }, + }, + countingBinding(), + ); + assert.equal(engine.memory.buffer instanceof SharedArrayBuffer, false); + assert.equal(engine.threads, 1); + await engine.destroy(); +}); + +test("finds a maximum the module accepts when the caller asks for more", async () => { + // The module's declared maximum is not visible from JS, so an over-large request has to be + // narrowed by probing; 64 pages here against the engine's default ceiling. + const engine = await WasmFfiEngine.create( + { module: threadedModule(), threads: 2, memory: { initial: 4 } }, + countingBinding(), + ); + assert.equal(engine.memory.buffer.byteLength, 4 * 64 * 1024); + await engine.destroy(); +}); + +test("round-trips requests, growing the reused request buffer to fit", async () => { + const engine = await WasmFfiEngine.create( + { + module: threadedModule(), + threads: 1, + memory: { initial: 64, maximum: 64 }, + }, + countingBinding(), + ); + // Sizes that cross the initial scratch and shrink back again: a stale pointer or a buffer that + // failed to grow would echo the wrong bytes. + for (const size of [1, 64, 100_000, 8, 250_000, 32]) { + const request = Uint8Array.from( + { length: size }, + (_v, i) => (i * 31 + size) & 0xff, + ); + assert.deepEqual(engine.call(request), request, `echo of ${size} bytes`); + } + await engine.destroy(); +}); + +test("uses the module's own memory when it exports one instead of importing", async () => { + // A Rust cdylib defines its own memory, so the one the engine would have made is not the one the + // module reads and writes: a request echoed through the wrong memory comes back as zeroes. Such + // a module also cannot be threaded, since every instance would have a memory to itself. + const engine = await WasmFfiEngine.create( + { module: buildModule({ shared: false, min: 4, max: 64, ownMemory: true }), threads: 4 }, + countingBinding(), + ); + assert.equal(engine.threads, 1, "a module owning its memory cannot share it with workers"); + assert.equal(engine.memory, engine.host.instance.exports.memory); + const request = Uint8Array.from({ length: 5000 }, (_v, i) => (i * 7) & 0xff); + assert.deepEqual(engine.call(request), request); + await engine.destroy(); +}); + +/** + * A module whose entry reports a failure the only way one compiled without exceptions can: it + * writes the reason to stderr and exits. Everything before the entry is the smallest wrapping the + * engine will accept, so the test is about what the caller sees, not about the module. + */ +function buildAbortingModule(message: string): WebAssembly.Module { + const leb = (n: number) => { + const out = []; + do { + const byte = n & 0x7f; + n >>>= 7; + out.push(n === 0 ? byte : byte | 0x80); + } while (n !== 0); + return out; + }; + const str = (s: string) => [s.length, ...[...s].map((c) => c.charCodeAt(0))]; + const section = (id: number, body: number[]) => [id, ...leb(body.length), ...body]; + const body = (locals: number[], code: number[]) => { + const bytes = [...locals, ...code, 0x0b]; + return [...leb(bytes.length), ...bytes]; + }; + const I32 = 0x7f; + const WASI = "wasi_snapshot_preview1"; + // Imported functions are numbered first, so the two WASI calls take 0 and 1. + const [FD_WRITE, PROC_EXIT] = [0, 1]; + // Scratch below the message: the iovec at 0, fd_write's byte count at 8. + const [IOV, NWRITTEN, MSG] = [0, 8, 16]; + const text = [...new TextEncoder().encode(message)]; + if (MSG + text.length >= 64 || text.length >= 64) { + throw new Error("the hand-rolled encoder only emits single-byte i32.const operands"); + } + + const types = section(1, [ + 4, + 0x60, 4, I32, I32, I32, I32, 1, I32, // fd_write + 0x60, 1, I32, 0, // proc_exit, free + 0x60, 4, I32, I32, I32, I32, 0, // entry + 0x60, 1, I32, 1, I32, // alloc + ]); + const imports = section(2, [ + 3, + ...str("env"), ...str("memory"), 0x02, 0x01, ...leb(1), ...leb(4), + ...str(WASI), ...str("fd_write"), 0x00, 0, + ...str(WASI), ...str("proc_exit"), 0x00, 1, + ]); + const functions = section(3, [3, 2, 3, 1]); + const globals = section(6, [1, I32, 0x01, 0x41, ...leb(4096), 0x0b]); + const exports = section(7, [ + 3, + ...str("ipc_ffi_entry"), 0x00, 2, + ...str("ipc_ffi_alloc"), 0x00, 3, + ...str("ipc_ffi_free"), 0x00, 4, + ]); + const STORE = [0x36, 0x02, 0x00]; + const code = section(10, [ + 3, + // entry: write the message to stderr, then exit non-zero without touching the out slots. + ...body( + [0], + [ + 0x41, IOV, 0x41, MSG, ...STORE, + 0x41, IOV + 4, 0x41, text.length, ...STORE, + 0x41, 2, 0x41, IOV, 0x41, 1, 0x41, NWRITTEN, 0x10, FD_WRITE, 0x1a, + 0x41, 1, 0x10, PROC_EXIT, + ], + ), + ...body([0], [0x23, 0, 0x23, 0, 0x20, 0, 0x6a, 0x24, 0]), + ...body([0], []), + ]); + const data = section(11, [1, 0x00, 0x41, MSG, 0x0b, ...leb(text.length), ...text]); + + return new WebAssembly.Module( + new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + ...types, ...imports, ...functions, ...globals, ...exports, ...code, ...data, + ]), + ); +} + +test("a module that reports a failure and exits raises it as an error", async () => { + const engine = await WasmFfiEngine.create( + { + module: buildAbortingModule("abort: not on the curve"), + threads: 1, + memory: { initial: 1, maximum: 4 }, + }, + countingBinding(), + ); + assert.throws( + () => engine.call(Uint8Array.of(1, 2, 3)), + (err: Error) => err.message.includes("not on the curve"), + "the text the module wrote is what the caller sees, not a bare proc_exit", + ); + await engine.destroy(); +}); diff --git a/ipc-runtime/ts/src/wasm_engine_stub.worker.ts b/ipc-runtime/ts/src/wasm_engine_stub.worker.ts new file mode 100644 index 00000000000..ca3c2451128 --- /dev/null +++ b/ipc-runtime/ts/src/wasm_engine_stub.worker.ts @@ -0,0 +1,2 @@ +// A worker that does nothing, for tests that observe when the engine asks for one. +export {}; diff --git a/ipc-runtime/ts/src/wasm_module_source.test.ts b/ipc-runtime/ts/src/wasm_module_source.test.ts new file mode 100644 index 00000000000..3d242461c1d --- /dev/null +++ b/ipc-runtime/ts/src/wasm_module_source.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { gzipSync } from "node:zlib"; +import { compileWasmModule } from "./wasm/module_source.js"; +import { nodePlatform } from "./wasm/node/platform.js"; + +// The smallest valid module: magic + version, no sections. +const EMPTY_MODULE = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, +]); + +test("compiles raw bytes", async () => { + const module = await compileWasmModule(EMPTY_MODULE); + assert.ok(module instanceof WebAssembly.Module); + assert.deepEqual(WebAssembly.Module.exports(module), []); +}); + +test("recognises and inflates gzipped bytes", async () => { + const module = await compileWasmModule( + new Uint8Array(gzipSync(EMPTY_MODULE)), + ); + assert.ok(module instanceof WebAssembly.Module); +}); + +test("accepts a gzip data: URL", async () => { + const b64 = Buffer.from(gzipSync(EMPTY_MODULE)).toString("base64"); + const module = await compileWasmModule(`data:application/gzip;base64,${b64}`); + assert.ok(module instanceof WebAssembly.Module); +}); + +test("reads file: URLs and plain paths through the platform", async () => { + const { writeFileSync, mkdtempSync } = await import("node:fs"); + const { join } = await import("node:path"); + const { tmpdir } = await import("node:os"); + const dir = mkdtempSync(join(tmpdir(), "ipc-runtime-wasm-")); + const path = join(dir, "empty.wasm.gz"); + writeFileSync(path, gzipSync(EMPTY_MODULE)); + assert.ok( + (await compileWasmModule(path, nodePlatform)) instanceof WebAssembly.Module, + ); + assert.ok( + (await compileWasmModule( + nodePlatform.resolvePath!(path), + nodePlatform, + )) instanceof WebAssembly.Module, + ); +}); + +test("rejects bytes that are neither wasm nor gzip", async () => { + await assert.rejects( + compileWasmModule(new Uint8Array([1, 2, 3, 4])), + /bad magic/, + ); +}); diff --git a/ipc-runtime/ts/tsconfig.json b/ipc-runtime/ts/tsconfig.json index 9ab2e0c930d..4df9ed2e851 100644 --- a/ipc-runtime/ts/tsconfig.json +++ b/ipc-runtime/ts/tsconfig.json @@ -9,7 +9,7 @@ "declaration": true, "outDir": "dest", "rootDir": "src", - "lib": ["ES2022"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["node"] }, "include": ["src"] diff --git a/ipc-runtime/ts/yarn.lock b/ipc-runtime/ts/yarn.lock index bd5dbab47f0..5ee925984da 100644 --- a/ipc-runtime/ts/yarn.lock +++ b/ipc-runtime/ts/yarn.lock @@ -11,6 +11,8 @@ __metadata: dependencies: "@types/node": "npm:^22" typescript: "npm:^5.6.3" + bin: + ipc-runtime-prepare-arch-packages: ./scripts/prepare_arch_packages.mjs languageName: unknown linkType: soft diff --git a/labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch b/labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch new file mode 100644 index 00000000000..a181839de8b --- /dev/null +++ b/labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Charlie <5764343+charlielye@users.noreply.github.com> +Date: Fri, 25 Sep 2026 12:29:55 +0000 +Subject: [PATCH] feat: consume bb.js-api alongside bb.js (aztec-packages + #25433) + +bb.js now depends on @aztec-foundation/bb.js-api as workspace:^, which only +resolves inside the barretenberg/ts workspace, and bb.js-api carries four +per-arch optional dependencies that are not published. use-local has to portal +all five or `yarn install` fails with "Workspace not found", so pin them the way +bb-avm-sim's family is already pinned. + +diff --git a/labs-aztec-toolchain/pins.mjs b/labs-aztec-toolchain/pins.mjs +index a949edd98067d2db3d90ec56b1384dc14712884b..1681251964fddc33a3797200a3a745dc976915da 100755 +--- a/labs-aztec-toolchain/pins.mjs ++++ b/labs-aztec-toolchain/pins.mjs +@@ -63,6 +63,15 @@ const LOCAL_PACKAGES = { + "@aztec-foundation/bb-avm-sim-linux-x64": + "portal:barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-x64", + "@aztec-foundation/bb.js": "portal:barretenberg/ts/bb.js", ++ "@aztec-foundation/bb.js-api": "portal:barretenberg/ts/bb.js-api", ++ "@aztec-foundation/bb.js-api-darwin-arm64": ++ "portal:barretenberg/ts/bb.js-api/packages/bb.js-api-darwin-arm64", ++ "@aztec-foundation/bb.js-api-darwin-x64": ++ "portal:barretenberg/ts/bb.js-api/packages/bb.js-api-darwin-x64", ++ "@aztec-foundation/bb.js-api-linux-arm64": ++ "portal:barretenberg/ts/bb.js-api/packages/bb.js-api-linux-arm64", ++ "@aztec-foundation/bb.js-api-linux-x64": ++ "portal:barretenberg/ts/bb.js-api/packages/bb.js-api-linux-x64", + "@aztec-foundation/cdb": "portal:barretenberg/ts/cdb", + "@aztec-foundation/constants-codegen": "portal:protocol/constants-codegen", + "@aztec-foundation/ipc-runtime": "portal:ipc-runtime/ts", +diff --git a/yarn-project/package.json b/yarn-project/package.json +index dd9af8f3625816c6ea09a5a196aed410a210d6cd..4f8e92c90b42e7fa8986170d9df21fa0dbe29377 100644 +--- a/yarn-project/package.json ++++ b/yarn-project/package.json +@@ -90,6 +90,11 @@ + "@aztec-foundation/bb-avm-sim-linux-arm64": "6.0.0-nightly.20260923", + "@aztec-foundation/bb-avm-sim-linux-x64": "6.0.0-nightly.20260923", + "@aztec-foundation/bb.js": "6.0.0-nightly.20260923", ++ "@aztec-foundation/bb.js-api": "6.0.0-nightly.20260923", ++ "@aztec-foundation/bb.js-api-darwin-arm64": "6.0.0-nightly.20260923", ++ "@aztec-foundation/bb.js-api-darwin-x64": "6.0.0-nightly.20260923", ++ "@aztec-foundation/bb.js-api-linux-arm64": "6.0.0-nightly.20260923", ++ "@aztec-foundation/bb.js-api-linux-x64": "6.0.0-nightly.20260923", + "@aztec-foundation/cdb": "6.0.0-nightly.20260923", + "@aztec-foundation/constants-codegen": "6.0.0-nightly.20260923", + "@aztec-foundation/ipc-runtime": "6.0.0-nightly.20260923", diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 4d1274ccf67..18015573f7e 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -15,6 +15,10 @@ hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(cache_content_hash .rebuild_pa # ipc-codegen from the sparse clone cpp/CMakeLists.txt makes at configure time. IPC_CODEGEN_DIR="$PKG/cpp/build/_deps/aztec-packages-src/ipc-codegen" +# Note this runs the codegen from the pinned release, not the working tree, so its flags must be +# what that release accepts. --client and --out are implied by --package in later versions and are +# still accepted there, but the pinned one requires --out; do not drop them until foundation.pin +# moves past the release that made them optional. function generate_ts_package { node --experimental-strip-types --no-warnings \ "$IPC_CODEGEN_DIR/src/generate.ts" \ diff --git a/native-packages/wsdb/yarn.lock b/native-packages/wsdb/yarn.lock index 179fc7a5a6f..aec1a626df9 100644 --- a/native-packages/wsdb/yarn.lock +++ b/native-packages/wsdb/yarn.lock @@ -8,6 +8,8 @@ __metadata: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec-labs%2Fwsdb-packages%40workspace%3A.": version: 0.0.0-use.local resolution: "@aztec-foundation/ipc-runtime@portal:../../ipc-runtime/ts::locator=%40aztec-labs%2Fwsdb-packages%40workspace%3A." + bin: + ipc-runtime-prepare-arch-packages: ./scripts/prepare_arch_packages.mjs languageName: node linkType: soft From a1a0938028f77db98db202c352daef07a1d7d05a Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:34:59 +0000 Subject: [PATCH 2/6] chore(labs-patches): order the bb.js-api patch after next's series Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Prk3bcqkopGENJUuUpT52q --- ...19-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename labs-patches/{0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch => 0019-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch} (100%) diff --git a/labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch b/labs-patches/0019-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch similarity index 100% rename from labs-patches/0010-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch rename to labs-patches/0019-feat-consume-bb.js-api-alongside-bb.js-aztec-package.patch From 43d73820ee9d9e334967372048fae21edc457a11 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:43:32 +0000 Subject: [PATCH 3/6] feat(bb)!: build wasm with exception handling; node 26, wasi-sdk 34 Every bb wasm build compiled with -fno-exceptions, so throw_or_abort called out to the JavaScript environment, whose throw unwound the module without running catch blocks or destructors, and the AVM simulator (which signals exceptional halts with C++ exceptions) could not build for wasm at all. wasi-sdk 33+ ships a sysroot with a C++ exception runtime, so the wasm and wasm-threads builds now use standard (exnref) wasm exception handling, and the toolchains move to versions that run it unflagged. bb wasm - arch.cmake: -fwasm-exceptions -mllvm -wasm-use-legacy-eh=false, linked with -lunwind, in place of -fno-exceptions. The wasm preset drops -DBB_NO_EXCEPTIONS and sets AVM off. - throw_or_abort throws std::runtime_error inside the module in wasm builds with exceptions, so bbapi answers with an ErrorResponse, as it does natively. bb.js's exception test now also checks the module no longer aborts (it fails on the old wasm). - The wasm build adds aztec, world_state_reference, vm2 (vm2_sim only) and crypto/merkle_tree, and the wasm build preset builds vm2_sim_smoke. That runs one hinted public transaction (vm2/testing/minimal_tx.testdata.bin) through AvmSimAPI::simulate_with_hinted_dbs after checking a TagMismatchException is caught; scripts/vm2_sim_smoke_test.sh runs it natively and in wasm under node and requires identical results. It runs in the default bb test_cmds. - wasm32 fixes in the simulator and the in-memory trees, where size_t is 32-bit: contract_crypto shifted a size_t by 32, which is undefined on wasm32 and would corrupt the first bytecode field; index_t to size_t casts in the in-memory indexed trees and their callers; ptrdiff_t in to_radix. - LLVM 23 (wasi-sdk 34) fixes: wasm32-wasip1-threads in place of the removed wasm32-wasi-threads target; missing and includes; static function templates in headers made inline (-Wunused-template); -Wno-c2y-extensions for google-benchmark, which builds with -pedantic-errors and uses __COUNTER__. - barretenberg.wasm grows 5% (17.17 MB to 18.03 MB), 3.4% gzipped (3.88 MB to 4.01 MB). UltraHonk 2^20 proving under wasmtime showed no difference beyond noise. Toolchains (build image and scripts/setup-container.sh) - node 24.15.0 to 26.12.0 (.nvmrc, bootstrap's expected minimum and install_node). Node 26 removed --experimental-transform-types; the ipc-codegen callers only need type stripping, so it is dropped. - wasi-sdk 27 to 34 at /opt/wasi-sdk. - wasmtime 20.0.2 to 46.0.3, the last release with wasi-threads (-Sthreads), which the threaded wasm tests and benchmarks use. Its invocations add -Wexceptions=y and -Wshared-memory=y. - playwright 1.49 to 1.63.0 (Chromium 153; exnref is unflagged from Chrome 137), in the image and headless-test. The CI image is not redeployed here. Until it is, the root bootstrap's transitional_toolchain_upgrade (run by prep and check, in CI or the devbox) installs wasi-sdk 34 at /opt/wasi-sdk and node, wasmtime and playwright's chromium under $HOME, passing PATH and PLAYWRIGHT_BROWSERS_PATH through to isolated test containers. In devbox:3.1 it takes about 36s the first time and 3s after. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .nvmrc | 2 +- .../.claude/skills/benchmark-chonk/SKILL.md | 2 +- .../.claude/skills/profile-chonk/SKILL.md | 2 +- .../.claude/skills/remote-bench/SKILL.md | 2 +- .../acir_tests/headless-test/package.json | 2 +- barretenberg/acir_tests/yarn.lock | 43 ++++--------- barretenberg/cpp/CLAUDE.md | 2 +- barretenberg/cpp/CMakePresets.json | 5 +- barretenberg/cpp/bootstrap.sh | 6 ++ barretenberg/cpp/cmake/arch.cmake | 6 +- barretenberg/cpp/cmake/benchmark.cmake | 4 ++ barretenberg/cpp/cmake/threading.cmake | 4 +- barretenberg/cpp/scripts/benchmark_wasm.sh | 2 +- .../cpp/scripts/benchmark_wasm_remote.sh | 2 +- .../cpp/scripts/profile_wasm_samply.sh | 2 +- barretenberg/cpp/scripts/run_wasi_command.mjs | 15 +++++ .../cpp/scripts/vm2_sim_smoke_test.sh | 19 ++++++ barretenberg/cpp/scripts/wasmtime.sh | 2 + barretenberg/cpp/src/CMakeLists.txt | 9 +++ .../cpp/src/barretenberg/bbapi/CMakeLists.txt | 2 +- .../commitment_schemes/shplonk/shplonk.hpp | 2 +- .../small_subgroup_ipa/small_subgroup_ipa.hpp | 2 +- .../src/barretenberg/common/ref_vector.hpp | 2 +- .../barretenberg/common/throw_or_abort.hpp | 7 +++ .../cpp/src/barretenberg/common/utils.cpp | 2 + .../src/barretenberg/crypto/CMakeLists.txt | 4 +- .../barretenberg/crypto/schnorr/schnorr.tcc | 2 +- .../retrieved_bytecodes_tree_check.cpp | 4 +- .../vm2/simulation/gadgets/to_radix.cpp | 2 +- .../written_public_data_slots_tree_check.cpp | 4 +- .../vm2/simulation/lib/contract_crypto.cpp | 2 +- .../simulation/lib/indexed_memory_tree.hpp | 14 +++-- .../src/barretenberg/vm2/tooling/stats.cpp | 1 + .../barretenberg/vm2_sim_smoke/CMakeLists.txt | 4 ++ .../vm2_sim_smoke/vm2_sim_smoke.cpp | 60 +++++++++++++++++++ .../memory_merkle_db.hpp | 6 +- barretenberg/rust/bootstrap.sh | 2 +- barretenberg/ts/bb.js/scripts/generate.sh | 2 +- .../src/bbapi/exception_handling.test.ts | 23 ++++++- bootstrap.sh | 40 +++++++++++-- build-images/src/Dockerfile | 24 ++++---- scripts/setup-container.sh | 22 +++---- 43 files changed, 263 insertions(+), 103 deletions(-) create mode 100644 barretenberg/cpp/scripts/run_wasi_command.mjs create mode 100755 barretenberg/cpp/scripts/vm2_sim_smoke_test.sh create mode 100644 barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt create mode 100644 barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 234574c30e2..c4cb4386deb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -51,7 +51,7 @@ body: attributes: label: Node Version description: Your Node.js version - placeholder: e.g. 24.15.0 + placeholder: e.g. 26.12.0 - type: textarea id: additional-context attributes: diff --git a/.nvmrc b/.nvmrc index f3c88209af5..3635429b74f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.15.0 +v26.12.0 diff --git a/barretenberg/.claude/skills/benchmark-chonk/SKILL.md b/barretenberg/.claude/skills/benchmark-chonk/SKILL.md index ff46ce600a6..6e7eb9f674d 100644 --- a/barretenberg/.claude/skills/benchmark-chonk/SKILL.md +++ b/barretenberg/.claude/skills/benchmark-chonk/SKILL.md @@ -131,7 +131,7 @@ HARDWARE_CONCURRENCY=8 BB_BENCH=1 \ ``` The wasmtime wrapper sets: -- `-Wthreads=y -Sthreads=y` — enable WASM threads and shared memory +- `-Wthreads=y -Wshared-memory=y -Wexceptions=y -Sthreads=y` — enable WASM threads, shared memory and exception handling - `--env HARDWARE_CONCURRENCY` — thread count - `--env BB_BENCH` — enable operation counting (requires configuring with `-DENABLE_WASM_BENCH=ON`; it is OFF by default in the `wasm-threads` preset to keep the shipped wasm fast) - `--dir=$HOME/.bb-crs --dir=.` — filesystem access for CRS and working directory diff --git a/barretenberg/.claude/skills/profile-chonk/SKILL.md b/barretenberg/.claude/skills/profile-chonk/SKILL.md index df2977eb6c7..6d382439b44 100644 --- a/barretenberg/.claude/skills/profile-chonk/SKILL.md +++ b/barretenberg/.claude/skills/profile-chonk/SKILL.md @@ -138,7 +138,7 @@ ssh $BB_SSH_KEY $BB_SSH_INSTANCE " cd $REMOTE_DIR HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY \ /home/ubuntu/.wasmtime/bin/wasmtime run \ - -Wthreads=y -Sthreads=y -Wshared-memory=y \ + -Wthreads=y -Wexceptions=y -Sthreads=y -Wshared-memory=y \ --env HARDWARE_CONCURRENCY --env HOME --env BB_BENCH=1 \ --dir=\$HOME/.bb-crs --dir=. \ ./bin/bb prove \ diff --git a/barretenberg/.claude/skills/remote-bench/SKILL.md b/barretenberg/.claude/skills/remote-bench/SKILL.md index a4845bc42d7..f34e43f4217 100644 --- a/barretenberg/.claude/skills/remote-bench/SKILL.md +++ b/barretenberg/.claude/skills/remote-bench/SKILL.md @@ -203,7 +203,7 @@ ssh $BB_SSH_KEY $BB_SSH_INSTANCE \ "cd $BB_SSH_CPP_PATH/build-wasm-threads && \ HARDWARE_CONCURRENCY=16 \ /home/ubuntu/.wasmtime/bin/wasmtime run \ - -Wthreads=y -Sthreads=y \ + -Wthreads=y -Wshared-memory=y -Wexceptions=y -Sthreads=y \ --env HARDWARE_CONCURRENCY \ --env HOME \ --env BB_BENCH=1 \ diff --git a/barretenberg/acir_tests/headless-test/package.json b/barretenberg/acir_tests/headless-test/package.json index 1f16ffb506e..0db3ad32e87 100644 --- a/barretenberg/acir_tests/headless-test/package.json +++ b/barretenberg/acir_tests/headless-test/package.json @@ -10,7 +10,7 @@ "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0", - "playwright": "1.49.0", + "playwright": "1.63.0", "puppeteer": "^24.22.3" }, "devDependencies": { diff --git a/barretenberg/acir_tests/yarn.lock b/barretenberg/acir_tests/yarn.lock index 91e9dd9eb49..0c3d79ceb2d 100644 --- a/barretenberg/acir_tests/yarn.lock +++ b/barretenberg/acir_tests/yarn.lock @@ -2466,16 +2466,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -2486,15 +2476,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -2675,7 +2656,7 @@ __metadata: dependencies: chalk: "npm:^5.3.0" commander: "npm:^12.1.0" - playwright: "npm:1.49.0" + playwright: "npm:1.63.0" puppeteer: "npm:^24.22.3" ts-node: "npm:^10.9.2" typescript: "npm:^5.4.2" @@ -4097,27 +4078,23 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.49.0": - version: 1.49.0 - resolution: "playwright-core@npm:1.49.0" +"playwright-core@npm:1.63.0": + version: 1.63.0 + resolution: "playwright-core@npm:1.63.0" bin: playwright-core: cli.js - checksum: 10c0/22c1a72fabdcc87bd1cd4d40a032d2c5b94cf94ba7484dc182048c3fa1c8ec26180b559d8cac4ca9870e8fd6bdf5ef9d9f54e7a31fd60d67d098fcffc5e4253b + checksum: 10c0/39c1ebae4029ca2e9371e00a5588d962038e24b6e0bdb7d0d27374dde04838f349b83ae0c8b3b648653d0935cba7fb9011f1201adaab9dd0db7801ad6c75e9aa languageName: node linkType: hard -"playwright@npm:1.49.0": - version: 1.49.0 - resolution: "playwright@npm:1.49.0" +"playwright@npm:1.63.0": + version: 1.63.0 + resolution: "playwright@npm:1.63.0" dependencies: - fsevents: "npm:2.3.2" - playwright-core: "npm:1.49.0" - dependenciesMeta: - fsevents: - optional: true + playwright-core: "npm:1.63.0" bin: playwright: cli.js - checksum: 10c0/e94d662747cd147d0573570fec90dadc013c1097595714036fc8934a075c5a82ab04a49111b03b1f762ea86429bdb7c94460901896901e20970b30ce817cc93f + checksum: 10c0/d1fd048a89be27c9aba6757dc4f70eff4a5163b49f439ad691767be8b2a482327d0dd1928b4f73c8fe0d8b33aab2c9dc2670424ab60f112d9e98e91b94e3b090 languageName: node linkType: hard diff --git a/barretenberg/cpp/CLAUDE.md b/barretenberg/cpp/CLAUDE.md index a31f266ece4..3e1cd891153 100644 --- a/barretenberg/cpp/CLAUDE.md +++ b/barretenberg/cpp/CLAUDE.md @@ -89,7 +89,7 @@ clang-format-20 -i These are load-bearing: violating them will compile on native but break WASM, or skew test output in CI. Use the listed alternative unconditionally. - **Logging: use `info(...)` / `vinfo(...)` from `barretenberg/common/log.hpp`, never `std::cout` or `std::cerr`.** The macros route through `log_function` and respect `bb_log_level`; direct `std::cout` is unfiltered, uncaptured in CI logs, and skews benchmark output. -- **Aborts and exceptions: use `throw_or_abort(msg)` from `barretenberg/common/throw_or_abort.hpp`, never bare `throw` or `std::abort()`.** WASM builds set `BB_NO_EXCEPTIONS`, which turns `throw` into `abort()` — bare `throw` compiles differently and misformats the message, and raw `std::abort()` drops the message entirely. For header-only libraries that must use exception syntax, use the `THROW`/`RETHROW` macros from `common/try_catch_shim.hpp`. +- **Aborts and exceptions: use `throw_or_abort(msg)` from `barretenberg/common/throw_or_abort.hpp`, never bare `throw` or `std::abort()`.** Builds that set `BB_NO_EXCEPTIONS` (the `clang20-no-exceptions` preset; WASM builds now use wasm exception handling) turn `throw_or_abort` into `abort()`, and raw `std::abort()` drops the message entirely. For header-only libraries that must use exception syntax, use the `THROW`/`RETHROW` macros from `common/try_catch_shim.hpp`. - **Assertions: use `BB_ASSERT(cond, msg)` / `BB_ASSERT_EQ` / `BB_ASSERT_NEQ` / `BB_ASSERT_GT` / etc. from `barretenberg/common/assert.hpp`, never bare `assert(...)`.** `BB_ASSERT` hooks into the benchmark-assertion framework and has distinct debug/release behavior; `assert` is stripped in release builds and silently diverges in WASM. - **Release builds: do not reach for the `release` CMake preset unless the user is reproducing a performance issue.** Debug/default presets are much faster to compile and sufficient for correctness work. diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index bcb0a8b9d32..07428005b14 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -422,7 +422,7 @@ "CMAKE_C_COMPILER_WORKS": "ON", "CMAKE_CXX_COMPILER_WORKS": "ON", "MULTITHREADING": "OFF", - "CMAKE_CXX_FLAGS": "-DBB_NO_EXCEPTIONS" + "AVM": "OFF" } }, { @@ -768,7 +768,8 @@ "targets": [ "barretenberg.wasm", "barretenberg.wasm.gz", - "barretenberg-debug.wasm" + "barretenberg-debug.wasm", + "vm2_sim_smoke" ] }, { diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 58478b1f6a7..769597da18b 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -358,6 +358,11 @@ function test_cmds_wasm_threads { echo "$hash:CPUS=8 HARDWARE_CONCURRENCY=8 barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/ecc_tests" } +function test_cmds_wasm { + # vm2_sim in wasm gives the native result for the same transaction, and catches the exceptions it relies on. + echo "$hash barretenberg/cpp/scripts/vm2_sim_smoke_test.sh" +} + function test_cmds_asan { local prefix="$hash:CPUS=4:MEM=8g" @@ -395,6 +400,7 @@ function test_cmds_nightly { function test_cmds { if [ -z "${1:-}" ]; then test_cmds_native + test_cmds_wasm if [ "$CI_FULL" -eq 1 ]; then test_cmds_wasm_threads test_cmds_asan diff --git a/barretenberg/cpp/cmake/arch.cmake b/barretenberg/cpp/cmake/arch.cmake index c8158dfb8cc..41132e9ea16 100644 --- a/barretenberg/cpp/cmake/arch.cmake +++ b/barretenberg/cpp/cmake/arch.cmake @@ -2,7 +2,11 @@ if(WASM) # Disable SLP vectorization on WASM as it's brokenly slow. To give an idea, with this off it still takes # 2m:18s to compile scalar_multiplication.cpp, and with it on I estimate it's 50-100 times longer. I never # had the patience to wait it out... - add_compile_options(-fno-exceptions -fno-slp-vectorize) + add_compile_options(-fno-slp-vectorize) + # Standard (exnref) wasm exception handling. wasi-sdk 33+ ships a sysroot built with it, selected by + # -fwasm-exceptions at link time; libunwind provides the unwinder. + add_compile_options(-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false) + add_link_options(-fwasm-exceptions -lunwind) endif() # Target skylake on x86 for AVX2 etc. ARM is handled by the zig wrapper scripts diff --git a/barretenberg/cpp/cmake/benchmark.cmake b/barretenberg/cpp/cmake/benchmark.cmake index fb72f700766..d18755e9d5d 100644 --- a/barretenberg/cpp/cmake/benchmark.cmake +++ b/barretenberg/cpp/cmake/benchmark.cmake @@ -18,3 +18,7 @@ if(NOT benchmark_FOUND) set_property(DIRECTORY ${benchmark_SOURCE_DIR} PROPERTY EXCLUDE_FROM_ALL) set_property(DIRECTORY ${benchmark_BINARY_DIR} PROPERTY EXCLUDE_FROM_ALL) endif() +# The library builds with -pedantic-errors, and clang 23 (wasi-sdk 34) flags its __COUNTER__ as a C2y extension. +if(TARGET benchmark AND CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 23) + target_compile_options(benchmark PRIVATE -Wno-c2y-extensions) +endif() diff --git a/barretenberg/cpp/cmake/threading.cmake b/barretenberg/cpp/cmake/threading.cmake index 5092e8064c6..99c25b93768 100644 --- a/barretenberg/cpp/cmake/threading.cmake +++ b/barretenberg/cpp/cmake/threading.cmake @@ -3,8 +3,8 @@ if(MULTITHREADING) add_compile_options(-pthread) add_link_options(-pthread) if(WASM) - add_compile_options(--target=wasm32-wasi-threads) - add_link_options(--target=wasm32-wasi-threads -Wl,--shared-memory) + add_compile_options(--target=wasm32-wasip1-threads) + add_link_options(--target=wasm32-wasip1-threads -Wl,--shared-memory) # Prevent indirect call type mismatch errors in thread_local destructors # (without this the benchmark flow fails at destruction point for WASM) add_compile_options(-fno-c++-static-destructors) diff --git a/barretenberg/cpp/scripts/benchmark_wasm.sh b/barretenberg/cpp/scripts/benchmark_wasm.sh index 0b4483c7c09..a307fe3ffe7 100755 --- a/barretenberg/cpp/scripts/benchmark_wasm.sh +++ b/barretenberg/cpp/scripts/benchmark_wasm.sh @@ -17,4 +17,4 @@ cmake --build --preset wasm-threads --target $BENCHMARK cd build-wasm-threads # Consistency with _wasm.sh targets / shorter $COMMAND. cp ./bin/$BENCHMARK . -wasmtime run --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Sthreads=y --dir=.. $COMMAND \ No newline at end of file +wasmtime run --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Wshared-memory=y -Wexceptions=y -Sthreads=y --dir=.. $COMMAND \ No newline at end of file diff --git a/barretenberg/cpp/scripts/benchmark_wasm_remote.sh b/barretenberg/cpp/scripts/benchmark_wasm_remote.sh index 62213556579..18ce7aa0543 100755 --- a/barretenberg/cpp/scripts/benchmark_wasm_remote.sh +++ b/barretenberg/cpp/scripts/benchmark_wasm_remote.sh @@ -27,4 +27,4 @@ ssh $BB_SSH_KEY $BB_SSH_INSTANCE "mkdir -p $BB_SSH_CPP_PATH/build-wasm-threads" scp $BB_SSH_KEY ./bin/$BENCHMARK $BB_SSH_INSTANCE:$BB_SSH_CPP_PATH/build-wasm-threads # run wasm benchmarking ssh $BB_SSH_KEY $BB_SSH_INSTANCE \ - "cd $BB_SSH_CPP_PATH/build-wasm-threads ; /home/ubuntu/.wasmtime/bin/wasmtime run --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Sthreads=y --dir=.. $COMMAND" + "cd $BB_SSH_CPP_PATH/build-wasm-threads ; /home/ubuntu/.wasmtime/bin/wasmtime run --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Wshared-memory=y -Wexceptions=y -Sthreads=y --dir=.. $COMMAND" diff --git a/barretenberg/cpp/scripts/profile_wasm_samply.sh b/barretenberg/cpp/scripts/profile_wasm_samply.sh index 218ed2b8f89..27d0e333f30 100755 --- a/barretenberg/cpp/scripts/profile_wasm_samply.sh +++ b/barretenberg/cpp/scripts/profile_wasm_samply.sh @@ -16,4 +16,4 @@ cmake --build --preset wasm-threads --target $BENCHMARK cd build-wasm-threads # Consistency with _wasm.sh targets / shorter $COMMAND. cp ./bin/$BENCHMARK . -samply record wasmtime run --profile=perfmap --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Sthreads=y --dir=.. $COMMAND \ No newline at end of file +samply record wasmtime run --profile=perfmap --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY -Wthreads=y -Wshared-memory=y -Wexceptions=y -Sthreads=y --dir=.. $COMMAND \ No newline at end of file diff --git a/barretenberg/cpp/scripts/run_wasi_command.mjs b/barretenberg/cpp/scripts/run_wasi_command.mjs new file mode 100644 index 00000000000..639a4a50281 --- /dev/null +++ b/barretenberg/cpp/scripts/run_wasi_command.mjs @@ -0,0 +1,15 @@ +// Runs a bb wasi command (a module that imports its memory) under Node's WASI, with `dir` preopened at /data. +// Usage: node run_wasi_command.mjs [args...] +import { readFileSync } from "node:fs"; +import { WASI } from "node:wasi"; + +const [wasmPath, dir, ...args] = process.argv.slice(2); +const module = await WebAssembly.compile(readFileSync(wasmPath)); +const wasi = new WASI({ version: "preview1", args: [wasmPath, ...args], preopens: { "/data": dir }, returnOnExit: true }); +const importObject = wasi.getImportObject(); +const memory = new WebAssembly.Memory({ initial: 512, maximum: 65536 }); +for (const { module: from, name, kind } of WebAssembly.Module.imports(module)) { + if (kind === "memory") (importObject[from] ??= {})[name] = memory; +} +const instance = await WebAssembly.instantiate(module, importObject); +process.exitCode = wasi.start({ exports: { ...instance.exports, memory } }); diff --git a/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh b/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh new file mode 100755 index 00000000000..254701d449d --- /dev/null +++ b/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Runs the same hinted public transaction through vm2_sim natively and in wasm (under node, so V8), and checks the +# wasm run catches the simulator's C++ exceptions and reaches the native result. +set -euo pipefail +cd $(dirname $0)/.. + +native_bin=$(scripts/preset-build-dir ${NATIVE_PRESET:-clang20})/bin/vm2_sim_smoke +wasm_bin=build-wasm/bin/vm2_sim_smoke +inputs_dir=src/barretenberg/vm2/testing +inputs=minimal_tx.testdata.bin + +native=$($native_bin $inputs_dir/$inputs | grep -E '^(caught|revert_code)') +wasm=$(node --no-warnings scripts/run_wasi_command.mjs $wasm_bin $inputs_dir /data/$inputs | + grep -E '^(caught|revert_code)') + +echo "native: $native" +echo "wasm: $wasm" +[ "$(echo "$wasm" | grep -c '^caught TagMismatchException')" -eq 1 ] +[ "$native" == "$wasm" ] diff --git a/barretenberg/cpp/scripts/wasmtime.sh b/barretenberg/cpp/scripts/wasmtime.sh index 3ad657d5798..f59c4588e4b 100755 --- a/barretenberg/cpp/scripts/wasmtime.sh +++ b/barretenberg/cpp/scripts/wasmtime.sh @@ -5,6 +5,8 @@ set -eu export WASMTIME_BACKTRACE_DETAILS=1 exec wasmtime run \ -Wthreads=y \ + -Wshared-memory=y \ + -Wexceptions=y \ -Sthreads=y \ ${HARDWARE_CONCURRENCY:+--env HARDWARE_CONCURRENCY} \ --env HOME \ diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 11967cf84c2..e6f2af8d45d 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -123,10 +123,19 @@ if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) # 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/vm2_wsdb) + add_subdirectory(barretenberg/vm2_sim_smoke) add_subdirectory(barretenberg/cdb) add_subdirectory(barretenberg/avm) endif() +if(WASM) + # The AVM simulator, without the AVM prover (the wasm presets set AVM off). + add_subdirectory(barretenberg/aztec) + add_subdirectory(barretenberg/world_state_reference) + add_subdirectory(barretenberg/vm2) + add_subdirectory(barretenberg/vm2_sim_smoke) +endif() + # Pull in ipc-runtime as a C++ dependency. Provides the `ipc_runtime` # library target (static, or INTERFACE under WASM with transport sources # stubbed) that bbapi/wsdb/etc link against for the codegen-emitted diff --git a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt index e7a90defb53..827c086d1de 100644 --- a/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/bbapi/CMakeLists.txt @@ -22,7 +22,7 @@ if(NOT FUZZING) ) add_custom_command( OUTPUT ${BB_GEN_OUTPUTS} - COMMAND node --experimental-strip-types --experimental-transform-types --no-warnings + COMMAND node --experimental-strip-types --no-warnings ${IPC_CODEGEN_DIR}/src/generate.ts --schema ${BB_SCHEMA} --lang cpp diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.hpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.hpp index 98b5e0dc69c..0112f0bb328 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.hpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/shplonk/shplonk.hpp @@ -615,7 +615,7 @@ template class ShplonkVerifier_ { * */ template -static std::vector compute_shplonk_batching_challenge_powers(const Fr& shplonk_batching_challenge, +inline std::vector compute_shplonk_batching_challenge_powers(const Fr& shplonk_batching_challenge, const size_t virtual_log_n, bool has_zk = false, bool committed_sumcheck = false) diff --git a/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.hpp b/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.hpp index 7422a1f9e86..ce188e77b32 100644 --- a/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.hpp +++ b/barretenberg/cpp/src/barretenberg/commitment_schemes/small_subgroup_ipa/small_subgroup_ipa.hpp @@ -420,7 +420,7 @@ template class SmallSubgroupIPAVerifier { * @return Polynomial */ template -static std::vector compute_challenge_polynomial_coeffs( +inline std::vector compute_challenge_polynomial_coeffs( const std::vector& multivariate_challenge) { diff --git a/barretenberg/cpp/src/barretenberg/common/ref_vector.hpp b/barretenberg/cpp/src/barretenberg/common/ref_vector.hpp index bfc92e13055..d4f1b63d5ec 100644 --- a/barretenberg/cpp/src/barretenberg/common/ref_vector.hpp +++ b/barretenberg/cpp/src/barretenberg/common/ref_vector.hpp @@ -173,7 +173,7 @@ template RefVector concatenate(const RefVector& ref_vector, c } /* @details Ensures a nested vector holds reference objects */ -template static std::vector> to_vector_of_ref_vectors(std::vector>& vec) +template inline std::vector> to_vector_of_ref_vectors(std::vector>& vec) { std::vector> result; for (std::vector& inner : vec) { diff --git a/barretenberg/cpp/src/barretenberg/common/throw_or_abort.hpp b/barretenberg/cpp/src/barretenberg/common/throw_or_abort.hpp index c6856f40c26..7956a978d22 100644 --- a/barretenberg/cpp/src/barretenberg/common/throw_or_abort.hpp +++ b/barretenberg/cpp/src/barretenberg/common/throw_or_abort.hpp @@ -1,9 +1,16 @@ #pragma once #include "barretenberg/env/throw_or_abort_impl.hpp" #include "log.hpp" +#include #include inline void throw_or_abort [[noreturn]] (std::string const& err) { +#if defined(__wasm__) && !defined(BB_NO_EXCEPTIONS) + // Throw inside the module, so catch blocks such as bbapi's error responses see it. The JavaScript environment's + // throw_or_abort_impl throws a JS error, which unwinds the module without running them or any destructors. + throw std::runtime_error(err); +#else throw_or_abort_impl(err.c_str()); +#endif } diff --git a/barretenberg/cpp/src/barretenberg/common/utils.cpp b/barretenberg/cpp/src/barretenberg/common/utils.cpp index 312ccb9a377..f55167a5273 100644 --- a/barretenberg/cpp/src/barretenberg/common/utils.cpp +++ b/barretenberg/cpp/src/barretenberg/common/utils.cpp @@ -1,5 +1,7 @@ #include "./utils.hpp" +#include + namespace bb::utils { std::vector hex_to_bytes(const std::string& hex) diff --git a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt index a2911b6b687..63c1926142f 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt @@ -12,6 +12,4 @@ add_subdirectory(ecdsa) add_subdirectory(aes128) add_subdirectory(poseidon2) -if (NOT WASM) - add_subdirectory(merkle_tree) -endif() +add_subdirectory(merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc index 16fe624c72d..b8f1d3d2eef 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc +++ b/barretenberg/cpp/src/barretenberg/crypto/schnorr/schnorr.tcc @@ -51,7 +51,7 @@ template static inline Fq compute_schnorr_challenge_dst() * @return e as a bb::fr element */ template -static typename G1::Fq schnorr_generate_challenge(const typename G1::Fq& message_field, +inline typename G1::Fq schnorr_generate_challenge(const typename G1::Fq& message_field, const typename G1::affine_element& pubkey, const typename G1::affine_element& R) { diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp index cb96ae66227..7ab61f3718a 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp @@ -17,8 +17,8 @@ bool RetrievedBytecodesTreeCheck::contains(const FF& class_id) { const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(class_id); - auto sibling_path = tree.get_sibling_path(low_leaf_index); - auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); + auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); + auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); indexed_tree_check.assert_read(class_id, std::nullopt, // No siloing diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp index 232d7ebac52..7976e78a186 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp @@ -50,7 +50,7 @@ std::pair, /* truncated */ bool> ToRadix::to_le_radix(const bool truncated = num_limbs < limbs.size(); if (truncated) { - limbs.erase(limbs.begin() + num_limbs, limbs.end()); + limbs.erase(limbs.begin() + static_cast(num_limbs), limbs.end()); } return { limbs, truncated }; diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp index e7908f33077..32b0d6e8e12 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp @@ -24,8 +24,8 @@ bool WrittenPublicDataSlotsTreeCheck::contains(const AztecAddress& contract_addr const auto& tree = written_public_data_slots_tree_stack.top(); const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(leaf_slot); - auto sibling_path = tree.get_sibling_path(low_leaf_index); - auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); + auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); + auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); indexed_tree_check.assert_read(slot, IndexedTreeSiloingParameters{ diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp index d74895f1def..370aa4bf476 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp @@ -58,7 +58,7 @@ FF compute_public_bytecode_first_field(size_t bytecode_size) // Fr<0x00000000000000000000000000000000000000000000000000016b480f8411f1> From: max fields in bytes = 3000 * 31 = // 16b48, Dom sep = f8411f1 static_assert(DOM_SEP__PUBLIC_BYTECODE <= UINT32_MAX, "Public bytecode domain separator must fit in 32 bits"); - return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + uint256_t(bytecode_size << 32)); + return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + (uint256_t(bytecode_size) << 32)); } FF compute_public_bytecode_commitment(std::span bytecode) diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp index 0bd01838ba0..c2575fb2298 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp @@ -150,20 +150,22 @@ SequentialInsertionResult IndexedMemoryTree:: for (const auto& leaf_to_insert : leaves_to_insert) { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - IndexedLeaf& low_leaf = leaves.at(find_low_leaf_result.index); + // index_t is 64-bit; size_t is 32-bit on wasm32. An index into this in-memory tree always fits. + const auto low_index = static_cast(find_low_leaf_result.index); + IndexedLeaf& low_leaf = leaves.at(low_index); - result.low_leaf_witness_data.push_back(LeafUpdateWitnessData( - low_leaf, find_low_leaf_result.index, tree.get_sibling_path(find_low_leaf_result.index))); + result.low_leaf_witness_data.push_back( + LeafUpdateWitnessData(low_leaf, find_low_leaf_result.index, tree.get_sibling_path(low_index))); if (!find_low_leaf_result.is_already_present) { // If the leaf is not already present, we point the low leaf to the new leaf and then insert the new leaf. IndexedLeaf new_indexed_leaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); - index_t insertion_index = leaves.size(); + size_t insertion_index = leaves.size(); low_leaf.nextIndex = insertion_index; low_leaf.nextKey = key; FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(find_low_leaf_result.index, low_leaf_hash); + tree.update_element(low_index, low_leaf_hash); append_leaf(new_indexed_leaf); FF new_leaf_hash = HashingPolicy::hash(new_indexed_leaf.get_hash_inputs()); @@ -176,7 +178,7 @@ SequentialInsertionResult IndexedMemoryTree:: // Update the current leaf's value, don't change it's link low_leaf = IndexedLeaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(find_low_leaf_result.index, low_leaf_hash); + tree.update_element(low_index, low_leaf_hash); // Push an empty insertion witness result.insertion_witness_data.push_back( diff --git a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp index 7f4113e93c7..548cf658c2f 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp @@ -1,5 +1,6 @@ #include "barretenberg/vm2/tooling/stats.hpp" +#include #include #include #include diff --git a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt new file mode 100644 index 00000000000..122ee5f5dfb --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt @@ -0,0 +1,4 @@ +# Runs one public transaction through vm2_sim from its hinted inputs. Built natively and under the wasm-eh preset, so +# the two can be compared; under wasm it also checks the simulator's C++ exceptions work. +add_executable(vm2_sim_smoke vm2_sim_smoke.cpp) +target_link_libraries(vm2_sim_smoke PRIVATE vm2_sim env) diff --git a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp new file mode 100644 index 00000000000..dc4981f2f8b --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp @@ -0,0 +1,60 @@ +// Runs one public transaction through vm2_sim from hinted inputs (AvmProvingInputs, msgpack), as `bb avm_simulate` +// does, and first checks that a C++ exception thrown inside the simulator is caught. +// +// Usage: vm2_sim_smoke +#include "barretenberg/vm2/avm_sim_api.hpp" +#include "barretenberg/vm2/common/tagged_value.hpp" + +#include +#include +#include + +using namespace bb::avm2; + +namespace { + +std::vector read_all(const char* path) +{ + std::vector data; + FILE* file = std::fopen(path, "rb"); + if (file == nullptr) { + return data; + } + uint8_t chunk[1 << 16]; + size_t n = 0; + while ((n = std::fread(chunk, 1, sizeof(chunk), file)) > 0) { + data.insert(data.end(), chunk, chunk + n); + } + std::fclose(file); + return data; +} + +} // namespace + +int main(int argc, char** argv) +{ + try { + auto sum = TaggedValue::from(1) + TaggedValue::from(1); + std::printf("FAIL: mismatched tags added without throwing (tag %d)\n", static_cast(sum.get_tag())); + return 1; + } catch (const TagMismatchException& e) { + std::printf("caught TagMismatchException: %s\n", e.what()); + } + + if (argc < 2) { + std::printf("usage: %s \n", argv[0]); + return 2; + } + auto bytes = read_all(argv[1]); + if (bytes.empty()) { + std::printf("FAIL: could not read %s\n", argv[1]); + return 1; + } + auto inputs = AvmSimAPI::ProvingInputs::from(bytes); + auto result = AvmSimAPI().simulate_with_hinted_dbs(inputs); + std::printf("revert_code=%d total_l2_gas=%u total_da_gas=%u\n", + static_cast(result.revert_code), + result.gas_used.total_gas.l2_gas, + result.gas_used.total_gas.da_gas); + return 0; +} diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp index 03dc055724d..5e9046d8328 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp @@ -103,7 +103,7 @@ template class MemoryIndexedTree { leaves_.push_back(Leaf(LeafType::padding(i), /*nextIndex=*/0, /*nextKey=*/0)); } for (size_t i = 0; i < initial_size; ++i) { - index_t next_index = i == (initial_size - 1) ? 0 : i + 1; + size_t next_index = i == (initial_size - 1) ? 0 : i + 1; leaves_[i].nextIndex = next_index; leaves_[i].nextKey = leaves_[next_index].leaf.get_key(); tree_.update_element(i, HashingPolicy::hash(leaves_[i].get_hash_inputs())); @@ -131,7 +131,7 @@ template class MemoryIndexedTree { Leaf get_leaf_preimage(index_t leaf_index) const { BB_ASSERT_LT(leaf_index, leaves_.size(), "Leaf index out of bounds"); - return leaves_[leaf_index]; + return leaves_[static_cast(leaf_index)]; } FF get_leaf_value(index_t leaf_index) const { return tree_.get_node(0, leaf_index); } @@ -149,7 +149,7 @@ template class MemoryIndexedTree { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - Leaf& low_leaf = leaves_[find_low_leaf_result.index]; + Leaf& low_leaf = leaves_[static_cast(find_low_leaf_result.index)]; result.low_leaf_witness_data.emplace_back( low_leaf, find_low_leaf_result.index, tree_.get_sibling_path(find_low_leaf_result.index)); diff --git a/barretenberg/rust/bootstrap.sh b/barretenberg/rust/bootstrap.sh index b49097e57f4..445dcff0d68 100755 --- a/barretenberg/rust/bootstrap.sh +++ b/barretenberg/rust/bootstrap.sh @@ -11,7 +11,7 @@ hash=$(hash_str $(cache_content_hash .rebuild_patterns)) # 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 \ + (cd barretenberg-rs && node --experimental-strip-types --no-warnings \ "$root/ipc-codegen/src/generate.ts" \ --schema "$root/barretenberg/cpp/src/barretenberg/bbapi/bb_schema.json" \ --lang rust \ diff --git a/barretenberg/ts/bb.js/scripts/generate.sh b/barretenberg/ts/bb.js/scripts/generate.sh index 5bb35ba4ca9..f9a0153c8ca 100755 --- a/barretenberg/ts/bb.js/scripts/generate.sh +++ b/barretenberg/ts/bb.js/scripts/generate.sh @@ -10,7 +10,7 @@ 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 \ +node --experimental-strip-types --no-warnings \ "$ROOT/ipc-codegen/src/generate.ts" \ --schema "$BBAPI/bb_schema.json" \ --lang ts \ 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 3c90963fd11..e13674e1a89 100644 --- a/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts +++ b/barretenberg/ts/bb.js/src/bbapi/exception_handling.test.ts @@ -23,9 +23,8 @@ describe('BBApi Exception Handling from bb.js', () => { g2Point: new Uint8Array(10), // Only 10 bytes (needs 128) - will cause exception }; - // In WASM builds, throw_or_abort calls abort directly which throws a generic Error - // In native builds with exceptions, our try-catch in bbapi converts it to ErrorResponse - // This test verifies that errors are catchable from bb.js (even if as generic Error in WASM) + // The wasm builds with exception handling, so bbapi's try-catch converts the C++ exception to an ErrorResponse, + // as it does natively. expect(() => { api.srsInitSrs(invalidCommand); }).toThrow(); @@ -49,4 +48,22 @@ describe('BBApi Exception Handling from bb.js', () => { expect((error as Error).message).toContain('invalid points_buf size'); } }); + + it('answers with an error response rather than aborting the module', async () => { + const logs: string[] = []; + const loggingBackend = await BarretenbergWasmSyncBackend.new(undefined, msg => logs.push(msg)); + try { + expect(() => + new SyncApi(loggingBackend).srsInitSrs({ + numPoints: 100, + pointsBuf: new Uint8Array(10), + g2Point: new Uint8Array(10), + }), + ).toThrow('invalid points_buf size'); + // The JS environment logs this when the module aborts rather than catching the exception itself. + expect(logs.filter(log => log.includes('aborted'))).toEqual([]); + } finally { + loggingBackend.destroy(); + } + }, 60000); }); diff --git a/bootstrap.sh b/bootstrap.sh index dde97c8dd0d..faa016dacf0 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -12,10 +12,10 @@ # Expected toolchain versions. export expected_min_clang_version=20.0.0 export expected_min_cmake_version=3.24 -export expected_min_node_version=24.12.0 +export expected_min_node_version=26.12.0 export expected_min_zig_version=0.15.1 export expected_abs_rust_version=1.89.0 -export expected_abs_wasi_version=27.0 +export expected_abs_wasi_version=34.0 export expected_abs_foundry_version=1.8.5 export expected_abs_yarn_version=4.13.0 @@ -66,8 +66,8 @@ function install_node { curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash . "$HOME/.nvm/nvm.sh" --no-use fi - nvm install --lts - nvm alias default lts/* + nvm install $expected_min_node_version + nvm alias default $expected_min_node_version } function install_node_utils { @@ -204,6 +204,36 @@ function cleanup { } trap cleanup EXIT +### TRANSITIONAL TOOLCHAIN UPGRADE ##################################################################################### +# TODO: Delete this section and its calls (prep, check) once CI runs a build image (and AMI) with the toolchains +# build-images/src/Dockerfile now installs. Until then, upgrade the older image in place on every run: wasi-sdk at +# /opt/wasi-sdk (only builds use it), and node, wasmtime and playwright's chromium under $HOME, which isolated test +# containers mount too, so PATH and PLAYWRIGHT_BROWSERS_PATH are passed through to them. +function transitional_toolchain_upgrade { + [ "$(os)" == "linux" ] && { [ "${CI:-0}" -eq 1 ] || [ "$(whoami)" == "aztec-dev" ]; } || return 0 + local dir=$HOME/.cache/aztec-transitional-toolchains arch=$(uname -m) + local node_version=v$expected_min_node_version wasmtime_version=v46.0.3 playwright_version=1.63.0 + local node_arch=$([ "$arch" == "aarch64" ] && echo arm64 || echo x64) + mkdir -p $dir + install_wasi_sdk + if [ ! -x $dir/node-$node_version/bin/node ]; then + curl -fsSL https://nodejs.org/dist/$node_version/node-$node_version-linux-$node_arch.tar.xz | tar xJ -C $dir + mv $dir/node-$node_version-linux-$node_arch $dir/node-$node_version + fi + if [ ! -x $dir/wasmtime-$wasmtime_version/wasmtime ]; then + curl -fsSL https://github.com/bytecodealliance/wasmtime/releases/download/$wasmtime_version/wasmtime-$wasmtime_version-$arch-linux.tar.xz | tar xJ -C $dir + mv $dir/wasmtime-$wasmtime_version-$arch-linux $dir/wasmtime-$wasmtime_version + fi + export PATH=$dir/node-$node_version/bin:$dir/wasmtime-$wasmtime_version:$PATH + export PLAYWRIGHT_BROWSERS_PATH=$dir/ms-playwright + local out + if ! out=$(npx -y playwright@$playwright_version install chromium --only-shell 2>&1); then + echo "$out" + return 1 + fi + export ENV_VARS_TO_INJECT="${ENV_VARS_TO_INJECT:-} PATH PLAYWRIGHT_BROWSERS_PATH" +} + ### TOOLCHAIN CHECKS ################################################################################################### function check_minimum_version { local min_version=$1 @@ -417,6 +447,7 @@ function stop_txes { } function prep { + transitional_toolchain_upgrade check_toolchains pull_submodules } @@ -853,6 +884,7 @@ case "$cmd" in git clean -ffdx ;; "check") + transitional_toolchain_upgrade check_toolchains echo "Toolchains look good! 🎉" ;; diff --git a/build-images/src/Dockerfile b/build-images/src/Dockerfile index 67dd8ced00c..cd6a6a5e3b2 100644 --- a/build-images/src/Dockerfile +++ b/build-images/src/Dockerfile @@ -61,9 +61,9 @@ RUN curl -L https://foundry.paradigm.xyz | bash \ # We use this image for test isolation. FROM base-build AS build -# APT repo for node (24.15.0) +# APT repo for node (26.12.0) RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -ENV NODE_MAJOR=24 +ENV NODE_MAJOR=26 RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list RUN apt update && \ @@ -102,7 +102,7 @@ RUN apt update && \ pkg-config \ libssl-dev \ # Node - nodejs=24.15.0-1nodesource1 \ + nodejs=26.12.0-1nodesource1 \ # Python (clang bindings for wasm bindgen.) python3 \ python3-clang \ @@ -121,10 +121,10 @@ RUN wget https://apt.llvm.org/llvm.sh && \ # Install wasi-sdk. RUN arch=$(uname -m) && \ if [ "$arch" = "aarch64" ]; then arch="arm64"; fi && \ - wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-27/wasi-sdk-27.0-${arch}-linux.tar.gz && \ - tar xvf wasi-sdk-27.0-${arch}-linux.tar.gz && \ - mv wasi-sdk-27.0-${arch}-linux /opt/wasi-sdk && \ - rm wasi-sdk-27.0-${arch}-linux.tar.gz + wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-34/wasi-sdk-34.0-${arch}-linux.tar.gz && \ + tar xvf wasi-sdk-34.0-${arch}-linux.tar.gz && \ + mv wasi-sdk-34.0-${arch}-linux /opt/wasi-sdk && \ + rm wasi-sdk-34.0-${arch}-linux.tar.gz # Install foundry. COPY --from=foundry /opt/foundry /opt/foundry @@ -154,9 +154,9 @@ RUN curl -fsSL https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_li && chmod +x /usr/local/bin/terraform \ && rm terraform.zip -# Install wasmtime. -RUN curl -fsSL https://github.com/bytecodealliance/wasmtime/releases/download/v20.0.2/wasmtime-v20.0.2-$(uname -m)-linux.tar.xz | tar xJ \ - && mv wasmtime-v20.0.2-$(uname -m)-linux/wasmtime /usr/local/bin \ +# Install wasmtime. 46 is the last release with wasi-threads (-Sthreads), which the threaded wasm tests use. +RUN curl -fsSL https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.3/wasmtime-v46.0.3-$(uname -m)-linux.tar.xz | tar xJ \ + && mv wasmtime-v46.0.3-$(uname -m)-linux/wasmtime /usr/local/bin \ && rm -rf wasmtime* # Install zig. @@ -171,7 +171,7 @@ RUN curl -sL https://github.com/mikefarah/yq/releases/download/v4.42.1/yq_linux_ -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq # Install playwright for browser testing. Ensure accessible for other users. -RUN npx -y playwright@1.49 install --with-deps && mv /root/.cache/ms-playwright /opt/ms-playwright +RUN npx -y playwright@1.63.0 install --with-deps && mv /root/.cache/ms-playwright /opt/ms-playwright ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright @@ -214,7 +214,7 @@ RUN apt update && \ zsh \ inotify-tools \ # Annoyingly unminimize upgrades nodejs. - nodejs=24.15.0-1nodesource1 \ + nodejs=26.12.0-1nodesource1 \ # smt_verification dependencies python3-venv \ python3-pip \ diff --git a/scripts/setup-container.sh b/scripts/setup-container.sh index a55da9c5ca6..02e6fece714 100644 --- a/scripts/setup-container.sh +++ b/scripts/setup-container.sh @@ -94,14 +94,14 @@ apt-get -y autoremove apt-get clean # ============================================================================= -# SECTION 2: Node.js 24 +# SECTION 2: Node.js 26 # ============================================================================= -log_info "Installing Node.js 24..." +log_info "Installing Node.js 26..." curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_26.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list apt-get update -apt-get install -y nodejs=24.15.0-1nodesource1 +apt-get install -y nodejs=26.12.0-1nodesource1 # ============================================================================= # SECTION 3: Clang 18/20 @@ -141,14 +141,14 @@ chmod +x /etc/profile.d/rust.sh # ============================================================================= # SECTION 5: wasi-sdk # ============================================================================= -log_info "Installing wasi-sdk 27..." +log_info "Installing wasi-sdk 34..." arch=$(uname -m) if [ "$arch" = "aarch64" ]; then arch="arm64"; fi -wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-27/wasi-sdk-27.0-${arch}-linux.tar.gz -tar xvf wasi-sdk-27.0-${arch}-linux.tar.gz -mv wasi-sdk-27.0-${arch}-linux /opt/wasi-sdk -rm wasi-sdk-27.0-${arch}-linux.tar.gz +wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-34/wasi-sdk-34.0-${arch}-linux.tar.gz +tar xvf wasi-sdk-34.0-${arch}-linux.tar.gz +mv wasi-sdk-34.0-${arch}-linux /opt/wasi-sdk +rm wasi-sdk-34.0-${arch}-linux.tar.gz # ============================================================================= # SECTION 6: Foundry @@ -209,8 +209,8 @@ curl -sL https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursu # ============================================================================= log_info "Installing wasmtime..." -curl -fsSL https://github.com/bytecodealliance/wasmtime/releases/download/v20.0.2/wasmtime-v20.0.2-$(uname -m)-linux.tar.xz | tar xJ -mv wasmtime-v20.0.2-$(uname -m)-linux/wasmtime /usr/local/bin +curl -fsSL https://github.com/bytecodealliance/wasmtime/releases/download/v46.0.3/wasmtime-v46.0.3-$(uname -m)-linux.tar.xz | tar xJ +mv wasmtime-v46.0.3-$(uname -m)-linux/wasmtime /usr/local/bin rm -rf wasmtime* # ============================================================================= From cc0672541994e74ea1350dd7dbff50fe533b16a1 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:28:17 +0000 Subject: [PATCH 4/6] chore(bb): leave the AVM simulator's wasm build to its own PR This PR keeps to the toolchain and exception handling. The vm2_sim_smoke program, its test, the vm2-in-wasm CMake and the wasm32 fixes in the simulator move to the PR that builds bb-avm-sim as a wasm module for its generated package. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- barretenberg/cpp/CMakePresets.json | 6 +- barretenberg/cpp/bootstrap.sh | 6 -- barretenberg/cpp/scripts/run_wasi_command.mjs | 15 ----- .../cpp/scripts/vm2_sim_smoke_test.sh | 19 ------ barretenberg/cpp/src/CMakeLists.txt | 9 --- .../src/barretenberg/crypto/CMakeLists.txt | 4 +- .../retrieved_bytecodes_tree_check.cpp | 4 +- .../vm2/simulation/gadgets/to_radix.cpp | 2 +- .../written_public_data_slots_tree_check.cpp | 4 +- .../vm2/simulation/lib/contract_crypto.cpp | 2 +- .../simulation/lib/indexed_memory_tree.hpp | 14 ++--- .../src/barretenberg/vm2/tooling/stats.cpp | 1 - .../barretenberg/vm2_sim_smoke/CMakeLists.txt | 4 -- .../vm2_sim_smoke/vm2_sim_smoke.cpp | 60 ------------------- .../memory_merkle_db.hpp | 6 +- 15 files changed, 20 insertions(+), 136 deletions(-) delete mode 100644 barretenberg/cpp/scripts/run_wasi_command.mjs delete mode 100755 barretenberg/cpp/scripts/vm2_sim_smoke_test.sh delete mode 100644 barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt delete mode 100644 barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 07428005b14..8088676132a 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -421,8 +421,7 @@ "CMAKE_FIND_ROOT_PATH_MODE_PACKAGE": "ONLY", "CMAKE_C_COMPILER_WORKS": "ON", "CMAKE_CXX_COMPILER_WORKS": "ON", - "MULTITHREADING": "OFF", - "AVM": "OFF" + "MULTITHREADING": "OFF" } }, { @@ -768,8 +767,7 @@ "targets": [ "barretenberg.wasm", "barretenberg.wasm.gz", - "barretenberg-debug.wasm", - "vm2_sim_smoke" + "barretenberg-debug.wasm" ] }, { diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 769597da18b..58478b1f6a7 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -358,11 +358,6 @@ function test_cmds_wasm_threads { echo "$hash:CPUS=8 HARDWARE_CONCURRENCY=8 barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/ecc_tests" } -function test_cmds_wasm { - # vm2_sim in wasm gives the native result for the same transaction, and catches the exceptions it relies on. - echo "$hash barretenberg/cpp/scripts/vm2_sim_smoke_test.sh" -} - function test_cmds_asan { local prefix="$hash:CPUS=4:MEM=8g" @@ -400,7 +395,6 @@ function test_cmds_nightly { function test_cmds { if [ -z "${1:-}" ]; then test_cmds_native - test_cmds_wasm if [ "$CI_FULL" -eq 1 ]; then test_cmds_wasm_threads test_cmds_asan diff --git a/barretenberg/cpp/scripts/run_wasi_command.mjs b/barretenberg/cpp/scripts/run_wasi_command.mjs deleted file mode 100644 index 639a4a50281..00000000000 --- a/barretenberg/cpp/scripts/run_wasi_command.mjs +++ /dev/null @@ -1,15 +0,0 @@ -// Runs a bb wasi command (a module that imports its memory) under Node's WASI, with `dir` preopened at /data. -// Usage: node run_wasi_command.mjs [args...] -import { readFileSync } from "node:fs"; -import { WASI } from "node:wasi"; - -const [wasmPath, dir, ...args] = process.argv.slice(2); -const module = await WebAssembly.compile(readFileSync(wasmPath)); -const wasi = new WASI({ version: "preview1", args: [wasmPath, ...args], preopens: { "/data": dir }, returnOnExit: true }); -const importObject = wasi.getImportObject(); -const memory = new WebAssembly.Memory({ initial: 512, maximum: 65536 }); -for (const { module: from, name, kind } of WebAssembly.Module.imports(module)) { - if (kind === "memory") (importObject[from] ??= {})[name] = memory; -} -const instance = await WebAssembly.instantiate(module, importObject); -process.exitCode = wasi.start({ exports: { ...instance.exports, memory } }); diff --git a/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh b/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh deleted file mode 100755 index 254701d449d..00000000000 --- a/barretenberg/cpp/scripts/vm2_sim_smoke_test.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Runs the same hinted public transaction through vm2_sim natively and in wasm (under node, so V8), and checks the -# wasm run catches the simulator's C++ exceptions and reaches the native result. -set -euo pipefail -cd $(dirname $0)/.. - -native_bin=$(scripts/preset-build-dir ${NATIVE_PRESET:-clang20})/bin/vm2_sim_smoke -wasm_bin=build-wasm/bin/vm2_sim_smoke -inputs_dir=src/barretenberg/vm2/testing -inputs=minimal_tx.testdata.bin - -native=$($native_bin $inputs_dir/$inputs | grep -E '^(caught|revert_code)') -wasm=$(node --no-warnings scripts/run_wasi_command.mjs $wasm_bin $inputs_dir /data/$inputs | - grep -E '^(caught|revert_code)') - -echo "native: $native" -echo "wasm: $wasm" -[ "$(echo "$wasm" | grep -c '^caught TagMismatchException')" -eq 1 ] -[ "$native" == "$wasm" ] diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index e6f2af8d45d..11967cf84c2 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -123,19 +123,10 @@ if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) # 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/vm2_wsdb) - add_subdirectory(barretenberg/vm2_sim_smoke) add_subdirectory(barretenberg/cdb) add_subdirectory(barretenberg/avm) endif() -if(WASM) - # The AVM simulator, without the AVM prover (the wasm presets set AVM off). - add_subdirectory(barretenberg/aztec) - add_subdirectory(barretenberg/world_state_reference) - add_subdirectory(barretenberg/vm2) - add_subdirectory(barretenberg/vm2_sim_smoke) -endif() - # Pull in ipc-runtime as a C++ dependency. Provides the `ipc_runtime` # library target (static, or INTERFACE under WASM with transport sources # stubbed) that bbapi/wsdb/etc link against for the codegen-emitted diff --git a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt index 63c1926142f..a2911b6b687 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt @@ -12,4 +12,6 @@ add_subdirectory(ecdsa) add_subdirectory(aes128) add_subdirectory(poseidon2) -add_subdirectory(merkle_tree) +if (NOT WASM) + add_subdirectory(merkle_tree) +endif() diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp index 7ab61f3718a..cb96ae66227 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp @@ -17,8 +17,8 @@ bool RetrievedBytecodesTreeCheck::contains(const FF& class_id) { const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(class_id); - auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); - auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); + auto sibling_path = tree.get_sibling_path(low_leaf_index); + auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); indexed_tree_check.assert_read(class_id, std::nullopt, // No siloing diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp index 7976e78a186..232d7ebac52 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp @@ -50,7 +50,7 @@ std::pair, /* truncated */ bool> ToRadix::to_le_radix(const bool truncated = num_limbs < limbs.size(); if (truncated) { - limbs.erase(limbs.begin() + static_cast(num_limbs), limbs.end()); + limbs.erase(limbs.begin() + num_limbs, limbs.end()); } return { limbs, truncated }; diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp index 32b0d6e8e12..e7908f33077 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp @@ -24,8 +24,8 @@ bool WrittenPublicDataSlotsTreeCheck::contains(const AztecAddress& contract_addr const auto& tree = written_public_data_slots_tree_stack.top(); const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(leaf_slot); - auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); - auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); + auto sibling_path = tree.get_sibling_path(low_leaf_index); + auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); indexed_tree_check.assert_read(slot, IndexedTreeSiloingParameters{ diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp index 370aa4bf476..d74895f1def 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp @@ -58,7 +58,7 @@ FF compute_public_bytecode_first_field(size_t bytecode_size) // Fr<0x00000000000000000000000000000000000000000000000000016b480f8411f1> From: max fields in bytes = 3000 * 31 = // 16b48, Dom sep = f8411f1 static_assert(DOM_SEP__PUBLIC_BYTECODE <= UINT32_MAX, "Public bytecode domain separator must fit in 32 bits"); - return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + (uint256_t(bytecode_size) << 32)); + return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + uint256_t(bytecode_size << 32)); } FF compute_public_bytecode_commitment(std::span bytecode) diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp index c2575fb2298..0bd01838ba0 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp @@ -150,22 +150,20 @@ SequentialInsertionResult IndexedMemoryTree:: for (const auto& leaf_to_insert : leaves_to_insert) { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - // index_t is 64-bit; size_t is 32-bit on wasm32. An index into this in-memory tree always fits. - const auto low_index = static_cast(find_low_leaf_result.index); - IndexedLeaf& low_leaf = leaves.at(low_index); + IndexedLeaf& low_leaf = leaves.at(find_low_leaf_result.index); - result.low_leaf_witness_data.push_back( - LeafUpdateWitnessData(low_leaf, find_low_leaf_result.index, tree.get_sibling_path(low_index))); + result.low_leaf_witness_data.push_back(LeafUpdateWitnessData( + low_leaf, find_low_leaf_result.index, tree.get_sibling_path(find_low_leaf_result.index))); if (!find_low_leaf_result.is_already_present) { // If the leaf is not already present, we point the low leaf to the new leaf and then insert the new leaf. IndexedLeaf new_indexed_leaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); - size_t insertion_index = leaves.size(); + index_t insertion_index = leaves.size(); low_leaf.nextIndex = insertion_index; low_leaf.nextKey = key; FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(low_index, low_leaf_hash); + tree.update_element(find_low_leaf_result.index, low_leaf_hash); append_leaf(new_indexed_leaf); FF new_leaf_hash = HashingPolicy::hash(new_indexed_leaf.get_hash_inputs()); @@ -178,7 +176,7 @@ SequentialInsertionResult IndexedMemoryTree:: // Update the current leaf's value, don't change it's link low_leaf = IndexedLeaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(low_index, low_leaf_hash); + tree.update_element(find_low_leaf_result.index, low_leaf_hash); // Push an empty insertion witness result.insertion_witness_data.push_back( diff --git a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp index 548cf658c2f..7f4113e93c7 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp @@ -1,6 +1,5 @@ #include "barretenberg/vm2/tooling/stats.hpp" -#include #include #include #include diff --git a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt deleted file mode 100644 index 122ee5f5dfb..00000000000 --- a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -# Runs one public transaction through vm2_sim from its hinted inputs. Built natively and under the wasm-eh preset, so -# the two can be compared; under wasm it also checks the simulator's C++ exceptions work. -add_executable(vm2_sim_smoke vm2_sim_smoke.cpp) -target_link_libraries(vm2_sim_smoke PRIVATE vm2_sim env) diff --git a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp b/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp deleted file mode 100644 index dc4981f2f8b..00000000000 --- a/barretenberg/cpp/src/barretenberg/vm2_sim_smoke/vm2_sim_smoke.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// Runs one public transaction through vm2_sim from hinted inputs (AvmProvingInputs, msgpack), as `bb avm_simulate` -// does, and first checks that a C++ exception thrown inside the simulator is caught. -// -// Usage: vm2_sim_smoke -#include "barretenberg/vm2/avm_sim_api.hpp" -#include "barretenberg/vm2/common/tagged_value.hpp" - -#include -#include -#include - -using namespace bb::avm2; - -namespace { - -std::vector read_all(const char* path) -{ - std::vector data; - FILE* file = std::fopen(path, "rb"); - if (file == nullptr) { - return data; - } - uint8_t chunk[1 << 16]; - size_t n = 0; - while ((n = std::fread(chunk, 1, sizeof(chunk), file)) > 0) { - data.insert(data.end(), chunk, chunk + n); - } - std::fclose(file); - return data; -} - -} // namespace - -int main(int argc, char** argv) -{ - try { - auto sum = TaggedValue::from(1) + TaggedValue::from(1); - std::printf("FAIL: mismatched tags added without throwing (tag %d)\n", static_cast(sum.get_tag())); - return 1; - } catch (const TagMismatchException& e) { - std::printf("caught TagMismatchException: %s\n", e.what()); - } - - if (argc < 2) { - std::printf("usage: %s \n", argv[0]); - return 2; - } - auto bytes = read_all(argv[1]); - if (bytes.empty()) { - std::printf("FAIL: could not read %s\n", argv[1]); - return 1; - } - auto inputs = AvmSimAPI::ProvingInputs::from(bytes); - auto result = AvmSimAPI().simulate_with_hinted_dbs(inputs); - std::printf("revert_code=%d total_l2_gas=%u total_da_gas=%u\n", - static_cast(result.revert_code), - result.gas_used.total_gas.l2_gas, - result.gas_used.total_gas.da_gas); - return 0; -} diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp index 5e9046d8328..03dc055724d 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp @@ -103,7 +103,7 @@ template class MemoryIndexedTree { leaves_.push_back(Leaf(LeafType::padding(i), /*nextIndex=*/0, /*nextKey=*/0)); } for (size_t i = 0; i < initial_size; ++i) { - size_t next_index = i == (initial_size - 1) ? 0 : i + 1; + index_t next_index = i == (initial_size - 1) ? 0 : i + 1; leaves_[i].nextIndex = next_index; leaves_[i].nextKey = leaves_[next_index].leaf.get_key(); tree_.update_element(i, HashingPolicy::hash(leaves_[i].get_hash_inputs())); @@ -131,7 +131,7 @@ template class MemoryIndexedTree { Leaf get_leaf_preimage(index_t leaf_index) const { BB_ASSERT_LT(leaf_index, leaves_.size(), "Leaf index out of bounds"); - return leaves_[static_cast(leaf_index)]; + return leaves_[leaf_index]; } FF get_leaf_value(index_t leaf_index) const { return tree_.get_node(0, leaf_index); } @@ -149,7 +149,7 @@ template class MemoryIndexedTree { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - Leaf& low_leaf = leaves_[static_cast(find_low_leaf_result.index)]; + Leaf& low_leaf = leaves_[find_low_leaf_result.index]; result.low_leaf_witness_data.emplace_back( low_leaf, find_low_leaf_result.index, tree_.get_sibling_path(find_low_leaf_result.index)); From a9a59c7ce537336bfbf869fcaef01def3c3a8560 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:52:47 +0000 Subject: [PATCH 5/6] feat(bb-avm-sim): a wasm backend for the bb-avm-sim package bb-avm-sim.wasm is the AVM simulator as a WASI reactor exporting the generated FFI entry (avm_ipc_ffi_entry), so @aztec-foundation/bb-avm-sim runs the spawned binary in node and the wasm module in browsers, as bb.js-api does for bb. The module has no route to world state or contract data yet, so it serves SimulateWithHints and answers Simulate with an error. bb-avm-sim's --wsdb and --cdb become optional on the same terms. - avm_ffi: the FFI entry and its context, built natively and for wasm. avm_ffi_tests calls the entry with vm2's minimal_tx fixture and checks the result against the public inputs it carries; it runs natively and, from the threads build, under wasmtime like ecc_tests. - The wasm build adds aztec, world_state_reference, vm2 (the simulator alone, AVM off) and avm, with the simulator's wasm32 fixes (a 32-bit shift in contract_crypto, size_t narrowing in the indexed trees). - The package's test runs one transaction through the binary, the wasm worker and the synchronous wasm service and requires the same result. - ipc-runtime's prepare_arch_packages reads a string bin, the form yarn rewrites the generated one-entry bin map to on install; it staged the binary as a file named 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- Makefile | 12 +- barretenberg/cpp/CMakePresets.json | 7 +- barretenberg/cpp/bootstrap.sh | 7 ++ barretenberg/cpp/src/CMakeLists.txt | 5 + .../cpp/src/barretenberg/avm/CMakeLists.txt | 65 +++++++++- .../cpp/src/barretenberg/avm/avm_execute.cpp | 21 ++-- .../cpp/src/barretenberg/avm/avm_execute.hpp | 7 +- .../cpp/src/barretenberg/avm/avm_ffi.test.cpp | 112 ++++++++++++++++++ .../barretenberg/avm/avm_ffi_dispatcher.cpp | 46 +++++++ .../src/barretenberg/avm/avm_ipc_server.cpp | 14 ++- .../src/barretenberg/avm/avm_ipc_server.hpp | 7 +- .../avm/avm_simulate_with_hints.cpp | 17 +++ .../avm/avm_simulate_with_hints.hpp | 16 +++ barretenberg/cpp/src/barretenberg/avm/cli.cpp | 4 +- .../src/barretenberg/crypto/CMakeLists.txt | 4 +- .../cpp/src/barretenberg/vm2/CLAUDE.md | 2 + .../retrieved_bytecodes_tree_check.cpp | 4 +- .../vm2/simulation/gadgets/to_radix.cpp | 2 +- .../written_public_data_slots_tree_check.cpp | 4 +- .../vm2/simulation/lib/contract_crypto.cpp | 2 +- .../simulation/lib/indexed_memory_tree.hpp | 14 ++- .../src/barretenberg/vm2/tooling/stats.cpp | 1 + .../memory_merkle_db.hpp | 6 +- barretenberg/ts/bootstrap.sh | 27 ++++- barretenberg/ts/scripts/bb_avm_sim.test.mjs | 94 +++++++++++++++ barretenberg/ts/scripts/bb_avm_sim_test.sh | 6 + .../ts/scripts/prepare_arch_packages.mjs | 7 +- 27 files changed, 459 insertions(+), 54 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/avm/avm_ffi.test.cpp create mode 100644 barretenberg/cpp/src/barretenberg/avm/avm_ffi_dispatcher.cpp create mode 100644 barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.cpp create mode 100644 barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.hpp create mode 100644 barretenberg/ts/scripts/bb_avm_sim.test.mjs create mode 100755 barretenberg/ts/scripts/bb_avm_sim_test.sh diff --git a/Makefile b/Makefile index eb1d841e6fa..70a6cffd7e3 100644 --- a/Makefile +++ b/Makefile @@ -263,7 +263,7 @@ bb-ts-cross-copy: bb-ts bb-cpp-cross # the same barretenberg/ts node_modules. bb-ts additionally emits the bb.js test commands, # which the test engine runs concurrently with the rest of the build out of that same tree, # so an unordered npm_install_deps here swaps files under a running node (SIGBUS). -bb-avm-sim: ipc-codegen ipc-runtime bb-cpp-native bb-ts +bb-avm-sim: ipc-codegen ipc-runtime bb-cpp-native bb-cpp-wasm bb-ts $(call build,$@,barretenberg/ts,build_bb_avm_sim) # Ordered after bb-cdb (and after bb-ts-cross-copy, which builds bb.js-api's cross copies) @@ -312,6 +312,9 @@ bb-sol: bb-cpp-native bb-crs l1-contracts-solc bb-cpp-native-tests: bb-cpp-native bb-cpp-chonk-inputs $(call test,$@,barretenberg/cpp,native) +bb-cpp-wasm-tests: bb-cpp-wasm-threads + $(call test,$@,barretenberg/cpp,wasm) + bb-cpp-wasm-threads-tests: bb-cpp-wasm-threads $(call test,$@,barretenberg/cpp,wasm_threads) @@ -327,6 +330,11 @@ bb-acir-tests: bb-acir bb-ts-tests: bb-ts $(call test,$@,barretenberg/ts) +# After bb-cdb, the last of the barretenberg/ts targets to install into node_modules, which the +# tests run out of. +bb-avm-sim-tests: bb-avm-sim bb-cdb + $(call test,$@,barretenberg/ts,bb_avm_sim) + bb-sol-tests: bb-sol $(call test,$@,barretenberg/sol) @@ -339,7 +347,7 @@ bb-bbup-tests: bb-bbup bb-rs-tests: bb-rs $(call test,$@,barretenberg/rust) -bb-tests: bb-cpp-native-tests bb-acir-tests bb-ts-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests +bb-tests: bb-cpp-native-tests bb-cpp-wasm-tests bb-acir-tests bb-ts-tests bb-avm-sim-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests bb-full-tests: bb-cpp-wasm-threads-tests bb-cpp-asan-tests bb-cpp-smt-tests diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 8088676132a..066300d6673 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -421,7 +421,8 @@ "CMAKE_FIND_ROOT_PATH_MODE_PACKAGE": "ONLY", "CMAKE_C_COMPILER_WORKS": "ON", "CMAKE_CXX_COMPILER_WORKS": "ON", - "MULTITHREADING": "OFF" + "MULTITHREADING": "OFF", + "AVM": "OFF" } }, { @@ -767,7 +768,8 @@ "targets": [ "barretenberg.wasm", "barretenberg.wasm.gz", - "barretenberg-debug.wasm" + "barretenberg-debug.wasm", + "bb-avm-sim.wasm" ] }, { @@ -787,6 +789,7 @@ "barretenberg.wasm.gz", "barretenberg-debug.wasm", "ecc_tests", + "avm_ffi_tests", "ultra_honk_bench", "bb" ] diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 58478b1f6a7..8b9c9275ba1 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -358,6 +358,12 @@ function test_cmds_wasm_threads { echo "$hash:CPUS=8 HARDWARE_CONCURRENCY=8 barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/ecc_tests" } +function test_cmds_wasm { + # The AVM simulator's in-process service, as bb-avm-sim.wasm runs it. From the threads build, whose shared memory + # wasmtime provides; the single-thread build imports a memory only a JS host supplies. + echo "$hash barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/avm_ffi_tests" +} + function test_cmds_asan { local prefix="$hash:CPUS=4:MEM=8g" @@ -395,6 +401,7 @@ function test_cmds_nightly { function test_cmds { if [ -z "${1:-}" ]; then test_cmds_native + test_cmds_wasm if [ "$CI_FULL" -eq 1 ]; then test_cmds_wasm_threads test_cmds_asan diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index ad492f8682b..caf89a6243a 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -119,6 +119,11 @@ add_subdirectory(barretenberg/vm2_stub) # consumes wasi_objects, so there is nothing to build here otherwise. if(WASM) add_subdirectory(barretenberg/wasi) + # The AVM simulator, without the AVM prover (the wasm presets set AVM off), as bb-avm-sim.wasm. + add_subdirectory(barretenberg/aztec) + add_subdirectory(barretenberg/world_state_reference) + add_subdirectory(barretenberg/vm2) + add_subdirectory(barretenberg/avm) endif() if(NOT FUZZING AND NOT WASM AND NOT BB_LITE) diff --git a/barretenberg/cpp/src/barretenberg/avm/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/avm/CMakeLists.txt index d05f6c59131..dfe69d9bfde 100644 --- a/barretenberg/cpp/src/barretenberg/avm/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/avm/CMakeLists.txt @@ -1,6 +1,16 @@ +# bb-avm-sim: the AVM simulator as a service over avm_schema.json. Two artifacts carry it to +# @aztec-foundation/bb-avm-sim (barretenberg/ts/bb-avm-sim): +# - the bb-avm-sim binary (native builds), served over IPC, reaching world state and contract +# data through its wsdb and cdb IPC clients; +# - bb-avm-sim.wasm, a WASI reactor (wasm builds) exporting the generated FFI entry +# (avm_ipc_ffi_entry). It has no route to world state or contract data yet, so it serves +# SimulateWithHints alone. set(AVM_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/avm_schema.json) set(AVM_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) set(AVM_GEN_OUTPUTS + ${AVM_GEN_DIR}/avm_dispatch.hpp + ${AVM_GEN_DIR}/avm_ffi.cpp + ${AVM_GEN_DIR}/avm_ffi.hpp ${AVM_GEN_DIR}/avm_ipc_server.hpp ${AVM_GEN_DIR}/avm_types.hpp ${AVM_GEN_DIR}/ipc_codegen/msgpack_adaptor.hpp @@ -20,16 +30,68 @@ add_custom_command( --lang cpp --out ${AVM_GEN_DIR} --server + --ffi --cpp-namespace bb::avm --cpp-include-dir barretenberg/avm/generated --prefix Avm --strip-method-prefix DEPENDS ${AVM_SCHEMA} ${IPC_CODEGEN_SRC} - COMMENT "Generating AVM IPC server from avm_schema.json" + COMMENT "Generating the AVM IPC server and FFI entry from avm_schema.json" VERBATIM ) add_custom_target(avm_ipc_generated DEPENDS ${AVM_GEN_OUTPUTS}) +# The tests read vm2's minimal transaction fixture, embedded so the wasm build needs no filesystem. +set(MINIMAL_TX_FIXTURE ${CMAKE_CURRENT_SOURCE_DIR}/../vm2/testing/minimal_tx.testdata.bin) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${MINIMAL_TX_FIXTURE}) +file(READ ${MINIMAL_TX_FIXTURE} MINIMAL_TX_HEX HEX) +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," MINIMAL_TX_BYTES "${MINIMAL_TX_HEX}") +set(AVM_TEST_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/include) +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/minimal_tx_fixture.hpp.tmp + "#pragma once\n// Generated by barretenberg/avm/CMakeLists.txt from vm2/testing/minimal_tx.testdata.bin.\n" + "inline constexpr unsigned char MINIMAL_TX_FIXTURE[] = { ${MINIMAL_TX_BYTES} };\n") +# Copied only when it changes, so a reconfigure does not rebuild the tests. +configure_file(${CMAKE_CURRENT_BINARY_DIR}/minimal_tx_fixture.hpp.tmp + ${AVM_TEST_INCLUDE_DIR}/barretenberg/avm/minimal_tx_fixture.hpp COPYONLY) + +# The service as a library over the generated FFI entry, which the wasm reactor exports and the +# tests call. The entry is generated at build time, so a clean tree's configure cannot glob it. +barretenberg_module_with_sources( + avm_ffi + SOURCE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/avm_ffi_dispatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/avm_simulate_with_hints.cpp + ${AVM_GEN_DIR}/avm_ffi.cpp + HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/avm_simulate_with_hints.hpp + TEST_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/avm_ffi.test.cpp + DEPENDENCIES vm2_sim +) +add_dependencies(avm_ffi_objects avm_ipc_generated) +if(TARGET avm_ffi_test_objects) + add_dependencies(avm_ffi_test_objects avm_ipc_generated) + target_include_directories(avm_ffi_test_objects PRIVATE ${AVM_TEST_INCLUDE_DIR}) +endif() + +if(WASM) + # A WASI reactor exporting the FFI entry: the module and its dependencies plus barretenberg/wasi + # (the env functions over WASI, and _initialize). It asks its host for WASI alone. + add_executable( + bb-avm-sim-debug.wasm + $ + $ + ) + target_link_libraries(bb-avm-sim-debug.wasm PRIVATE vm2_sim) + target_link_options(bb-avm-sim-debug.wasm PRIVATE -nostartfiles -Wl,--no-entry,--export-dynamic) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/bin/bb-avm-sim.wasm + COMMAND ${CMAKE_STRIP} ${CMAKE_BINARY_DIR}/bin/bb-avm-sim-debug.wasm -o ${CMAKE_BINARY_DIR}/bin/bb-avm-sim.wasm + DEPENDS bb-avm-sim-debug.wasm + COMMENT "Stripping debug info to create bb-avm-sim.wasm" + ) + add_custom_target(bb-avm-sim.wasm ALL DEPENDS ${CMAKE_BINARY_DIR}/bin/bb-avm-sim.wasm) + return() +endif() + add_executable( bb-avm-sim main.cpp @@ -47,6 +109,7 @@ target_link_libraries( bb-avm-sim PRIVATE barretenberg + avm_ffi vm2_sim wsdb_ipc_merkle_db cdb_ipc_client diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp index b5c77675232..994fef8f2f5 100644 --- a/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp +++ b/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp @@ -1,8 +1,8 @@ #include "barretenberg/avm/avm_execute.hpp" +#include "barretenberg/avm/avm_simulate_with_hints.hpp" #include "barretenberg/common/log.hpp" #include "barretenberg/serialize/msgpack.hpp" #include "barretenberg/serialize/msgpack_impl.hpp" -#include "barretenberg/vm2/avm_sim_api.hpp" #include "barretenberg/vm2/common/avm_io.hpp" #include "barretenberg/vm2/simulation/lib/cancellation_token.hpp" #include "barretenberg/vm2/simulation_helper.hpp" @@ -52,6 +52,10 @@ template static T deserialize_from_msgpack(const std::vector void handle_simulate(AvmRequest& request, wire::AvmSimulate&& command, Responder respond) { + if (request.cdb_client == nullptr || request.wsdb_client == nullptr) { + respond.error("Simulate needs bb-avm-sim started with --wsdb and --cdb"); + return; + } // Reuse the process-lifetime token (cleared of any prior cancellation) instead // of allocating a per-request one the signal handler could outlive. g_sim_cancellation_token->reset(); @@ -64,7 +68,7 @@ void handle_simulate(AvmRequest& request, wire::AvmSimulate&& command, Responder uint64_t fork_id = sim_inputs.ws_revision.forkId; vinfo("Using external WSDB fork ", fork_id, " for AVM simulation"); - request.cdb_client.set_fork_id(fork_id); + request.cdb_client->set_fork_id(fork_id); g_active_cancellation_token.store(cancellation_token.get(), std::memory_order_release); @@ -76,18 +80,18 @@ void handle_simulate(AvmRequest& request, wire::AvmSimulate&& command, Responder .includeUncommitted = true, }; - bb::avm2::simulation::WsdbIpcMerkleDB merkle_db(request.wsdb_client, revision); + bb::avm2::simulation::WsdbIpcMerkleDB merkle_db(*request.wsdb_client, revision); AvmSimulationHelper simulation_helper; auto result = sim_inputs.config.collect_hints - ? simulation_helper.simulate_for_hint_collection_internal(request.cdb_client, + ? simulation_helper.simulate_for_hint_collection_internal(*request.cdb_client, merkle_db, sim_inputs.config, sim_inputs.tx, sim_inputs.global_variables, sim_inputs.protocol_contracts, cancellation_token) - : simulation_helper.simulate_fast_internal(request.cdb_client, + : simulation_helper.simulate_fast_internal(*request.cdb_client, merkle_db, sim_inputs.config, sim_inputs.tx, @@ -112,12 +116,7 @@ void handle_simulate_with_hints(AvmRequest& request, (void)request; try { - auto proving_inputs = deserialize_from_msgpack(command.inputs); - - AvmSimAPI api; - auto result = api.simulate_with_hinted_dbs(proving_inputs); - - respond.ok(wire::AvmSimulateWithHintsResponse{ .result = serialize_to_msgpack(result) }); + respond.ok(wire::AvmSimulateWithHintsResponse{ .result = simulate_with_hints(command.inputs) }); } catch (const std::exception& e) { respond.error(e.what()); } diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_execute.hpp b/barretenberg/cpp/src/barretenberg/avm/avm_execute.hpp index 37f2a85dd5c..c9a5764eb5c 100644 --- a/barretenberg/cpp/src/barretenberg/avm/avm_execute.hpp +++ b/barretenberg/cpp/src/barretenberg/avm/avm_execute.hpp @@ -19,11 +19,12 @@ extern std::atomic g_active_cancellation_t /** * @brief Context passed to each command's execute() method. - * Provides access to WSDB and CDB IPC clients. + * Provides access to WSDB and CDB IPC clients. Both are null when bb-avm-sim runs without them, which leaves + * SimulateWithHints alone working. */ struct AvmRequest { - cdb::CdbIpcContractDB& cdb_client; - wsdb::WsdbIpcClient& wsdb_client; + cdb::CdbIpcContractDB* cdb_client = nullptr; + wsdb::WsdbIpcClient* wsdb_client = nullptr; }; } // namespace bb::avm diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_ffi.test.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_ffi.test.cpp new file mode 100644 index 00000000000..371be43aaca --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/avm/avm_ffi.test.cpp @@ -0,0 +1,112 @@ +#include "barretenberg/avm/generated/avm_ffi.hpp" +#include "barretenberg/avm/minimal_tx_fixture.hpp" +#include "barretenberg/serialize/msgpack_impl.hpp" +#include "barretenberg/vm2/common/avm_io.hpp" +#include "barretenberg/vm2/common/tagged_value.hpp" + +#include + +#include +#include + +using namespace bb; +using namespace bb::avm; + +namespace { + +const std::vector MINIMAL_TX(std::begin(MINIMAL_TX_FIXTURE), std::end(MINIMAL_TX_FIXTURE)); + +/** A decoded response frame: its tag, and the payload (or the service's error message). */ +struct Response { + std::string tag; + std::vector result; + std::string error; +}; + +/** Call the service through the generated in-process FFI entry: the path the wasm transport takes. */ +Response call_ffi(const char* name, const auto& cmd) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(name)); + pk.pack(cmd); + + uint8_t* output = nullptr; + size_t output_len = 0; + avm_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); + auto unpacked = msgpack::unpack(reinterpret_cast(output), output_len); + avm_ipc_ffi_free(output); + + auto obj = unpacked.get(); + Response response{ .tag = std::string(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size) }; + if (response.tag == "AvmErrorResponse") { + wire::AvmErrorResponse error; + obj.via.array.ptr[1].convert(error); + response.error = error.message; + } else { + wire::AvmSimulateWithHintsResponse ok; + obj.via.array.ptr[1].convert(ok); + response.result = ok.result; + } + return response; +} + +} // namespace + +// The fixture carries the public inputs the transaction produced, so the simulation's outputs can be checked against +// them. A build where a wider type narrows (size_t is 32-bit on wasm32) gets them wrong. +TEST(AvmFfi, SimulateWithHintsMatchesTheFixturesPublicInputs) +{ + auto response = call_ffi("AvmSimulateWithHints", wire::AvmSimulateWithHints{ .inputs = MINIMAL_TX }); + ASSERT_EQ(response.tag, "AvmSimulateWithHintsResponse") << response.error; + + avm2::TxSimulationResult result; + msgpack::unpack(reinterpret_cast(response.result.data()), response.result.size()) + .get() + .convert(result); + auto expected = avm2::AvmProvingInputs::from(MINIMAL_TX).public_inputs; + EXPECT_EQ(result.revert_code == avm2::RevertCode::OK, !expected.reverted); + EXPECT_EQ(result.gas_used.total_gas, expected.end_gas_used); + EXPECT_EQ(result.public_tx_effect.transaction_fee, expected.transaction_fee); + EXPECT_EQ(result.public_tx_effect.note_hashes.size(), expected.accumulated_data_array_lengths.note_hashes); + EXPECT_EQ(result.public_tx_effect.nullifiers.size(), expected.accumulated_data_array_lengths.nullifiers); + EXPECT_EQ(result.public_tx_effect.public_data_writes.size(), + expected.accumulated_data_array_lengths.public_data_writes); +} + +// The exceptions the simulator signals exceptional halts with are caught where it expects, including in wasm. +TEST(AvmFfi, SimulatorExceptionsAreCaught) +{ + EXPECT_THROW(avm2::TaggedValue::from(1) + avm2::TaggedValue::from(1), + avm2::TagMismatchException); +} + +// Inputs that fail to decode throw inside the simulator, and the entry answers with an error rather than ending the +// module, so the next call still succeeds. +TEST(AvmFfi, MalformedInputsGetAnErrorAndTheEntryKeepsAnswering) +{ + auto bad = call_ffi("AvmSimulateWithHints", wire::AvmSimulateWithHints{ .inputs = { 1, 2, 3 } }); + EXPECT_EQ(bad.tag, "AvmErrorResponse"); + EXPECT_FALSE(bad.error.empty()); + + auto good = call_ffi("AvmSimulateWithHints", wire::AvmSimulateWithHints{ .inputs = MINIMAL_TX }); + EXPECT_EQ(good.tag, "AvmSimulateWithHintsResponse") << good.error; +} + +TEST(AvmFfi, SimulateAnswersWithAnError) +{ + auto response = call_ffi("AvmSimulate", wire::AvmSimulate{ .inputs = MINIMAL_TX }); + EXPECT_EQ(response.tag, "AvmErrorResponse"); + EXPECT_NE(response.error.find("SimulateWithHints"), std::string::npos) << response.error; +} + +// wasm does not find main() in gtest_main (see ecc_tests' secp256r1.test.cpp). +#ifdef __wasm__ +GTEST_API_ int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_ffi_dispatcher.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_ffi_dispatcher.cpp new file mode 100644 index 00000000000..a76d52f9eab --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/avm/avm_ffi_dispatcher.cpp @@ -0,0 +1,46 @@ +/** + * @file avm_ffi_dispatcher.cpp + * @brief Names the context the AVM handlers run over in the in-process FFI entry (avm_ipc_ffi_entry), the entry + * bb-avm-sim.wasm exports. + * + * The in-process build has no route to a world state or contract DB yet, so it serves SimulateWithHints, whose hints + * answer every read, and answers Simulate with an error. + */ +#include "barretenberg/avm/avm_simulate_with_hints.hpp" +#include "barretenberg/avm/generated/avm_ffi.hpp" + +namespace bb::avm { + +namespace { +struct AvmFfiRequest {}; +} // namespace + +template <> +void handle_simulate(AvmFfiRequest& /*request*/, + wire::AvmSimulate&& /*command*/, + Responder respond) +{ + respond.error("Simulate needs a world state and contract DB, which the in-process bb-avm-sim cannot reach yet; " + "SimulateWithHints needs neither"); +} + +template <> +void handle_simulate_with_hints(AvmFfiRequest& /*request*/, + wire::AvmSimulateWithHints&& command, + Responder respond) +{ + respond.ok(wire::AvmSimulateWithHintsResponse{ .result = simulate_with_hints(command.inputs) }); +} + +/** + * @brief The dispatcher behind the generated in-process FFI entry (see avm_ffi.hpp). The context is stateless, so one + * serves every call. + */ +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + static AvmFfiRequest request; + static AsyncDispatchHandler handler = make_avm_handler(request); + return handler; +} + +} // namespace bb::avm diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.cpp index 197840afd7e..a5830f871aa 100644 --- a/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.cpp +++ b/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.cpp @@ -60,9 +60,11 @@ int execute_avm_server(const std::string& input_path, const std::string& wsdb_pa constexpr int max_retries = 600; constexpr int retry_delay_ms = 100; - info("Connecting to aztec-wsdb at ", wsdb_path); std::unique_ptr wsdb_client; - for (int attempt = 0; attempt < max_retries; ++attempt) { + if (!wsdb_path.empty()) { + info("Connecting to aztec-wsdb at ", wsdb_path); + } + for (int attempt = 0; !wsdb_path.empty() && attempt < max_retries; ++attempt) { try { wsdb_client = std::make_unique(wsdb_path); break; @@ -75,9 +77,11 @@ int execute_avm_server(const std::string& input_path, const std::string& wsdb_pa } } - info("Connecting to CDB at ", cdb_path); std::unique_ptr cdb_client; - for (int attempt = 0; attempt < max_retries; ++attempt) { + if (!cdb_path.empty()) { + info("Connecting to CDB at ", cdb_path); + } + for (int attempt = 0; !cdb_path.empty() && attempt < max_retries; ++attempt) { try { cdb_client = std::make_unique(cdb_path); break; @@ -90,7 +94,7 @@ int execute_avm_server(const std::string& input_path, const std::string& wsdb_pa } } - AvmRequest request{ .cdb_client = *cdb_client, .wsdb_client = *wsdb_client }; + AvmRequest request{ .cdb_client = cdb_client.get(), .wsdb_client = wsdb_client.get() }; info("bb-avm-sim IPC server ready"); diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.hpp b/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.hpp index e7164cf3c13..5a1a967611a 100644 --- a/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.hpp +++ b/barretenberg/cpp/src/barretenberg/avm/avm_ipc_server.hpp @@ -8,11 +8,12 @@ namespace bb::avm { * @brief Start the bb-avm-sim IPC server. * * Connects to WSDB and CDB as IPC clients, then runs the server loop - * dispatching incoming simulation commands. + * dispatching incoming simulation commands. Without either path it serves + * SimulateWithHints alone, which reads neither. * * @param input_path IPC path for TS client connections. - * @param wsdb_path IPC path to the running WSDB server. - * @param cdb_path IPC path to the running CDB server. + * @param wsdb_path IPC path to the running WSDB server, or empty. + * @param cdb_path IPC path to the running CDB server, or empty. * @return 0 on success, non-zero on error. */ int execute_avm_server(const std::string& input_path, const std::string& wsdb_path, const std::string& cdb_path); diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.cpp new file mode 100644 index 00000000000..df0e0de371f --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.cpp @@ -0,0 +1,17 @@ +#include "barretenberg/avm/avm_simulate_with_hints.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include "barretenberg/serialize/msgpack_impl.hpp" +#include "barretenberg/vm2/avm_sim_api.hpp" +#include "barretenberg/vm2/common/avm_io.hpp" + +namespace bb::avm { + +std::vector simulate_with_hints(const std::vector& inputs) +{ + auto result = avm2::AvmSimAPI().simulate_with_hinted_dbs(avm2::AvmProvingInputs::from(inputs)); + msgpack::sbuffer buf; + msgpack::pack(buf, result); + return { buf.data(), buf.data() + buf.size() }; +} + +} // namespace bb::avm diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.hpp b/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.hpp new file mode 100644 index 00000000000..afe20ffa34d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/avm/avm_simulate_with_hints.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace bb::avm { + +/** + * @brief Simulate a transaction from its hinted inputs, reading no world state or contract DB. + * + * @param inputs A msgpack AvmProvingInputs, whose hints answer every read the simulation makes. + * @return The msgpack TxSimulationResult. + */ +std::vector simulate_with_hints(const std::vector& inputs); + +} // namespace bb::avm diff --git a/barretenberg/cpp/src/barretenberg/avm/cli.cpp b/barretenberg/cpp/src/barretenberg/avm/cli.cpp index 4b91ccd44f0..d003e65c976 100644 --- a/barretenberg/cpp/src/barretenberg/avm/cli.cpp +++ b/barretenberg/cpp/src/barretenberg/avm/cli.cpp @@ -25,10 +25,10 @@ int parse_and_run_avm(int argc, char* argv[]) msgpack_run_command->add_option("-i,--input", input_path, "IPC socket/shm path (.sock or .shm)")->required(); std::string wsdb_path; - msgpack_run_command->add_option("--wsdb", wsdb_path, "WSDB server IPC path")->required(); + msgpack_run_command->add_option("--wsdb", wsdb_path, "WSDB server IPC path (Simulate needs it)"); std::string cdb_path; - msgpack_run_command->add_option("--cdb", cdb_path, "CDB server IPC path")->required(); + msgpack_run_command->add_option("--cdb", cdb_path, "CDB server IPC path (Simulate needs it)"); // Parse CLI try { diff --git a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt index a2911b6b687..63c1926142f 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/crypto/CMakeLists.txt @@ -12,6 +12,4 @@ add_subdirectory(ecdsa) add_subdirectory(aes128) add_subdirectory(poseidon2) -if (NOT WASM) - add_subdirectory(merkle_tree) -endif() +add_subdirectory(merkle_tree) diff --git a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md index f6cf8749745..69ce78cde46 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md +++ b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md @@ -60,6 +60,8 @@ Configure once: `cmake --preset clang20-assert`. All commands below are from `ba - Build: `cmake --build --preset clang20-assert --target vm2_tests`. Run from `barretenberg/cpp/build`: `./bin/vm2_tests --gtest_filter="*test_name*"`. 3. **`bb-avm-sim`** — Simulation-only IPC server that TS drives. - Build: `cmake --build --preset clang20-assert --target bb-avm-sim`. For TS: from `barretenberg/cpp/`, run `(cd ../ts && ./bootstrap.sh build_bb_avm_sim)` then bootstrap `yarn-project` (see yarn-project/CLAUDE.md). +4. **`bb-avm-sim.wasm`**: the same service in-process, the `@aztec-foundation/bb-avm-sim` package's wasm backend. It serves `SimulateWithHints` only, since it has no route to world state or contract data yet. + - Build: `cmake --preset wasm && cmake --build --preset wasm`. Test: `cmake --preset wasm-threads && cmake --build --preset wasm-threads --target avm_ffi_tests`, then `scripts/wasmtime.sh build-wasm-threads/bin/avm_ffi_tests`; natively `./build/bin/avm_ffi_tests`. ## AVM and TypeScript diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp index cb96ae66227..7ab61f3718a 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/retrieved_bytecodes_tree_check.cpp @@ -17,8 +17,8 @@ bool RetrievedBytecodesTreeCheck::contains(const FF& class_id) { const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(class_id); - auto sibling_path = tree.get_sibling_path(low_leaf_index); - auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); + auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); + auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); indexed_tree_check.assert_read(class_id, std::nullopt, // No siloing diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp index 232d7ebac52..7976e78a186 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/to_radix.cpp @@ -50,7 +50,7 @@ std::pair, /* truncated */ bool> ToRadix::to_le_radix(const bool truncated = num_limbs < limbs.size(); if (truncated) { - limbs.erase(limbs.begin() + num_limbs, limbs.end()); + limbs.erase(limbs.begin() + static_cast(num_limbs), limbs.end()); } return { limbs, truncated }; diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp index e7908f33077..32b0d6e8e12 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/written_public_data_slots_tree_check.cpp @@ -24,8 +24,8 @@ bool WrittenPublicDataSlotsTreeCheck::contains(const AztecAddress& contract_addr const auto& tree = written_public_data_slots_tree_stack.top(); const auto snapshot = tree.get_snapshot(); auto [exists, low_leaf_index] = tree.get_low_indexed_leaf(leaf_slot); - auto sibling_path = tree.get_sibling_path(low_leaf_index); - auto low_leaf_preimage = tree.get_leaf_preimage(low_leaf_index); + auto sibling_path = tree.get_sibling_path(static_cast(low_leaf_index)); + auto low_leaf_preimage = tree.get_leaf_preimage(static_cast(low_leaf_index)); indexed_tree_check.assert_read(slot, IndexedTreeSiloingParameters{ diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp index d74895f1def..370aa4bf476 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/contract_crypto.cpp @@ -58,7 +58,7 @@ FF compute_public_bytecode_first_field(size_t bytecode_size) // Fr<0x00000000000000000000000000000000000000000000000000016b480f8411f1> From: max fields in bytes = 3000 * 31 = // 16b48, Dom sep = f8411f1 static_assert(DOM_SEP__PUBLIC_BYTECODE <= UINT32_MAX, "Public bytecode domain separator must fit in 32 bits"); - return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + uint256_t(bytecode_size << 32)); + return FF(uint256_t(DOM_SEP__PUBLIC_BYTECODE) + (uint256_t(bytecode_size) << 32)); } FF compute_public_bytecode_commitment(std::span bytecode) diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp index 0bd01838ba0..c2575fb2298 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/indexed_memory_tree.hpp @@ -150,20 +150,22 @@ SequentialInsertionResult IndexedMemoryTree:: for (const auto& leaf_to_insert : leaves_to_insert) { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - IndexedLeaf& low_leaf = leaves.at(find_low_leaf_result.index); + // index_t is 64-bit; size_t is 32-bit on wasm32. An index into this in-memory tree always fits. + const auto low_index = static_cast(find_low_leaf_result.index); + IndexedLeaf& low_leaf = leaves.at(low_index); - result.low_leaf_witness_data.push_back(LeafUpdateWitnessData( - low_leaf, find_low_leaf_result.index, tree.get_sibling_path(find_low_leaf_result.index))); + result.low_leaf_witness_data.push_back( + LeafUpdateWitnessData(low_leaf, find_low_leaf_result.index, tree.get_sibling_path(low_index))); if (!find_low_leaf_result.is_already_present) { // If the leaf is not already present, we point the low leaf to the new leaf and then insert the new leaf. IndexedLeaf new_indexed_leaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); - index_t insertion_index = leaves.size(); + size_t insertion_index = leaves.size(); low_leaf.nextIndex = insertion_index; low_leaf.nextKey = key; FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(find_low_leaf_result.index, low_leaf_hash); + tree.update_element(low_index, low_leaf_hash); append_leaf(new_indexed_leaf); FF new_leaf_hash = HashingPolicy::hash(new_indexed_leaf.get_hash_inputs()); @@ -176,7 +178,7 @@ SequentialInsertionResult IndexedMemoryTree:: // Update the current leaf's value, don't change it's link low_leaf = IndexedLeaf(leaf_to_insert, low_leaf.nextIndex, low_leaf.nextKey); FF low_leaf_hash = HashingPolicy::hash(low_leaf.get_hash_inputs()); - tree.update_element(find_low_leaf_result.index, low_leaf_hash); + tree.update_element(low_index, low_leaf_hash); // Push an empty insertion witness result.insertion_witness_data.push_back( diff --git a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp index 7f4113e93c7..548cf658c2f 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp +++ b/barretenberg/cpp/src/barretenberg/vm2/tooling/stats.cpp @@ -1,5 +1,6 @@ #include "barretenberg/vm2/tooling/stats.hpp" +#include #include #include #include diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp index 03dc055724d..5e9046d8328 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp @@ -103,7 +103,7 @@ template class MemoryIndexedTree { leaves_.push_back(Leaf(LeafType::padding(i), /*nextIndex=*/0, /*nextKey=*/0)); } for (size_t i = 0; i < initial_size; ++i) { - index_t next_index = i == (initial_size - 1) ? 0 : i + 1; + size_t next_index = i == (initial_size - 1) ? 0 : i + 1; leaves_[i].nextIndex = next_index; leaves_[i].nextKey = leaves_[next_index].leaf.get_key(); tree_.update_element(i, HashingPolicy::hash(leaves_[i].get_hash_inputs())); @@ -131,7 +131,7 @@ template class MemoryIndexedTree { Leaf get_leaf_preimage(index_t leaf_index) const { BB_ASSERT_LT(leaf_index, leaves_.size(), "Leaf index out of bounds"); - return leaves_[leaf_index]; + return leaves_[static_cast(leaf_index)]; } FF get_leaf_value(index_t leaf_index) const { return tree_.get_node(0, leaf_index); } @@ -149,7 +149,7 @@ template class MemoryIndexedTree { FF key = leaf_to_insert.get_key(); GetLowIndexedLeafResponse find_low_leaf_result = get_low_indexed_leaf(key); - Leaf& low_leaf = leaves_[find_low_leaf_result.index]; + Leaf& low_leaf = leaves_[static_cast(find_low_leaf_result.index)]; result.low_leaf_witness_data.emplace_back( low_leaf, find_low_leaf_result.index, tree_.get_sibling_path(find_low_leaf_result.index)); diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index f67f51c915a..e64e4c7ee9f 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -31,8 +31,9 @@ function generate_bb_avm_sim_package { --binary-name "$BB_AVM_SIM_BINARY" \ --prefix Avm \ --strip-method-prefix \ - --package-transports uds \ - --package-ipc-path-args 'msgpack,run,--input,{path}' + --package-transports uds,wasm \ + --package-ipc-path-args 'msgpack,run,--input,{path}' \ + --package-wasm-module bb-avm-sim.wasm } # Server binding package for the AVM CDB protocol: generated wire types + @@ -139,6 +140,13 @@ function build_bb_js_api { prepare_bb_js_api_arch_packages "$(arch)-$(os)=build/$(arch)-$(os)/bb" } +# The single-thread module only: the simulator runs no threads. +function copy_bb_avm_sim_wasm { + rm -rf bb-avm-sim/wasm + mkdir -p bb-avm-sim/wasm + cp "$ROOT/barretenberg/cpp/build-wasm/bin/bb-avm-sim.wasm" bb-avm-sim/wasm/bb-avm-sim.wasm +} + function copy_bb_avm_sim_native { local target_dir="bb-avm-sim/build/$(arch)-$(os)" mkdir -p "$target_dir" @@ -173,6 +181,7 @@ function build_bb_js { function build_bb_avm_sim { echo_header "bb-avm-sim package build" generate_packages + copy_bb_avm_sim_wasm copy_bb_avm_sim_native npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$BB_AVM_SIM_PACKAGE" build @@ -230,8 +239,17 @@ function build { build_cdb } +# `test_cmds bb_avm_sim` lists the bb-avm-sim package's tests alone: they need its build +# (bb-avm-sim), which bb.js's tests do not. function test_cmds { - (cd bb.js && ./bootstrap.sh test_cmds) + case "${1:-}" in + bb_avm_sim) + echo "$hash barretenberg/ts/scripts/bb_avm_sim_test.sh" + ;; + *) + (cd bb.js && ./bootstrap.sh test_cmds) + ;; + esac } function bench_cmds { @@ -280,6 +298,7 @@ function get_projects { function release_bb_avm_sim { generate_packages + copy_bb_avm_sim_wasm copy_bb_avm_sim_native copy_bb_avm_sim_cross npm_install_deps "$IPC_RUNTIME_PKG" @@ -336,7 +355,7 @@ function release { release_bb_bin } -export -f generate_bb_avm_sim_package copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages +export -f generate_bb_avm_sim_package copy_bb_avm_sim_wasm copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages export -f generate_bb_js_api_package copy_bb_js_api_wasm copy_bb_js_api_native copy_bb_js_api_cross prepare_bb_js_api_arch_packages build_bb_js_api_ts build_bb_js_api export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim cross_copy_bb_js_api release release_cdb release_bb_js_api diff --git a/barretenberg/ts/scripts/bb_avm_sim.test.mjs b/barretenberg/ts/scripts/bb_avm_sim.test.mjs new file mode 100644 index 00000000000..f63e1faa7de --- /dev/null +++ b/barretenberg/ts/scripts/bb_avm_sim.test.mjs @@ -0,0 +1,94 @@ +// Tests of the generated @aztec-foundation/bb-avm-sim package, from its build: one public transaction through the +// spawned bb-avm-sim binary and through the wasm module (the browser's backend) gives the same result. +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { after, before, test } from "node:test"; + +import { + AvmError, + AvmService, + AvmServiceSync, +} from "@aztec-foundation/bb-avm-sim"; +import { unpack } from "msgpackr"; + +const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { + encoding: "utf8", +}).trim(); +// Hinted inputs (AvmProvingInputs), carrying the public inputs the transaction produced. +const inputs = readFileSync( + join( + root, + "barretenberg/cpp/src/barretenberg/vm2/testing/minimal_tx.testdata.bin", + ), +); + +/** The simulation result, less its stats, which include timings. */ +function decodeResult({ result }) { + const { stats, ...rest } = unpack(result); + return rest; +} + +const services = {}; +before(async () => { + // No --wsdb or --cdb: the process then serves SimulateWithHints alone, as the wasm module does. + services.process = await AvmService.create({ backend: "process" }); + services.wasm = await AvmService.create({ backend: "wasm" }); +}); +after(async () => { + await Promise.all( + Object.values(services).map((service) => service.destroy()), + ); +}); + +test("the process and the wasm module give the same result, the one the transaction produced", async () => { + const fromProcess = decodeResult( + await services.process.simulateWithHints({ inputs }), + ); + const fromWasm = decodeResult( + await services.wasm.simulateWithHints({ inputs }), + ); + assert.deepEqual(fromWasm, fromProcess); + + const { publicInputs } = unpack(inputs); + assert.deepEqual(fromWasm.gasUsed.totalGas, publicInputs.endGasUsed); + assert.deepEqual( + Buffer.from(fromWasm.publicTxEffect.transactionFee), + Buffer.from(publicInputs.transactionFee), + ); + assert.equal(fromWasm.revertCode !== 0, publicInputs.reverted); +}); + +test("the synchronous wasm service gives the same result", async () => { + const sync = await AvmServiceSync.create({ backend: "wasm" }); + try { + const fromSync = decodeResult(sync.simulateWithHints({ inputs })); + assert.deepEqual( + fromSync, + decodeResult(await services.process.simulateWithHints({ inputs })), + ); + } finally { + sync.destroy(); + } +}); + +test("a simulation that throws is an AvmError, and the service keeps answering", async () => { + for (const service of Object.values(services)) { + await assert.rejects( + service.simulateWithHints({ inputs: new Uint8Array([1, 2, 3]) }), + AvmError, + ); + decodeResult(await service.simulateWithHints({ inputs })); + } +}); + +test("Simulate, which needs world state and contract data, is an AvmError on both", async () => { + for (const service of Object.values(services)) { + await assert.rejects(service.simulate({ inputs }), (error) => { + assert.ok(error instanceof AvmError); + assert.match(error.message, /Simulate needs/); + return true; + }); + } +}); diff --git a/barretenberg/ts/scripts/bb_avm_sim_test.sh b/barretenberg/ts/scripts/bb_avm_sim_test.sh new file mode 100755 index 00000000000..cdf3e1b9c82 --- /dev/null +++ b/barretenberg/ts/scripts/bb_avm_sim_test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Runs the @aztec-foundation/bb-avm-sim package tests: the spawned bb-avm-sim binary and the wasm module. +# Needs the package built (make bb-avm-sim, or ./bootstrap.sh build_bb_avm_sim here). +set -euo pipefail +cd "$(dirname "$0")/.." +node --test --test-reporter=spec scripts/bb_avm_sim.test.mjs diff --git a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs index 2cf6e38695c..d2cb9e94f89 100755 --- a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs +++ b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs @@ -18,13 +18,14 @@ const PLATFORMS = [ ]; const pkg = JSON.parse(readFileSync('package.json', 'utf8')); -const binaryName = Object.keys(pkg.bin ?? {})[0]; +// The scoped name's last segment, which is what the per-platform directories are named after. +const stem = pkg.name.split('/').pop(); +// A string bin names its command after the package, which is the form yarn rewrites a one-entry map to on install. +const binaryName = typeof pkg.bin === 'string' ? stem : Object.keys(pkg.bin ?? {})[0]; if (!binaryName) { console.error(`prepare_arch_packages: ${pkg.name} declares no bin, so it ships no binary`); process.exit(1); } -// The scoped name's last segment, which is what the per-platform directories are named after. -const stem = pkg.name.split('/').pop(); const overrides = new Map(); for (const arg of process.argv.slice(2)) { From 2effb6904f99c63954fea82dc6090ff452baca00 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:19:10 +0000 Subject: [PATCH 6/6] test(bb): run the AVM wasm tests from the single-threaded build bb links its wasm with --import-memory. Under wasi-threads wasmtime makes that memory, but for a single-threaded build the CLI fills no env.memory, so the tests ran from the threads build. wasmtime.sh's SINGLE_THREADED=1 now preloads a module exporting one, and avm_ffi_tests runs from the build bb-avm-sim.wasm comes from. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- Makefile | 2 +- barretenberg/cpp/CMakePresets.json | 4 ++-- barretenberg/cpp/bootstrap.sh | 5 ++--- barretenberg/cpp/scripts/wasm_env_memory.wat | 3 +++ barretenberg/cpp/scripts/wasmtime.sh | 9 ++++++++- barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 barretenberg/cpp/scripts/wasm_env_memory.wat diff --git a/Makefile b/Makefile index 70a6cffd7e3..74f3c636546 100644 --- a/Makefile +++ b/Makefile @@ -312,7 +312,7 @@ bb-sol: bb-cpp-native bb-crs l1-contracts-solc bb-cpp-native-tests: bb-cpp-native bb-cpp-chonk-inputs $(call test,$@,barretenberg/cpp,native) -bb-cpp-wasm-tests: bb-cpp-wasm-threads +bb-cpp-wasm-tests: bb-cpp-wasm $(call test,$@,barretenberg/cpp,wasm) bb-cpp-wasm-threads-tests: bb-cpp-wasm-threads diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 066300d6673..5dfec5c889a 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -769,7 +769,8 @@ "barretenberg.wasm", "barretenberg.wasm.gz", "barretenberg-debug.wasm", - "bb-avm-sim.wasm" + "bb-avm-sim.wasm", + "avm_ffi_tests" ] }, { @@ -789,7 +790,6 @@ "barretenberg.wasm.gz", "barretenberg-debug.wasm", "ecc_tests", - "avm_ffi_tests", "ultra_honk_bench", "bb" ] diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 8b9c9275ba1..6528a27f1e1 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -359,9 +359,8 @@ function test_cmds_wasm_threads { } function test_cmds_wasm { - # The AVM simulator's in-process service, as bb-avm-sim.wasm runs it. From the threads build, whose shared memory - # wasmtime provides; the single-thread build imports a memory only a JS host supplies. - echo "$hash barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/avm_ffi_tests" + # The AVM simulator's in-process service, from the single-threaded build bb-avm-sim.wasm comes from. + echo "$hash SINGLE_THREADED=1 barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm/bin/avm_ffi_tests" } function test_cmds_asan { diff --git a/barretenberg/cpp/scripts/wasm_env_memory.wat b/barretenberg/cpp/scripts/wasm_env_memory.wat new file mode 100644 index 00000000000..2af340e87de --- /dev/null +++ b/barretenberg/cpp/scripts/wasm_env_memory.wat @@ -0,0 +1,3 @@ +;; The linear memory a single-threaded bb wasm imports as env.memory (bb links with --import-memory, so its host +;; sizes the memory). The wasmtime CLI fills only the imports WASI defines, so wasmtime.sh preloads this as `env`. +(module (memory (export "memory") 512 65536)) diff --git a/barretenberg/cpp/scripts/wasmtime.sh b/barretenberg/cpp/scripts/wasmtime.sh index f59c4588e4b..4a847407fd4 100755 --- a/barretenberg/cpp/scripts/wasmtime.sh +++ b/barretenberg/cpp/scripts/wasmtime.sh @@ -1,13 +1,20 @@ #!/usr/bin/env bash # Helper for passing environment variables to wasm and common config. # Allows accessing ~/.bb-crs and ./ (more can be added as parameters to this script). +# SINGLE_THREADED=1 runs a single-threaded build: wasi-threads would reject its unshared memory import, so it gets +# the memory from wasm_env_memory.wat instead. set -eu export WASMTIME_BACKTRACE_DETAILS=1 +if [ "${SINGLE_THREADED:-0}" -eq 1 ]; then + memory_args=(--preload env=$(dirname $0)/wasm_env_memory.wat) +else + memory_args=(-Sthreads=y) +fi exec wasmtime run \ -Wthreads=y \ -Wshared-memory=y \ -Wexceptions=y \ - -Sthreads=y \ + "${memory_args[@]}" \ ${HARDWARE_CONCURRENCY:+--env HARDWARE_CONCURRENCY} \ --env HOME \ ${MAIN_ARGS:+--env MAIN_ARGS} \ diff --git a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md index 69ce78cde46..147f92012fe 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md +++ b/barretenberg/cpp/src/barretenberg/vm2/CLAUDE.md @@ -61,7 +61,7 @@ Configure once: `cmake --preset clang20-assert`. All commands below are from `ba 3. **`bb-avm-sim`** — Simulation-only IPC server that TS drives. - Build: `cmake --build --preset clang20-assert --target bb-avm-sim`. For TS: from `barretenberg/cpp/`, run `(cd ../ts && ./bootstrap.sh build_bb_avm_sim)` then bootstrap `yarn-project` (see yarn-project/CLAUDE.md). 4. **`bb-avm-sim.wasm`**: the same service in-process, the `@aztec-foundation/bb-avm-sim` package's wasm backend. It serves `SimulateWithHints` only, since it has no route to world state or contract data yet. - - Build: `cmake --preset wasm && cmake --build --preset wasm`. Test: `cmake --preset wasm-threads && cmake --build --preset wasm-threads --target avm_ffi_tests`, then `scripts/wasmtime.sh build-wasm-threads/bin/avm_ffi_tests`; natively `./build/bin/avm_ffi_tests`. + - Build: `cmake --preset wasm && cmake --build --preset wasm` (also builds `avm_ffi_tests`). Test: `SINGLE_THREADED=1 scripts/wasmtime.sh build-wasm/bin/avm_ffi_tests`, and natively `./build/bin/avm_ffi_tests`. ## AVM and TypeScript