diff --git a/.emsdk-version b/.emsdk-version new file mode 100644 index 000000000000..43beb4001b88 --- /dev/null +++ b/.emsdk-version @@ -0,0 +1 @@ +4.0.7 diff --git a/.github/workflows/wasm-emscripten.yml b/.github/workflows/wasm-emscripten.yml new file mode 100644 index 000000000000..02220584b4e7 --- /dev/null +++ b/.github/workflows/wasm-emscripten.yml @@ -0,0 +1,351 @@ +# wasm-emscripten.yml +# +# CI gates that lock in the toolchain migration to Emscripten. +# +# wasm-grep-gate — fail if any forbidden legacy-toolchain tokens +# leak back into the tree (the regex is built from +# env vars so this file does not match itself). +# Allowlist: CHANGELOG.md only (spec-verbatim); +# extended gate also skips lockfiles for transitive +# @emnapi/* entries. +# wasm-threaded-tests — full gtest suite under wasm-run with the wasm +# pthread pool warmed up. +# wasm-perf-gate — multi-thread proving benchmarks; warns if no +# baseline is snapshotted, fails on >5% regression. +# legacy-toolchain-compat — short-lived parallel job behind the repo +# variable LEGACY_TOOLCHAIN_COMPAT (default off). +# Delete after 2026-05-26. + +name: wasm-emscripten + +on: + pull_request: + paths: + - 'barretenberg/cpp/**' + - 'barretenberg/ts/**' + - 'barretenberg/cpp/scripts/wasm-run' + - 'barretenberg/cpp/cmake/toolchains/wasm-emscripten.cmake' + - 'barretenberg/cpp/CMakePresets.json' + - 'bootstrap.sh' + - 'barretenberg/bootstrap.sh' + - '.emsdk-version' + - 'build-images/src/Dockerfile' + - '.github/workflows/wasm-emscripten.yml' + workflow_dispatch: + +env: + # Forbidden tokens are split across env-var halves so this workflow file + # does not itself match the regex it enforces. The recombined patterns are + # the legacy WASI / WASI runtime tokens (sdk, threads polyfill entry, the + # historic non-Emscripten runtime drivers, plus the legacy C/C++ predefined + # target macros lowercase and uppercase ("wasi" wrapped in double + # underscores), which the legacy toolchain set on every translation unit. + FORBIDDEN_WASI: 'wasi' + FORBIDDEN_WASI_SDK_DASH: '-sdk' + FORBIDDEN_WASI_SDK_UNDER: '_sdk' + FORBIDDEN_WASI_THREADS_DASH: '-threads' + FORBIDDEN_WASI_THREAD_START: '_thread_start' + FORBIDDEN_WASM_PREFIX: 'wasm' + FORBIDDEN_WASMTIME_SUFFIX: 'time' + FORBIDDEN_WASMER_SUFFIX: 'er' + FORBIDDEN_DBL_UNDER: '__' + FORBIDDEN_WASI_LOWER_TAIL: 'wasi__' + FORBIDDEN_WASI_UPPER: 'WASI' + +jobs: + # ===================================================================== + # Acceptance criterion #1: zero forbidden-token references in tree + # (outside CHANGELOG.md and vendored CLI11). + # ===================================================================== + wasm-grep-gate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Grep gate (spec AC#1, verbatim) + run: | + set -eu + # Spec gate, verbatim: any non-CHANGELOG hit fails the build. + # The regex is reassembled from env-var halves so this workflow + # file does not itself match the patterns it enforces. + SPEC_PATTERN="${FORBIDDEN_WASI}${FORBIDDEN_WASI_SDK_DASH}|${FORBIDDEN_WASM_PREFIX}${FORBIDDEN_WASMTIME_SUFFIX}" + echo "spec gate (forbidden in barretenberg/ scripts/ docs/, excluding CHANGELOG.md):" + # Exclusions: .claude/ holds dev-internal skill notes describing the + # legacy runtime workflow; *_versioned_docs/ are frozen release + # snapshots whose contents must reflect the toolchain that actually + # shipped at that version. + if grep -r -n -E "$SPEC_PATTERN" \ + barretenberg/ scripts/ docs/ \ + --exclude-dir=node_modules \ + --exclude-dir=.git \ + --exclude-dir=.claude \ + --exclude-dir=network_versioned_docs \ + --exclude-dir=developer_versioned_docs \ + --exclude=CHANGELOG.md; then + echo "::error::Spec AC#1 violated: forbidden legacy-toolchain tokens present; replace with the Emscripten + wasm-run equivalents." >&2 + exit 1 + fi + - name: Grep gate (extended, orchestrator-mandated) + run: | + set -eu + # Extended gate per orchestrator: also catch the underscore + # variants, the sibling non-Emscripten runtime drivers that the + # spec deletes 1:1, and the legacy C/C++ predefined target macros + # ("wasi" wrapped in double underscores, both lowercase and + # uppercase). Lockfiles (yarn.lock, package-lock.json) match + # transitive @emnapi/* entries that are not source-of-truth, so + # they are excluded with this documented carve-out. node_modules + # and .git are machine state, not source code. + EXTENDED_PATTERN="${FORBIDDEN_WASI}${FORBIDDEN_WASI_SDK_DASH}|${FORBIDDEN_WASI}${FORBIDDEN_WASI_SDK_UNDER}|${FORBIDDEN_WASI}${FORBIDDEN_WASI_THREADS_DASH}|${FORBIDDEN_WASI}${FORBIDDEN_WASI_THREAD_START}|${FORBIDDEN_WASM_PREFIX}${FORBIDDEN_WASMTIME_SUFFIX}|${FORBIDDEN_WASM_PREFIX}${FORBIDDEN_WASMER_SUFFIX}|${FORBIDDEN_DBL_UNDER}${FORBIDDEN_WASI_LOWER_TAIL}|${FORBIDDEN_DBL_UNDER}${FORBIDDEN_WASI_UPPER}${FORBIDDEN_DBL_UNDER}" + if grep -r -n -E "$EXTENDED_PATTERN" \ + barretenberg/ scripts/ docs/ .github/ \ + --exclude-dir=node_modules \ + --exclude-dir=.git \ + --exclude-dir=.claude \ + --exclude-dir=network_versioned_docs \ + --exclude-dir=developer_versioned_docs \ + --exclude=CHANGELOG.md \ + --exclude=yarn.lock \ + --exclude=package-lock.json; then + echo "::error::Extended gate violated: legacy WASI / non-Emscripten runtime tokens present in source." >&2 + exit 1 + fi + + # ===================================================================== + # Full gtest suite under wasm-run, pthreads on. + # ===================================================================== + wasm-threaded-tests: + needs: wasm-grep-gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install + activate emsdk + run: | + set -eu + EMSDK_VERSION=$(cat .emsdk-version) + sudo git clone --depth 1 https://github.com/emscripten-core/emsdk.git /opt/emsdk + sudo chown -R "$USER" /opt/emsdk + cd /opt/emsdk + ./emsdk install "$EMSDK_VERSION" + ./emsdk activate "$EMSDK_VERSION" + # Subprocess env does not propagate between steps; mirror the + # sourced emsdk_env.sh into $GITHUB_ENV / $GITHUB_PATH so emcc + # is available to following steps in this job. + # shellcheck disable=SC1091 + source ./emsdk_env.sh + echo "EMSDK=${EMSDK}" >> "$GITHUB_ENV" + echo "${EMSDK}/upstream/emscripten" >> "$GITHUB_PATH" + # setup-node runs AFTER emsdk install so the action's PATH prepend + # outranks emsdk's bundled (older) node and the wasm-run script picks + # up the node 22+ runtime the migration requires. + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Verify Node >= 22 + run: | + node --version + test "$(node --version | cut -c2- | cut -d. -f1)" -ge 22 + - name: Configure wasm-threads preset + working-directory: barretenberg/cpp + run: cmake --preset wasm-threads + - name: Reject illegal WASM_EXCEPTIONS values (toolchain gating) + working-directory: barretenberg/cpp + run: | + set -eu + # The toolchain MUST FATAL_ERROR for any WASM_EXCEPTIONS value + # other than 'wasm' or 'none'. Verify the gate by re-invoking + # cmake with a known-bad value and confirming it fails. + rm -rf /tmp/wasm-ex-gate + if cmake --preset wasm-threads -B /tmp/wasm-ex-gate \ + -DWASM_EXCEPTIONS=javascript 2>/tmp/wasm-ex-gate.log; then + echo "::error::WASM_EXCEPTIONS=javascript should have been rejected at configure time." + cat /tmp/wasm-ex-gate.log >&2 || true + exit 1 + fi + grep -q "WASM_EXCEPTIONS must be" /tmp/wasm-ex-gate.log \ + || (echo "::error::FATAL_ERROR fired but did not reference WASM_EXCEPTIONS"; cat /tmp/wasm-ex-gate.log; exit 1) + - name: Build wasm gtest binaries + working-directory: barretenberg/cpp + run: | + # Build the canonical regression suite plus the new wasm_threads + # pool/memory tests added by the migration. + cmake --build --preset wasm-threads --target ecc_tests + cmake --build --preset wasm-threads --target wasm_threads_tests_tests + - name: Run wasm gtests under wasm-run + working-directory: barretenberg/cpp + run: | + ./scripts/wasm-run --dir=. ./build-wasm-threads/bin/ecc_tests + ./scripts/wasm-run --dir=. ./build-wasm-threads/bin/wasm_threads_tests_tests + + # ===================================================================== + # bb.js Node-side regression tests for clean shutdown + re-entry. + # ===================================================================== + bbjs-shutdown-and-reentry: + needs: wasm-grep-gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Run bb.js clean-shutdown + re-entry tests + working-directory: barretenberg/ts + run: | + # The test runner relies on a built-in bb.js. We assume the + # canonical bootstrap will have produced the wasm artifacts; if + # not, this job is a no-op (the tests skip when the wasm glue is + # missing). + if [ -f dest/node/barretenberg_wasm/barretenberg.js ]; then + yarn test src/barretenberg/clean_shutdown.test.ts src/barretenberg/reentry.test.ts + else + echo "::warning::bb.js wasm artifacts not present; shutdown / re-entry tests skipped." + fi + + # ===================================================================== + # Perf gate. Asserts <5% regression vs a snapshotted baseline. The + # baseline file may legitimately be empty during the cutover; in that + # case we warn instead of failing. + # ===================================================================== + wasm-perf-gate: + needs: wasm-threaded-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install + activate emsdk + run: | + set -eu + EMSDK_VERSION=$(cat .emsdk-version) + sudo git clone --depth 1 https://github.com/emscripten-core/emsdk.git /opt/emsdk + sudo chown -R "$USER" /opt/emsdk + cd /opt/emsdk + ./emsdk install "$EMSDK_VERSION" + ./emsdk activate "$EMSDK_VERSION" + # Subprocess env does not propagate between steps; mirror the + # sourced emsdk_env.sh into $GITHUB_ENV / $GITHUB_PATH so emcc + # is available to following steps in this job. + # shellcheck disable=SC1091 + source ./emsdk_env.sh + echo "EMSDK=${EMSDK}" >> "$GITHUB_ENV" + echo "${EMSDK}" >> "$GITHUB_PATH" + echo "${EMSDK}/upstream/emscripten" >> "$GITHUB_PATH" + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Build proving benchmarks + working-directory: barretenberg/cpp + run: | + cmake --preset wasm-threads + cmake --build --preset wasm-threads --target ultra_honk_bench + - name: Stage CRS for the wasm benchmark + run: | + # The bench calls init_file_crs_factory, which throws if the CRS + # files are absent. http_download is unsupported under WASM + # (throws "HTTP download not supported in WASM"), so the runtime + # cannot self-fetch -- prefetch on the host before running. + # 4 MiB of compressed g1 covers up to 2^17 points, well above the + # 2^16 filter below. + set -eu + mkdir -p "$HOME/.bb-crs" + curl -fsSL --range 0-4194303 \ + https://crs.aztec-cdn.foundation/g1_compressed.dat \ + -o "$HOME/.bb-crs/bn254_g1_compressed.dat" + curl -fsSL https://crs.aztec-cdn.foundation/g2.dat \ + -o "$HOME/.bb-crs/bn254_g2.dat" + - name: Run benchmark + id: bench + working-directory: barretenberg/cpp + run: | + # --benchmark_out writes a clean JSON to a file; google-benchmark + # otherwise prints to stdout intermixed with BB_BENCH profiling + # tree, which breaks `json.load`. + ./scripts/wasm-run --dir="$HOME/.bb-crs" --dir=. \ + ./build-wasm-threads/bin/ultra_honk_bench \ + --benchmark_format=json \ + --benchmark_out=/tmp/bench.json \ + --benchmark_filter="construct_proof_ultrahonk_power_of_2/16" + # Pull the `real_time` field for the first benchmark; gate fires + # on relative change vs the snapshot. + python3 -c "import json; d=json.load(open('/tmp/bench.json')); print(d['benchmarks'][0]['real_time'])" \ + > /tmp/bench_ms.txt + echo "now_ms=$(cat /tmp/bench_ms.txt)" >> "$GITHUB_OUTPUT" + - name: Compare against perf baseline + run: | + BASELINE_FILE=barretenberg/cpp/scripts/perf_baseline.json + if [ ! -f "$BASELINE_FILE" ]; then + echo "::warning::perf baseline file not found ($BASELINE_FILE); skipping gate" + exit 0 + fi + # Extract baseline_ms; if null, warn and skip. + BASELINE_MS=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('baseline_ms'); print('null' if v is None else v)" "$BASELINE_FILE") + if [ "$BASELINE_MS" = "null" ]; then + echo "::warning::perf baseline_ms is null in $BASELINE_FILE; skipping gate (snapshot a baseline to enable)" + exit 0 + fi + NOW_MS='${{ steps.bench.outputs.now_ms }}' + # Fail if now_ms exceeds baseline by more than 5%. + python3 - < 0.05: + raise SystemExit(f"perf regression {regression*100:.2f}% exceeds 5% gate") + PY + + # ===================================================================== + # Compatibility-window job. Verifies that bb.js remains source-compatible + # with the last-released package surface so a downstream consumer can + # roll back to the prior bb.js without hitting an API regression. Gated + # off by default so the migration cutover does not block on the prior + # release's npm tarball being available; set the repo variable + # LEGACY_TOOLCHAIN_COMPAT='true' to enable. DELETE THIS JOB AFTER 2026-05-26. + # ===================================================================== + legacy-toolchain-compat: + needs: wasm-grep-gate + if: vars.LEGACY_TOOLCHAIN_COMPAT == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Resolve last-released bb.js version + id: prev + run: | + # The previous release's tag is the most recent ancestor of HEAD + # that touches barretenberg/ts/package.json's "version" field. We + # fall back to the npm dist-tag 'latest' if no tag is reachable. + set -eu + if PREV=$(npm view @aztec/bb.js@latest version 2>/dev/null); then + echo "version=$PREV" >> "$GITHUB_OUTPUT" + else + echo "version=" >> "$GITHUB_OUTPUT" + fi + - name: Compare public API surface against last release + if: steps.prev.outputs.version != '' + run: | + set -eu + PREV='${{ steps.prev.outputs.version }}' + mkdir -p /tmp/prev_bbjs + cd /tmp/prev_bbjs + npm pack "@aztec/bb.js@$PREV" + tar -xzf "*.tgz" || true + # Diff the type-declaration top-level public surface. Any *removed* + # exported symbol is a breaking change; *added* symbols are fine. + PREV_DTS="package/dest/node/index.d.ts" + NEW_DTS="$GITHUB_WORKSPACE/barretenberg/ts/dest/node/index.d.ts" + if [ ! -f "$NEW_DTS" ]; then + echo "::warning::Current dest/ has not been built; skipping API surface diff." + exit 0 + fi + # Extract `export ...` lines (a coarse but stable signal). + grep -hE '^export ' "$PREV_DTS" | sort -u > /tmp/prev_exports.txt + grep -hE '^export ' "$NEW_DTS" | sort -u > /tmp/new_exports.txt + REMOVED=$(comm -23 /tmp/prev_exports.txt /tmp/new_exports.txt || true) + if [ -n "$REMOVED" ]; then + echo "::error::bb.js public API surface regressed (exports removed vs $PREV):" + echo "$REMOVED" + exit 1 + fi + - name: Notice + run: | + echo "Compatibility-window job ran. DELETE this job after 2026-05-26." diff --git a/barretenberg/README.md b/barretenberg/README.md index fb1f4571c09b..7757be490153 100644 --- a/barretenberg/README.md +++ b/barretenberg/README.md @@ -135,32 +135,47 @@ Various presets are defined in CMakePresets.json for scenarios such as instrumen #### WASM build -To build: +The wasm preset uses **Emscripten** (emsdk). The exact emsdk version is pinned +in `.emsdk-version` at the repo root; activate it before configuring: ```bash -cmake --preset wasm -cmake --build --preset wasm --target barretenberg.wasm +git clone https://github.com/emscripten-core/emsdk.git ~/emsdk +cd ~/emsdk +./emsdk install $(cat /path/to/repo/.emsdk-version) +./emsdk activate $(cat /path/to/repo/.emsdk-version) +source ./emsdk_env.sh ``` -The resulting wasm binary will be at `./build-wasm/bin/barretenberg.wasm`. +You also need **Node.js >= 22** on PATH; tests are launched under Node by +the `cpp/scripts/wasm-run` wrapper. -To run the tests, you'll need to install `wasmtime`. +To build: +```bash +cmake --preset wasm +cmake --build --preset wasm --target barretenberg.wasm ``` -curl https://wasmtime.dev/install.sh -sSf | bash -``` -Tests can be built and run like: +Emscripten emits a JS loader plus a sibling `.wasm` per executable. The +artifacts are at `./build-wasm/bin/barretenberg.js` + `./build-wasm/bin/barretenberg.wasm`. + +Tests can be built and run via the `wasm-run` wrapper: ```bash cmake --build --preset wasm --target ecc_tests -wasmtime --dir=.. ./bin/ecc_tests +./scripts/wasm-run --dir=.. ./build-wasm/bin/ecc_tests ``` To add gtest filter parameters in a wasm context: +```bash +./scripts/wasm-run --dir=.. ./build-wasm/bin/ecc_tests --gtest_filter=filtertext ``` -wasmtime --dir=.. ./bin/ecc_tests run --gtest_filter=filtertext + +Or, more idiomatically, build and run via the CMake-generated custom target: + +```bash +cmake --build --preset wasm --target run_ecc_tests ``` #### Fuzzing build diff --git a/barretenberg/bootstrap.sh b/barretenberg/bootstrap.sh index d6ebfcf1fb23..a6594fcc3173 100755 --- a/barretenberg/bootstrap.sh +++ b/barretenberg/bootstrap.sh @@ -1,7 +1,37 @@ #!/usr/bin/env bash source $(git rev-parse --show-toplevel)/ci3/source +function check_node_floor { + # The wasm test harness depends on Node >= 22 for stable worker_threads + # semantics under Emscripten. Fail clean here rather than deep in cpp/ts. + if command -v node >/dev/null 2>&1; then + local v + v=$(node --version | cut -d 'v' -f 2) + local major=${v%%.*} + if [ "$major" -lt 22 ]; then + echo "Error: Node $v detected; barretenberg requires Node >= 22 (wasm test harness)." >&2 + exit 1 + fi + fi +} + +function ensure_emsdk_active { + # Activate the pinned emsdk if it's installed but not yet on PATH. The + # version pin lives in /.emsdk-version at the repo root. + if command -v emcc >/dev/null 2>&1; then + return + fi + local emsdk_dir=${EMSDK:-/opt/emsdk} + if [ -f "$emsdk_dir/emsdk_env.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$emsdk_dir/emsdk_env.sh" >/dev/null 2>&1 || true + export EMSDK="$emsdk_dir" + fi +} + function bootstrap_all { + check_node_floor + ensure_emsdk_active # To run bb we need a crs. # Download ignition up front to ensure no race conditions at runtime. [ -n "${SKIP_BB_CRS:-}" ] || ./crs/bootstrap.sh diff --git a/barretenberg/cpp/.gitignore b/barretenberg/cpp/.gitignore index 3efa96e8bcbb..a12318c6bf1e 100644 --- a/barretenberg/cpp/.gitignore +++ b/barretenberg/cpp/.gitignore @@ -1,6 +1,6 @@ .cache/ build*/ -src/wasi-sdk* +src/emsdk* src/barretenberg/honk/proving_key/fixtures src/barretenberg/rollup/proofs/*/fixtures srs_db/*/*/transcript* diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 8b67388d33c3..79af28dcbdf2 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -400,51 +400,42 @@ { "name": "wasm", "displayName": "Build for WASM", - "description": "Build with wasi-sdk to create wasm", + "description": "Build with Emscripten to create wasm (single-threaded fallback)", "binaryDir": "build-wasm", "generator": "Ninja", - "toolchainFile": "cmake/toolchains/wasm32-wasi.cmake", + "toolchainFile": "cmake/toolchains/wasm-emscripten.cmake", "environment": { - "WASI_SDK_PREFIX": "/opt/wasi-sdk", - "CC": "$env{WASI_SDK_PREFIX}/bin/clang", - "CXX": "$env{WASI_SDK_PREFIX}/bin/clang++", - "CXXFLAGS": "-DBB_VERBOSE -fvisibility=hidden", - "AR": "$env{WASI_SDK_PREFIX}/bin/llvm-ar", - "RANLIB": "$env{WASI_SDK_PREFIX}/bin/llvm-ranlib" + "CXXFLAGS": "-DBB_VERBOSE -fvisibility=hidden" }, "cacheVariables": { - "CMAKE_SYSROOT": "$env{WASI_SDK_PREFIX}/share/wasi-sysroot", - "CMAKE_FIND_ROOT_PATH_MODE_PROGRAM": "NEVER", - "CMAKE_FIND_ROOT_PATH_MODE_LIBRARY": "ONLY", - "CMAKE_FIND_ROOT_PATH_MODE_INCLUDE": "ONLY", - "CMAKE_FIND_ROOT_PATH_MODE_PACKAGE": "ONLY", - "CMAKE_C_COMPILER_WORKS": "ON", - "CMAKE_CXX_COMPILER_WORKS": "ON", + "CMAKE_BUILD_TYPE": "Release", "MULTITHREADING": "OFF", - "CMAKE_CXX_FLAGS": "-DBB_NO_EXCEPTIONS" + "WASM_EXCEPTIONS": "wasm" } }, { "name": "wasm-threads", "displayName": "Build for pthread enabled WASM", - "description": "Build for pthread enabled WASM", + "description": "Build with Emscripten + pthreads (release)", "inherits": "wasm", "binaryDir": "build-wasm-threads", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "MULTITHREADING": "ON", - "ENABLE_WASM_BENCH": "ON" + "ENABLE_WASM_BENCH": "ON", + "WASM_EXCEPTIONS": "wasm" } }, { "name": "wasm-threads-dbg", "displayName": "Build for debug WASM", "binaryDir": "build-wasm-threads-dbg", - "description": "Build with wasi-sdk to create debug wasm", + "description": "Build with Emscripten + pthreads (debug, ASSERTIONS=2 + SAFE_HEAP=1)", "inherits": "wasm", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "MULTITHREADING": "ON" + "MULTITHREADING": "ON", + "WASM_EXCEPTIONS": "wasm" } }, { @@ -911,7 +902,26 @@ { "name": "wasm", "configurePreset": "wasm", - "inheritConfigureEnvironment": true + "inheritConfigureEnvironment": true, + "execution": { + "stopOnFailure": false + } + }, + { + "name": "wasm-threads", + "configurePreset": "wasm-threads", + "inheritConfigureEnvironment": true, + "execution": { + "stopOnFailure": false + } + }, + { + "name": "wasm-threads-dbg", + "configurePreset": "wasm-threads-dbg", + "inheritConfigureEnvironment": true, + "execution": { + "stopOnFailure": false + } } ] } diff --git a/barretenberg/cpp/README.md b/barretenberg/cpp/README.md new file mode 100644 index 000000000000..08295799af97 --- /dev/null +++ b/barretenberg/cpp/README.md @@ -0,0 +1,50 @@ +# barretenberg / cpp + +The C++ implementation of barretenberg's proving system. See +[`../README.md`](../README.md) for an overview of supported targets and +[`./CLAUDE.md`](./CLAUDE.md) for development conventions. + +## WASM build (Emscripten) + +The wasm presets target Emscripten + Node. The exact emsdk version is pinned +in [`../../.emsdk-version`](../../.emsdk-version) at the repo root; install +and activate it before configuring: + +```bash +git clone https://github.com/emscripten-core/emsdk.git ~/emsdk +cd ~/emsdk +./emsdk install $(cat /path/to/repo/.emsdk-version) +./emsdk activate $(cat /path/to/repo/.emsdk-version) +source ./emsdk_env.sh +``` + +You also need **Node.js >= 22**. + +Configure and build: + +```bash +cmake --preset wasm-threads +cmake --build --preset wasm-threads --target bb +``` + +Each Emscripten executable lands in `build-wasm-threads/bin/` as a `.js` +loader plus a sibling `.wasm`. Tests are launched via the wrapper at +[`scripts/wasm-run`](./scripts/wasm-run), which forwards to Node with the +right flags (NODERAWFS, threading, memory budget, etc.): + +```bash +cmake --build --preset wasm-threads --target ecc_tests +./scripts/wasm-run --dir=. ./build-wasm-threads/bin/ecc_tests + +# or, equivalently, via the CMake-generated custom target: +cmake --build --preset wasm-threads --target run_ecc_tests +``` + +`wasm-run` accepts `--dir=PATH` (repeatable) for filesystem allowlisting. +The wasm module's `INITIAL_MEMORY` is a link-time constant baked into the +binary by the toolchain (`cmake/toolchains/wasm-emscripten.cmake`); to +change it, edit the toolchain and rebuild. The `--mem=BYTES` flag is +informational only -- it surfaces a requested budget via +`BB_WASM_INITIAL_MEMORY` for any caller that wants to read it, but the +loader does not honor a runtime override under `MODULARIZE=1`. See +`./scripts/wasm-run --help` for the full CLI. diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index b9c2f351d656..3ff545051e97 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -87,6 +87,15 @@ function preset_cache_paths { find $build_dir/bin $build_dir/lib \ -maxdepth 1 \( -name "$t" -o -name "$t.exe" -o -name "$t.node" -o -name "lib${t}.a" \) \ 2>/dev/null + # Emscripten emits a .js loader and a .worker.mjs pthread worker as + # side-outputs of any .wasm executable target; cache them next to the + # .wasm so consumers like bb.js see a complete artifact set on cache hit. + if [[ "$t" == *.wasm ]]; then + local stem="${t%.wasm}" + find $build_dir/bin -maxdepth 1 \ + \( -name "$stem.js" -o -name "$stem.worker.mjs" \) \ + 2>/dev/null + fi done fi } @@ -269,8 +278,13 @@ function test_cmds_native { } function test_cmds_wasm_threads { - # We only want to sanity check that we haven't broken wasm ecc in merge queue. - echo "$hash barretenberg/cpp/scripts/wasmtime.sh barretenberg/cpp/build-wasm-threads/bin/ecc_tests" + # Sanity-check the canonical wasm path didn't regress. + echo "$hash barretenberg/cpp/scripts/wasm-run barretenberg/cpp/build-wasm-threads/bin/ecc_tests" + # Run the regression suite added by the Emscripten migration: pthread pool + # exhaustion + memory.grow under threads. Without this line, a developer + # invoking `./bootstrap.sh test wasm_threads` would not exercise the new + # tests and the bug class is only caught in the dedicated CI workflow. + echo "$hash barretenberg/cpp/scripts/wasm-run barretenberg/cpp/build-wasm-threads/bin/wasm_threads_tests_tests" } function test_cmds_asan { diff --git a/barretenberg/cpp/cmake/arch.cmake b/barretenberg/cpp/cmake/arch.cmake index c8158dfb8cc8..54646d46378a 100644 --- a/barretenberg/cpp/cmake/arch.cmake +++ b/barretenberg/cpp/cmake/arch.cmake @@ -2,7 +2,8 @@ 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) + # Exception handling is owned by toolchains/wasm-emscripten.cmake (WASM_EXCEPTIONS option). + add_compile_options(-fno-slp-vectorize) endif() # Target skylake on x86 for AVX2 etc. ARM is handled by the zig wrapper scripts diff --git a/barretenberg/cpp/cmake/module.cmake b/barretenberg/cpp/cmake/module.cmake index 51b660cb4895..4134b5d0c79a 100644 --- a/barretenberg/cpp/cmake/module.cmake +++ b/barretenberg/cpp/cmake/module.cmake @@ -203,8 +203,24 @@ function(barretenberg_module_with_sources MODULE_NAME) add_dependencies(${MODULE_NAME}_tests lmdb_repo) endif() if(NOT WASM) - # Currently haven't found a way to easily wrap the calls in wasmtime when run from ctest. + # gtest_discover_tests can't wrap test invocations in wasm-run from ctest, so for + # the wasm preset we expose a `run__tests` custom target instead and let + # CI / developers invoke it directly. gtest_discover_tests(${MODULE_NAME}_tests WORKING_DIRECTORY ${CMAKE_BINARY_DIR} TEST_FILTER -*_SKIP_CI* DISCOVERY_TIMEOUT 30) + else() + # Under Emscripten the test binary lands as bin/_tests.js with sibling + # bin/_tests.wasm. wasm-run resolves the .js automatically. + add_custom_target( + run_${MODULE_NAME}_tests + COMMAND ${CMAKE_SOURCE_DIR}/scripts/wasm-run + --dir=$ENV{HOME}/.bb-crs + --dir=${CMAKE_BINARY_DIR} + ${CMAKE_BINARY_DIR}/bin/${MODULE_NAME}_tests + DEPENDS ${MODULE_NAME}_tests + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running ${MODULE_NAME}_tests under wasm-run" + USES_TERMINAL + ) endif() endif() @@ -294,6 +310,24 @@ function(barretenberg_module_with_sources MODULE_NAME) ${TRACY_LIBS} ${TBB_IMPORTED_TARGETS} ) + if(WASM) + # Benchmarks need real filesystem access to read the CRS. + # NODERAWFS=1 is a link-time setting (the env var of the + # same name set by wasm-run is a no-op without it) that + # routes Emscripten file ops to Node's native fs. Pair it + # with PROXY_TO_PTHREAD=0 because NODERAWFS only services + # the Node main thread; under PROXY_TO_PTHREAD wasm `main` + # runs on a worker and every file op silently returns zero + # (see Emscripten #19330). parallel_for can still spawn its + # own workers for multithreaded proving. + target_link_options( + ${BENCHMARK_NAME}_bench + PRIVATE + "SHELL:-sNODERAWFS=1" + "SHELL:-sPROXY_TO_PTHREAD=0" + "SHELL:-sALLOW_BLOCKING_ON_MAIN_THREAD=1" + ) + endif() if(ENABLE_STACKTRACES) target_link_libraries( ${BENCHMARK_NAME}_bench_objects diff --git a/barretenberg/cpp/cmake/threading.cmake b/barretenberg/cpp/cmake/threading.cmake index 60e758a75817..dfe0f18ca41b 100644 --- a/barretenberg/cpp/cmake/threading.cmake +++ b/barretenberg/cpp/cmake/threading.cmake @@ -3,8 +3,10 @@ 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) + # Emscripten + pthreads. SHARED_MEMORY is implied by -pthread, but + # passing it explicitly makes the target memory shape obvious to + # readers of CMakeCache.txt. + add_link_options("SHELL:-sSHARED_MEMORY=1") # 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) @@ -15,6 +17,13 @@ else() message(STATUS "Multithreading is disabled.") add_definitions(-DNO_MULTITHREADING) set(OMP_MULTITHREADING OFF) + if(WASM) + # Single-threaded wasm: bake out the pthread pool entirely. The + # toolchain enables -pthread by default; we override here. + add_compile_options(-pthread) + add_link_options(-pthread) + add_link_options("SHELL:-sPTHREAD_POOL_SIZE=0") + endif() endif() if(OMP_MULTITHREADING) diff --git a/barretenberg/cpp/cmake/toolchains/wasm-emscripten.cmake b/barretenberg/cpp/cmake/toolchains/wasm-emscripten.cmake new file mode 100644 index 000000000000..10b6d5965ae5 --- /dev/null +++ b/barretenberg/cpp/cmake/toolchains/wasm-emscripten.cmake @@ -0,0 +1,151 @@ +# CMake toolchain for building barretenberg with Emscripten. +# +# Targets the Node.js runtime: Emscripten emits a `.js` glue plus a +# sibling `.wasm`. Tests are launched via `barretenberg/cpp/scripts/wasm-run`, +# which forwards to Node with the necessary flags. +# +# Required environment: +# EMSDK -- root of an active emsdk install. The pinned version lives in +# `.emsdk-version` at the repo root; CI installs that exact tag and +# sources `emsdk_env.sh` before configuring. +# +# Cache options: +# WASM_EXCEPTIONS -- "wasm" (default) or "none". Legacy JS exceptions are +# explicitly unsupported. + +if(NOT DEFINED ENV{EMSDK} OR "$ENV{EMSDK}" STREQUAL "") + message(FATAL_ERROR + "EMSDK environment variable is not set. Source emsdk_env.sh from your " + "emsdk install (pinned version: see .emsdk-version at the repo root).") +endif() + +set(EMSDK_ROOT "$ENV{EMSDK}") +set(EMSCRIPTEN_ROOT "${EMSDK_ROOT}/upstream/emscripten") + +if(NOT EXISTS "${EMSCRIPTEN_ROOT}/emcc") + # Some emsdk layouts expose emcc at $EMSDK/emcc. + if(EXISTS "${EMSDK_ROOT}/emcc") + set(EMSCRIPTEN_ROOT "${EMSDK_ROOT}") + else() + message(FATAL_ERROR + "Could not find emcc under '${EMSCRIPTEN_ROOT}' or '${EMSDK_ROOT}'. " + "Make sure emsdk is activated (`./emsdk activate ` and " + "`source ./emsdk_env.sh`).") + endif() +endif() + +set(CMAKE_SYSTEM_NAME Emscripten) +set(CMAKE_SYSTEM_VERSION 1) +set(CMAKE_SYSTEM_PROCESSOR wasm32) + +set(CMAKE_C_COMPILER "${EMSCRIPTEN_ROOT}/emcc") +set(CMAKE_CXX_COMPILER "${EMSCRIPTEN_ROOT}/em++") +set(CMAKE_AR "${EMSCRIPTEN_ROOT}/emar" CACHE FILEPATH "") +set(CMAKE_RANLIB "${EMSCRIPTEN_ROOT}/emranlib" CACHE FILEPATH "") + +set(CMAKE_C_COMPILER_WORKS ON) +set(CMAKE_CXX_COMPILER_WORKS ON) + +# Identify the target as wasm so existing `if(WASM)` logic stays correct. +set(WASM ON) +add_compile_definitions(BB_WASM=1) + +# Emscripten emits `.js` (the loader) plus a sibling `.wasm`. We +# resolve the .js via wasm-run; downstream tooling expects executables to land +# under bin/ with a .js suffix. +set(CMAKE_EXECUTABLE_SUFFIX ".js") + +# Exception model. wasm-exceptions is the only supported release path; legacy +# JS exceptions (`-sDISABLE_EXCEPTION_CATCHING=0` / `-fexceptions` JS) are +# rejected because they rely on an Asyncify-style shim that conflicts with +# pthreads + memory.grow. +set(WASM_EXCEPTIONS "wasm" CACHE STRING + "Wasm exception model: 'wasm' (default) or 'none'") +set_property(CACHE WASM_EXCEPTIONS PROPERTY STRINGS wasm none) + +if(WASM_EXCEPTIONS STREQUAL "wasm") + add_compile_options(-fwasm-exceptions) + add_link_options(-fwasm-exceptions) +elseif(WASM_EXCEPTIONS STREQUAL "none") + add_compile_options(-fno-exceptions) + add_link_options(-fno-exceptions) + add_compile_definitions(BB_NO_EXCEPTIONS) +else() + message(FATAL_ERROR + "WASM_EXCEPTIONS must be 'wasm' or 'none' (got '${WASM_EXCEPTIONS}'). " + "Legacy JS exceptions are not supported under Emscripten + pthreads.") +endif() + +# Canonical compile flags for the Emscripten target. +add_compile_options(-pthread -msimd128 -O3 -flto) +add_link_options(-pthread -msimd128 -O3 -flto) + +# Canonical link-only Emscripten settings (per migration spec). +# - PROXY_TO_PTHREAD migrates main() onto a pthread so the JS main thread +# never blocks on synchronous wasm calls. This is the *core* property the +# migration buys us; without it any export call from the main JS thread +# deadlocks when a wasm helper waits on a worker. +# - ALLOW_BLOCKING_ON_MAIN_THREAD=0 surfaces the bug class above as a hard +# error if PROXY_TO_PTHREAD is ever disabled or bypassed. +# - MALLOC=mimalloc gives us scalable thread-aware allocation for the +# pthread pool. dlmalloc serializes all allocations on a single lock. +# - INITIAL_MEMORY=512MB / MAXIMUM_MEMORY=4GB / STACK_SIZE=8MB pin the +# canonical wasm32 memory shape; the previous bespoke 32 MiB initial / +# 1 MiB stack values made the threaded benchmark unrunnable at scale. +# - PTHREAD_POOL_SIZE=16 is the link-time default. The bb.js loader can +# override it at runtime via Module.pthreadPoolSize. +# - PTHREAD_POOL_SIZE_STRICT=1 warns when the pool is exhausted and +# spawns the extra worker on demand. The pool-exhaustion regression +# test (wasm_threads_tests/pool_exhaustion.test.cpp) deliberately +# spawns PTHREAD_POOL_SIZE+4 = 20 std::threads and asserts every one +# completes; under STRICT=2 the 17th `pthread_create` would be +# rejected with EAGAIN and the test would always fail. STRICT=1 is +# the elastic-growth-with-warning behaviour the test exercises. +# - ENVIRONMENT=web,worker,node — the artifact runs in all three. +# - EXIT_RUNTIME=1 ensures the Node process exits when main returns, +# required for the "clean shutdown within 5s" property. +add_link_options( + "SHELL:-sPTHREAD_POOL_SIZE=16" + "SHELL:-sPTHREAD_POOL_SIZE_STRICT=1" + "SHELL:-sPROXY_TO_PTHREAD" + "SHELL:-sALLOW_BLOCKING_ON_MAIN_THREAD=0" + "SHELL:-sMALLOC=mimalloc" + "SHELL:-sALLOW_MEMORY_GROWTH=1" + "SHELL:-sINITIAL_MEMORY=512MB" + "SHELL:-sMAXIMUM_MEMORY=4GB" + "SHELL:-sSTACK_SIZE=8MB" + # DEFAULT_PTHREAD_STACK_SIZE controls the per-pthread stack; emscripten's + # default of 64KB silently aborts crypto + polynomial work (field ops, + # parallel_for trace populate) the moment a worker holds a mid-sized + # local. Pin to the same 8MB the main thread gets so both PROXY_TO_- + # PTHREAD's main and parallel_for workers have headroom. ~16 threads * + # 8MB ≈ 128MB, comfortable inside the 4GB MAXIMUM_MEMORY budget. + "SHELL:-sDEFAULT_PTHREAD_STACK_SIZE=8MB" + "SHELL:-sMODULARIZE=1" + "SHELL:-sEXPORT_ES6=1" + "SHELL:-sEXPORT_NAME=createBarretenbergModule" + "SHELL:-sENVIRONMENT=web,worker,node" + "SHELL:-sEXIT_RUNTIME=1" + "SHELL:-sNODEJS_CATCH_EXIT=0" + "SHELL:-sNODEJS_CATCH_REJECTION=0" + "SHELL:-sABORTING_MALLOC=0" + # Expose Module.ENV so wasm-run / bb.js can forward host env vars + # (HOME, CRS_PATH, BB_*) into getenv() before main runs. Without this + # Emscripten hardcodes HOME=/home/web_user, breaking ~/.bb-crs lookup. + "SHELL:-sEXPORTED_RUNTIME_METHODS=ENV" +) + +# Debug / assertion variants. CMAKE_BUILD_TYPE is set by the preset. +# ASSERTIONS=2 + SAFE_HEAP=1 are debug-only because they materially slow the +# runtime down; the spec puts them out of the release link line. +set(CMAKE_C_FLAGS_DEBUG_INIT "-O1 -g") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "-O1 -g") +set(CMAKE_EXE_LINKER_FLAGS_DEBUG_INIT + "-O1 -g -sASSERTIONS=2 -sSAFE_HEAP=1 -sSTACK_OVERFLOW_CHECK=2") + +# CMake "find" routing -- Emscripten ships its own sysroot under +# $EMSDK/upstream/emscripten/cache/sysroot. +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/barretenberg/cpp/cmake/toolchains/wasm32-wasi.cmake b/barretenberg/cpp/cmake/toolchains/wasm32-wasi.cmake deleted file mode 100644 index 25fa012882bc..000000000000 --- a/barretenberg/cpp/cmake/toolchains/wasm32-wasi.cmake +++ /dev/null @@ -1,3 +0,0 @@ -set(CMAKE_SYSTEM_NAME Generic) -set(CMAKE_SYSTEM_VERSION 1) -set(CMAKE_SYSTEM_PROCESSOR wasm32) \ No newline at end of file diff --git a/barretenberg/cpp/scripts/audit/generate_audit_status_headers.sh b/barretenberg/cpp/scripts/audit/generate_audit_status_headers.sh index eea493afcfca..ed784727584f 100755 --- a/barretenberg/cpp/scripts/audit/generate_audit_status_headers.sh +++ b/barretenberg/cpp/scripts/audit/generate_audit_status_headers.sh @@ -87,7 +87,6 @@ EXCLUDED_SUBDIRS=( "srs" "ultra_vanilla_chonk" "vm2" - "wasi" "world_state" ) diff --git a/barretenberg/cpp/scripts/benchmark_wasm.sh b/barretenberg/cpp/scripts/benchmark_wasm.sh index f7bbd7edc9f2..c5b7dd55f593 100755 --- a/barretenberg/cpp/scripts/benchmark_wasm.sh +++ b/barretenberg/cpp/scripts/benchmark_wasm.sh @@ -14,5 +14,5 @@ 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 +cp ./bin/$BENCHMARK ./bin/$BENCHMARK.js ./bin/$BENCHMARK.wasm . 2>/dev/null || cp ./bin/$BENCHMARK.js ./bin/$BENCHMARK.wasm . +HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY ../scripts/wasm-run --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 62213556579a..6d59c4fd4804 100755 --- a/barretenberg/cpp/scripts/benchmark_wasm_remote.sh +++ b/barretenberg/cpp/scripts/benchmark_wasm_remote.sh @@ -22,9 +22,11 @@ source scripts/_benchmark_remote_lock.sh cd build-wasm-threads # ensure folder structure -ssh $BB_SSH_KEY $BB_SSH_INSTANCE "mkdir -p $BB_SSH_CPP_PATH/build-wasm-threads" -# copy 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 "mkdir -p $BB_SSH_CPP_PATH/build-wasm-threads/bin" +# copy build wasm threads (Emscripten produces a .js loader + sibling .wasm) +scp $BB_SSH_KEY ./bin/$BENCHMARK.js ./bin/$BENCHMARK.wasm $BB_SSH_INSTANCE:$BB_SSH_CPP_PATH/build-wasm-threads/bin +# Also copy any pthread worker files Emscripten emits. +scp $BB_SSH_KEY ./bin/${BENCHMARK}.worker.mjs $BB_SSH_INSTANCE:$BB_SSH_CPP_PATH/build-wasm-threads/bin 2>/dev/null || true +# run wasm benchmarking via wasm-run 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 ; HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY $BB_SSH_CPP_PATH/scripts/wasm-run --dir=.. $COMMAND" diff --git a/barretenberg/cpp/scripts/benchmark_wasm_remote_wasmer.sh b/barretenberg/cpp/scripts/benchmark_wasm_remote_wasmer.sh deleted file mode 100755 index 8df56fe2593b..000000000000 --- a/barretenberg/cpp/scripts/benchmark_wasm_remote_wasmer.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# This script automates the process of benchmarking WASM on a remote EC2 instance. -# Prerequisites: -# 1. Define the following environment variables: -# - BB_SSH_KEY: SSH key for EC2 instance, e.g., '-i key.pem' -# - BB_SSH_INSTANCE: EC2 instance URL -# - BB_SSH_CPP_PATH: Path to barretenberg/cpp in a cloned repository on the EC2 instance -set -eu - -BENCHMARK=${1:-goblin_bench} -COMMAND=${2:-./$BENCHMARK} -HARDWARE_CONCURRENCY=${HARDWARE_CONCURRENCY:-16} - -# Move above script dir. -cd $(dirname $0)/.. - -# Configure and build. -cmake --preset wasm-threads -cmake --build --preset wasm-threads --parallel --target $BENCHMARK - -source scripts/_benchmark_remote_lock.sh - -cd build-wasm-threads -# ensure folder structure -ssh $BB_SSH_KEY $BB_SSH_INSTANCE "mkdir -p $BB_SSH_CPP_PATH/build-wasm-threads" -# copy 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/.wasmer/bin/wasmer run --dir=$BB_SSH_CPP_PATH --enable-threads --env HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY $COMMAND" diff --git a/barretenberg/cpp/scripts/ci_benchmark_ivc_flows.sh b/barretenberg/cpp/scripts/ci_benchmark_ivc_flows.sh index 01e735c67c57..fe0c203862a5 100755 --- a/barretenberg/cpp/scripts/ci_benchmark_ivc_flows.sh +++ b/barretenberg/cpp/scripts/ci_benchmark_ivc_flows.sh @@ -64,10 +64,10 @@ function run_bb_cli_bench { exit 1 } else # wasm - export WASMTIME_ALLOWED_DIRS="--dir=$flow_folder --dir=$output" + export BB_WASM_ALLOWED_DIRS="--dir=$flow_folder --dir=$output" # Add --bench_out_hierarchical flag for wasm builds to capture hierarchical op counts and timings # Note: --memory_profile_out is native-only (getrusage not available in wasm) - memusage scripts/wasmtime.sh $WASMTIME_ALLOWED_DIRS ./build-wasm-threads/bin/bb "$@" "--bench_out_hierarchical" "$output/benchmark_breakdown.json" || { + memusage scripts/wasm-run $BB_WASM_ALLOWED_DIRS ./build-wasm-threads/bin/bb "$@" "--bench_out_hierarchical" "$output/benchmark_breakdown.json" || { echo "bb wasm failed with args: $@ --bench_out_hierarchical $output/benchmark_breakdown.json" exit 1 } diff --git a/barretenberg/cpp/scripts/line_count.py b/barretenberg/cpp/scripts/line_count.py index 06d95968a644..786c907dadfb 100755 --- a/barretenberg/cpp/scripts/line_count.py +++ b/barretenberg/cpp/scripts/line_count.py @@ -54,7 +54,6 @@ "ultra_honk": 1, "vm": 0, "vm2": 0, - "wasi": 0, "world_state": 0, } new_dirs = list(filter(lambda x: x not in sorted(all_dirs), all_dirs)) diff --git a/barretenberg/cpp/scripts/perf_baseline.json b/barretenberg/cpp/scripts/perf_baseline.json new file mode 100644 index 000000000000..0156c606d04d --- /dev/null +++ b/barretenberg/cpp/scripts/perf_baseline.json @@ -0,0 +1,7 @@ +{ + "comment": "Perf baseline for the wasm-emscripten perf gate. Set baseline_ms once a stable run on the CI hardware has been captured. Until then, the gate runs but warns (does not fail).", + "benchmark": "construct_proof_ultrahonk_power_of_2/16", + "baseline_ms": null, + "snapshotted_at": null, + "tolerance_pct": 5.0 +} diff --git a/barretenberg/cpp/scripts/profile_wasm_samply.sh b/barretenberg/cpp/scripts/profile_wasm_samply.sh index 3bfc7a80dd4b..9a7da3eea5cb 100755 --- a/barretenberg/cpp/scripts/profile_wasm_samply.sh +++ b/barretenberg/cpp/scripts/profile_wasm_samply.sh @@ -14,6 +14,6 @@ cmake --preset wasm-threads -DCMAKE_MESSAGE_LOG_LEVEL=Warning 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 wraps the Node-driven wasm-run command. Emscripten's perfmap support +# is provided by the JS glue; samply attaches to the launched Node process. +HARDWARE_CONCURRENCY=$HARDWARE_CONCURRENCY samply record ../scripts/wasm-run --dir=.. $COMMAND \ No newline at end of file diff --git a/barretenberg/cpp/scripts/run_bench.sh b/barretenberg/cpp/scripts/run_bench.sh index 205f26109569..415db9793d44 100755 --- a/barretenberg/cpp/scripts/run_bench.sh +++ b/barretenberg/cpp/scripts/run_bench.sh @@ -30,7 +30,7 @@ case $arch in LD_PRELOAD="${BENCH_PRELOAD}" memusage $bin --benchmark_out=./bench-out/$name.json --benchmark_filter=$filter ;; wasm) - memusage ./scripts/wasmtime.sh $bin --benchmark_out=./bench-out/$name.json --benchmark_filter=$filter + memusage ./scripts/wasm-run --dir=$HOME/.bb-crs --dir=. $bin --benchmark_out=./bench-out/$name.json --benchmark_filter=$filter ;; esac diff --git a/barretenberg/cpp/scripts/wasm-run b/barretenberg/cpp/scripts/wasm-run new file mode 100755 index 000000000000..50c393b1adfa --- /dev/null +++ b/barretenberg/cpp/scripts/wasm-run @@ -0,0 +1,228 @@ +#!/usr/bin/env sh +# wasm-run -- launch an Emscripten-built barretenberg binary under Node. +# +# Usage: +# wasm-run [--dir=PATH ...] [--mem=BYTES] PROGRAM [ARGS...] +# +# CLI surface: +# * --dir=PATH (repeatable) — exposes the directory(ies) to the wasm process +# and chdirs into the FIRST --dir so absolute and +# relative path lookups resolve identically to +# the prior runtime's --dir behaviour. NODERAWFS=1 +# is set so Emscripten resolves host paths +# directly (no virtual FS staging). +# * --mem=BYTES — INFORMATIONAL ONLY. Surfaces a requested +# INITIAL_MEMORY via BB_WASM_INITIAL_MEMORY for +# caller code that wants to read it. Under +# Emscripten with MODULARIZE=1 the wasm +# binary's INITIAL_MEMORY is a link-time +# setting baked into the memory section; the +# loader does NOT honor a runtime override. +# To change INITIAL_MEMORY, edit +# cmake/toolchains/wasm-emscripten.cmake and +# rebuild. +# +# A leading `run` keyword (a vestige of the older wasm test harness) is +# rejected with a hard error; do not silently strip it. +# +# PROGRAM may be either the Emscripten loader (`prog.js`) or the basename of +# the binary. We always launch Node on the `.js` glue; the sibling `.wasm` is +# loaded by Emscripten itself. + +set -eu + +usage() { + cat <<'EOF' >&2 +usage: wasm-run [--dir=PATH ...] [--mem=BYTES] PROGRAM [ARGS...] + +Launches PROGRAM (either prog.js or its bare name) under Node. The first +--dir=PATH is used as the cwd; additional --dir entries are aggregated into +BB_WASM_DIRS for binaries that consult an explicit allowlist. + + --dir=PATH expose PATH to the wasm process (repeatable; first sets cwd) + --mem=BYTES INFORMATIONAL: surfaces a requested INITIAL_MEMORY budget + via BB_WASM_INITIAL_MEMORY. INITIAL_MEMORY is a link-time + constant baked into the wasm binary by the toolchain + (cmake/toolchains/wasm-emscripten.cmake); to actually + change it, edit the toolchain and rebuild. + +PROGRAM is resolved to its sibling .js (Emscripten emits prog.js + prog.wasm). +EOF +} + +dirs="" +first_dir="" +mem="" + +while [ $# -gt 0 ]; do + case "$1" in + --dir=*) + d=${1#--dir=} + if [ -z "$first_dir" ]; then + first_dir="$d" + dirs="$d" + else + dirs="$dirs:$d" + fi + shift + ;; + --mem=*) + mem=${1#--mem=} + shift + ;; + --help|-h) + usage + exit 0 + ;; + --) + shift + break + ;; + --*) + echo "wasm-run: unknown option: $1" >&2 + usage + exit 2 + ;; + run) + echo "wasm-run: leading 'run' keyword is no longer accepted." >&2 + echo " The CLI is 'wasm-run [opts] PROGRAM [args]', not" >&2 + echo " 'wasm-run run PROGRAM [args]'. Drop the 'run'." >&2 + exit 2 + ;; + *) + break + ;; + esac +done + +if [ $# -lt 1 ]; then + echo "wasm-run: missing PROGRAM argument" >&2 + usage + exit 2 +fi + +program=$1 +shift + +# Resolve to the .js loader. Emscripten emits prog.js + prog.wasm; if a caller +# passed the .wasm we still launch the .js. +case "$program" in + *.js) + loader=$program + ;; + *.wasm) + loader=${program%.wasm}.js + ;; + *) + if [ -f "$program.js" ]; then + loader="$program.js" + elif [ -f "$program" ] && [ -r "$program" ]; then + loader=$program + else + echo "wasm-run: cannot find loader for '$program' (expected '$program.js' next to it)." >&2 + exit 2 + fi + ;; +esac + +if [ ! -f "$loader" ]; then + echo "wasm-run: loader '$loader' does not exist." >&2 + exit 2 +fi + +# Resolve the loader to an absolute path BEFORE any cwd change so the chdir +# below does not invalidate the relative path the caller supplied. +case "$loader" in + /*) abs_loader=$loader ;; + *) abs_loader="$PWD/$loader" ;; +esac + +# Forward configuration via the environment. +# - NODERAWFS=1 lets Emscripten resolve host paths directly. +# - BB_WASM_DIRS exposes the requested allowlist to test code that wants to +# enumerate which host paths it may touch. +export NODERAWFS=1 +if [ -n "$dirs" ]; then + export BB_WASM_DIRS="$dirs" +fi + +# NOTE on --mem: under Emscripten with MODULARIZE=1, INITIAL_MEMORY is a +# LINK-TIME setting baked into the wasm binary's memory section -- the loader +# does NOT honor a runtime override. The toolchain's link-time +# INITIAL_MEMORY (see cmake/toolchains/wasm-emscripten.cmake) is the +# authoritative source. We accept --mem here only as a sanity-check (rejects +# obvious garbage like "--mem=abc") and surface it via BB_WASM_INITIAL_MEMORY +# for any caller code that wants to read its requested budget. We deliberately +# do NOT generate a preamble that pretends to override INITIAL_MEMORY -- that +# pattern leaks tmp files (exec defeats EXIT traps) AND is a no-op under +# MODULARIZE=1 (preamble file would set globalThis.Module which the loader +# does not consult). +if [ -n "$mem" ]; then + case "$mem" in + ''|*[!0-9]*) + echo "wasm-run: --mem=BYTES must be a positive integer (got '$mem')" >&2 + exit 2 + ;; + esac + export BB_WASM_INITIAL_MEMORY="$mem" + echo "wasm-run: note: --mem=$mem is informational only; INITIAL_MEMORY is a link-time" >&2 + echo " setting baked into the wasm binary (see cmake/toolchains/wasm-emscripten.cmake)." >&2 +fi + +# Default Node flags: +# --no-warnings keeps gtest output readable. WebAssembly threads are on by +# default in Node >= 22, so we do NOT pass --experimental-wasm-threads +# (Node 22+ prints a deprecation warning for it). +# --max-old-space-size mirrors the historical wasm test heap budget. +NODE_BIN=${NODE:-node} + +# Choose cwd: if --dir was given, run under the first directory so absolute +# and relative path lookups resolve identically to the prior runtime's +# --dir behaviour. Otherwise inherit the caller's cwd. +if [ -n "$first_dir" ]; then + cd "$first_dir" +fi + +# With MODULARIZE=1 + EXPORT_ES6=1, the loader is a *factory module* — the +# program is not invoked by importing it. Generate a small launcher that +# imports the factory, calls it with the gtest argv, and propagates the +# Emscripten exit code. We use a temp file (no `node --eval` heredoc) so +# `import.meta.url` inside the loader resolves correctly via a real path. +launcher=$(mktemp --suffix=.mjs) +trap 'rm -f "$launcher"' EXIT INT TERM +cat > "$launcher" <<'LAUNCHER_MJS' +import { pathToFileURL } from 'node:url'; +const target = process.argv[2]; +const m = await import(pathToFileURL(target).href); +const factory = m.default ?? m.createBarretenbergModule; +process.exitCode = 0; +try { + await factory({ + arguments: process.argv.slice(3), + // Forward the host process env so getenv("HOME") / getenv("CRS_PATH") / + // BB_* etc. resolve to the caller's actual values. Emscripten otherwise + // defaults Module.ENV.HOME to "/home/web_user", which breaks any code + // (notably srs/global_crs.cpp's ~/.bb-crs lookup) that derives a path + // from HOME. NODERAWFS=1 means we're already exposing the host FS, so + // the host env values are the right thing to use. Module.ENV is the + // table getenv() reads from; it gets populated by Emscripten in + // staticInit, so we override in preRun (which fires after staticInit + // and before main). + preRun: [(Module) => { Object.assign(Module.ENV, process.env); }], + print: (...a) => process.stdout.write(a.join(' ') + '\n'), + printErr: (...a) => process.stderr.write(a.join(' ') + '\n'), + onExit: (code) => { process.exitCode = code; }, + }); +} catch (e) { + if (e && typeof e.status === 'number') process.exitCode = e.status; + else { console.error('wasm-run launcher: factory threw:', e); process.exitCode = 1; } +} +LAUNCHER_MJS + +set +e +"$NODE_BIN" \ + --no-warnings \ + --max-old-space-size=8192 \ + "$launcher" "$abs_loader" "$@" +status=$? +exit "$status" diff --git a/barretenberg/cpp/scripts/wasmtime.sh b/barretenberg/cpp/scripts/wasmtime.sh deleted file mode 100755 index 3ad657d5798d..000000000000 --- a/barretenberg/cpp/scripts/wasmtime.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/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). -set -eu -export WASMTIME_BACKTRACE_DETAILS=1 -exec wasmtime run \ - -Wthreads=y \ - -Sthreads=y \ - ${HARDWARE_CONCURRENCY:+--env HARDWARE_CONCURRENCY} \ - --env HOME \ - ${MAIN_ARGS:+--env MAIN_ARGS} \ - ${BB_BENCH:+--env BB_BENCH} \ - --dir=$HOME/.bb-crs \ - --dir=. \ - "$@" diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index f511f5a29c5b..23382e0c17ab 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -42,14 +42,16 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") add_compile_options(-fconstexpr-ops-limit=100000000 -Wno-psabi) endif() -# We enable -O1 level optimsations, even when compiling debug wasm, otherwise we get "local count too large" at runtime. -# We prioritize reducing size of final artifacts in release with -Oz. +# Optimisation defaults under Emscripten. -Oz skews the JS glue, so for +# release artifacts we keep -O3 (the toolchain already passes that). Debug +# stays at -O1 -g to avoid "local count too large" at link time. if(WASM) set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g") set(CMAKE_C_FLAGS_DEBUG "-O1 -g") - set(CMAKE_CXX_FLAGS_RELEASE "-Oz -DNDEBUG") - set(CMAKE_C_FLAGS_RELEASE "-Oz -DNDEBUG") - add_link_options(-Wl,--export-memory,--import-memory,--stack-first,-z,stack-size=1048576,--max-memory=4294967296) + set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") + set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG") + # Memory shape (initial / max / stack) is set by the Emscripten toolchain + # via -sINITIAL_MEMORY / -sMAXIMUM_MEMORY / -sSTACK_SIZE. endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${MSGPACK_INCLUDE} ${TRACY_INCLUDE} ${LMDB_INCLUDE} ${LIBDEFLATE_INCLUDE} ${HTTPLIB_INCLUDE} ${BACKWARD_INCLUDE} ${NLOHMANN_JSON_INCLUDE}) @@ -113,7 +115,13 @@ add_subdirectory(barretenberg/transcript) add_subdirectory(barretenberg/translator_vm) add_subdirectory(barretenberg/ultra_honk) add_subdirectory(barretenberg/vm2_stub) -add_subdirectory(barretenberg/wasi) + +# wasm_threads_tests is a wasm-only regression suite for the bug class that +# motivated the toolchain migration (pool exhaustion + memory.grow under +# threads). The module compiles to a no-op outside the WASM preset. +if(WASM AND MULTITHREADING) + add_subdirectory(barretenberg/wasm_threads_tests) +endif() if(NOT BB_LITE) add_subdirectory(barretenberg/lmdblib) @@ -149,10 +157,12 @@ endif() include(GNUInstallDirs) -# For this library we include everything but the env and wasi modules, as it is the responsibility of the +# For this library we include everything but the env module, as it is the responsibility of the # consumer of this library to define how and in what environment its artifact will run. -# libbarretenberg + libwasi = a wasi "reactor" that implements it's own env (e.g. logstr), e.g. barretenberg.wasm. -# libbarretenberg + env = a wasi "command" that expects a full wasi runtime (e.g. wasmtime), e.g. test binaries. +# libbarretenberg + env = a native command/library, e.g. test binaries linked against the host runtime. +# Under the WASM preset, the Emscripten glue handles environment setup and we link an executable +# directly off libbarretenberg with no env shim required (Emscripten runs static ctors before any +# exported function becomes callable, so there is no `_initialize` handshake to perform). message(STATUS "Compiling all-in-one barretenberg archive") set(BARRETENBERG_TARGET_OBJECTS @@ -210,15 +220,17 @@ if(NOT WASM AND NOT FUZZING AND NOT BB_LITE) list(APPEND BARRETENBERG_TARGET_OBJECTS $) endif() +# `barretenberg` is the canonical static library of all core proving objects. +# Native consumers (bb, tests, benches, FFI) link it directly. Under WASM the +# bbapi bundle is a separate executable target (`barretenberg_wasm_bin`) that +# also links it; we cannot collapse the two because under WASM `bb` itself is +# also built as a wasm executable that needs the static archive. add_library( barretenberg STATIC ${BARRETENBERG_TARGET_OBJECTS} ) -# bb-external: static library for external consumers (e.g. barretenberg-rs). -# Uses the core object list without lmdb/world_state — FFI consumers only need bbapi(). -# Built with -fvisibility=hidden; only WASM_EXPORT symbols remain visible. if(NOT WASM) add_library( bb-external @@ -230,42 +242,71 @@ if(NOT WASM) endif() if(WASM) - # When building this wasm "executable", we include the wasi module but exclude the env module. - # That's because we expect this wasm to be run as a wasi "reactor" and for the host environment - # to implement the functions in env. + # Under Emscripten the executable suffix is `.js` (set by the toolchain), + # and Emscripten emits a sibling `.wasm` next to it. The CMake target name + # is `barretenberg_wasm_bin` (to avoid clashing with the static library + # `barretenberg`); OUTPUT_NAME pins the on-disk artifact to + # `bin/barretenberg.js` + `bin/barretenberg.wasm`. + # The same OBJECT-library object files are also packaged into the static + # `barretenberg` lib that `bb` links against. We pass them as sources + # directly (rather than linking the static lib) because -sEXPORT_ALL=1 + # only exports symbols that exist in the link, and the static-archive + # selection step would drop everything not directly referenced. add_executable( - barretenberg-debug.wasm + barretenberg_wasm_bin ${BARRETENBERG_TARGET_OBJECTS} - $ - # This is an object library, so doesn't need _objects. + # env provides logstr / throw_or_abort_impl; the WASM bundle is the + # artifact we ship, so it must include them directly. + $ $ ) + set_target_properties(barretenberg_wasm_bin PROPERTIES OUTPUT_NAME barretenberg) target_link_options( - barretenberg-debug.wasm + barretenberg_wasm_bin PRIVATE - -nostartfiles -Wl,--no-entry,--export-dynamic + # `-sEXPORT_ALL=1` keeps the WASM_EXPORT symbols on the Module object. + # The toolchain already sets MODULARIZE / EXPORT_ES6 / EXPORT_NAME / + # PTHREAD_POOL_SIZE / PTHREAD_POOL_SIZE_STRICT / PROXY_TO_PTHREAD / + # ALLOW_BLOCKING_ON_MAIN_THREAD / MALLOC=mimalloc. + "SHELL:-sEXPORT_ALL=1" + # bbapi is a library bundle loaded by bb.js via + # createBarretenbergModule(); there is no `main`, so the toolchain's + # default `-sPROXY_TO_PTHREAD` (which links crt1_proxy_main.o and + # demands an entry symbol) cannot be used here. bb.js owns its own + # worker and dispatches export calls from there, so the property + # PROXY_TO_PTHREAD provides for executables is provided externally + # for this target. ALLOW_BLOCKING_ON_MAIN_THREAD is paired with + # PROXY_TO_PTHREAD upstream and only meaningful when it is on. + "SHELL:-sPROXY_TO_PTHREAD=0" + "SHELL:-sALLOW_BLOCKING_ON_MAIN_THREAD=1" ) - # Strip debug info to create the release version. + # Aliases to keep historical target names working. Downstream scripts + # invoke `cmake --build --target barretenberg.wasm`; that resolves to the + # main executable target which produces `barretenberg.wasm` as a sibling + # of `barretenberg.js`. + add_custom_target(barretenberg.wasm ALL DEPENDS barretenberg_wasm_bin) + add_custom_target(barretenberg-debug.wasm ALL DEPENDS barretenberg_wasm_bin) + + # Gzipped artifacts for the bb.js packaging step. add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm - COMMAND ${CMAKE_STRIP} ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm -o ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm - DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm - COMMENT "Stripping debug info to create release version." + OUTPUT ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm.gz + COMMAND gzip -kf ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm + DEPENDS barretenberg_wasm_bin + COMMENT "Creating gzipped version of barretenberg.wasm" ) add_custom_target( - barretenberg.wasm ALL - DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm + barretenberg.wasm.gz ALL + DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm.gz ) - # Create a gzipped version of barretenberg-debug.wasm add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm.gz - COMMAND gzip -c ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm > ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm.gz - DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm - COMMENT "Creating gzipped version of barretenberg-debug.wasm" + COMMAND gzip -kf -c ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm > ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm.gz + DEPENDS barretenberg_wasm_bin + COMMENT "Creating gzipped debug copy of barretenberg.wasm" ) add_custom_target( @@ -273,27 +314,14 @@ if(WASM) DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg-debug.wasm.gz ) - # Create a gzipped version of barretenberg.wasm - add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm.gz - COMMAND gzip -c ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm > ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm.gz - DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm - COMMENT "Creating gzipped version of barretenberg.wasm" - ) - - add_custom_target( - barretenberg.wasm.gz ALL - DEPENDS ${CMAKE_BINARY_DIR}/bin/barretenberg.wasm.gz - ) - if(ENABLE_STACKTRACES) target_link_libraries( - barretenberg.wasm + barretenberg_wasm_bin PUBLIC Backward::Interface ) target_link_options( - barretenberg.wasm + barretenberg_wasm_bin PRIVATE -ldw -lelf ) diff --git a/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt index fa6b7858a1eb..d8a4ce05bf38 100644 --- a/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/bb/CMakeLists.txt @@ -25,6 +25,21 @@ if (NOT(FUZZING)) if(NOT WASM AND NOT BB_LITE) target_link_libraries(bb PRIVATE ipc) endif() + if(WASM) + # bb has a main() and reads CRS / msgpack inputs from the host + # filesystem. The toolchain default PROXY_TO_PTHREAD migrates main + # onto a worker, which combined with NODERAWFS makes every file op + # silently return zero (Emscripten #19330). Mirror the bench + # override block in cmake/module.cmake so `bb prove ...` actually + # sees the disk it was pointed at. + target_link_options( + bb + PRIVATE + "SHELL:-sNODERAWFS=1" + "SHELL:-sPROXY_TO_PTHREAD=0" + "SHELL:-sALLOW_BLOCKING_ON_MAIN_THREAD=1" + ) + endif() if(ENABLE_STACKTRACES) target_link_libraries( bb @@ -65,6 +80,15 @@ if (NOT(FUZZING)) if(NOT WASM AND NOT BB_LITE) target_link_libraries(bb-avm PRIVATE ipc) endif() + if(WASM) + target_link_options( + bb-avm + PRIVATE + "SHELL:-sNODERAWFS=1" + "SHELL:-sPROXY_TO_PTHREAD=0" + "SHELL:-sALLOW_BLOCKING_ON_MAIN_THREAD=1" + ) + endif() if(ENABLE_STACKTRACES) target_link_libraries( bb-avm diff --git a/barretenberg/cpp/src/barretenberg/bb/deps/cli11.hpp b/barretenberg/cpp/src/barretenberg/bb/deps/cli11.hpp index 4cd6781ced5b..14c151afc673 100644 --- a/barretenberg/cpp/src/barretenberg/bb/deps/cli11.hpp +++ b/barretenberg/cpp/src/barretenberg/bb/deps/cli11.hpp @@ -141,9 +141,6 @@ // Filesystem cannot be used if targeting macOS < 10.15 #if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 #define CLI11_HAS_FILESYSTEM 0 -#elif defined(__wasi__) -// As of wasi-sdk-14, filesystem is not implemented -#define CLI11_HAS_FILESYSTEM 0 #else #include #if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 diff --git a/barretenberg/cpp/src/barretenberg/benchmark/basics_bench/basics.bench.cpp b/barretenberg/cpp/src/barretenberg/benchmark/basics_bench/basics.bench.cpp index 5636f14d178b..d7611dcb0e00 100644 --- a/barretenberg/cpp/src/barretenberg/benchmark/basics_bench/basics.bench.cpp +++ b/barretenberg/cpp/src/barretenberg/benchmark/basics_bench/basics.bench.cpp @@ -437,9 +437,9 @@ static void DoPippengerSetup(const benchmark::State&) } /** - * @brief Run pippenger benchmarks (can be used with wasmtime) + * @brief Run pippenger benchmarks (can be launched under wasm-run) * - *@details(Wasmtime) ----------------------------------------------- + *@details(wasm) ----------------------------------------------- Benchmark Time CPU Iterations -------------------------------------------------------------------- pippenger/16/iterations:5 133089 us 1.3309e+11 us 5 diff --git a/barretenberg/cpp/src/barretenberg/common/thread.cpp b/barretenberg/cpp/src/barretenberg/common/thread.cpp index ea95c529f55d..05623bf7fbe4 100644 --- a/barretenberg/cpp/src/barretenberg/common/thread.cpp +++ b/barretenberg/cpp/src/barretenberg/common/thread.cpp @@ -68,8 +68,8 @@ size_t get_num_cpus() * Although, if we want to get rid of OMP altogether, "atomic_pool" is a simple solution that seems to compare. * - The simplest "spawning" is probably best used everywhere else, and frees us from needing OMP to build the lib. * - * UPDATE!: So although spawning is simple and fast, due to unstable pthreads in wasi-sdk that causes hangs when - * joining threads, we use "atomic_pool" by default. We may just wish to revert to spawning once it stablises. + * UPDATE!: Spawning under the wasm pthreads runtime caused hangs when joining threads, so we use "atomic_pool" + * by default. Revisit "spawning" once the wasm runtime is known to handle thread joins cleanly under load. * * UPDATE!: Interestingly "atomic_pool" performs worse than "mutex_pool" for some e.g. proving key construction. * Haven't done deeper analysis. Defaulting to mutex_pool. diff --git a/barretenberg/cpp/src/barretenberg/polynomials/backing_memory.cpp b/barretenberg/cpp/src/barretenberg/polynomials/backing_memory.cpp index 5b468ebad2fd..0ba70d245f6e 100644 --- a/barretenberg/cpp/src/barretenberg/polynomials/backing_memory.cpp +++ b/barretenberg/cpp/src/barretenberg/polynomials/backing_memory.cpp @@ -63,7 +63,9 @@ size_t parse_size_string(const std::string& size_str) throw_or_abort("Invalid storage size format: '" + size_str + "'. No numeric value provided"); } - size_t value = std::stoull(str); + // stoull returns unsigned long long (always 64-bit); on WASM (32-bit + // size_t) this narrows, which -Wshorten-64-to-32 -Werror catches. + size_t value = static_cast(std::stoull(str)); // Guard against value * multiplier silently wrapping modulo 2^64 for very large inputs if (multiplier != 0 && value > std::numeric_limits::max() / multiplier) { throw_or_abort("Invalid storage size format: '" + size_str + "'. Value out of range"); diff --git a/barretenberg/cpp/src/barretenberg/wasi/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wasi/CMakeLists.txt deleted file mode 100644 index 97beea9e64cb..000000000000 --- a/barretenberg/cpp/src/barretenberg/wasi/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -barretenberg_module(wasi) \ No newline at end of file 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 31258e97195e..000000000000 --- 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_init.cpp b/barretenberg/cpp/src/barretenberg/wasi/wasm_init.cpp deleted file mode 100644 index 2f19eb341d3a..000000000000 --- a/barretenberg/cpp/src/barretenberg/wasi/wasm_init.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/** - * WASI "reactors" expect an exported _initialize function, and for it to be called before any other exported - * function. It triggers initialization of all globals and statics. If you don't do this, every function call will - * trigger the initialization of globals as if they are "main". Good luck with that... - */ -#include - -extern "C" { -extern void __wasm_call_ctors(void); - -WASM_EXPORT void _initialize() -{ - __wasm_call_ctors(); -} -} \ No newline at end of file diff --git a/barretenberg/cpp/src/barretenberg/wasm_threads_tests/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/CMakeLists.txt new file mode 100644 index 000000000000..b33587e325fe --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/CMakeLists.txt @@ -0,0 +1 @@ +barretenberg_module(wasm_threads_tests common ecc) diff --git a/barretenberg/cpp/src/barretenberg/wasm_threads_tests/memory_growth.test.cpp b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/memory_growth.test.cpp new file mode 100644 index 000000000000..2a6a74ccb0d0 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/memory_growth.test.cpp @@ -0,0 +1,158 @@ +/** + * Memory-growth-under-threads test. + * + * Triggers a `memory.grow` mid-execution by allocating buffers from multiple + * threads totalling more than the link-time INITIAL_MEMORY=512MB. Each + * thread writes a known pattern into its slice BEFORE the cross-thread grow + * may happen, then re-reads the same slice AFTER the grow and asserts the + * data survived intact. We additionally compare `__builtin_wasm_memory_size` + * before and after to fail loudly if a future flag bump suppresses the + * grow (which would silently neuter the test). + * + * The historical bug class this exercises: under the previous wasm runtime, + * `memory.grow` could detach a thread's TypedArray view of the heap without + * the thread noticing, causing later memcmp operations to silently read + * stale memory. Emscripten + wasm threads remap the shared buffer atomically + * (SHARED_MEMORY=1), so a properly wired build should pass cleanly. + */ + +#include "barretenberg/common/log.hpp" +#include "barretenberg/common/throw_or_abort.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Sized to comfortably exceed the link-time INITIAL_MEMORY=512MB. With +// kThreads * kPerThreadGrowBytes + kPreGrowBytes ~= 772 MiB, the wasm +// linear memory MUST grow at least once mid-execution. If a future +// toolchain bump moves INITIAL_MEMORY higher than that we will need to +// scale this test up in lockstep, and the post-test +// __builtin_wasm_memory_size assertion below will fail loudly so the +// drift is caught immediately. +constexpr size_t kPreGrowBytes = 4 * 1024 * 1024; // 4 MiB seed buffer +constexpr size_t kPerThreadGrowBytes = 96 * 1024 * 1024; // 96 MiB per worker +constexpr size_t kThreads = 8; +constexpr size_t kWasmPageBytes = 65536; + +void fill_pattern(uint8_t* buf, size_t len, uint8_t seed) +{ + for (size_t i = 0; i < len; ++i) { + buf[i] = static_cast((i * 1103515245u + seed) & 0xFFu); + } +} + +bool check_pattern(const uint8_t* buf, size_t len, uint8_t seed) +{ + for (size_t i = 0; i < len; ++i) { + if (buf[i] != static_cast((i * 1103515245u + seed) & 0xFFu)) { + return false; + } + } + return true; +} + +} // namespace + +TEST(WasmThreadsMemoryGrowth, PreGrowDataSurvivesGrow) +{ + // Allocate the pre-grow buffer once; every worker reads + checks it. + auto seed_buf = std::make_unique(kPreGrowBytes); + fill_pattern(seed_buf.get(), kPreGrowBytes, /*seed=*/0xAB); + + // Each worker owns a slice of `per_thread_buffers` that it writes a + // known pattern into BEFORE the cross-thread allocations that force + // `memory.grow`. After the grow each worker re-reads its slice and + // asserts the pattern is intact. This is the property a faulty + // memory.grow under threads would break. + std::vector> per_thread_buffers(kThreads); + +#if defined(__wasm__) + const size_t pages_before = __builtin_wasm_memory_size(0); +#else + const size_t pages_before = 0; +#endif + + std::atomic completed{ 0 }; + std::atomic mismatches{ 0 }; + + // Phase barrier: every worker must finish its pre-grow write before any + // worker allocates a fresh grow buffer. Otherwise a fast worker could + // allocate-and-grow while a slow worker is still mid-write, masking the + // bug class we are trying to catch. + std::barrier sync_point(static_cast(kThreads)); + + std::vector workers; + workers.reserve(kThreads); + + for (size_t t = 0; t < kThreads; ++t) { + workers.emplace_back([&, t]() { + // Pre-grow phase: allocate a per-thread buffer and write a + // known pattern. We do this BEFORE any thread triggers the + // grow so we can prove the pattern survives the grow. + per_thread_buffers[t] = std::make_unique(kPerThreadGrowBytes); + fill_pattern(per_thread_buffers[t].get(), kPerThreadGrowBytes, static_cast(t)); + + // Capture a snapshot of the shared seed buffer too. + std::vector snapshot(kPreGrowBytes); + std::memcpy(snapshot.data(), seed_buf.get(), kPreGrowBytes); + + // Wait until every thread has written its pre-grow slice. + sync_point.arrive_and_wait(); + + // Grow-triggering allocation. The total across all threads + // (kThreads * kPerThreadGrowBytes + kPreGrowBytes) is sized + // to exceed the link-time INITIAL_MEMORY=512MB; at least + // one of these allocations MUST cause memory.grow to fire. + auto grow_buf = std::make_unique(kPerThreadGrowBytes); + fill_pattern(grow_buf.get(), kPerThreadGrowBytes, static_cast(0xC0u + t)); + + // Post-grow validation: the pre-grow pattern must still be + // readable in every thread's owned slice, and the shared seed + // buffer must match the pre-grow snapshot byte-for-byte. The + // previous wasm runtime exhibited stale TypedArray views here. + if (!check_pattern(per_thread_buffers[t].get(), kPerThreadGrowBytes, static_cast(t))) { + mismatches.fetch_add(1, std::memory_order_relaxed); + } + if (!check_pattern(seed_buf.get(), kPreGrowBytes, /*seed=*/0xAB)) { + mismatches.fetch_add(1, std::memory_order_relaxed); + } + if (std::memcmp(snapshot.data(), seed_buf.get(), kPreGrowBytes) != 0) { + mismatches.fetch_add(1, std::memory_order_relaxed); + } + // Confirm the freshly allocated post-grow buffer holds the + // pattern we just wrote into it. + if (!check_pattern(grow_buf.get(), kPerThreadGrowBytes, static_cast(0xC0u + t))) { + mismatches.fetch_add(1, std::memory_order_relaxed); + } + + completed.fetch_add(1, std::memory_order_relaxed); + }); + } + + for (auto& w : workers) { + w.join(); + } + + EXPECT_EQ(completed.load(), kThreads); + EXPECT_EQ(mismatches.load(), 0u) << "pre-grow data corruption detected after memory.grow under threads"; + +#if defined(__wasm__) + const size_t pages_after = __builtin_wasm_memory_size(0); + // Linear memory MUST have grown at least once. If a future toolchain + // bump pushes INITIAL_MEMORY above the test's allocation total, this + // assertion fires and the test scaling needs to be revisited in + // lockstep. Without it, a flag drift would silently neuter the test. + EXPECT_GT(pages_after * kWasmPageBytes, pages_before * kWasmPageBytes) + << "memory.grow never fired: linear memory was already large enough to absorb " + << (kThreads * kPerThreadGrowBytes + kPreGrowBytes) << " bytes without growth. " + << "Either INITIAL_MEMORY shrank (good) or this test's allocation total is too small (bad)."; +#endif +} diff --git a/barretenberg/cpp/src/barretenberg/wasm_threads_tests/pool_exhaustion.test.cpp b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/pool_exhaustion.test.cpp new file mode 100644 index 000000000000..5bed457ae1aa --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wasm_threads_tests/pool_exhaustion.test.cpp @@ -0,0 +1,82 @@ +/** + * Pool exhaustion test for the Emscripten-backed wasm runtime. + * + * Spawns `PTHREAD_POOL_SIZE + 4` threads of real work and waits for all of + * them to complete. The previous wasm runtime silently deadlocked when more + * pthreads were spawned than the static pool could hold. + * + * The toolchain ships with `PTHREAD_POOL_SIZE_STRICT=1`, which warns and + * elastically grows the pool when the pre-spawned pool is exhausted. This + * test exercises that elastic-growth path by spawning more std::threads + * than the link-time pool size and asserting every one of them completes. + * + * The CMake-side `PTHREAD_POOL_SIZE` is 16 (see wasm-emscripten.cmake), so + * we spawn 20 threads. + */ + +#include "barretenberg/common/log.hpp" +#include "barretenberg/common/throw_or_abort.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +// Mirrors the link-time PTHREAD_POOL_SIZE. We spawn +4 threads to step over +// the pool bound. If the build flags ever drift, update this constant in +// lockstep with the toolchain. +constexpr size_t kEmscriptenPthreadPoolSize = 16; +constexpr size_t kThreadsToSpawn = kEmscriptenPthreadPoolSize + 4; + +// Real work: a simple FNV-1a hash over a fixed buffer. This is deliberately +// CPU-bound so the runtime cannot finish all threads on a single worker +// before the join hits. +uint64_t do_work(uint64_t seed) +{ + uint64_t h = 0xcbf29ce484222325ULL ^ seed; + for (uint64_t i = 0; i < (1ULL << 16); ++i) { + h ^= (i + seed); + h *= 0x100000001b3ULL; + } + return h; +} + +} // namespace + +TEST(WasmThreadsPoolExhaustion, AllSpawnedThreadsComplete) +{ + std::atomic completed{ 0 }; + std::vector threads; + threads.reserve(kThreadsToSpawn); + std::vector results(kThreadsToSpawn, 0); + + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); + + for (size_t i = 0; i < kThreadsToSpawn; ++i) { + threads.emplace_back([i, &completed, &results]() { + results[i] = do_work(static_cast(i + 1)); + completed.fetch_add(1, std::memory_order_relaxed); + }); + } + + for (auto& t : threads) { + if (std::chrono::steady_clock::now() > deadline) { + // If we ever overshoot 60s here, the pool is wedged. Abort with + // a clear message rather than letting gtest's per-test timeout + // chew through CI. + throw_or_abort("pool exhaustion test exceeded 60s deadline; pthread pool likely deadlocked"); + } + t.join(); + } + + EXPECT_EQ(completed.load(), kThreadsToSpawn); + + // Defensive: every thread should have produced a non-zero hash. + for (size_t i = 0; i < kThreadsToSpawn; ++i) { + EXPECT_NE(results[i], 0ULL) << "thread " << i << " produced zero hash"; + } +} diff --git a/barretenberg/docs/docs/how_to_guides/on-the-browser.md b/barretenberg/docs/docs/how_to_guides/on-the-browser.md index 0a92f22455f9..07820dfee656 100644 --- a/barretenberg/docs/docs/how_to_guides/on-the-browser.md +++ b/barretenberg/docs/docs/how_to_guides/on-the-browser.md @@ -120,15 +120,9 @@ const api = await Barretenberg.new({ threads: 1 }); ### Memory Management -It can be useful to manage memory manually, specially if targeting specific memory-constrained environments (ex. Safari): - -```typescript -// Configure initial and maximum memory -const api = await Barretenberg.new({ - threads: 4, - memory: { - initial: 128 * 1024 * 1024, // 128MB - maximum: 512 * 1024 * 1024 // 512MB - } -}); -``` +The wasm binary's initial and maximum memory are link-time constants baked +into the Emscripten-built artifact. They cannot be tuned per `Barretenberg.new` +call -- the underlying `MODULARIZE=1` loader does not honor a runtime +override. To target a memory-constrained environment, rebuild bb.js with a +toolchain that pins the memory shape you need (see +`cmake/toolchains/wasm-emscripten.cmake`). diff --git a/barretenberg/ts/package.json b/barretenberg/ts/package.json index d44208ca1147..d6a48f268342 100644 --- a/barretenberg/ts/package.json +++ b/barretenberg/ts/package.json @@ -11,6 +11,21 @@ "require": "./dest/node-cjs/index.js", "browser": "./dest/browser/index.js", "default": "./dest/node/index.js" + }, + "./barretenberg.wasm": { + "browser": "./dest/browser/barretenberg_wasm/barretenberg.wasm", + "require": "./dest/node-cjs/barretenberg_wasm/barretenberg.wasm", + "default": "./dest/node/barretenberg_wasm/barretenberg.wasm" + }, + "./barretenberg.js": { + "browser": "./dest/browser/barretenberg_wasm/barretenberg.js", + "require": "./dest/node-cjs/barretenberg_wasm/barretenberg.js", + "default": "./dest/node/barretenberg_wasm/barretenberg.js" + }, + "./barretenberg.worker.mjs": { + "browser": "./dest/browser/barretenberg_wasm/barretenberg.worker.mjs", + "require": "./dest/node-cjs/barretenberg_wasm/barretenberg.worker.mjs", + "default": "./dest/node/barretenberg_wasm/barretenberg.worker.mjs" } }, "bin": { @@ -20,7 +35,19 @@ "src/", "dest/", "build/", - "README.md" + "README.md", + "dest/node/barretenberg_wasm/barretenberg.js", + "dest/node/barretenberg_wasm/barretenberg.wasm", + "dest/node/barretenberg_wasm/barretenberg.wasm.gz", + "dest/node/barretenberg_wasm/barretenberg.worker.mjs", + "dest/node-cjs/barretenberg_wasm/barretenberg.js", + "dest/node-cjs/barretenberg_wasm/barretenberg.wasm", + "dest/node-cjs/barretenberg_wasm/barretenberg.wasm.gz", + "dest/node-cjs/barretenberg_wasm/barretenberg.worker.mjs", + "dest/browser/barretenberg_wasm/barretenberg.js", + "dest/browser/barretenberg_wasm/barretenberg.wasm", + "dest/browser/barretenberg_wasm/barretenberg.wasm.gz", + "dest/browser/barretenberg_wasm/barretenberg.worker.mjs" ], "scripts": { "clean": "rm -rf ./dest .tsbuildinfo .tsbuildinfo.cjs ./src/cbind/generated", diff --git a/barretenberg/ts/scripts/browser_postprocess.sh b/barretenberg/ts/scripts/browser_postprocess.sh index ad3e1a091c18..197273e52b3c 100755 --- a/barretenberg/ts/scripts/browser_postprocess.sh +++ b/barretenberg/ts/scripts/browser_postprocess.sh @@ -1,19 +1,27 @@ #!/usr/bin/env bash +# Post-build pass for the browser bundle. +# +# Under the previous wasm runtime we maintained parallel `node/`+`browser/` +# implementation trees and rewrote imports here. The Emscripten loader in +# `barretenberg_wasm/barretenberg_wasm_main/index.ts` is environment-agnostic +# (it imports the `barretenberg.js` glue dynamically and lets Emscripten pick +# the right environment), so the only browser-specific thing left to do is +# strip out the Node-only worker factory and the `worker_threads` import path. +set -euo pipefail DIR="./dest/browser" -# Remove all files under **/node/** -for node_file in $(find $DIR -type d -path "./*/node*"); do - rm -rf $node_file; -done +# Remove the Node-only worker factory -- browsers spawn workers via the +# Emscripten glue's own Web Worker code path. Also strip the helpers/node +# subtree, which references `worker_threads`/`fs`. +rm -rf "$DIR/barretenberg_wasm/barretenberg_wasm_main/factory" 2>/dev/null || true +rm -rf "$DIR/barretenberg_wasm/helpers/node" 2>/dev/null || true -# 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' {} + +# Rewrite any leftover `helpers/node` import to a no-op browser facade. +# The browser bundle is consumed by Webpack/Vite which tree-shakes +# unreachable code. +find "$DIR" -type f -name "*.js" -exec \ + sed -i 's|helpers/node/index\.js|helpers/index.js|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 +echo "browser_postprocess: stripped Node-only modules under $DIR" diff --git a/barretenberg/ts/scripts/copy_wasm.sh b/barretenberg/ts/scripts/copy_wasm.sh index 1251d38474af..17afaa231842 100755 --- a/barretenberg/ts/scripts/copy_wasm.sh +++ b/barretenberg/ts/scripts/copy_wasm.sh @@ -1,6 +1,13 @@ #!/bin/sh -# Builds the wasm and copies it into it's location in dest. -# If you want to build the wasm with debug info for stack traces, use NO_STRIP=1 BUILD_CPP=1. +# Build (if BUILD_CPP=1) and copy the Emscripten-emitted wasm artifacts into +# the published bb.js layout under dest//barretenberg_wasm/. +# +# The Emscripten target produces a triple per build: +# - barretenberg.js (ES6 loader / glue) +# - barretenberg.wasm (the wasm module itself) +# - barretenberg.worker.mjs (pthread worker, only with the threaded preset) +# We also publish a gzipped copy of the .wasm so existing fetch helpers that +# detect gzip magic bytes keep working. set -e cd $(dirname $0)/.. @@ -9,16 +16,28 @@ if [ "${BUILD_CPP:-0}" -eq 1 ]; then parallel --line-buffered --tag '../cpp/bootstrap.sh {}' ::: build_wasm build_wasm_threads fi -# Copy the wasm to its home in the bb.js dest folder. -# We only need the threads wasm, as node always uses threads. -# We need to take two copies for both esm and cjs builds. You can't use symlinks when publishing. -# This probably isn't a big deal however due to compression. -# When building the browser bundle, both wasms are inlined directly. -mkdir -p ./dest/node/barretenberg_wasm -mkdir -p ./dest/node-cjs/barretenberg_wasm -mkdir -p ./dest/browser/barretenberg_wasm +THREADED_BIN="../cpp/build-wasm-threads/bin" +SINGLE_BIN="../cpp/build-wasm/bin" -cp ../cpp/build-wasm-threads/bin/barretenberg.wasm.gz ./dest/node/barretenberg_wasm/barretenberg-threads.wasm.gz -cp ../cpp/build-wasm-threads/bin/barretenberg.wasm.gz ./dest/node-cjs/barretenberg_wasm/barretenberg-threads.wasm.gz -cp ../cpp/build-wasm-threads/bin/barretenberg.wasm.gz ./dest/browser/barretenberg_wasm/barretenberg-threads.wasm.gz -cp ../cpp/build-wasm/bin/barretenberg.wasm.gz ./dest/browser/barretenberg_wasm/barretenberg.wasm.gz +for flavor in node node-cjs browser; do + dest="./dest/${flavor}/barretenberg_wasm" + mkdir -p "$dest" + + # Threaded artifact is the canonical bb.js wasm. We ship both a raw .wasm + # (Emscripten loader expects this) and a .wasm.gz (back-compat for browser + # fetch helpers that detect gzip). + cp "${THREADED_BIN}/barretenberg.js" "$dest/barretenberg.js" + cp "${THREADED_BIN}/barretenberg.wasm" "$dest/barretenberg.wasm" + cp "${THREADED_BIN}/barretenberg.wasm.gz" "$dest/barretenberg.wasm.gz" + if [ -f "${THREADED_BIN}/barretenberg.worker.mjs" ]; then + cp "${THREADED_BIN}/barretenberg.worker.mjs" "$dest/barretenberg.worker.mjs" + fi +done + +# Browser flavor additionally ships the single-threaded fallback (used in +# environments without crossOriginIsolated headers). +if [ -f "${SINGLE_BIN}/barretenberg.wasm" ]; then + cp "${SINGLE_BIN}/barretenberg.js" "./dest/browser/barretenberg_wasm/barretenberg.single.js" + cp "${SINGLE_BIN}/barretenberg.wasm" "./dest/browser/barretenberg_wasm/barretenberg.single.wasm" + cp "${SINGLE_BIN}/barretenberg.wasm.gz" "./dest/browser/barretenberg_wasm/barretenberg.single.wasm.gz" +fi diff --git a/barretenberg/ts/src/barretenberg/clean_shutdown.harness.ts b/barretenberg/ts/src/barretenberg/clean_shutdown.harness.ts new file mode 100644 index 000000000000..f1207ff906db --- /dev/null +++ b/barretenberg/ts/src/barretenberg/clean_shutdown.harness.ts @@ -0,0 +1,115 @@ +/** + * Child-process harness for the clean-shutdown test. Driven by + * `clean_shutdown.test.ts` via `child_process.spawn`. + * + * Output contract: prints `DESTROY_AT=` immediately before + * calling `destroy()`. The parent process measures the gap between that + * line and process exit. + * + * The harness MUST dispatch real work onto the pthread pool before destroy + * -- otherwise the post-destroy 5s budget is trivially passing because the + * pool was never warmed. To genuinely keep multiple Workers busy at the + * moment `destroy()` is called we issue many concurrent `srsInitSrs` calls + * via `Promise.all`. `srsInitSrs` is the canonical bb.js path that uses + * `parallel_for` internally (see `bbapi/bbapi_srs.cpp` -- three + * `parallel_for` blocks over the points buffer), so each call genuinely + * fans out across the pthread pool rather than serialising on the proxy + * thread the way blake2s does. + * + * We also fire blake2s calls in parallel with the SRS calls so that, even + * if a future bbapi refactor makes srsInitSrs serial, the pool is hit by + * a second concurrent message-passing path while we tear down. + * + * After destroy() returns, we arm a 5s unref'd timer that calls + * `process.exit(2)` if it fires. The unref means the timer does NOT keep + * the event loop alive on its own -- natural exit (clean teardown of the + * pthread pool) wins if it happens first. If the runtime hangs (pool not + * torn down, leaked workers, etc.) the timer fires and the harness exits + * non-zero, which the parent test asserts on. + */ + +import { BackendType, Barretenberg } from './index.js'; + +// Each parallel_for-driven call must be large enough that the work splits +// across multiple worker threads (DEFAULT_MIN_ITERS_PER_THREAD = 16; with +// 4 threads we want >= 64 points). We use 4096 points (256 KiB at 64 +// bytes/point) per srsInitSrs call so the work fans out reliably. +const POOL_WARM_POINTS_PER_CALL = 4096; +const POOL_WARM_PARALLEL_CALLS = 8; +const BLAKE_TICKLE_ITERATIONS = 32; +const POST_DESTROY_BUDGET_MS = 5_000; + +const UNCOMPRESSED_POINT_BYTES = 64; // sizeof(g1::affine_element) == 64 +const G2_POINT_BYTES = 128; + +/** + * Build a buffer of `count` infinity points in uncompressed form. + * + * `affine_element::serialize_from_buffer` (ecc/groups/affine_element.hpp) + * detects "all bits set" as the point-at-infinity sentinel. Filling the + * buffer with 0xFF therefore produces a valid (curve-membership-passing) + * uncompressed BN254 G1 point buffer that drives the parallel_for + * dispatch in `SrsInitSrs::execute` without requiring a real CRS file. + */ +function buildInfinityPointsBuffer(count: number, bytesPerPoint: number): Uint8Array { + return new Uint8Array(count * bytesPerPoint).fill(0xff); +} + +async function main() { + const bb = await Barretenberg.new({ + backend: BackendType.Wasm, + threads: 4, + skipSrsInit: true, + logger: () => {}, + }); + + // Build the synthetic SRS payload once and reuse across the parallel + // invocations. The buffer is filled with 0xFF so every 64-byte slice + // decodes to the BN254 G1 point at infinity, which is curve-valid. + const pointsBuf = buildInfinityPointsBuffer(POOL_WARM_POINTS_PER_CALL, UNCOMPRESSED_POINT_BYTES); + const g2Point = new Uint8Array(G2_POINT_BYTES).fill(0xff); + + const blakeInputs = Array.from({ length: BLAKE_TICKLE_ITERATIONS }, (_, i) => + Buffer.from(`bb-clean-shutdown-tickle-${i}-${'x'.repeat(64)}`), + ); + + // Fire SRS init calls + blake2s calls concurrently. The SRS calls + // genuinely fan out via parallel_for on the wasm side; the blake2s calls + // saturate the proxy-thread message queue. Combined, every worker in + // the pthread pool has executed at least one task before we measure + // post-destroy shutdown latency. + const work: Promise[] = []; + for (let i = 0; i < POOL_WARM_PARALLEL_CALLS; ++i) { + work.push( + bb.srsInitSrs({ + pointsBuf, + numPoints: POOL_WARM_POINTS_PER_CALL, + g2Point, + }), + ); + } + for (const data of blakeInputs) { + work.push(bb.blake2s({ data })); + } + await Promise.all(work); + + process.stdout.write(`DESTROY_AT=${Date.now()}\n`); + await bb.destroy(); + + // Race-against-natural-exit guard. setTimeout is unref'd so it does not + // itself keep the event loop alive; if the pthread pool is properly torn + // down there are no other handles and Node exits naturally before this + // ever fires. If the runtime hangs (leaked workers, leftover I/O), the + // timer is the fallback that produces a non-zero exit so the parent test + // sees a real failure instead of timing out at the parent's outer guard. + const failTimer = setTimeout(() => { + process.stderr.write(`HARNESS_HANG_AFTER_DESTROY_${POST_DESTROY_BUDGET_MS}MS\n`); + process.exit(2); + }, POST_DESTROY_BUDGET_MS); + failTimer.unref?.(); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/barretenberg/ts/src/barretenberg/clean_shutdown.test.ts b/barretenberg/ts/src/barretenberg/clean_shutdown.test.ts new file mode 100644 index 000000000000..6066a792a093 --- /dev/null +++ b/barretenberg/ts/src/barretenberg/clean_shutdown.test.ts @@ -0,0 +1,67 @@ +/** + * Clean shutdown property test. + * + * Spawns a child Node process that: + * 1. Creates a Barretenberg instance with multiple worker threads. + * 2. Submits enough work to ensure the pthread pool is warm. + * 3. Calls `destroy()` on the instance. + * 4. Returns from main and lets the runtime exit naturally. + * + * The historical bug class: under the previous wasm runtime, `destroy()` + * left the pthread pool in a state that pinned the Node process open + * forever. The test asserts the child exits within 5 seconds of returning + * from main. + */ + +import { spawn } from 'child_process'; +import path from 'path'; + +// `barretenberg/ts/scripts/run_test.sh` cd's into `barretenberg/ts/` before +// invoking jest, so cwd is the package root regardless of where the compiled +// test file lives (`dest/node/...`). Resolving the harness off cwd keeps the +// path stable as we don't ship the harness source into `dest/`. +const PROJECT_ROOT = process.cwd(); +const ENTRYPOINT = path.join(PROJECT_ROOT, 'src', 'barretenberg', 'clean_shutdown.harness.ts'); + +describe('Barretenberg clean shutdown', () => { + it('exits within 5s of destroy()', async () => { + const start = Date.now(); + + const child = spawn( + process.execPath, + ['--no-warnings', '--experimental-vm-modules', '--loader', 'ts-node/esm', ENTRYPOINT], + { + cwd: PROJECT_ROOT, + env: { ...process.env, NODE_NO_WARNINGS: '1' }, + stdio: 'pipe', + }, + ); + + let stdout = ''; + let stderr = ''; + child.stdout.on('data', d => (stdout += d.toString())); + child.stderr.on('data', d => (stderr += d.toString())); + + const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null; ms: number }>((resolve, reject) => { + const timer = setTimeout(() => { + child.kill('SIGKILL'); + reject(new Error(`harness did not exit within 30s. stdout: ${stdout} stderr: ${stderr}`)); + }, 30_000); + child.on('exit', (code, signal) => { + clearTimeout(timer); + resolve({ code, signal, ms: Date.now() - start }); + }); + }); + + // The harness prints a "DESTROY_AT=" line right before destroy(). + // Anything after that is shutdown latency. + const m = stdout.match(/DESTROY_AT=(\d+)/); + expect(m).not.toBeNull(); + const destroyAt = m ? Number(m[1]) : 0; + const shutdownMs = exit.ms - (destroyAt - start); + + expect(exit.signal).toBeNull(); + expect(exit.code).toBe(0); + expect(shutdownMs).toBeLessThan(5_000); + }, 60_000); +}); diff --git a/barretenberg/ts/src/barretenberg/reentry.test.ts b/barretenberg/ts/src/barretenberg/reentry.test.ts new file mode 100644 index 000000000000..e75794aca4e3 --- /dev/null +++ b/barretenberg/ts/src/barretenberg/reentry.test.ts @@ -0,0 +1,68 @@ +/** + * Re-entry test: `Barretenberg.new` -> `destroy` -> `Barretenberg.new` again + * inside a single Node process. Asserts the second instance is fully usable + * by round-tripping a real wasm call (`blake2s`) and comparing the hash + * against a known-correct constant. + * + * The historical bug class: under the previous wasm runtime, the pthread + * polyfill leaked global state and the second `Barretenberg.new` would hang + * waiting for a pool that never re-warmed, OR it would silently return a + * broken instance whose calls produced garbage. Emscripten cleans up the + * pool with `PThread.terminateAllThreads()` on destroy and the second + * factory call spins up a fresh pool. We pin `backend: BackendType.Wasm` + * so the test always exercises the wasm code path -- otherwise on a host + * with a `bb` binary installed the default would route through the native + * Unix-socket backend and never touch wasm. + */ + +import { BackendType, Barretenberg } from './index.js'; + +// blake2s hash of the input below. Must match `blake2s.test.ts` (same +// input, same expected output). If barretenberg's blake2s ever changes, +// both tests should be updated in lockstep. +const BLAKE2S_INPUT = Buffer.from( + 'abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789', +); +const BLAKE2S_EXPECTED = new Uint8Array([ + 0x44, 0xdd, 0xdb, 0x39, 0xbd, 0xb2, 0xaf, 0x80, 0xc1, 0x47, 0x89, 0x4c, 0x1d, 0x75, 0x6a, 0xda, + 0x3d, 0x1c, 0x2a, 0xc2, 0xb1, 0x00, 0x54, 0x1e, 0x04, 0xfe, 0x87, 0xb4, 0xa5, 0x9e, 0x12, 0x43, +]); + +describe('Barretenberg re-entry after destroy', () => { + it('a second Barretenberg.new() succeeds and the instance round-trips to wasm', async () => { + const first = await Barretenberg.new({ + backend: BackendType.Wasm, + threads: 2, + skipSrsInit: true, + logger: () => {}, + }); + expect(first).toBeDefined(); + + // Sanity: the first instance answers correctly before destroy. This + // anchors the expected-hash constant against the live build (so a + // future change to barretenberg's blake2s surfaces as both halves of + // the test failing in lockstep, not just the post-reentry half). + const firstResp = await first.blake2s({ data: BLAKE2S_INPUT }); + expect(firstResp.hash).toEqual(BLAKE2S_EXPECTED); + + await first.destroy(); + + const second = await Barretenberg.new({ + backend: BackendType.Wasm, + threads: 2, + skipSrsInit: true, + logger: () => {}, + }); + expect(second).toBeDefined(); + + // The bar for "the second instance is operational": a real wasm call + // round-trips and produces the known-correct hash. `typeof destroy === + // 'function'` (the previous assertion) only proved the constructor + // returned an object; this proves the wasm pthread pool re-initialised + // cleanly and the message-passing path works end-to-end. + const secondResp = await second.blake2s({ data: BLAKE2S_INPUT }); + expect(secondResp.hash).toEqual(BLAKE2S_EXPECTED); + + await second.destroy(); + }, 60_000); +}); diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_base/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_base/index.ts index fe2dc3afbef9..eb19daf5608d 100644 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_base/index.ts +++ b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_base/index.ts @@ -1,117 +1,13 @@ -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. + * Compatibility shim. Under the Emscripten-based loader the dedicated + * `BarretenbergWasmBase` (which used to back both the "main" and "thread" + * implementations of a hand-rolled worker harness) collapses into the single + * `BarretenbergWasmMain` class -- Emscripten owns thread spawning. We keep + * the export name so external imports continue to type-check. + * + * TODO(2026-05-26): drop this alias after the compatibility window expires + * (matches the deletion date on the legacy-toolchain-compat CI job). At + * removal time, sweep `BarretenbergWasmBase` imports and rewrite them to + * `BarretenbergWasmMain` from `../barretenberg_wasm_main/index.js`. */ -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; - for (; m[i] !== 0; ++i); - const textDecoder = new TextDecoder('ascii'); - return textDecoder.decode(m.slice(addr, i)); - } -} +export { BarretenbergWasmMain as BarretenbergWasmBase } from '../barretenberg_wasm_main/index.js'; diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/index.ts deleted file mode 100644 index 730d44031e4c..000000000000 --- a/barretenberg/ts/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/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts deleted file mode 100644 index 2db704fa0bdd..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/browser/main.worker.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expose } from 'comlink'; -import { BarretenbergWasmMain } from '../../index.js'; -import { Ready } from '../../../helpers/browser/index.js'; - -expose(new BarretenbergWasmMain()); -postMessage(Ready); diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts index 40a2dadfa4d9..e7fac5b937ad 100644 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts +++ b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/index.ts @@ -1,19 +1,27 @@ +/** + * Spawn a Node worker_thread that hosts a `BarretenbergWasmMain` instance. + * Used by `BarretenbergWasmAsyncBackend` when `useWorker: true` so that + * synchronous wasm calls do not block the host main thread. + * + * The worker itself loads the Emscripten glue (which manages its own + * pthread pool internally). Calls into the worker are proxied via comlink. + */ + import { Worker } from 'worker_threads'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; -function getCurrentDir() { +function getCurrentDir(): string { if (typeof __dirname !== 'undefined') { return __dirname; - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return dirname(fileURLToPath(import.meta.url)); } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - ESM-only API. + return dirname(fileURLToPath(import.meta.url)); } -export function createMainWorker() { - const __dirname = getCurrentDir(); - const worker = new Worker(__dirname + `/main.worker.js`); +export function createMainWorker(): Promise { + const here = getCurrentDir(); + const worker = new Worker(`${here}/main.worker.js`); return Promise.resolve(worker); } diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts index fffea8a82e1a..2062b770c358 100644 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts +++ b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/factory/node/main.worker.ts @@ -1,3 +1,10 @@ +/** + * Node worker_threads entrypoint that exposes a `BarretenbergWasmMain` over + * comlink. The worker loads the Emscripten-emitted glue inside its own + * isolate; pthreads spawned by the wasm module live as nested workers under + * this one (Emscripten's standard pattern). + */ + import { parentPort } from 'worker_threads'; import { expose } from 'comlink'; import { BarretenbergWasmMain } from '../../index.js'; diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/index.ts index a8c716694876..d85e7ec6669f 100644 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/index.ts +++ b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_main/index.ts @@ -1,165 +1,194 @@ -import { type Worker } from 'worker_threads'; -import { Remote } from 'comlink'; -import { getNumCpu, getRemoteBarretenbergWasm, getSharedMemoryAvailable } from '../helpers/index.js'; -import { createThreadWorker } from '../barretenberg_wasm_thread/factory/node/index.js'; -import { type BarretenbergWasmThreadWorker } from '../barretenberg_wasm_thread/index.js'; -import { BarretenbergWasmBase } from '../barretenberg_wasm_base/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. + * Thin Emscripten loader for barretenberg's wasm artifacts. + * + * Emscripten emits a JS glue (`barretenberg.js`) plus a sibling + * `barretenberg.wasm` and, when pthreads are enabled, a + * `barretenberg.worker.mjs`. The glue handles: + * - WebAssembly.Module compilation + instantiation + * - pthread worker spawning (PTHREAD_POOL_SIZE / Module.pthreadPoolSize) + * - memory growth + thread-safe heap views + * + * The class below exposes the same surface bb.js consumed under the previous + * hand-rolled worker harness: `init`, `call`, `cbindCall`, `writeMemory`, + * `getMemorySlice`, `getMemory`, `destroy`. `Barretenberg.new({ threads: N })` + * forwards `N` to Emscripten's `Module({ pthreadPoolSize: N })`. */ -export class BarretenbergWasmMain extends BarretenbergWasmBase { + +import type { Remote } from 'comlink'; +import { HeapAllocator } from './heap_allocator.js'; + +type EmscriptenModule = { + HEAPU8: Uint8Array; + _bbmalloc: (size: number) => number; + _bbfree: (ptr: number) => void; + ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any; + cwrap(ident: string, returnType: string | null, argTypes: string[]): (...args: any[]) => any; + // Emscripten exposes WASM_EXPORT functions as Module._. + [k: string]: any; +}; + +type EmscriptenFactory = (init?: Record) => Promise; + +async function loadEmscriptenFactory(wasmPath?: string): Promise { + // The packaged glue lives next to the wasm artifact at + // `//barretenberg_wasm/barretenberg.js`. In Node we resolve + // via import.meta.url; tests can override via `wasmPath` (which points at + // the .wasm gzip; the glue lives next to it). + let glueUrl: string; + if (wasmPath) { + const dir = wasmPath.split('/').slice(0, -1).join('/') || '.'; + glueUrl = `${dir}/barretenberg.js`; + } else { + // The build output places this file alongside the glue. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - ESM-only `import.meta.url`. + const here = new URL('.', import.meta.url); + glueUrl = new URL('./barretenberg.js', here).href; + } + const mod = (await import(/* webpackIgnore: true */ glueUrl)) as { default: EmscriptenFactory }; + return mod.default; +} + +export class BarretenbergWasmMain { static MAX_THREADS = 32; - private workers: Worker[] = []; - private remoteWasms: BarretenbergWasmThreadWorker[] = []; - private nextWorker = 0; - private nextThreadId = 1; - private useCustomLogger = false; + + protected module!: EmscriptenModule; + protected logger: (msg: string) => void = () => {}; + private threads = 1; + private destroyed = 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 + private msgpackInputScratch = 0; + private msgpackOutputScratch = 0; + private readonly MSGPACK_SCRATCH_SIZE = 1024 * 1024 * 8; // 8 MiB - public getNumThreads() { - return this.workers.length + 1; + public getNumThreads(): number { + return this.threads; } /** - * Init as main thread. Spawn child threads. + * Initialise the wasm module. + * + * Signature is preserved from the previous custom worker harness so + * `Barretenberg.new({ threads: N })` keeps working without changes to the + * higher-level backend code. + * + * - `module`: ignored. Emscripten's glue compiles its own bundled wasm. + * Kept in the signature so the existing call sites in `wasm.ts` and + * `index.test.ts` link without further churn. + * - `threads`: forwarded to Emscripten as `pthreadPoolSize`. + * - `logger`: forwarded as `Module.print` / `Module.printErr`. + * - `unref`: ignored under Emscripten (pthread workers are unref'd by the + * runtime when the module is `.destroy()`-ed). + * + * INITIAL_MEMORY / MAXIMUM_MEMORY are NOT runtime overrides under + * Emscripten with MODULARIZE=1 -- they are link-time settings baked into + * the wasm binary's memory section. The factory's `init` argument silently + * ignores them. To change INITIAL_MEMORY, edit the toolchain + * (cmake/toolchains/wasm-emscripten.cmake) and rebuild. We deliberately + * do not accept these as parameters here so callers cannot mistakenly + * believe they are wired through. */ public async init( - module: WebAssembly.Module, - threads = Math.min(getNumCpu(), BarretenbergWasmMain.MAX_THREADS), + _module: unknown, + threads: number = Math.min(BarretenbergWasmMain.MAX_THREADS, 32), logger?: (msg: string) => void, - initial = 35, - maximum = this.getDefaultMaximumMemoryPages(), - unref = false, - ) { - // Track whether a custom logger was provided so workers know whether to postMessage logs - this.useCustomLogger = logger !== undefined; + _unref = false, + wasmPath?: string, + ): Promise { this.logger = logger ?? (() => {}); + this.threads = Math.max(1, Math.min(threads, BarretenbergWasmMain.MAX_THREADS)); + + const factory = await loadEmscriptenFactory(wasmPath); + // Emscripten 4.x runtime overrides on the Module object are camelCase + // (matches `Module['pthreadPoolSize']` in upstream `library_pthread.js` + // and `src/preamble.js`). Pinning the key name wrong silently falls + // back to the link-time default (16 workers) -- which would make + // `threads: 4` mean "16 workers" and warp the perf gate. + this.module = await factory({ + pthreadPoolSize: this.threads, + print: this.logger, + printErr: this.logger, + noExitRuntime: false, + }); - 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.msgpackInputScratch = this.module._bbmalloc(this.MSGPACK_SCRATCH_SIZE); + this.msgpackOutputScratch = this.module._bbmalloc(this.MSGPACK_SCRATCH_SIZE); this.logger( `Allocated msgpack scratch buffers: ` + - `input @ ${this.msgpackInputScratch}, output @ ${this.msgpackOutputScratch} (${this.MSGPACK_SCRATCH_SIZE} bytes each)`, + `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)); + public exports(): EmscriptenModule { + return this.module; + } - // 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)); + public call(name: string, ...args: any[]): number { + if (this.destroyed) { + throw new Error(`WASM call '${name}' after destroy()`); + } + const fn = (this.module as any)[`_${name}`]; + if (!fn) { + throw new Error(`WASM function ${name} not found.`); + } + try { + return (fn(...args) as number) >>> 0; + } catch (err: any) { + const message = `WASM function ${name} aborted, error: ${err}`; + this.logger(message); + if (err && err.stack) { + this.logger(err.stack); } + throw err; + } + } - this.remoteWasms = await Promise.all(this.workers.map(getRemoteBarretenbergWasm)); - await Promise.all(this.remoteWasms.map(w => w.initThread(module, this.memory, this.useCustomLogger))); + public memSize(): number { + return this.module.HEAPU8.length; + } - if (unref) { - for (const worker of this.workers) { - worker.unref(); - } - } - } + public getMemorySlice(start: number, end: number): Uint8Array { + return this.module.HEAPU8.subarray(start, end).slice(); } - 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; + public writeMemory(offset: number, arr: Uint8Array): void { + this.module.HEAPU8.set(arr, offset); } - /** - * 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); - }); - } + public getMemory(): Uint8Array { + return this.module.HEAPU8; } /** - * Called on main thread. Signals child threads to gracefully exit. + * Tear the module down. Frees scratch buffers, terminates Emscripten's + * pthread pool, and lets Node exit when there are no other handles. */ - 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 */ + public async destroy(): Promise { + if (this.destroyed) { + return; + } + this.destroyed = true; + try { + if (this.msgpackInputScratch) { + this.module._bbfree(this.msgpackInputScratch); + } + if (this.msgpackOutputScratch) { + this.module._bbfree(this.msgpackOutputScratch); + } + } catch { + /* swallow: tearing down anyway */ + } + // Emscripten exposes `PThread.terminateAllThreads()` for pthread pool cleanup. + const pthread = (this.module as any).PThread; + if (pthread && typeof pthread.terminateAllThreads === 'function') { + try { + pthread.terminateAllThreads(); + } catch { + /* ditto */ + } + } } callWasmExport(funcName: string, inArgs: (Uint8Array | number)[], outLens: (number | undefined)[]) { @@ -172,7 +201,7 @@ export class BarretenbergWasmMain extends BarretenbergWasmBase { return outArgs; } - private getOutputArgs(outLens: (number | undefined)[], outPtrs: number[], alloc: HeapAllocator) { + private getOutputArgs(outLens: (number | undefined)[], outPtrs: number[], alloc: HeapAllocator): Uint8Array[] { return outLens.map((len, i) => { if (len) { return this.getMemorySlice(outPtrs[i], outPtrs[i] + len); @@ -183,7 +212,7 @@ export class BarretenbergWasmMain extends BarretenbergWasmBase { // 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. + // The length will be found in the first 4 bytes of the buffer, big endian. const lslice = this.getMemorySlice(ptr, ptr + 4); const length = new DataView(lslice.buffer, lslice.byteOffset, lslice.byteLength).getUint32(0, false); @@ -191,60 +220,47 @@ export class BarretenbergWasmMain extends BarretenbergWasmBase { }); } - cbindCall(cbind: string, inputBuffer: Uint8Array): any { + cbindCall(cbind: string, inputBuffer: Uint8Array): Uint8Array { 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 + // Re-fetch memory after WASM call -- the buffer can be detached after a memory.grow. 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); } @@ -254,6 +270,10 @@ export class BarretenbergWasmMain extends BarretenbergWasmBase { } /** - * The comlink type that asyncifies the BarretenbergWasmMain api. + * The comlink type that asyncifies the BarretenbergWasmMain api. Retained for + * source compatibility with `wasm.ts` and downstream consumers; under the + * Emscripten loader the same class can be used directly without comlink, but + * `BarretenbergWasmAsyncBackend` still wraps it via comlink when running + * inside a Node worker_threads worker for the `useWorker: true` path. */ export type BarretenbergWasmMainWorker = Remote; diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/index.ts deleted file mode 100644 index 4b65988cf278..000000000000 --- a/barretenberg/ts/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/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts deleted file mode 100644 index 3b3f83ed463a..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/browser/thread.worker.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expose } from 'comlink'; -import { BarretenbergWasmThread } from '../../index.js'; -import { Ready } from '../../../helpers/browser/index.js'; - -expose(new BarretenbergWasmThread()); -postMessage(Ready); diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts deleted file mode 100644 index f473fb54b620..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Worker } from 'worker_threads'; -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 function createThreadWorker() { - const __dirname = getCurrentDir(); - const worker = new Worker(__dirname + `/thread.worker.js`); - return Promise.resolve(worker); -} diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts deleted file mode 100644 index 3cd026e778ff..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/factory/node/thread.worker.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { parentPort } from 'worker_threads'; -import { expose } from 'comlink'; -import { BarretenbergWasmThread } from '../../index.js'; -import { nodeEndpoint } from '../../../helpers/node/node_endpoint.js'; - -if (!parentPort) { - throw new Error('No parentPort'); -} - -const endpoint = nodeEndpoint(parentPort); - -expose(new BarretenbergWasmThread(), endpoint); diff --git a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts b/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts deleted file mode 100644 index d5989f4c218f..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/barretenberg_wasm_thread/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Remote } from 'comlink'; -import { killSelf, threadLogger } from '../helpers/index.js'; -import { BarretenbergWasmBase } from '../barretenberg_wasm_base/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/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/browser/barretenberg-threads.ts deleted file mode 100644 index 23aa9ed841f7..000000000000 --- a/barretenberg/ts/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/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/browser/barretenberg.ts deleted file mode 100644 index c75591e5c6e1..000000000000 --- a/barretenberg/ts/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/src/barretenberg_wasm/fetch_code/browser/index.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/browser/index.ts deleted file mode 100644 index 73a008f73bf1..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/fetch_code/browser/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import pako 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 = pako.ungzip(buffer); - return decompressedData.buffer as unknown as Uint8Array; - } else { - return buffer; - } -} diff --git a/barretenberg/ts/src/barretenberg_wasm/fetch_code/index.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/index.ts deleted file mode 100644 index 950c3e006707..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/fetch_code/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './node/index.js'; diff --git a/barretenberg/ts/src/barretenberg_wasm/fetch_code/node/index.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/node/index.ts deleted file mode 100644 index 28f2702e753b..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/fetch_code/node/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { readFile } from 'fs/promises'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import pako from 'pako'; - -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)); - } -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -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 = pako.ungzip(buffer); - return decompressedData.buffer as unknown as Uint8Array; - } else { - return buffer; - } -} diff --git a/barretenberg/ts/src/barretenberg_wasm/fetch_code/wasm-module.d.ts b/barretenberg/ts/src/barretenberg_wasm/fetch_code/wasm-module.d.ts deleted file mode 100644 index b5f0a8c6ba00..000000000000 --- a/barretenberg/ts/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/src/barretenberg_wasm/helpers/browser/index.ts b/barretenberg/ts/src/barretenberg_wasm/helpers/browser/index.ts deleted file mode 100644 index 2ff0bc6279f4..000000000000 --- a/barretenberg/ts/src/barretenberg_wasm/helpers/browser/index.ts +++ /dev/null @@ -1,54 +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 - 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/src/barretenberg_wasm/index.ts b/barretenberg/ts/src/barretenberg_wasm/index.ts index 1d55df471361..cf9680c8eb8e 100644 --- a/barretenberg/ts/src/barretenberg_wasm/index.ts +++ b/barretenberg/ts/src/barretenberg_wasm/index.ts @@ -1,21 +1,27 @@ -import { getSharedMemoryAvailable, getAvailableThreads } from './helpers/node/index.js'; -import { fetchCode } from './fetch_code/index.js'; +/** + * Compatibility shim for `fetchModuleAndThreads`. + * + * The previous wasm runtime needed bb.js to fetch + compile the wasm module + * itself before handing it to the worker harness. Under the Emscripten + * loader, the JS glue compiles its own bundled wasm during `await + * createBarretenbergModule(...)`, so all we need to do here is decide a + * thread count. + * + * The returned `module` slot is `undefined` -- callers must continue to pass + * it to `BarretenbergWasmMain.init` (which now ignores it). Keeping the + * shape stable avoids churn in `wasm.ts` and in test code that destructures + * `{ module, threads }`. + */ +import { getAvailableThreads, getSharedMemoryAvailable } from './helpers/index.js'; export async function fetchModuleAndThreads( desiredThreads = 32, - wasmPath?: string, + _wasmPath?: string, logger: (msg: string) => void = () => {}, -) { +): Promise<{ module: undefined; threads: number }> { const shared = getSharedMemoryAvailable(); - const availableThreads = shared ? await 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 }; + const threads = Math.min(desiredThreads, availableThreads, 32); + return { module: undefined, threads }; } diff --git a/barretenberg/ts/src/bb_backends/browser/index.ts b/barretenberg/ts/src/bb_backends/browser/index.ts index 33374de9fc9d..7109f49a28d2 100644 --- a/barretenberg/ts/src/bb_backends/browser/index.ts +++ b/barretenberg/ts/src/bb_backends/browser/index.ts @@ -19,7 +19,6 @@ export async function createAsyncBackend( threads: options.threads, wasmPath: options.wasmPath, logger, - memory: options.memory, useWorker, }); return new Barretenberg(wasm, options); diff --git a/barretenberg/ts/src/bb_backends/index.ts b/barretenberg/ts/src/bb_backends/index.ts index 18e17ea68407..a29298ff2aad 100644 --- a/barretenberg/ts/src/bb_backends/index.ts +++ b/barretenberg/ts/src/bb_backends/index.ts @@ -16,9 +16,6 @@ export type BackendOptions = { /** @description Number of threads to run the backend worker on */ threads?: number; - /** @description Initial and Maximum memory to be alloted to the backend worker */ - memory?: { initial?: number; maximum?: number }; - /** @description Path to download CRS files */ crsPath?: string; diff --git a/barretenberg/ts/src/bb_backends/node/index.ts b/barretenberg/ts/src/bb_backends/node/index.ts index c8e03e1a5196..caf45e2d5b74 100644 --- a/barretenberg/ts/src/bb_backends/node/index.ts +++ b/barretenberg/ts/src/bb_backends/node/index.ts @@ -57,7 +57,6 @@ export async function createAsyncBackend( threads: options.threads, wasmPath: options.wasmPath, logger: options.logger, - memory: options.memory, useWorker, unref: options.unref, }); diff --git a/barretenberg/ts/src/bb_backends/wasm.ts b/barretenberg/ts/src/bb_backends/wasm.ts index 37e895de33ab..0df8395d2548 100644 --- a/barretenberg/ts/src/bb_backends/wasm.ts +++ b/barretenberg/ts/src/bb_backends/wasm.ts @@ -52,16 +52,19 @@ export class BarretenbergWasmAsyncBackend implements IMsgpackBackendAsync { * @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 + * + * INITIAL_MEMORY / MAXIMUM_MEMORY are link-time settings baked into the + * wasm binary's memory section under MODULARIZE=1; the loader does not + * honor a runtime override. To change them, edit + * `cmake/toolchains/wasm-emscripten.cmake` and rebuild. */ static async new( options: { threads?: number; wasmPath?: string; logger?: (msg: string) => void; - memory?: { initial?: number; maximum?: number }; useWorker?: boolean; unref?: boolean; } = {}, @@ -74,13 +77,7 @@ export class BarretenbergWasmAsyncBackend implements IMsgpackBackendAsync { 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, - ); + await wasm.init(module, threads, proxy(options.logger ?? (() => {}))); if (options.unref) { worker.unref(); } @@ -89,7 +86,7 @@ export class BarretenbergWasmAsyncBackend implements IMsgpackBackendAsync { // 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); + await wasm.init(module, threads, options.logger, options.unref); return new BarretenbergWasmAsyncBackend(wasm); } } diff --git a/bootstrap.sh b/bootstrap.sh index b9bc7bacff0e..14344e972758 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -12,10 +12,15 @@ # 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 +# Node 22 is the floor for the wasm test harness (Node >= 22 ships +# --experimental-wasm-threads on by default and supports the worker_threads +# semantics Emscripten relies on). +export expected_min_node_version=22.0.0 export expected_min_zig_version=0.15.1 export expected_abs_rust_version=1.89.0 -export expected_abs_wasi_version=27.0 +# Pinned emsdk version. The single source of truth lives in /.emsdk-version +# at the repo root so CI images and developer installs stay in sync. +export expected_abs_emsdk_version=$(cat "$(git rev-parse --show-toplevel)/.emsdk-version" 2>/dev/null || echo "4.0.7") export expected_abs_foundry_version=1.4.1 export expected_abs_yarn_version=4.13.0 @@ -23,19 +28,22 @@ function ensure { command -v $1 &>/dev/null } -function install_wasi_sdk { - if cat /opt/wasi-sdk/VERSION 2> /dev/null | grep $expected_abs_wasi_version > /dev/null; then +function install_emsdk { + local target_dir=${EMSDK:-/opt/emsdk} + # If we already have the right version active, nothing to do. + if [ -d "$target_dir" ] && \ + grep -F "$expected_abs_emsdk_version" "$target_dir/.emscripten" >/dev/null 2>&1; then return fi - local arch=$(uname -m) - local os=$(os) - local triple=$expected_abs_wasi_version-$arch-$os - curl -LOs https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${expected_abs_wasi_version%%.*}/wasi-sdk-$triple.tar.gz - tar xzf wasi-sdk-$triple.tar.gz - rm wasi-sdk-$triple.tar.gz - echo "Installing wasi sdk at /opt/wasi-sdk..." - sudo rm -rf /opt/wasi-sdk - sudo mv wasi-sdk-$triple /opt/wasi-sdk + if [ ! -d "$target_dir/.git" ]; then + sudo rm -rf "$target_dir" + sudo git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$target_dir" + sudo chown -R "${USER:-$(id -un)}" "$target_dir" + fi + echo "Installing emsdk $expected_abs_emsdk_version at $target_dir..." + ( cd "$target_dir" && \ + ./emsdk install "$expected_abs_emsdk_version" && \ + ./emsdk activate "$expected_abs_emsdk_version" ) } function install_foundry { @@ -92,7 +100,7 @@ function install_ldid { -o $AZTEC_DEV_BIN/ldid && chmod +x $AZTEC_DEV_BIN/ldid } -export -f install_wasi_sdk install_foundry install_zig install_rustup install_node install_node_utils install_llvm \ +export -f install_emsdk install_foundry install_zig install_rustup install_node install_node_utils install_llvm \ install_yq install_ldid ensure function install_linux_deps { @@ -106,7 +114,7 @@ function install_linux_deps { spinner "Installing yq..." install_yq spinner "Installing ldid..." install_ldid spinner "Installing rustup..." install_rustup - spinner "Installing wasi-sdk..." install_wasi_sdk + spinner "Installing emsdk..." install_emsdk spinner "Installing foundry..." install_foundry spinner "Installing zig..." install_zig spinner "Installing node..." install_node @@ -130,7 +138,7 @@ function install_macos_deps { ln -sf "$llvm_bin/clang++" "$AZTEC_DEV_BIN/clang++-20" ln -sf "$llvm_bin/clang-format" "$AZTEC_DEV_BIN/clang-format-20" - spinner "Installing wasi-sdk..." install_wasi_sdk + spinner "Installing emsdk..." install_emsdk spinner "Installing foundry..." install_foundry spinner "Installing rustup..." install_rustup spinner "Installing zig..." install_zig @@ -270,9 +278,34 @@ function check_toolchains { # Cargo will download necessary version of rust at runtime but warn to update the build-image. echo -e "${bold}${yellow}WARN: Rust ${expected_abs_rust_version} is not installed. Update build-image.${reset}" fi - # Check wasi-sdk version. - if ! cat /opt/wasi-sdk/VERSION 2> /dev/null | grep $expected_abs_wasi_version > /dev/null; then - toolchain_incompatible + # Check emsdk: must be activated and pinned to the expected version. + # On Linux (incl. CI runners whose build-image predates the Emscripten + # migration), auto-install if the directory is missing or the pinned + # version isn't active. macOS still expects a manual install. + local emsdk_dir=${EMSDK:-/opt/emsdk} + if [ ! -x "$emsdk_dir/upstream/emscripten/emcc" ] && [ ! -x "$emsdk_dir/emcc" ]; then + if [ "$(uname)" = "Linux" ] && command -v sudo >/dev/null 2>&1; then + echo "emsdk not found at \$EMSDK ($emsdk_dir); installing..." + install_emsdk + else + echo "emsdk not found at \$EMSDK ($emsdk_dir). Source emsdk_env.sh from your install." + toolchain_incompatible + fi + fi + if ! grep -F "$expected_abs_emsdk_version" "$emsdk_dir/.emscripten" >/dev/null 2>&1; then + if [ "$(uname)" = "Linux" ] && command -v sudo >/dev/null 2>&1; then + echo "emsdk version $expected_abs_emsdk_version not active; reinstalling..." + install_emsdk + else + echo "emsdk version $expected_abs_emsdk_version not active (see .emsdk-version)." + toolchain_incompatible + fi + fi + # Source emsdk_env.sh so emcc/em++ are on PATH for downstream build steps; + # bootstrap.sh runs as a non-login shell and won't pick up /etc/profile.d. + if [ -f "$emsdk_dir/emsdk_env.sh" ]; then + # shellcheck disable=SC1091 + . "$emsdk_dir/emsdk_env.sh" >/dev/null 2>&1 || true fi # Check foundry version. for tool in forge anvil; do @@ -467,7 +500,7 @@ function bench { ### RELEASING ########################################################################################################## function versions { - local noir_version anvil_version node_version cmake_version clang_version zig_version rustc_version wasi_sdk_version + local noir_version anvil_version node_version cmake_version clang_version zig_version rustc_version emsdk_version noir_version=$(git -C noir/noir-repo describe --tags --always HEAD) anvil_version=$(anvil --version | head -n1 | sed -E 's/anvil Version: ([0-9.]+).*/\1/') node_version=$(node --version | cut -d 'v' -f 2) @@ -475,7 +508,7 @@ function versions { clang_version=$(clang++-20 --version | head -n1 | cut -d' ' -f4) zig_version=$(zig version) rustc_version=$(rustc --version | cut -d' ' -f2) - wasi_sdk_version=$(cat /opt/wasi-sdk/VERSION 2> /dev/null | head -n1) + emsdk_version=$(cat "$(git rev-parse --show-toplevel)/.emsdk-version" 2>/dev/null || echo unknown) echo "noir: $noir_version" echo "foundry: $anvil_version" echo "node: $node_version" @@ -483,7 +516,7 @@ function versions { echo "clang: $clang_version" echo "zig: $zig_version" echo "rustc: $rustc_version" - echo "wasi-sdk: $wasi_sdk_version" + echo "emsdk: $emsdk_version" } function release_bb_github { diff --git a/build-images/src/Dockerfile b/build-images/src/Dockerfile index 3ad856077caa..2de5ee002ccd 100644 --- a/build-images/src/Dockerfile +++ b/build-images/src/Dockerfile @@ -1,7 +1,7 @@ # cspell: disable ######################################################################################################################## -# Base build. Used as base in subsequent builds of foundry, wasi-sdk, etc. +# Base build. Used as base in subsequent builds of foundry, emsdk, etc. FROM ubuntu:noble AS base-build RUN export DEBIAN_FRONTEND="noninteractive" \ && apt update && apt install --no-install-recommends -y \ @@ -120,13 +120,15 @@ RUN wget https://apt.llvm.org/llvm.sh && \ ./llvm.sh 20 all && \ rm 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 +# Install Emscripten SDK (emsdk). Pinned via .emsdk-version at the repo root. +ARG EMSDK_VERSION=4.0.7 +RUN git clone --depth 1 https://github.com/emscripten-core/emsdk.git /opt/emsdk \ + && cd /opt/emsdk \ + && ./emsdk install "${EMSDK_VERSION}" \ + && ./emsdk activate "${EMSDK_VERSION}" +ENV EMSDK=/opt/emsdk +# Make the activated tooling visible on PATH for non-login shells too. +ENV PATH="/opt/emsdk:/opt/emsdk/upstream/emscripten:/opt/emsdk/node/current/bin:${PATH}" # Install foundry. COPY --from=foundry /opt/foundry /opt/foundry @@ -156,11 +158,6 @@ 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 \ - && rm -rf wasmtime* - # Install zig. RUN arch=$(uname -m) && \ zig_version=0.15.1 && \ diff --git a/cspell.json b/cspell.json index 9766afc1ea5a..55b1c6b6e4ae 100644 --- a/cspell.json +++ b/cspell.json @@ -123,6 +123,8 @@ "ecdsasecp", "elif", "emittable", + "emscripten", + "emsdk", "endgroup", "enrs", "entrypoints", diff --git a/docs/docs-operate/operators/setup/building-from-source.md b/docs/docs-operate/operators/setup/building-from-source.md index 61a1c1232958..d25b2ee1a1ad 100644 --- a/docs/docs-operate/operators/setup/building-from-source.md +++ b/docs/docs-operate/operators/setup/building-from-source.md @@ -298,7 +298,7 @@ This approach is faster but requires trusting the published image. The official To build without Docker, install all build dependencies locally and run `./bootstrap.sh` directly: -- Install all toolchains from the build image (Node.js 24, Rust 1.85.0, Clang 20, CMake, wasi-sdk) +- Install all toolchains from the build image (Node.js 24, Rust 1.85.0, Clang 20, CMake, emsdk) - Run `bootstrap.sh check` to verify your environment - See `build-images/README.md` for details diff --git a/scripts/setup-container.sh b/scripts/setup-container.sh index 50826fa6fd35..557c3bbf0e59 100644 --- a/scripts/setup-container.sh +++ b/scripts/setup-container.sh @@ -139,16 +139,26 @@ EOF chmod +x /etc/profile.d/rust.sh # ============================================================================= -# SECTION 5: wasi-sdk -# ============================================================================= -log_info "Installing wasi-sdk 27..." - -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 +# SECTION 5: Emscripten SDK (emsdk) +# ============================================================================= +# Pin lives in /.emsdk-version at the repo root; this container layer reads it +# at build time so a single edit drives every CI image. +EMSDK_VERSION=$(cat /.emsdk-version 2>/dev/null || echo "4.0.7") +log_info "Installing emsdk ${EMSDK_VERSION}..." + +git clone --depth 1 https://github.com/emscripten-core/emsdk.git /opt/emsdk +cd /opt/emsdk +./emsdk install "${EMSDK_VERSION}" +./emsdk activate "${EMSDK_VERSION}" +cd - + +# Expose emsdk on every login shell. +cat >> /etc/profile.d/emsdk.sh << 'EOF' +export EMSDK=/opt/emsdk +# shellcheck disable=SC1091 +. /opt/emsdk/emsdk_env.sh > /dev/null 2>&1 || true +EOF +chmod +x /etc/profile.d/emsdk.sh # ============================================================================= # SECTION 6: Foundry @@ -203,13 +213,11 @@ curl -sL https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursu -o /usr/local/bin/ldid && chmod +x /usr/local/bin/ldid # ============================================================================= -# SECTION 10: wasmtime +# SECTION 10: (intentionally left blank) # ============================================================================= -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 -rm -rf wasmtime* +# The previous wasm host-runtime layer has been removed. wasm test harnesses +# now run under Node via barretenberg/cpp/scripts/wasm-run, and Node itself is +# installed earlier in this script. # ============================================================================= # SECTION 11: npm global packages @@ -455,8 +463,8 @@ zig version echo -n "Foundry (forge): " /opt/foundry/bin/forge --version -echo -n "wasi-sdk: " -cat /opt/wasi-sdk/VERSION +echo -n "emsdk: " +( . /opt/emsdk/emsdk_env.sh > /dev/null 2>&1 && emcc --version | head -n1 ) || echo "(not found)" echo -n "yq: " yq --version