From 13161f1acfd7eb8f13dadb0dc8b066dbad5b6e25 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:13:37 +0000 Subject: [PATCH 1/4] feat: cut simulator over to generated bb-avm-sim IPC service Replace the in-process NAPI AVM with an out-of-process bb-avm-sim binary reached over IPC. The simulator drives a pool of bb-avm-sim processes (AvmSimulatorPool) behind an AvmExecutor that also runs the CDB server answering the C++ AVM's contract-data callbacks; per-fork work goes through AvmExecutor.forFork. The pool prewarms one process on spawn so the first simulate() runs at steady-state cost. Also adds the bench_compare script + bench-regression skill for PR perf checks. --- .claude/skills/bench-regression/SKILL.md | 90 ++++ .../cpp/src/barretenberg/avm/avm_execute.cpp | 25 +- .../barretenberg/nodejs_module/CMakeLists.txt | 2 +- .../avm_simulate/avm_simulate_napi.cpp | 429 ------------------ .../avm_simulate/avm_simulate_napi.hpp | 72 --- .../avm_simulate/ts_callback_contract_db.cpp | 260 ----------- .../avm_simulate/ts_callback_contract_db.hpp | 149 ------ .../avm_simulate/ts_callback_utils.cpp | 289 ------------ .../avm_simulate/ts_callback_utils.hpp | 114 ----- .../nodejs_module/init_module.cpp | 8 - .../vm2/simulation/lib/cancellation_token.hpp | 7 + ci3/bench_compare | 180 ++++++++ .../ts/recursive_verification/config.yaml | 15 +- ipc-runtime/ts/src/uds_server.ts | 13 +- yarn-project/.gitignore | 1 + yarn-project/.prettierignore | 2 +- yarn-project/aztec-node/package.json | 1 + .../aztec-node/src/aztec-node/server.test.ts | 8 +- .../aztec-node/src/aztec-node/server.ts | 36 +- .../src/avm_proving_tests/avm_bulk.test.ts | 1 + .../avm_check_circuit1.test.ts | 1 + .../avm_check_circuit2.test.ts | 39 +- .../avm_check_circuit3.test.ts | 1 + .../avm_check_circuit_amm.test.ts | 1 + .../avm_check_circuit_token.test.ts | 1 + .../avm_contract_class_limits.test.ts | 1 + .../avm_contract_updates.test.ts | 11 +- .../avm_proving_tests/avm_mega_bulk.test.ts | 1 + .../avm_proven_gadgets.test.ts | 2 + .../avm_proving_tests/avm_proving_tester.ts | 36 +- .../avm_public_fee_payment.test.ts | 1 + yarn-project/bootstrap.sh | 2 + yarn-project/end-to-end/.gitignore | 1 + .../src/avm_integration.test.ts | 1 + .../src/rollup_ivc_integration.test.ts | 1 + yarn-project/native/src/native_module.ts | 178 -------- yarn-project/package.json | 5 + yarn-project/prover-node/package.json | 1 + .../src/actions/rerun-epoch-proving-job.ts | 24 +- yarn-project/prover-node/src/factory.ts | 4 + .../prover-node/src/job/epoch-proving-job.ts | 39 +- .../prover-node/src/prover-node.test.ts | 5 +- yarn-project/prover-node/src/prover-node.ts | 4 +- yarn-project/simulator/eslint.config.js | 3 + yarn-project/simulator/package.json | 6 +- .../simulator/src/public/avm_executor.ts | 66 +++ .../simulator/src/public/avm_simulator.ts | 15 + .../src/public/avm_simulator_pool.ts | 185 ++++++++ .../src/public/cdb_ipc_server.test.ts | 25 + .../simulator/src/public/cdb_ipc_server.ts | 197 ++++++++ .../simulator/src/public/fixtures/index.ts | 1 + .../fixtures/public_processor_test_env.ts | 89 ++++ .../fixtures/public_tx_simulation_tester.ts | 57 ++- .../public/fuzzing/avm_fuzzer_simulator.ts | 38 +- yarn-project/simulator/src/public/index.ts | 5 + .../simulator/src/public/public_db_sources.ts | 2 +- .../apps_tests/deployments.test.ts | 54 +-- .../apps_tests/timeout_race.test.ts | 378 --------------- .../public_processor/apps_tests/token.test.ts | 53 +-- .../public_processor/public_processor.ts | 39 +- .../apps_tests/amm.test.ts | 1 + .../apps_tests/avm_gadgets.test.ts | 3 +- .../apps_tests/avm_test.test.ts | 1 + .../apps_tests/bench.test.ts | 37 +- .../apps_tests/cpp_exception_handling.test.ts | 11 +- .../apps_tests/token.test.ts | 1 + .../contract_provider_for_cpp.ts | 125 ----- .../dumping_public_tx_simulator.ts | 42 +- .../public/public_tx_simulator/factories.ts | 15 +- .../src/public/public_tx_simulator/index.ts | 7 +- .../public_tx_simulator.ts | 144 +++--- .../public_tx_simulator_base.ts | 14 +- .../public_tx_simulator_interface.ts | 10 +- .../src/interfaces/merkle_tree_operations.ts | 5 +- .../txe/esbuild/stubs/world_state_stub.ts | 16 + .../oracle/txe_oracle_top_level_context.ts | 27 +- .../txe/src/state_machine/synchronizer.ts | 21 +- .../src/checkpoint_builder.test.ts | 6 +- .../src/checkpoint_builder.ts | 17 +- .../src/validator.integration.test.ts | 13 +- yarn-project/world-state/package.json | 1 - yarn-project/world-state/src/index.ts | 1 + yarn-project/world-state/src/native/index.ts | 1 + .../src/native/native_world_state_instance.ts | 7 +- yarn-project/world-state/tsconfig.json | 3 - yarn-project/yarn.lock | 59 ++- 86 files changed, 1441 insertions(+), 2422 deletions(-) create mode 100644 .claude/skills/bench-regression/SKILL.md delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.cpp delete mode 100644 barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.hpp create mode 100755 ci3/bench_compare create mode 100644 yarn-project/simulator/src/public/avm_executor.ts create mode 100644 yarn-project/simulator/src/public/avm_simulator.ts create mode 100644 yarn-project/simulator/src/public/avm_simulator_pool.ts create mode 100644 yarn-project/simulator/src/public/cdb_ipc_server.test.ts create mode 100644 yarn-project/simulator/src/public/cdb_ipc_server.ts create mode 100644 yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts delete mode 100644 yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts delete mode 100644 yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts diff --git a/.claude/skills/bench-regression/SKILL.md b/.claude/skills/bench-regression/SKILL.md new file mode 100644 index 000000000000..f39a89061175 --- /dev/null +++ b/.claude/skills/bench-regression/SKILL.md @@ -0,0 +1,90 @@ +--- +name: bench-regression +description: Check a PR for performance regressions by comparing its benchmark run against its merge-base on next. Use when a dev asks "did my PR regress anything?" or wants to vet benchmark impact before merge. +argument-hint: [pr-ref] (defaults to HEAD) +--- + +# PR benchmark regression check + +Compares a PR's benchmark run against its baseline (merge-base on `next`, optionally a window of +baseline runs) and reports only the regressions that (a) are statistically real and (b) plausibly +relate to what the PR changed. The data-pulling and diff math are done by `ci3/bench_compare`; your +job is to get a run, invoke the script, then filter the output with judgement. + +## How benchmark data is stored (context) + +Each CI benchmark run uploads a single `bench-out/bench.json` (a flat array of `{name, unit, value}`, +`customSmallerIsBetter` — higher = worse) to the build-cache, keyed by the commit's **git tree hash** +(`bench-.tar.gz`, ~40 KB). A PR only produces one on an *uploadable* run: labels +`ci-full-no-test-cache`, `ci-full`, or a merge-queue run. `ci3/bench_compare` pulls these per-run +blobs — never the multi-MB per-branch `data.js` graph history. + +## Steps + +### 1. Get a benchmark run for the PR + +- Default to `HEAD` (or the ref the user gives). Try step 2 directly — `ci3/bench_compare` fails + clearly if the commit has no bench data. +- **If there's no run and the user is fine using the last one:** walk back the branch for the most + recent benched commit — `for c in $(git rev-list -n 30 HEAD); do ci3/bench_compare "$c" >/dev/null 2>&1 && echo "$c" && break; done` — and use that commit. +- **If a fresh run is needed:** tell the user to add the `ci-full-no-test-cache` label to the PR (that + triggers x-bench on a dedicated metal box and uploads) and to re-run this once CI finishes. Do not + block waiting unless asked. + +### 2. Determine the baseline branch — do NOT assume `next` + +The comparison is `merge-base(PR, baseline)`, so the baseline MUST be the PR's *actual* target branch. +A PR onto `v5-next` or a `merge-train/*` branch compared against `next` would report the entire +branch-vs-branch perf delta as bogus regressions. Get it from the PR itself: + +```bash +gh pr view --json baseRefName -q .baseRefName # e.g. next, v5-next, merge-train/spartan +git fetch origin # ensure origin/ is current +``` + +(You do NOT need to worry about cross-branch data mixups: the bench cache is content-addressed by git +tree hash, so `next`, `v5-next`, etc. already occupy different keys — a next commit's tree is only ever +benched by next-content. The only thing you control, and must get right, is *which branch's lineage* +you merge-base against.) + +### 3. Run the comparison + +```bash +ci3/bench_compare --baseline origin/ --window 5 --out /tmp/bench-report.json +``` + +- Always pass `--baseline` explicitly (the script auto-detects via `gh` if omitted, but be explicit). + It prints `baseline = …` to stderr — confirm it matches the PR's target before trusting the numbers. +- `--window 5` averages the merge-base + 4 preceding baseline runs, yielding `stddev` and `z` per bench + — essential for separating real regressions from flaky benches. Use `--window 1` if history is sparse. +- JSON on `--out` is `{ meta, benches:[{name,unit,pr,baseline,n,pct,stddev,z,status}] }`, sorted by + `pct` desc. Human summary on stderr. + +### 4. Filter to what matters (your judgement) + +Read the JSON and keep a regression only if **all** hold: + +- **Statistically real:** `z >= ~4` (the PR value is ≥4 baseline-stddevs above the mean). A large + `pct` with small/absent `z` is baseline noise — a flaky bench (e.g. `p2p/BatchTxRequester/.../duration` + hitting a 30 s timeout, `pct` in the thousands) — **ignore it**. When window is 1 (no `z`), fall back + to requiring a large `pct` AND a meaningful absolute magnitude. +- **Material magnitude:** skip tiny absolutes (e.g. `0.01 ms -> 0.02 ms` = +100% but noise). +- **Plausibly caused by the PR:** map the bench `name` prefix to the PR's changed areas. Get the diff + with `git diff --name-only $(git merge-base HEAD origin/next)..HEAD | cut -d/ -f1-2 | sort -u`, then + keep benches whose names start with a touched area (`yarn-project/simulator`, `barretenberg/cpp`, + `avm-transpiler`, …) and treat far-away regressions (a TS-only PR moving a C++ proving bench) as + drift/noise, not this PR. + +### 5. Report + +List the surviving regressions with `baseline -> pr unit`, `pct`, and `z`, grouped by area, and give a +one-line verdict (clean / N real regressions in ). Note anything you filtered out and why, so +the dev can override your judgement. + +## Notes + +- Baseline is `origin/next` by default; `git fetch` it first if stale (`ci3/bench_compare` errors if + the ref is missing). +- Requires `jq`, `python3`, and read access to the build-cache (the public `build-cache.aztec-labs.com` + endpoint needs no creds; the in-VPC S3 path is a fallback). +- To author or wire up new benchmarks, see the `adding-benchmarks` skill; this skill only *checks* them. diff --git a/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp b/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp index 7cb4e72e629e..4d00b7ec156a 100644 --- a/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp +++ b/barretenberg/cpp/src/barretenberg/avm/avm_execute.cpp @@ -13,8 +13,24 @@ namespace bb::avm { using namespace bb::avm2; using namespace bb::world_state; -// Global cancellation token for the currently active simulation. -// Set before simulation starts, cleared after. SIGUSR1 handler reads this to cancel. +// Cancellation for the single in-flight simulation. bb-avm-sim runs exactly one +// simulation at a time; the SIGUSR1 handler (which may run on any thread) cancels +// it through g_active_cancellation_token. +// +// The token is process-lifetime and never freed. A per-request token would let +// the signal handler dereference a pointer to a token the completing request had +// already freed (use-after-free), since the handler can run concurrently with the +// request thread unwinding. g_active_cancellation_token points at this token only +// while a simulation runs and is null otherwise, so a signal between simulations +// is a safe no-op. +// +// NOTE: cancellation is process-scoped (a signal), not request-scoped. A signal +// delivered late — after its target finished and the next simulation began on this +// process — would cancel the wrong simulation. The pool runs one simulation per +// process and signals only the in-flight one, so this isn't exercised today; +// hardening it would need a request-scoped cancel channel rather than a signal. +const avm2::simulation::CancellationTokenPtr g_sim_cancellation_token = + std::make_shared(); // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::atomic g_active_cancellation_token{ nullptr }; @@ -36,7 +52,10 @@ template static T deserialize_from_msgpack(const std::vector void handle_simulate(AvmRequest& request, wire::AvmSimulate&& command, Responder respond) { - auto cancellation_token = std::make_shared(); + // Reuse the process-lifetime token (cleared of any prior cancellation) instead + // of allocating a per-request one the signal handler could outlive. + g_sim_cancellation_token->reset(); + const avm2::simulation::CancellationTokenPtr& cancellation_token = g_sim_cancellation_token; try { auto sim_inputs = deserialize_from_msgpack(command.inputs); diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt index b3c83492445b..736f67b83f2b 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt @@ -27,7 +27,7 @@ string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR ${NODE_API_HEADERS_DIR}) add_library(nodejs_module SHARED ${SOURCE_FILES}) set_target_properties(nodejs_module PROPERTIES PREFIX "" SUFFIX ".node") target_include_directories(nodejs_module PRIVATE ${NODE_API_HEADERS_DIR} ${NODE_ADDON_API_DIR}) -target_link_libraries(nodejs_module PRIVATE ipc ipc_runtime vm2_sim wsdb_ipc_merkle_db) +target_link_libraries(nodejs_module PRIVATE ipc ipc_runtime lmdblib) # On macOS, Node.js N-API symbols are provided by the runtime, not at link time if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp deleted file mode 100644 index 819dbf0b7f14..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp +++ /dev/null @@ -1,429 +0,0 @@ -#include "barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp" - -#include -#include -#include - -#include "barretenberg/common/log.hpp" -#include "barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.hpp" -#include "barretenberg/nodejs_module/util/async_op.hpp" -#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl/msgpack_impl.hpp" -#include "barretenberg/vm2/avm_sim_api.hpp" -#include "barretenberg/vm2/common/avm_io.hpp" -#include "barretenberg/vm2/simulation/lib/cancellation_token.hpp" -#include "barretenberg/vm2_wsdb/wsdb_ipc_merkle_db.hpp" -#include "barretenberg/wsdb/generated/wsdb_ipc_client.hpp" - -namespace bb::nodejs { -namespace { - -// Log levels from TS foundation/src/log/log-levels.ts: ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', -// 'trace'] Map: 0=silent, 1=fatal, 2=error, 3=warn, 4=info, 5=verbose, 6=debug, 7=trace - -// Helper to set logging level based on TS log level -inline void set_logging_from_level(int ts_log_level) -{ - // Map TS log level (0-7) to C++ LogLevel enum - // TS: 0=silent, 1=fatal, 2=error, 3=warn, 4=info, 5=verbose, 6=debug, 7=trace - // C++: SILENT=0, FATAL=1, ERROR=2, WARN=3, INFO=4, VERBOSE=5, DEBUG=6, TRACE=7 - // They map 1:1 - if (ts_log_level >= 0 && ts_log_level <= 7) { - bb_log_level = static_cast(ts_log_level); - } else { - log_warn("Invalid log level from TypeScript: ", ts_log_level, ". Using default."); - } -} - -// Map C++ LogLevel enum to TypeScript log level string -// C++ LogLevel: SILENT=0, FATAL=1, ERROR=2, WARN=3, INFO=4, VERBOSE=5, DEBUG=6, TRACE=7 -// TS LogLevels: ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace'] -inline const char* cpp_log_level_to_ts(LogLevel level) -{ - switch (level) { - case LogLevel::SILENT: - return "silent"; - case LogLevel::FATAL: - return "fatal"; - case LogLevel::ERROR: - return "error"; - case LogLevel::WARN: - return "warn"; - case LogLevel::INFO: - return "info"; - case LogLevel::VERBOSE: - return "verbose"; - case LogLevel::DEBUG: - return "debug"; - case LogLevel::TRACE: - return "trace"; - default: - return "info"; - } -} - -// Helper to create a LogFunction wrapper from a ThreadSafeFunction -// This allows C++ logging to call back to TypeScript logger from worker threads -LogFunction create_log_function_from_tsfn(const std::shared_ptr& logger_tsfn) -{ - return [logger_tsfn](LogLevel level, const std::string& msg) { - // Convert C++ LogLevel to TS log level string - const char* ts_level = cpp_log_level_to_ts(level); - - // Call TypeScript logger function on the JS main thread - // Using BlockingCall to ensure synchronous execution - // Ignore errors - logging failures shouldn't crash the simulation - // NOTE: We copy the string because it might be destroyed before the callback is called. - logger_tsfn->BlockingCall([ts_level, msg](Napi::Env env, Napi::Function js_logger) { - // Create arguments: (level: string, msg: string) - auto level_js = Napi::String::New(env, ts_level); - auto msg_js = Napi::String::New(env, msg); - js_logger.Call({ level_js, msg_js }); - }); - }; -} - -// Callback method names -constexpr const char* CALLBACK_GET_CONTRACT_INSTANCE = "getContractInstance"; -constexpr const char* CALLBACK_GET_CONTRACT_CLASS = "getContractClass"; -constexpr const char* CALLBACK_ADD_CONTRACTS = "addContracts"; -constexpr const char* CALLBACK_GET_BYTECODE = "getBytecodeCommitment"; -constexpr const char* CALLBACK_GET_DEBUG_NAME = "getDebugFunctionName"; -constexpr const char* CALLBACK_CREATE_CHECKPOINT = "createCheckpoint"; -constexpr const char* CALLBACK_COMMIT_CHECKPOINT = "commitCheckpoint"; -constexpr const char* CALLBACK_REVERT_CHECKPOINT = "revertCheckpoint"; - -// RAII helper to automatically release thread-safe functions -// Used inside the async lambda to ensure cleanup in all code paths -class TsfnReleaser { - std::vector> tsfns_; - - public: - explicit TsfnReleaser(std::vector> tsfns) - : tsfns_(std::move(tsfns)) - {} - - ~TsfnReleaser() - { - for (auto& tsfn : tsfns_) { - if (tsfn) { - tsfn->Release(); - } - } - } - - // Prevent copying and moving - TsfnReleaser(const TsfnReleaser&) = delete; - TsfnReleaser& operator=(const TsfnReleaser&) = delete; - TsfnReleaser(TsfnReleaser&&) = delete; - TsfnReleaser& operator=(TsfnReleaser&&) = delete; -}; - -// Helper to create thread-safe function wrapper -inline std::shared_ptr make_tsfn(Napi::Env env, Napi::Function fn, const char* name) -{ - return std::make_shared(Napi::ThreadSafeFunction::New(env, fn, name, 0, 1)); -} - -// Bundle all contract-related thread-safe functions with named access -struct ContractTsfns { - std::shared_ptr instance; - std::shared_ptr class_; - std::shared_ptr add_contracts; - std::shared_ptr bytecode; - std::shared_ptr debug_name; - std::shared_ptr create_checkpoint; - std::shared_ptr commit_checkpoint; - std::shared_ptr revert_checkpoint; - - std::vector> to_vector() const - { - return { instance, class_, add_contracts, bytecode, debug_name, create_checkpoint, - commit_checkpoint, revert_checkpoint }; - } -}; - -// Helper to validate and extract contract provider callbacks -struct ContractCallbacks { - static constexpr const char* ALL_METHODS[] = { CALLBACK_GET_CONTRACT_INSTANCE, CALLBACK_GET_CONTRACT_CLASS, - CALLBACK_ADD_CONTRACTS, CALLBACK_GET_BYTECODE, - CALLBACK_GET_DEBUG_NAME, CALLBACK_CREATE_CHECKPOINT, - CALLBACK_COMMIT_CHECKPOINT, CALLBACK_REVERT_CHECKPOINT }; - - static void validate(Napi::Env env, Napi::Object provider) - { - for (const char* method : ALL_METHODS) { - if (!provider.Has(method)) { - throw Napi::TypeError::New( - env, std::string("contractProvider must have ") + method + " method. Missing methods: " + method); - } - } - } - - static Napi::Function get(Napi::Object provider, const char* name) - { - return provider.Get(name).As(); - } -}; -} // namespace - -Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info) -{ - Napi::Env env = cb_info.Env(); - - // Validate arguments - expects 3-6 arguments - // arg[0]: inputs Buffer (required) - // arg[1]: contractProvider object (required) - // arg[2]: worldStateHandle external (required) - // arg[3]: logLevel number (optional) - index into TS LogLevels array, -1 if omitted - // arg[4]: loggerFunction (optional) - can be null/undefined - // arg[5]: cancellationToken external (optional) - if (cb_info.Length() < 3) { - throw Napi::TypeError::New( - env, - "Wrong number of arguments. Expected 3-6 arguments: inputs Buffer, contractProvider " - "object, worldStateHandle, optional logLevel, optional loggerFunction, and optional cancellationToken."); - } - - /******************************* - *** AvmFastSimulationInputs *** - *******************************/ - if (!cb_info[0].IsBuffer()) { - throw Napi::TypeError::New(env, - "First argument must be a Buffer containing serialized AvmFastSimulationInputs"); - } - // Extract the inputs buffer - auto inputs_buffer = cb_info[0].As>(); - size_t length = inputs_buffer.Length(); - // Copy the buffer data into C++ memory (we can't access Napi objects from worker thread) - auto data = std::make_shared>(inputs_buffer.Data(), inputs_buffer.Data() + length); - - /*********************************** - *** ContractProvider (required) *** - ***********************************/ - if (!cb_info[1].IsObject()) { - throw Napi::TypeError::New(env, "Second argument must be a contractProvider object"); - } - // Extract and validate contract provider callbacks - auto contract_provider = cb_info[1].As(); - ContractCallbacks::validate(env, contract_provider); - // Create thread-safe function wrappers for callbacks - // These allow us to call TypeScript from the C++ worker thread - ContractTsfns tsfns{ - .instance = make_tsfn(env, - ContractCallbacks::get(contract_provider, CALLBACK_GET_CONTRACT_INSTANCE), - CALLBACK_GET_CONTRACT_INSTANCE), - .class_ = make_tsfn( - env, ContractCallbacks::get(contract_provider, CALLBACK_GET_CONTRACT_CLASS), CALLBACK_GET_CONTRACT_CLASS), - .add_contracts = - make_tsfn(env, ContractCallbacks::get(contract_provider, CALLBACK_ADD_CONTRACTS), CALLBACK_ADD_CONTRACTS), - .bytecode = - make_tsfn(env, ContractCallbacks::get(contract_provider, CALLBACK_GET_BYTECODE), CALLBACK_GET_BYTECODE), - .debug_name = - make_tsfn(env, ContractCallbacks::get(contract_provider, CALLBACK_GET_DEBUG_NAME), CALLBACK_GET_DEBUG_NAME), - .create_checkpoint = make_tsfn( - env, ContractCallbacks::get(contract_provider, CALLBACK_CREATE_CHECKPOINT), CALLBACK_CREATE_CHECKPOINT), - .commit_checkpoint = make_tsfn( - env, ContractCallbacks::get(contract_provider, CALLBACK_COMMIT_CHECKPOINT), CALLBACK_COMMIT_CHECKPOINT), - .revert_checkpoint = make_tsfn( - env, ContractCallbacks::get(contract_provider, CALLBACK_REVERT_CHECKPOINT), CALLBACK_REVERT_CHECKPOINT), - }; - - /********************************** - *** WSDB IPC path (required) *** - **********************************/ - if (!cb_info[2].IsString()) { - throw Napi::TypeError::New(env, "Third argument must be a WSDB IPC path (string)"); - } - std::string wsdb_ipc_path = cb_info[2].As().Utf8Value(); - - /*************************** - *** LogLevel (optional) *** - ***************************/ - int log_level = -1; - if (cb_info.Length() > 3 && cb_info[3].IsNumber()) { - log_level = cb_info[3].As().Int32Value(); - set_logging_from_level(log_level); - } - - /********************************* - *** LoggerFunction (optional) *** - *********************************/ - std::shared_ptr logger_tsfn = nullptr; - if (cb_info.Length() > 4 && !cb_info[4].IsNull() && !cb_info[4].IsUndefined()) { - if (cb_info[4].IsFunction()) { - // Logger function provided - create thread-safe wrapper - auto logger_function = cb_info[4].As(); - logger_tsfn = make_tsfn(env, logger_function, "LoggerCallback"); - // Create LogFunction wrapper and set it as the global log function - // This will be used by C++ logging macros (info, debug, vinfo, important) - set_log_function(create_log_function_from_tsfn(logger_tsfn)); - } else { - throw Napi::TypeError::New(env, "Fifth argument must be a logger function, null, or undefined"); - } - } - - /************************************* - *** Cancellation Token (optional) *** - *************************************/ - avm2::simulation::CancellationTokenPtr cancellation_token = nullptr; - if (cb_info.Length() > 5 && cb_info[5].IsExternal()) { - auto token_external = cb_info[5].As>(); - // Wrap the raw pointer in a shared_ptr that does NOT delete (since the External owns it) - cancellation_token = std::shared_ptr( - token_external.Data(), [](avm2::simulation::CancellationToken*) { - // No-op deleter: the External (via shared_ptr destructor callback) owns the token - }); - } - - /********************************************************** - *** Create Deferred Promise and launch async operation *** - **********************************************************/ - - auto deferred = std::make_shared(env); - ThreadedAsyncOperation::Run( - env, deferred, [data, tsfns, logger_tsfn, wsdb_ipc_path, cancellation_token](msgpack::sbuffer& result_buffer) { - // Collect all thread-safe functions including logger for cleanup - auto all_tsfns = tsfns.to_vector(); - all_tsfns.push_back(logger_tsfn); - // Ensure all thread-safe functions are released in all code paths - TsfnReleaser releaser = TsfnReleaser(std::move(all_tsfns)); - - try { - // Deserialize inputs from msgpack - avm2::AvmFastSimulationInputs inputs; - msgpack::object_handle obj_handle = - msgpack::unpack(reinterpret_cast(data->data()), data->size()); - msgpack::object obj = obj_handle.get(); - obj.convert(inputs); - - // Create TsCallbackContractDB with TypeScript callbacks - TsCallbackContractDB contract_db(*tsfns.instance, - *tsfns.class_, - *tsfns.add_contracts, - *tsfns.bytecode, - *tsfns.debug_name, - *tsfns.create_checkpoint, - *tsfns.commit_checkpoint, - *tsfns.revert_checkpoint); - - // Connect to aztec-wsdb and wrap in a WsdbIpcMerkleDB that implements - // LowLevelMerkleDBInterface. The connection is per-simulation; aztec-wsdb is a - // long-running server that the TS layer spawned and owns. - bb::wsdb::WsdbIpcClient wsdb_client(wsdb_ipc_path); - bb::avm2::simulation::WsdbIpcMerkleDB merkle_db(wsdb_client, inputs.ws_revision); - - avm2::AvmSimAPI avm; - avm2::TxSimulationResult result = avm.simulate(inputs, contract_db, merkle_db, cancellation_token); - - // Serialize the simulation result with msgpack into the return buffer to TS. - msgpack::pack(result_buffer, result); - } catch (const avm2::simulation::CancelledException& e) { - // Cancellation is an expected condition, rethrow with context - throw std::runtime_error("Simulation cancelled"); - } catch (const std::exception& e) { - // Rethrow with context (RAII wrappers will clean up automatically) - throw std::runtime_error(std::string("AVM simulation failed: ") + e.what()); - } catch (...) { - throw std::runtime_error("AVM simulation failed with unknown exception"); - } - }); - - return deferred->Promise(); -} - -Napi::Value AvmSimulateNapi::simulateWithHintedDbs(const Napi::CallbackInfo& cb_info) -{ - Napi::Env env = cb_info.Env(); - - // Validate arguments - expects 2 arguments - // arg[0]: inputs Buffer (required) - AvmProvingInputs - // arg[1]: logLevel number (required) - index into TS LogLevels array - if (cb_info.Length() < 2) { - throw Napi::TypeError::New(env, - "Wrong number of arguments. Expected 2 arguments: AvmProvingInputs/AvmCircuitInputs " - "msgpack Buffer and logLevel."); - } - - if (!cb_info[0].IsBuffer()) { - throw Napi::TypeError::New( - env, "First argument must be a Buffer containing serialized AvmProvingInputs/AvmCircuitInputs"); - } - - if (!cb_info[1].IsNumber()) { - throw Napi::TypeError::New(env, "Second argument must be a log level number (0-7)"); - } - - // Extract log level and set logging flags - int log_level = cb_info[1].As().Int32Value(); - set_logging_from_level(log_level); - - // Extract the inputs buffer - auto inputs_buffer = cb_info[0].As>(); - size_t length = inputs_buffer.Length(); - - // Copy the buffer data into C++ memory (we can't access Napi objects from worker thread) - auto data = std::make_shared>(inputs_buffer.Data(), inputs_buffer.Data() + length); - - // Create a deferred promise - auto deferred = std::make_shared(env); - - // Run on a dedicated std::thread (not libuv pool) - ThreadedAsyncOperation::Run(env, deferred, [data](msgpack::sbuffer& result_buffer) { - try { - // Deserialize inputs from msgpack - avm2::AvmProvingInputs inputs; - msgpack::object_handle obj_handle = - msgpack::unpack(reinterpret_cast(data->data()), data->size()); - msgpack::object obj = obj_handle.get(); - obj.convert(inputs); - - // Create AVM Sim API and run simulation with the hinted DBs - // All hints are already in the inputs, so no runtime contract DB callbacks needed - avm2::AvmSimAPI avm; - avm2::TxSimulationResult result = avm.simulate_with_hinted_dbs(inputs); - - // Serialize the simulation result with msgpack into the return buffer to TS. - msgpack::pack(result_buffer, result); - } catch (const std::exception& e) { - // Rethrow with context - throw std::runtime_error(std::string("AVM simulation with hinted DBs failed: ") + e.what()); - } catch (...) { - throw std::runtime_error("AVM simulation with hinted DBs failed with unknown exception"); - } - }); - - return deferred->Promise(); -} - -Napi::Value AvmSimulateNapi::createCancellationToken(const Napi::CallbackInfo& cb_info) -{ - Napi::Env env = cb_info.Env(); - - // Create a new CancellationToken. We use a shared_ptr to manage the lifetime, - // and the destructor callback in the External will clean it up when GC runs. - auto* token = new avm2::simulation::CancellationToken(); - - // Create an External with a destructor callback that deletes the token - return Napi::External::New( - env, token, [](Napi::Env /*env*/, avm2::simulation::CancellationToken* t) { delete t; }); -} - -Napi::Value AvmSimulateNapi::cancelSimulation(const Napi::CallbackInfo& cb_info) -{ - Napi::Env env = cb_info.Env(); - - if (cb_info.Length() < 1 || !cb_info[0].IsExternal()) { - throw Napi::TypeError::New(env, "Expected a CancellationToken External as argument"); - } - - auto token_external = cb_info[0].As>(); - avm2::simulation::CancellationToken* token = token_external.Data(); - - // Signal cancellation - this is thread-safe (atomic store) - token->cancel(); - - return env.Undefined(); -} - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp deleted file mode 100644 index b01ccc446a47..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include - -namespace bb::nodejs { - -/** - * @brief NAPI wrapper for the C++ AVM simulation. - * - * This class provides the bridge between TypeScript and the C++ avm_simulate*() functions. - * It handles deserialization of inputs, execution on a worker thread, and serialization of results. - * - * The simulate variation uses real world state and uses callbacks to TS for contract DB. - * - * The simulateWithHintedDbs variation uses pre-collected hints for world state and contracts DB. - * There are no callbacks to TS or direct calls to world state. - */ -class AvmSimulateNapi { - public: - /** - * @brief NAPI function to simulate AVM execution - * - * Expected arguments: - * - info[0]: Buffer containing serialized AvmFastSimulationInputs (msgpack) - * - info[1]: Object with contract provider callbacks: - * - getContractInstance(address: string): Promise - * - getContractClass(classId: string): Promise - * - info[2]: WSDB IPC path (string) — TS layer spawned aztec-wsdb at this path - * - info[3]: Log level number (0-7) - * - info[4]: External CancellationToken handle (optional) - * - * Returns: Promise containing serialized simulation results - * - * @param info NAPI callback info containing arguments - * @return Napi::Value Promise that resolves with simulation results - */ - static Napi::Value simulate(const Napi::CallbackInfo& info); - - /** - * @brief NAPI function to simulate AVM execution with pre-collected hints - * - * Expected arguments: - * - info[0]: Buffer containing serialized AvmProvingInputs (msgpack) - * - * @param info NAPI callback info containing arguments - * @return Napi::Value Promise that resolves with simulation results - */ - static Napi::Value simulateWithHintedDbs(const Napi::CallbackInfo& info); - - /** - * @brief Create a cancellation token that can be used to cancel a simulation. - * - * Returns: External - a handle to a new cancellation token - * - * @param info NAPI callback info (no arguments expected) - * @return Napi::Value External handle to the cancellation token - */ - static Napi::Value createCancellationToken(const Napi::CallbackInfo& info); - - /** - * @brief Cancel a simulation by signaling the provided cancellation token. - * - * Expected arguments: - * - info[0]: External CancellationToken handle - * - * @param info NAPI callback info containing the token - * @return Napi::Value undefined - */ - static Napi::Value cancelSimulation(const Napi::CallbackInfo& info); -}; - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.cpp deleted file mode 100644 index 7a3db13e83bf..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "ts_callback_contract_db.hpp" -#include "ts_callback_utils.hpp" - -#include -#include - -#include "barretenberg/common/log.hpp" - -namespace bb::nodejs { - -TsCallbackContractDB::TsCallbackContractDB(Napi::ThreadSafeFunction instanceCallback, - Napi::ThreadSafeFunction classCallback, - Napi::ThreadSafeFunction addContractsCallback, - Napi::ThreadSafeFunction bytecodeCommitmentCallback, - Napi::ThreadSafeFunction debugNameCallback, - Napi::ThreadSafeFunction createCheckpointCallback, - Napi::ThreadSafeFunction commitCheckpointCallback, - Napi::ThreadSafeFunction revertCheckpointCallback) - : contract_instance_callback_(std::move(instanceCallback)) - , contract_class_callback_(std::move(classCallback)) - , add_contracts_callback_(std::move(addContractsCallback)) - , bytecode_commitment_callback_(std::move(bytecodeCommitmentCallback)) - , debug_name_callback_(std::move(debugNameCallback)) - , create_checkpoint_callback_(std::move(createCheckpointCallback)) - , commit_checkpoint_callback_(std::move(commitCheckpointCallback)) - , revert_checkpoint_callback_(std::move(revertCheckpointCallback)) -{} - -std::optional TsCallbackContractDB::get_contract_instance( - const bb::avm2::AztecAddress& address) const -{ - if (released_) { - throw std::runtime_error("Cannot call get_contract_instance after releasing callbacks"); - } - - debug("TsCallbackContractDB: Fetching contract instance for address ", address); - - try { - auto result_data = - invoke_single_string_callback(contract_instance_callback_, format(address), "contract instance"); - - if (!result_data.has_value()) { - vinfo("Contract instance not found: ", address); - return std::nullopt; - } - - auto instance = deserialize_from_msgpack(*result_data, "contract instance"); - return std::make_optional(std::move(instance)); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to get contract instance for address ") + format(address) + ": " + - e.what()); - } -} - -std::optional TsCallbackContractDB::get_contract_class( - const bb::avm2::ContractClassId& class_id) const -{ - if (released_) { - throw std::runtime_error("Cannot call get_contract_class after releasing callbacks"); - } - - debug("TsCallbackContractDB: Fetching contract class for class_id ", class_id); - - try { - auto result_data = invoke_single_string_callback(contract_class_callback_, format(class_id), "contract class"); - - if (!result_data.has_value()) { - vinfo("Contract class not found: ", class_id); - return std::nullopt; - } - - auto contract_class = deserialize_from_msgpack(*result_data, "contract class"); - return std::make_optional(std::move(contract_class)); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to get contract class for class_id ") + format(class_id) + ": " + - e.what()); - } -} - -void TsCallbackContractDB::add_contracts(const bb::avm2::ContractDeploymentData& contract_deployment_data) -{ - if (released_) { - throw std::runtime_error("Cannot call add_contracts after releasing callbacks"); - } - - debug("TsCallbackContractDB: Adding contracts"); - - try { - auto serialized_data = serialize_to_msgpack(contract_deployment_data); - invoke_buffer_void_callback(add_contracts_callback_, std::move(serialized_data), "add_contracts"); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to add contracts: ") + e.what()); - } -} - -std::optional TsCallbackContractDB::get_bytecode_commitment( - const bb::avm2::ContractClassId& class_id) const -{ - if (released_) { - throw std::runtime_error("Cannot call get_bytecode_commitment after releasing callbacks"); - } - - debug("TsCallbackContractDB: Fetching bytecode commitment for class_id ", class_id); - - try { - auto result_data = - invoke_single_string_callback(bytecode_commitment_callback_, format(class_id), "bytecode commitment"); - - if (!result_data.has_value()) { - vinfo("Bytecode commitment not found: ", class_id); - return std::nullopt; - } - - auto commitment = deserialize_from_msgpack(*result_data, "bytecode commitment"); - return commitment; - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to get bytecode commitment for class_id ") + format(class_id) + - ": " + e.what()); - } -} - -std::optional TsCallbackContractDB::get_debug_function_name(const bb::avm2::AztecAddress& address, - const bb::avm2::FF& selector) const -{ - if (released_) { - throw std::runtime_error("Cannot call get_debug_function_name after releasing callbacks"); - } - - debug("TsCallbackContractDB: Fetching debug function name for address ", address, " selector ", selector); - - try { - auto result_data = invoke_double_string_callback( - debug_name_callback_, format(address), format(selector), "debug function name"); - - if (!result_data.has_value()) { - debug("Debug function name not found for address ", address, " selector ", selector); - return std::nullopt; - } - - // Convert the vector of bytes back to a string - std::string name(result_data->begin(), result_data->end()); - return name; - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to get debug function name for address ") + format(address) + - " selector " + format(selector) + ": " + e.what()); - } -} - -void TsCallbackContractDB::create_checkpoint() -{ - if (released_) { - throw std::runtime_error("Cannot call create_checkpoint after releasing callbacks"); - } - - debug("TsCallbackContractDB: Creating checkpoint"); - - try { - // Call the TypeScript callback with no arguments - auto result = invoke_ts_callback_with_promise( - create_checkpoint_callback_, - "create_checkpoint", - [](Napi::Env env, Napi::Function js_callback, std::shared_ptr data) { - auto js_result = js_callback.Call({}); - - if (!js_result.IsPromise()) { - data->error_message = "TypeScript callback did not return a Promise"; - data->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - auto resolve_handler = create_void_resolve_handler(env, data); - auto reject_handler = create_reject_handler(env, data); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to create checkpoint: ") + e.what()); - } -} - -void TsCallbackContractDB::commit_checkpoint() -{ - if (released_) { - throw std::runtime_error("Cannot call commit_checkpoint after releasing callbacks"); - } - - debug("TsCallbackContractDB: Committing checkpoint"); - - try { - // Call the TypeScript callback with no arguments - auto result = invoke_ts_callback_with_promise( - commit_checkpoint_callback_, - "commit_checkpoint", - [](Napi::Env env, Napi::Function js_callback, std::shared_ptr data) { - auto js_result = js_callback.Call({}); - - if (!js_result.IsPromise()) { - data->error_message = "TypeScript callback did not return a Promise"; - data->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - auto resolve_handler = create_void_resolve_handler(env, data); - auto reject_handler = create_reject_handler(env, data); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to commit checkpoint: ") + e.what()); - } -} - -void TsCallbackContractDB::revert_checkpoint() -{ - if (released_) { - throw std::runtime_error("Cannot call revert_checkpoint after releasing callbacks"); - } - - debug("TsCallbackContractDB: Reverting checkpoint"); - - try { - // Call the TypeScript callback with no arguments - auto result = invoke_ts_callback_with_promise( - revert_checkpoint_callback_, - "revert_checkpoint", - [](Napi::Env env, Napi::Function js_callback, std::shared_ptr data) { - auto js_result = js_callback.Call({}); - - if (!js_result.IsPromise()) { - data->error_message = "TypeScript callback did not return a Promise"; - data->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - auto resolve_handler = create_void_resolve_handler(env, data); - auto reject_handler = create_reject_handler(env, data); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to revert checkpoint: ") + e.what()); - } -} - -void TsCallbackContractDB::release() -{ - if (!released_) { - contract_instance_callback_.Release(); - contract_class_callback_.Release(); - add_contracts_callback_.Release(); - bytecode_commitment_callback_.Release(); - debug_name_callback_.Release(); - create_checkpoint_callback_.Release(); - commit_checkpoint_callback_.Release(); - revert_checkpoint_callback_.Release(); - released_ = true; - } -} - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.hpp deleted file mode 100644 index 25e5a2a3576f..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_contract_db.hpp +++ /dev/null @@ -1,149 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "barretenberg/vm2/common/aztec_types.hpp" -#include "barretenberg/vm2/simulation/interfaces/db.hpp" - -namespace bb::nodejs { - -/** - * @brief Implementation of ContractDBInterface that uses NAPI callbacks to TypeScript - * - * This class bridges C++ contract data queries to TypeScript's PublicContractsDB. - * During simulation, when C++ needs contract instances or classes, it calls back - * to TypeScript through thread-safe NAPI functions. - * - * Thread Safety: - * - Uses Napi::ThreadSafeFunction to safely call TypeScript from C++ worker threads - * - BlockingCall ensures synchronous execution with the JavaScript event loop - * - * Lifecycle: - * - Thread-safe functions must be released after use to avoid memory leaks - * - Caller is responsible for releasing TSFNs by calling release() - */ -class TsCallbackContractDB : public avm2::simulation::ContractDBInterface { - public: - /** - * @brief Constructs a callback-based contracts database - * - * @param instanceCallback Thread-safe function to fetch contract instances from TypeScript - * Expected signature: (address: string) => Promise - * @param classCallback Thread-safe function to fetch contract classes from TypeScript - * Expected signature: (classId: string) => Promise - * @param addContractsCallback Thread-safe function to add contracts - * Expected signature: (contractDeploymentData: Buffer) => Promise - * @param bytecodeCommitmentCallback Thread-safe function to fetch bytecode commitments - * Expected signature: (classId: string) => Promise - * @param debugNameCallback Thread-safe function to fetch debug function names - * Expected signature: (address: string, selector: string) => Promise - * @param createCheckpointCallback Thread-safe function to create a checkpoint - * Expected signature: () => Promise - * @param commitCheckpointCallback Thread-safe function to commit a checkpoint - * Expected signature: () => Promise - * @param revertCheckpointCallback Thread-safe function to revert a checkpoint - * Expected signature: () => Promise - */ - TsCallbackContractDB(Napi::ThreadSafeFunction instanceCallback, - Napi::ThreadSafeFunction classCallback, - Napi::ThreadSafeFunction addContractsCallback, - Napi::ThreadSafeFunction bytecodeCommitmentCallback, - Napi::ThreadSafeFunction debugNameCallback, - Napi::ThreadSafeFunction createCheckpointCallback, - Napi::ThreadSafeFunction commitCheckpointCallback, - Napi::ThreadSafeFunction revertCheckpointCallback); - - /** - * @brief Fetches a contract instance by address - * - * Calls back to TypeScript to retrieve the contract instance. The TypeScript callback - * should return a msgpack-serialized ContractInstanceHint buffer, or undefined if not found. - * - * @param address The contract address to lookup - * @return std::optional The contract instance if found, nullopt otherwise - */ - std::optional get_contract_instance( - const bb::avm2::AztecAddress& address) const override; - - /** - * @brief Fetches a contract class by class ID - * - * Calls back to TypeScript to retrieve the contract class. The TypeScript callback - * should return a msgpack-serialized ContractClassHint buffer, or undefined if not found. - * - * @param class_id The contract class ID to lookup - * @return std::optional The contract class if found, nullopt otherwise - */ - std::optional get_contract_class(const bb::avm2::ContractClassId& class_id) const override; - - /** - * @brief Adds contracts from deployment data - * - * @param contract_deployment_data The contract deployment data - */ - void add_contracts(const bb::avm2::ContractDeploymentData& contract_deployment_data) override; - - /** - * @brief Fetches bytecode commitment for a contract class - * - * @param class_id The contract class ID - * @return std::optional The bytecode commitment if found, nullopt otherwise - */ - std::optional get_bytecode_commitment(const bb::avm2::ContractClassId& class_id) const override; - - /** - * @brief Fetches debug function name for a contract function - * - * @param address The contract address - * @param selector The function selector - * @return std::optional The function name if found, nullopt otherwise - */ - std::optional get_debug_function_name(const bb::avm2::AztecAddress& address, - const bb::avm2::FF& selector) const override; - - /** - * @brief Creates a new checkpoint - * - * Creates a checkpoint in the TypeScript contracts DB, enabling rollbacks to current state. - */ - void create_checkpoint() override; - - /** - * @brief Commits the current checkpoint - * - * Accepts the current checkpoint's state as latest. - */ - void commit_checkpoint() override; - - /** - * @brief Reverts the current checkpoint - * - * Discards the current checkpoint's state and rolls back to the previous checkpoint. - */ - void revert_checkpoint() override; - - /** - * @brief Releases the thread-safe function handles - * - * Must be called before destruction to properly clean up NAPI resources. - * This tells Node.js that the C++ side is done with the callbacks. - */ - void release(); - - private: - Napi::ThreadSafeFunction contract_instance_callback_; - Napi::ThreadSafeFunction contract_class_callback_; - Napi::ThreadSafeFunction add_contracts_callback_; - Napi::ThreadSafeFunction bytecode_commitment_callback_; - Napi::ThreadSafeFunction debug_name_callback_; - Napi::ThreadSafeFunction create_checkpoint_callback_; - Napi::ThreadSafeFunction commit_checkpoint_callback_; - Napi::ThreadSafeFunction revert_checkpoint_callback_; - - // Track whether TSFNs have been released to avoid double-release - mutable bool released_ = false; -}; - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.cpp deleted file mode 100644 index 166bc187dc76..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.cpp +++ /dev/null @@ -1,289 +0,0 @@ -#include "ts_callback_utils.hpp" - -#include -#include - -#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl/msgpack_impl.hpp" - -namespace bb::nodejs { - -std::string extract_error_from_napi_value(const Napi::CallbackInfo& cb_info) -{ - if (cb_info.Length() > 0) { - if (cb_info[0].IsString()) { - return cb_info[0].As().Utf8Value(); - } - if (cb_info[0].IsObject()) { - auto err_obj = cb_info[0].As(); - auto msg = err_obj.Get("message"); - if (msg.IsString()) { - return msg.As().Utf8Value(); - } - } - } - return "Unknown error from TypeScript"; -} - -Napi::Function create_buffer_resolve_handler(Napi::Env env, std::shared_ptr cb_results) -{ - // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler. - // This prevents use-after-free when timeouts occur before the Promise resolves. - return Napi::Function::New( - env, - [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value { - Napi::Env env = cb_info.Env(); - try { - // Check if first arg is undefined or null - if (cb_info.Length() > 0 && !cb_info[0].IsUndefined() && !cb_info[0].IsNull()) { - // Check if the first argument is a buffer - if (cb_info[0].IsBuffer()) { - auto buffer = cb_info[0].As>(); - std::vector vec(buffer.Data(), buffer.Data() + buffer.Length()); - cb_results->result_promise.set_value(std::move(vec)); - } else { - cb_results->error_message = "Callback returned non-Buffer value"; - cb_results->result_promise.set_value(std::nullopt); - } - } else { - // Got undefined/null - not found - cb_results->result_promise.set_value(std::nullopt); - } - } catch (const std::exception& e) { - cb_results->error_message = std::string("Exception in resolve handler: ") + e.what(); - cb_results->result_promise.set_value(std::nullopt); - } - return env.Undefined(); - }, - "resolveHandler"); -} - -Napi::Function create_string_resolve_handler(Napi::Env env, std::shared_ptr cb_results) -{ - // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler. - return Napi::Function::New( - env, - [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value { - Napi::Env env = cb_info.Env(); - try { - // Check if first arg is undefined or null - if (cb_info.Length() > 0 && !cb_info[0].IsUndefined() && !cb_info[0].IsNull()) { - // Check if the first argument is a string - if (cb_info[0].IsString()) { - std::string name = cb_info[0].As().Utf8Value(); - std::vector vec(name.begin(), name.end()); - cb_results->result_promise.set_value(std::move(vec)); - } else { - cb_results->error_message = "Callback returned non-string value"; - cb_results->result_promise.set_value(std::nullopt); - } - } else { - // Got undefined/null - not found - cb_results->result_promise.set_value(std::nullopt); - } - } catch (const std::exception& e) { - cb_results->error_message = std::string("Exception in resolve handler: ") + e.what(); - cb_results->result_promise.set_value(std::nullopt); - } - return env.Undefined(); - }, - "resolveHandler"); -} - -Napi::Function create_void_resolve_handler(Napi::Env env, std::shared_ptr cb_results) -{ - // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler. - return Napi::Function::New( - env, - [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value { - cb_results->result_promise.set_value(std::nullopt); - return cb_info.Env().Undefined(); - }, - "resolveHandler"); -} - -Napi::Function create_reject_handler(Napi::Env env, std::shared_ptr cb_results) -{ - // Capture shared_ptr by value to ensure CallbackResults outlives the Promise handler. - return Napi::Function::New( - env, - [cb_results](const Napi::CallbackInfo& cb_info) -> Napi::Value { - cb_results->error_message = extract_error_from_napi_value(cb_info); - cb_results->result_promise.set_value(std::nullopt); - return cb_info.Env().Undefined(); - }, - "rejectHandler"); -} - -void attach_promise_handlers(Napi::Promise promise, Napi::Function resolve_handler, Napi::Function reject_handler) -{ - auto then_prop = promise.Get("then"); - if (!then_prop.IsFunction()) { - throw std::runtime_error("Promise does not have .then() method"); - } - - auto then_fn = then_prop.As(); - then_fn.Call(promise, { resolve_handler, reject_handler }); -} - -template std::vector serialize_to_msgpack(const T& data) -{ - msgpack::sbuffer buffer; - msgpack::pack(buffer, data); - return std::vector(buffer.data(), buffer.data() + buffer.size()); -} - -template T deserialize_from_msgpack(const std::vector& data, const std::string& type_name) -{ - try { - T result; - msgpack::object_handle obj_handle = msgpack::unpack(reinterpret_cast(data.data()), data.size()); - msgpack::object obj = obj_handle.get(); - obj.convert(result); - return result; - } catch (const std::exception& e) { - throw std::runtime_error(std::string("Failed to deserialize ") + type_name + ": " + e.what()); - } -} - -std::optional> invoke_ts_callback_with_promise( - const Napi::ThreadSafeFunction& callback, - const std::string& operation_name, - std::function)> call_js_function, - std::chrono::seconds timeout) -{ - // Create promise/future pair for synchronization. - // The shared_ptr is passed to call_js_function which MUST capture it in Promise handlers. - // This ensures CallbackResults outlives the Promise, even if we timeout and return early. - auto callback_data = std::make_shared(); - auto future = callback_data->result_promise.get_future(); - - // Call TypeScript callback on the JS main thread. - // We pass the shared_ptr to the call_js_function so it can be captured by Promise handlers. - auto status = callback.BlockingCall( - callback_data.get(), - [call_js_function, callback_data](Napi::Env env, Napi::Function js_callback, CallbackResults* /*cb_results*/) { - try { - // Call the TypeScript function with the shared_ptr (not raw pointer). - // The call_js_function MUST capture this shared_ptr in Promise handlers. - call_js_function(env, js_callback, callback_data); - - } catch (const std::exception& e) { - callback_data->error_message = std::string("Exception calling TypeScript: ") + e.what(); - callback_data->result_promise.set_value(std::nullopt); - } - }); - - if (status != napi_ok) { - throw std::runtime_error("Failed to invoke TypeScript callback for " + operation_name); - } - - // Wait for the promise to be fulfilled (with timeout). - // If timeout occurs, we throw but callback_data stays alive via shared_ptr in Promise handlers. - auto wait_status = future.wait_for(timeout); - if (wait_status == std::future_status::timeout) { - throw std::runtime_error("Timeout waiting for TypeScript callback for " + operation_name); - } - - // Get the result - auto result_data = future.get(); - - // Check for errors - if (!callback_data->error_message.empty()) { - throw std::runtime_error("Error from TypeScript callback: " + callback_data->error_message); - } - - return result_data; -} - -std::optional> invoke_single_string_callback(const Napi::ThreadSafeFunction& callback, - const std::string& input_str, - const std::string& operation_name) -{ - return invoke_ts_callback_with_promise( - callback, - operation_name, - [input_str](Napi::Env env, Napi::Function js_callback, std::shared_ptr cb_results) { - auto js_input = Napi::String::New(env, input_str); - auto js_result = js_callback.Call({ js_input }); - - if (!js_result.IsPromise()) { - cb_results->error_message = "TypeScript callback did not return a Promise"; - cb_results->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - // Pass shared_ptr to handlers so CallbackResults outlives the Promise - auto resolve_handler = create_buffer_resolve_handler(env, cb_results); - auto reject_handler = create_reject_handler(env, cb_results); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); -} - -std::optional> invoke_double_string_callback(const Napi::ThreadSafeFunction& callback, - const std::string& input_str1, - const std::string& input_str2, - const std::string& operation_name) -{ - return invoke_ts_callback_with_promise( - callback, - operation_name, - [input_str1, - input_str2](Napi::Env env, Napi::Function js_callback, std::shared_ptr cb_results) { - auto js_input1 = Napi::String::New(env, input_str1); - auto js_input2 = Napi::String::New(env, input_str2); - auto js_result = js_callback.Call({ js_input1, js_input2 }); - - if (!js_result.IsPromise()) { - cb_results->error_message = "TypeScript callback did not return a Promise"; - cb_results->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - // Pass shared_ptr to handlers so CallbackResults outlives the Promise - auto resolve_handler = create_string_resolve_handler(env, cb_results); - auto reject_handler = create_reject_handler(env, cb_results); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); -} - -void invoke_buffer_void_callback(const Napi::ThreadSafeFunction& callback, - std::vector buffer_data, - const std::string& operation_name) -{ - auto result = invoke_ts_callback_with_promise( - callback, - operation_name, - [buffer_data = std::move(buffer_data)]( - Napi::Env env, Napi::Function js_callback, std::shared_ptr cb_results) { - auto js_buffer = Napi::Buffer::Copy(env, buffer_data.data(), buffer_data.size()); - auto js_result = js_callback.Call({ js_buffer }); - - if (!js_result.IsPromise()) { - cb_results->error_message = "TypeScript callback did not return a Promise"; - cb_results->result_promise.set_value(std::nullopt); - return; - } - - auto promise = js_result.As(); - // Pass shared_ptr to handlers so CallbackResults outlives the Promise - auto resolve_handler = create_void_resolve_handler(env, cb_results); - auto reject_handler = create_reject_handler(env, cb_results); - attach_promise_handlers(promise, resolve_handler, reject_handler); - }); - - // For void callbacks, we just need to ensure no errors occurred - // The result itself is ignored (will be nullopt for void) -} - -// Explicit template instantiations for types used in this codebase -template std::vector serialize_to_msgpack(const bb::avm2::ContractDeploymentData& data); -template bb::avm2::ContractInstance deserialize_from_msgpack(const std::vector& data, - const std::string& type_name); -template bb::avm2::ContractClass deserialize_from_msgpack(const std::vector& data, - const std::string& type_name); -template bb::avm2::FF deserialize_from_msgpack(const std::vector& data, const std::string& type_name); - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.hpp deleted file mode 100644 index acadc2251130..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/ts_callback_utils.hpp +++ /dev/null @@ -1,114 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "barretenberg/vm2/common/aztec_types.hpp" - -namespace bb::nodejs { - -/** - * @brief Helper struct to pass data between C++ worker thread and JS main thread - */ -struct CallbackResults { - std::promise>> result_promise; - std::string error_message; -}; - -/** - * @brief Extracts error message from a Napi value (string or Error object) - */ -std::string extract_error_from_napi_value(const Napi::CallbackInfo& cb_info); - -/** - * @brief Creates a resolve handler for promises that return Buffer | undefined - * @note Takes shared_ptr to ensure CallbackResults outlives the Promise handler - */ -Napi::Function create_buffer_resolve_handler(Napi::Env env, std::shared_ptr cb_results); - -/** - * @brief Creates a resolve handler for promises that return string | undefined - * @note Takes shared_ptr to ensure CallbackResults outlives the Promise handler - */ -Napi::Function create_string_resolve_handler(Napi::Env env, std::shared_ptr cb_results); - -/** - * @brief Creates a resolve handler for promises that return void - * @note Takes shared_ptr to ensure CallbackResults outlives the Promise handler - */ -Napi::Function create_void_resolve_handler(Napi::Env env, std::shared_ptr cb_results); - -/** - * @brief Creates a reject handler for promises - * @note Takes shared_ptr to ensure CallbackResults outlives the Promise handler - */ -Napi::Function create_reject_handler(Napi::Env env, std::shared_ptr cb_results); - -/** - * @brief Attaches resolve and reject handlers to a promise - */ -void attach_promise_handlers(Napi::Promise promise, Napi::Function resolve_handler, Napi::Function reject_handler); - -/** - * @brief Serializes data to msgpack format - */ -template std::vector serialize_to_msgpack(const T& data); - -/** - * @brief Deserializes msgpack data to a specific type - */ -template T deserialize_from_msgpack(const std::vector& data, const std::string& type_name); - -/** - * @brief Generic callback invoker that handles the full BlockingCall pattern - * - * This template function encapsulates the entire promise-based async callback flow: - * 1. Creates promise/future synchronization - * 2. Invokes JS callback via BlockingCall - * 3. Handles promise resolution/rejection - * 4. Waits with timeout - * 5. Returns optional result - * - * @note The call_js_function receives a shared_ptr to CallbackResults. The shared_ptr MUST be - * captured by the Promise handlers to ensure the CallbackResults outlives the Promise. - * This prevents use-after-free when timeouts occur before the Promise resolves. - */ -std::optional> invoke_ts_callback_with_promise( - const Napi::ThreadSafeFunction& callback, - const std::string& operation_name, - std::function)> call_js_function, - std::chrono::seconds timeout = std::chrono::seconds(60)); - -/** - * @brief Helper for callbacks that take a single string argument and return Buffer | undefined - */ -std::optional> invoke_single_string_callback(const Napi::ThreadSafeFunction& callback, - const std::string& input_str, - const std::string& operation_name); - -/** - * @brief Helper for callbacks that take two string arguments and return string | undefined - */ -std::optional> invoke_double_string_callback(const Napi::ThreadSafeFunction& callback, - const std::string& input_str1, - const std::string& input_str2, - const std::string& operation_name); - -/** - * @brief Helper for callbacks that take a buffer and return void - */ -void invoke_buffer_void_callback(const Napi::ThreadSafeFunction& callback, - std::vector buffer_data, - const std::string& operation_name); - -/** - * @brief Converts an FF (field element) to a hex string - */ -std::string ff_to_string(const bb::avm2::FF& value); - -} // namespace bb::nodejs diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp index 5be9e9149a85..1a5a0d0dd396 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp @@ -1,4 +1,3 @@ -#include "barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp" #include "barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp" #include "barretenberg/nodejs_module/msgpack_client/msgpack_client_async.hpp" #include "barretenberg/nodejs_module/msgpack_client/msgpack_client_wrapper.hpp" @@ -11,13 +10,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) bb::nodejs::msgpack_client::MsgpackClientWrapper::get_class(env)); exports.Set(Napi::String::New(env, "MsgpackClientAsync"), bb::nodejs::msgpack_client::MsgpackClientAsync::get_class(env)); - exports.Set(Napi::String::New(env, "avmSimulate"), Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::simulate)); - exports.Set(Napi::String::New(env, "avmSimulateWithHintedDbs"), - Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::simulateWithHintedDbs)); - exports.Set(Napi::String::New(env, "createCancellationToken"), - Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::createCancellationToken)); - exports.Set(Napi::String::New(env, "cancelSimulation"), - Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::cancelSimulation)); return exports; } diff --git a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/cancellation_token.hpp b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/cancellation_token.hpp index 1a8651337a19..7d36729bb120 100644 --- a/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/cancellation_token.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2/simulation/lib/cancellation_token.hpp @@ -53,6 +53,13 @@ class CancellationToken { */ void cancel() { cancelled_.store(true, std::memory_order_release); } + /** + * @brief Clear a prior cancellation so the token can be reused. Called from the + * simulation thread before starting a new simulation. Lets a single + * process-lifetime token back successive simulations without reallocation. + */ + void reset() { cancelled_.store(false, std::memory_order_release); } + /** * @brief Check if cancellation has been signaled. Called from C++ simulation thread. * diff --git a/ci3/bench_compare b/ci3/bench_compare new file mode 100755 index 000000000000..1c716a2c915e --- /dev/null +++ b/ci3/bench_compare @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Compare a PR commit's benchmark run against its merge-base on a baseline branch (default: next), +# optionally averaging a window of baseline runs, and emit the relative perf diffs as JSON. +# +# Per-run benchmark data lives in the CI build-cache keyed by the commit's *git tree hash* +# (`bench-.tar.gz` -> `bench-out/bench.json`, a flat array of {name, unit, value}). This +# pulls only those ~40 KB per-run blobs; it never touches the multi-MB per-branch `data.js` history. +# +# It deliberately does NOT decide which benchmarks matter — it emits every comparable bench, plus a +# stddev/z-score when a window is used. The caller (the `bench-regression` skill) filters to the +# benches relevant to what the PR changes and drops the noise. +# +# BASELINE BRANCH — read this. The cache is content-addressed by git *tree hash*, so branches with +# different perf (next vs v5-next vs merge-train/*) already live under different keys and never cross- +# contaminate. But you MUST merge-base against the PR's *actual* base branch: comparing a v5-next PR +# against `next` would surface the whole v5-next-vs-next delta as bogus regressions. If --baseline is +# not given, this auto-detects the base of the current branch's PR via `gh` and falls back to +# origin/next with a loud warning. The chosen baseline is always printed to stderr — check it. +# Caveat: the cache key carries no branch/repo/hardware provenance, so a tree benched under a different +# environment (e.g. the private fork) yields whichever result was uploaded first; comparisons are only +# trustworthy within one bench infrastructure. +# +# Usage: ci3/bench_compare [PR_REF] [--baseline REF] [--window N] [--out FILE] +# PR_REF commit whose bench run to inspect (default: HEAD) +# --baseline baseline branch/ref to merge-base against (default: auto-detect, else origin/next) +# --window N average the merge-base + N-1 preceding baseline runs (default: 1) +# --out FILE write JSON here (default: stdout); the human summary always goes to stderr +# +# JSON (stdout): { meta:{pr_commit,merge_base,baseline_branch,baseline_runs,counts}, benches:[ +# { name, unit, pr, baseline, n, status, pct, stddev, z } ] } sorted by pct desc. +set -uo pipefail # not -e: failures are handled explicitly, and `cond && break` under -e would exit. +[ "${BUILD_SYSTEM_DEBUG:-}" = 1 ] && set -x + +BASELINE="${BASELINE:-}" # empty => auto-detect the PR's base branch below +WINDOW=1 +PR_REF="HEAD" +OUT="" +SCAN_LIMIT=300 +# Same endpoints cache_download uses: the in-CI S3 http endpoint, else the public cache CDN. +if [ "${CI:-0}" -eq 1 ]; then + CACHE_ENDPOINT="http://aztec-ci-artifacts.s3.amazonaws.com/build-cache" +else + CACHE_ENDPOINT="https://build-cache.aztec-labs.com" +fi + +die() { echo "bench_compare: error: $*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --baseline) BASELINE="$2"; shift 2 ;; + --window) WINDOW="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + -h|--help) sed -n '2,22p' "$0"; exit 0 ;; + --*) die "unknown flag $1" ;; + *) PR_REF="$1"; shift ;; + esac +done + +command -v jq >/dev/null || die "jq not found" +command -v python3 >/dev/null || die "python3 not found" + +# Resolve the baseline branch. Explicit --baseline (or $BASELINE) wins; otherwise auto-detect the base +# of the current branch's PR via gh, and only then fall back to origin/next. Always announced — a wrong +# baseline silently invalidates every number, so never let it be a silent default. +if [ -z "$BASELINE" ]; then + det="" + command -v gh >/dev/null && det="$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null)" + if [ -n "$det" ]; then + BASELINE="origin/$det" + echo "bench_compare: auto-detected PR base branch -> $BASELINE" >&2 + else + BASELINE="origin/next" + echo "bench_compare: WARNING baseline defaulted to origin/next (no PR base detected). Pass --baseline for v5-next / merge-train PRs." >&2 + fi +fi +echo "bench_compare: baseline = $BASELINE" >&2 + +# Fetch a per-run bench.json for a tree hash. Tries the http cache (no creds), then aws s3 (creds). +fetch_bench() { # -> 0 if found + local tree="$1" dest="$2" tmp + tmp="$(mktemp -d)" + if curl -fsSL "$CACHE_ENDPOINT/bench-$tree.tar.gz" -o "$tmp/b.tgz" 2>/dev/null \ + || aws s3 cp "s3://aztec-ci-artifacts/build-cache/bench-$tree.tar.gz" "$tmp/b.tgz" >/dev/null 2>&1; then + if tar -xzf "$tmp/b.tgz" -C "$tmp" 2>/dev/null && [ -f "$tmp/bench-out/bench.json" ]; then + cp "$tmp/bench-out/bench.json" "$dest"; rm -rf "$tmp"; return 0 + fi + fi + rm -rf "$tmp"; return 1 +} + +pr_commit="$(git rev-parse --verify "$PR_REF^{commit}" 2>/dev/null)" || die "bad PR ref '$PR_REF'" +git rev-parse --verify "$BASELINE" >/dev/null 2>&1 || die "baseline '$BASELINE' not found (git fetch?)" +base_commit="$(git merge-base "$pr_commit" "$BASELINE")" || die "no merge-base between PR and $BASELINE" +pr_tree="$(git rev-parse "$pr_commit^{tree}")" + +work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT + +fetch_bench "$pr_tree" "$work/pr.json" \ + || die "no bench data for PR commit $pr_commit (tree $pr_tree). Trigger a run (label 'ci-full-no-test-cache') or pass a benched commit." + +# Baseline set: merge-base, then first-parent predecessors; take the first $WINDOW that have data. +baseline_files=(); n=0 +for c in $(git rev-list --first-parent "$base_commit" -n "$SCAN_LIMIT"); do + if [ "$n" -ge "$WINDOW" ]; then break; fi + t="$(git rev-parse "$c^{tree}")" + if fetch_bench "$t" "$work/base_$n.json"; then + baseline_files+=("$work/base_$n.json"); n=$((n + 1)) + fi +done +[ "${#baseline_files[@]}" -gt 0 ] || die "no bench data for baseline (merge-base $base_commit or predecessors)" + +META_JSON="$(jq -n \ + --arg pr "$pr_commit" --arg base "$base_commit" --arg branch "$BASELINE" \ + --argjson n "${#baseline_files[@]}" \ + '{pr_commit:$pr, merge_base:$base, baseline_branch:$branch, baseline_runs:$n}')" + +python3 - "$META_JSON" "$work/pr.json" "${baseline_files[@]}" <<'PY' > "${OUT:-/dev/stdout}" +import json, sys, math + +meta = json.loads(sys.argv[1]) +pr = sys.argv[2] +bases = sys.argv[3:] + +def load(path): + with open(path) as f: + arr = json.load(f) + out = {} + for e in arr: + try: + out[e["name"]] = (float(e["value"]), e.get("unit", "")) + except (ValueError, TypeError, KeyError): + pass # drop non-numeric / malformed + return out + +pr = load(pr) +bases = [load(p) for p in bases] + +names = set(pr).union(*bases) if bases else set(pr) +rows = [] +for name in names: + vals = [b[name][0] for b in bases if name in b] + unit = next((b[name][1] for b in bases if name in b), pr.get(name, (0, ""))[1]) + prv = pr[name][0] if name in pr else None + mean = sum(vals) / len(vals) if vals else None + std = (math.sqrt(sum((v - mean) ** 2 for v in vals) / len(vals)) if len(vals) > 1 else 0.0) if vals else None + row = {"name": name, "unit": unit, "pr": prv, "baseline": mean, "n": len(vals)} + if prv is None: + row["status"] = "only_in_baseline" + elif mean is None: + row["status"] = "only_in_pr" + else: + row["status"] = "compared" + row["pct"] = ((prv - mean) / abs(mean) * 100) if mean != 0 else None + row["stddev"] = std + row["z"] = ((prv - mean) / std) if std and std > 0 else None + rows.append(row) + +cmp = [r for r in rows if r["status"] == "compared" and r.get("pct") is not None] +cmp.sort(key=lambda r: r["pct"], reverse=True) +only_pr = [r for r in rows if r["status"] == "only_in_pr"] +only_base = [r for r in rows if r["status"] == "only_in_baseline"] +meta["counts"] = {"compared": len(cmp), "only_in_pr": len(only_pr), "only_in_baseline": len(only_base)} +print(json.dumps({"meta": meta, "benches": cmp + only_pr + only_base})) + +def fmt(r): + z = f", z={r['z']:.1f}" if r.get("z") is not None else "" + return f" {r['pct']:+7.1f}% {r['baseline']:.4g} -> {r['pr']:.4g} {r['unit']}{z} {r['name']}" +print(f"\nbaseline: {meta['baseline_branch']} @ {meta['merge_base'][:10]} " + f"(window {meta['baseline_runs']} run(s)) vs PR {meta['pr_commit'][:10]}", file=sys.stderr) +print(f"compared {len(cmp)} | only-in-PR {len(only_pr)} | only-in-baseline {len(only_base)}", file=sys.stderr) +print("\ntop 15 regressions (PR larger; customSmallerIsBetter):", file=sys.stderr) +for r in cmp[:15]: + print(fmt(r), file=sys.stderr) +print("\ntop 5 improvements:", file=sys.stderr) +for r in cmp[-5:][::-1]: + print(fmt(r), file=sys.stderr) +PY + +[ -n "$OUT" ] && echo "bench_compare: wrote $OUT" >&2 +exit 0 diff --git a/docs/examples/ts/recursive_verification/config.yaml b/docs/examples/ts/recursive_verification/config.yaml index d3f14e158679..38d09f0803dd 100644 --- a/docs/examples/ts/recursive_verification/config.yaml +++ b/docs/examples/ts/recursive_verification/config.yaml @@ -26,10 +26,13 @@ dependencies: - "link:@aztec/noir-acvm_js:noir/packages/acvm_js" - "link:@aztec/noir-noirc_abi:noir/packages/noirc_abi" - "link:@aztec/noir-types:noir/packages/types" - # Runtime deps of @aztec/bb.js (linked, so its own deps are not auto-installed) + # Runtime deps of @aztec/bb.js (linked, so its own deps are not auto-installed). + # Version ranges must mirror bb.js's package.json: these are bare `yarn add`s, so leaving them + # unversioned installs the latest major and breaks when a dep ships a breaking release (e.g. pako 3.0.0 + # dropped the ESM default export that bb.js's `import pako from 'pako'` relies on). - "npm:pako@^2.1.0" - - "npm:msgpackr" - - "npm:comlink" - - "npm:idb-keyval" - - "npm:commander" - - "npm:tslib" + - "npm:msgpackr@^1.11.2" + - "npm:comlink@^4.4.1" + - "npm:idb-keyval@^6.2.1" + - "npm:commander@^12.1.0" + - "npm:tslib@^2.4.0" diff --git a/ipc-runtime/ts/src/uds_server.ts b/ipc-runtime/ts/src/uds_server.ts index d63598afd64b..309cee40a7d0 100644 --- a/ipc-runtime/ts/src/uds_server.ts +++ b/ipc-runtime/ts/src/uds_server.ts @@ -25,6 +25,7 @@ export type IpcServerHandler = ( export class UdsIpcServer { private server: net.Server; private nextClientId = 0; + private connections = new Set(); private readonly unlinkOnExit = () => { try { fs.unlinkSync(this.socketPath); @@ -79,6 +80,12 @@ export class UdsIpcServer { } async close(): Promise { + // Force-close live connections (matching the C++ server's shutdown) so close() + // resolves promptly instead of blocking until every client happens to disconnect. + for (const conn of this.connections) { + conn.destroy(); + } + this.connections.clear(); await new Promise((resolve) => this.server.close(() => resolve())); process.removeListener("exit", this.unlinkOnExit); try { @@ -90,6 +97,8 @@ export class UdsIpcServer { private handleConnection(conn: net.Socket, handler: IpcServerHandler): void { const clientId = this.nextClientId++; + this.connections.add(conn); + conn.on("close", () => this.connections.delete(conn)); let buffer = Buffer.alloc(0); let chain: Promise = Promise.resolve(); @@ -111,7 +120,9 @@ export class UdsIpcServer { return; } if (buffer.length < 4 + len) break; - const payload = new Uint8Array(buffer.subarray(4, 4 + len)); + // Copy into a standalone Buffer (not a subarray view, and not a plain Uint8Array): handlers + // decode with msgpackr, which relies on Buffer semantics for correct string/binary decoding. + const payload = Buffer.from(buffer.subarray(4, 4 + len)); buffer = buffer.subarray(4 + len); const prev = chain; diff --git a/yarn-project/.gitignore b/yarn-project/.gitignore index c1a513244107..df4f3a3b8951 100644 --- a/yarn-project/.gitignore +++ b/yarn-project/.gitignore @@ -61,6 +61,7 @@ cli-wallet/test/data cli/src/config/generated ethereum/src/generated slasher/src/generated +simulator/src/public/cdb/generated .claude/settings.local.json .yarn/* diff --git a/yarn-project/.prettierignore b/yarn-project/.prettierignore index 2ef26151862a..4900b8cce190 100644 --- a/yarn-project/.prettierignore +++ b/yarn-project/.prettierignore @@ -20,5 +20,5 @@ noir-protocol-circuits-types/src/vk_tree.ts noir-protocol-circuits-types/src/client_artifacts_helper.ts constants/src/constants.gen.ts cli/src/config/generated +simulator/src/public/cdb/generated sqlite3mc-wasm/vendor/ - diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index f8fa3ffc639b..143123f88659 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -67,6 +67,7 @@ "dependencies": { "@aztec/archiver": "workspace:^", "@aztec/bb-prover": "workspace:^", + "@aztec/bb.js": "workspace:^", "@aztec/blob-client": "workspace:^", "@aztec/blob-lib": "workspace:^", "@aztec/constants": "workspace:^", diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 6742b6666af0..a2c73fe688fd 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -235,7 +235,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), ); @@ -772,7 +772,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), undefined, @@ -962,7 +962,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), undefined, @@ -1033,7 +1033,7 @@ describe('aztec node', () => { globalVariablesBuilder, mock(), epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), ); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index c950080b5bf3..6c5dda35bb95 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -42,7 +42,7 @@ import { type SequencerPublisher, createAutomineSequencer, } from '@aztec/sequencer-client'; -import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; +import { AvmExecutor, PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; import { AttestationsBlockWatcher, AttestedInvalidProposalWatcher, @@ -187,6 +187,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb public readonly tracer: Tracer; + /** IPC backends to clean up on stop (CDB, AVM). WSDB is cleaned up by world state. */ + private ipcBackends: Array<{ destroy?(): Promise }> = []; + constructor( protected config: AztecNodeConfig, protected readonly p2pClient: P2P, @@ -215,6 +218,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb private keyStoreManager?: KeystoreManager, private debugLogStore: DebugLogStore = new NullDebugLogStore(), private readonly automineSequencer?: AutomineSequencer, + // AVM execution backend for public simulation. Wired in production (createAndSync); absent in unit/TXE + // nodes that don't drive public execution, hence optional and asserted at the simulation call site. + private avmExecutor?: AvmExecutor, ) { this.metrics = new NodeMetrics(telemetry, 'AztecNodeService'); this.tracer = telemetry.getTracer('AztecNodeService'); @@ -498,7 +504,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb ): Promise { const config = { ...inputConfig }; // Copy the config so we dont mutate the input object const log = deps.logger ?? createLogger('node'); - const packageVersion = getPackageVersion(); + const packageVersion = getPackageVersion() ?? ''; const telemetry = deps.telemetry ?? getTelemetryClient(); const dateProvider = deps.dateProvider ?? new DateProvider(); const ethereumChain = createEthereumChain(config.l1RpcUrls, config.l1ChainId); @@ -598,6 +604,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const nativeWs = await createWorldState(config, options.genesis); const initialHeader = nativeWs.getInitialHeader(); const initialBlockHash = await initialHeader.hash(); + + log.info('WSDB ready, creating AVM executor'); + const avmExecutor = await AvmExecutor.spawn({ + wsdbIpcPath: nativeWs.getIpcPath(), + logger: (msg: string) => log.debug(msg), + }); + started.push({ stop: () => avmExecutor[Symbol.asyncDispose]() }); + const archiver = await createArchiver( config, { blobClient, epochCache, telemetry, dateProvider }, @@ -646,6 +660,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb rollupVersion: BigInt(config.rollupVersion), l1GenesisTime, slotDuration: Number(slotDuration), + rollupManaLimit, }; const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig); @@ -688,7 +703,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb worldStateSynchronizer, archiver, dateProvider, + avmExecutor, telemetry, + undefined, // debugLogStore ); let validatorClient: ValidatorClient | undefined; @@ -893,6 +910,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb worldStateSynchronizer, archiver, dateProvider, + avmExecutor, telemetry, debugLogStore, ); @@ -976,6 +994,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb epochCache, blobClient, keyStoreManager, + avmExecutor, }); if (!options.dontStartProverNode) { @@ -1015,8 +1034,12 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb keyStoreManager, debugLogStore, automineSequencer, + avmExecutor, ); + // Register the AVM executor for cleanup on stop (it owns the AVM pool + CDB server). + node.ipcBackends.push({ destroy: () => avmExecutor[Symbol.asyncDispose]() }); + return node; } catch (err) { log.error('Failed during node creation, stopping started resources', err); @@ -1287,6 +1310,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb await tryStop(this.worldStateSynchronizer); await tryStop(this.blockSource); await tryStop(this.blobClient); + // Destroy IPC backends (CDB, AVM). WSDB is cleaned up by worldStateSynchronizer. + for (const backend of this.ipcBackends) { + try { + await backend.destroy?.(); + } catch (e) { + this.log.warn(`Error destroying IPC backend: ${e}`); + } + } await tryStop(this.telemetry); this.log.info(`Stopped Aztec Node`); } @@ -1595,6 +1626,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, + this.avmExecutor!, new DateProvider(), this.telemetry, this.log.getBindings(), diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_bulk.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_bulk.test.ts index fc051498a17e..981bad1eadec 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_bulk.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_bulk.test.ts @@ -28,6 +28,7 @@ describe('AVM proven bulk test', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts index b74cb0f18dcc..d5f231762184 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit1.test.ts @@ -32,6 +32,7 @@ describe('AVM check-circuit – unhappy paths 1', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts index 555c6bfb4bc7..607ffce68ca0 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts @@ -26,6 +26,7 @@ describe('AVM check-circuit – unhappy paths 2', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -53,28 +54,32 @@ describe('AVM check-circuit – unhappy paths 2', () => { it('top-level exceptional halts due to a non-existent contract in app-logic and teardown', async () => { // don't insert contracts into trees, and make sure retrieval fails - const tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly=*/ true); - // Note: we need to specify the contract artifacts here because we intentionally skip registration, - // so the tester can't retrieve them on its own. - await tester.simProveVerify( - sender, - /*setupCalls=*/ [], - /*appCalls=*/ [ - { + const noContractsTester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly=*/ true); + try { + // Note: we need to specify the contract artifacts here because we intentionally skip registration, + // so the tester can't retrieve them on its own. + await noContractsTester.simProveVerify( + sender, + /*setupCalls=*/ [], + /*appCalls=*/ [ + { + address: avmTestContractInstance.address, + fnName: 'add_args_return', + args: [new Fr(1), new Fr(2)], + contractArtifact: AvmTestContractArtifact, + }, + ], + /*teardownCall=*/ { address: avmTestContractInstance.address, fnName: 'add_args_return', args: [new Fr(1), new Fr(2)], contractArtifact: AvmTestContractArtifact, }, - ], - /*teardownCall=*/ { - address: avmTestContractInstance.address, - fnName: 'add_args_return', - args: [new Fr(1), new Fr(2)], - contractArtifact: AvmTestContractArtifact, - }, - /*expectRevert=*/ true, - ); + /*expectRevert=*/ true, + ); + } finally { + await noContractsTester.close(); + } }); it('error during revertible insertions - skips to teardown', async () => { diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts index 8c1f55d90644..379a0eb24cf1 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit3.test.ts @@ -29,6 +29,7 @@ describe('AVM check-circuit – unhappy paths 3', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_amm.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_amm.test.ts index 077f70b5fdd3..b7f602273ffa 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_amm.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_amm.test.ts @@ -29,6 +29,7 @@ describe('AVM proven AMM', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_token.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_token.test.ts index e912a261fce4..bf4be181406e 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_token.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_token.test.ts @@ -28,6 +28,7 @@ describe('AVM proven TokenContract', () => { }); afterAll(async () => { + await tester.close(); await worldStateService.close(); if (process.env.BENCH_OUTPUT) { mkdirSync(path.dirname(process.env.BENCH_OUTPUT), { recursive: true }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts index 961cd3655e23..71cb06d8018b 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_class_limits.test.ts @@ -34,6 +34,7 @@ describe('AVM check-circuit - contract class limits', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts index b043f8793b8b..f4797a12b236 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_contract_updates.test.ts @@ -23,12 +23,15 @@ describe('AVM check-circuit - contract updates', () => { let avmTestContractInstance: ContractInstanceWithAddress; let worldStateService: NativeWorldStateService; + let tester: AvmProvingTester | undefined; beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); }); afterEach(async () => { + await tester?.close(); + tester = undefined; await worldStateService.close(); }); @@ -59,7 +62,7 @@ describe('AVM check-circuit - contract updates', () => { // Contract was not originally the avmTestContract const originalClassId = new Fr(27); const globals = defaultGlobals(); - const tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); + tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], @@ -98,7 +101,7 @@ describe('AVM check-circuit - contract updates', () => { // Contract was not originally the avmTestContract const originalClassId = new Fr(27); const globals = defaultGlobals(); - const tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); + tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, @@ -139,7 +142,7 @@ describe('AVM check-circuit - contract updates', () => { const newClassId = new Fr(27); const globals = defaultGlobals(); - const tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); + tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, @@ -176,7 +179,7 @@ describe('AVM check-circuit - contract updates', () => { const newClassId = new Fr(27); const globals = defaultGlobals(); - const tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); + tester = await AvmProvingTester.new(worldStateService, /*checkCircuitOnly*/ true, globals); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], sender, diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_mega_bulk.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_mega_bulk.test.ts index 00b9ec0dab1d..a59e002cbf9e 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_mega_bulk.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_mega_bulk.test.ts @@ -29,6 +29,7 @@ describe.skip('AVM proven MEGA bulk test', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_gadgets.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_gadgets.test.ts index 1765487d6383..3040fcbe98cb 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_gadgets.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proven_gadgets.test.ts @@ -39,6 +39,7 @@ describe.skip('AVM proven gadgets test', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -121,6 +122,7 @@ describe('AVM proven gadgets test: test vectors', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts index 53d2a0fc5e71..8053bf60b2f3 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts @@ -1,12 +1,14 @@ import type { AvmStat } from '@aztec/bb.js'; import { Timer } from '@aztec/foundation/timer'; import { + type MeasuredSimulatorFactory, PublicTxSimulationTester, SimpleContractDataSource, type TestEnqueuedCall, type TestExecutorMetrics, type TestPrivateInsertions, } from '@aztec/simulator/public/fixtures'; +import { AvmExecutor, MeasuredPublicTxSimulator, PublicContractsDB } from '@aztec/simulator/server'; import type { PublicTxResult } from '@aztec/simulator/server'; import { AvmCircuitInputs, AvmCircuitPublicInputs, PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -40,9 +42,9 @@ export class AvmProvingTester extends PublicTxSimulationTester { merkleTrees: MerkleTreeWriteOperations, globals?: GlobalVariables, metrics?: TestExecutorMetrics, + simulatorFactory?: MeasuredSimulatorFactory, ) { - // simulator factory is undefined because for proving, we use the default C++ simulator - super(merkleTrees, contractDataSource, globals, metrics, /*simulatorFactory=*/ undefined, provingConfig); + super(merkleTrees, contractDataSource, globals, metrics, simulatorFactory, provingConfig); } static async new( @@ -53,7 +55,35 @@ export class AvmProvingTester extends PublicTxSimulationTester { ) { const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await worldStateService.fork(); - return new AvmProvingTester(checkCircuitOnly, contractDataSource, merkleTrees, globals, metrics); + + const forkId = merkleTrees.getRevision().forkId; + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const forkedSimulator = avmExecutor.forFork( + forkId, + new PublicContractsDB(contractDataSource), + globals?.timestamp ?? 0n, + ); + const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredPublicTxSimulator(forkedSimulator, g, m, c, undefined, forkId); + + const tester = new AvmProvingTester( + checkCircuitOnly, + contractDataSource, + merkleTrees, + globals, + metrics, + simulatorFactory, + ); + tester.avmExecutor = avmExecutor; + return tester; + } + + public override async close(): Promise { + const results = await Promise.allSettled([super.close(), this.bbJsFactory.destroy()]); + const errors = results.flatMap(result => (result.status === 'rejected' ? [result.reason] : [])); + if (errors.length > 0) { + throw new AggregateError(errors, `Failed to close AVM proving tester`); + } } /** diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts index ff6145055b89..e9a22dc28b52 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_public_fee_payment.test.ts @@ -31,6 +31,7 @@ describe('AVM check-circuit – public fee payment', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bootstrap.sh b/yarn-project/bootstrap.sh index e61409d3e4c2..9cf927164e0f 100755 --- a/yarn-project/bootstrap.sh +++ b/yarn-project/bootstrap.sh @@ -5,6 +5,7 @@ function hash { hash_str \ $(../noir/bootstrap.sh hash) \ $(../barretenberg/bootstrap.sh hash) \ + $(../ipc-codegen/bootstrap.sh hash) \ $(cache_content_hash ../{avm-transpiler,noir-projects,l1-contracts,yarn-project}/.rebuild_patterns) } @@ -137,6 +138,7 @@ function compile_all { noir-protocol-circuits-types \ protocol-contracts \ pxe \ + simulator \ standard-contracts cat joblog.txt diff --git a/yarn-project/end-to-end/.gitignore b/yarn-project/end-to-end/.gitignore index 6f5e974188a4..32623224a21f 100644 --- a/yarn-project/end-to-end/.gitignore +++ b/yarn-project/end-to-end/.gitignore @@ -3,6 +3,7 @@ results bench-out chonk-pinned-flows dumped-avm-circuit-inputs +example-app-ivc-inputs-out ultrahonk-bench-inputs web/main.js* consensys_web3signer_* diff --git a/yarn-project/ivc-integration/src/avm_integration.test.ts b/yarn-project/ivc-integration/src/avm_integration.test.ts index 53080dde21c1..fe7bf76f175c 100644 --- a/yarn-project/ivc-integration/src/avm_integration.test.ts +++ b/yarn-project/ivc-integration/src/avm_integration.test.ts @@ -119,6 +119,7 @@ describe('AVM Integration', () => { }); afterEach(async () => { + await simTester.close(); await worldStateService.close(); }); diff --git a/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts b/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts index e5b90dab3dd7..461a3a43787b 100644 --- a/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts +++ b/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts @@ -88,6 +88,7 @@ describe('Rollup IVC Integration', () => { simConfig, ); const avmSimulationResult = await bulkTest(simTester, logger, AvmTestContractArtifact); + await simTester.close(); await worldStateService.close(); expect(avmSimulationResult.revertCode.isOK()).toBe(true); diff --git a/yarn-project/native/src/native_module.ts b/yarn-project/native/src/native_module.ts index ccb88fff8391..25d477906e35 100644 --- a/yarn-project/native/src/native_module.ts +++ b/yarn-project/native/src/native_module.ts @@ -1,6 +1,4 @@ import { findNapiBinary } from '@aztec/bb.js'; -import { type LogLevel, LogLevels, type Logger } from '@aztec/foundation/log'; -import { Semaphore } from '@aztec/foundation/queue'; import { createRequire } from 'module'; @@ -22,179 +20,3 @@ function loadNativeModule(): Record { const nativeModule: Record = loadNativeModule(); export const NativeLMDBStore: NativeClassCtor = nativeModule.LMDBStore as NativeClassCtor; - -/** - * Contract provider interface for callbacks to fetch contract data. - * These callbacks are invoked by C++ during simulation when contract data is needed. - */ -export interface ContractProvider { - /** - * Fetch a contract instance by address. - * @param address - The contract address as a string (hex format) - * @returns Promise resolving to msgpack-serialized ContractInstanceHint buffer, or undefined if not found - */ - getContractInstance(address: string): Promise; - /** - * Fetch a contract class by class ID. - * @param classId - The contract class ID as a string (hex format) - * @returns Promise resolving to msgpack-serialized ContractClassHint buffer, or undefined if not found - */ - getContractClass(classId: string): Promise; - - /** - * Add contracts from deployment data. - * @param contractDeploymentData - Msgpack-serialized ContractDeploymentData buffer - * @returns Promise that resolves when contracts are added - */ - addContracts(contractDeploymentData: Buffer): Promise; - - /** - * Fetch the bytecode commitment for a contract class. - * @param classId - The contract class ID as a string (hex format) - * @returns Promise resolving to msgpack-serialized Fr buffer, or undefined if not found - */ - getBytecodeCommitment(classId: string): Promise; - - /** - * Fetch the debug function name for a contract function. - * @param address - The contract address as a string (hex format) - * @param selector - The function selector as a string (hex format) - * @returns Promise resolving to function name string, or undefined if not found - */ - getDebugFunctionName(address: string, selector: string): Promise; - - /** - * Create a new checkpoint for the contract database state. - * Enables rollback to current state in case of a revert. - * @returns Promise that resolves when checkpoint is created - */ - createCheckpoint(): Promise; - - /** - * Commit the current checkpoint, accepting its state as latest. - * @returns Promise that resolves when checkpoint is committed - */ - commitCheckpoint(): Promise; - - /** - * Revert the current checkpoint, discarding its state and rolling back. - * @returns Promise that resolves when checkpoint is reverted - */ - revertCheckpoint(): Promise; -} - -// Internal native functions with numeric log level -const nativeAvmSimulate = nativeModule.avmSimulate as ( - inputs: Buffer, - contractProvider: ContractProvider, - wsdbIpcPath: string, - logLevel: number, - logFunction?: any, - cancellationToken?: any, -) => Promise; - -const nativeAvmSimulateWithHintedDbs = nativeModule.avmSimulateWithHintedDbs as ( - inputs: Buffer, - logLevel: number, -) => Promise; - -const nativeCreateCancellationToken = nativeModule.createCancellationToken as () => any; -const nativeCancelSimulation = nativeModule.cancelSimulation as (token: any) => void; - -/** - * Cancellation token handle used to cancel C++ AVM simulation. - * The token is created via createCancellationToken() and can be cancelled via cancelSimulation(). - * Pass it to avmSimulate to enable cancellation support. - */ -export type CancellationToken = any; - -/** - * Create a new cancellation token for C++ simulation. - * This token can be passed to avmSimulate and later cancelled via cancelSimulation(). - * @returns A handle to a cancellation token - */ -export function createCancellationToken(): CancellationToken { - return nativeCreateCancellationToken(); -} - -/** - * Signal cancellation to a C++ simulation. - * The simulation will stop at the next opcode or before the next WorldState write. - * @param token - The cancellation token previously passed to avmSimulate - */ -export function cancelSimulation(token: CancellationToken): void { - nativeCancelSimulation(token); -} - -/** - * Maximum number of concurrent AVM simulations. Each simulation spawns a dedicated OS thread, - * so this controls resource usage. Defaults to 4. Set to 0 for unlimited. - */ -export const AVM_MAX_CONCURRENT_SIMULATIONS = parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10); -const avmSimulationSemaphore = - AVM_MAX_CONCURRENT_SIMULATIONS > 0 ? new Semaphore(AVM_MAX_CONCURRENT_SIMULATIONS) : null; - -async function withAvmConcurrencyLimit(fn: () => Promise): Promise { - if (!avmSimulationSemaphore) { - return fn(); - } - await avmSimulationSemaphore.acquire(); - try { - return await fn(); - } finally { - avmSimulationSemaphore.release(); - } -} - -/** - * AVM simulation function that takes serialized inputs and a contract provider. - * The contract provider enables C++ to callback to TypeScript for contract data during simulation. - * - * Simulations run on dedicated std::threads (not the libuv thread pool), so there is no risk - * of libuv thread pool exhaustion or deadlock from C++ BlockingCall callbacks. - * Concurrency is limited by AVM_MAX_CONCURRENT_SIMULATIONS (default 4, 0 = unlimited). - * - * @param inputs - Msgpack-serialized AvmFastSimulationInputs buffer - * @param contractProvider - Object with callbacks for fetching contract instances and classes - * @param wsdbIpcPath - IPC path of the running aztec-wsdb process. The C++ AVM connects per - * simulation and constructs an IPC-backed merkle DB. - * @param logLevel - Optional log level to control C++ verbosity (only used if loggerFunction is provided) - * @param logger - Optional logger object for C++ logging callbacks - * @param cancellationToken - Optional token to enable cancellation support - * @returns Promise resolving to msgpack-serialized AvmCircuitPublicInputs buffer - */ -export function avmSimulate( - inputs: Buffer, - contractProvider: ContractProvider, - wsdbIpcPath: string, - logLevel: LogLevel = 'info', - logger?: Logger, - cancellationToken?: CancellationToken, -): Promise { - return withAvmConcurrencyLimit(() => - nativeAvmSimulate( - inputs, - contractProvider, - wsdbIpcPath, - LogLevels.indexOf(logLevel), - logger ? (level: LogLevel, msg: string) => logger[level](msg) : null, - cancellationToken, - ), - ); -} - -/** - * AVM simulation function that uses pre-collected hints from TypeScript simulation. - * All contract data and merkle tree hints are included in the AvmCircuitInputs, so no runtime - * callbacks to TS or WS pointer are needed. - * - * Simulations run on dedicated std::threads (not the libuv thread pool). - * Concurrency is limited by AVM_MAX_CONCURRENT_SIMULATIONS (default 4, 0 = unlimited). - * - * @param inputs - Msgpack-serialized AvmCircuitInputs (AvmProvingInputs in C++) buffer - * @param logLevel - Log level to control C++ verbosity - * @returns Promise resolving to msgpack-serialized simulation results buffer - */ -export function avmSimulateWithHintedDbs(inputs: Buffer, logLevel: LogLevel = 'info'): Promise { - return withAvmConcurrencyLimit(() => nativeAvmSimulateWithHintedDbs(inputs, LogLevels.indexOf(logLevel))); -} diff --git a/yarn-project/package.json b/yarn-project/package.json index 4c4e5b73e41c..4769224cc8ac 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -84,6 +84,11 @@ "typescript": "^5.3.3" }, "resolutions": { + "@aztec/bb-avm-sim": "portal:../barretenberg/ts/bb-avm-sim", + "@aztec/bb-avm-sim-darwin-arm64": "portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-arm64", + "@aztec/bb-avm-sim-darwin-x64": "portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-x64", + "@aztec/bb-avm-sim-linux-arm64": "portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-arm64", + "@aztec/bb-avm-sim-linux-x64": "portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-x64", "@aztec/bb.js": "portal:../barretenberg/ts/bb.js", "@aztec/ipc-runtime": "portal:../ipc-runtime/ts", "@aztec/wsdb": "portal:../wsdb/ts", diff --git a/yarn-project/prover-node/package.json b/yarn-project/prover-node/package.json index 4dafb39d980c..3d65c643dfa2 100644 --- a/yarn-project/prover-node/package.json +++ b/yarn-project/prover-node/package.json @@ -58,6 +58,7 @@ "dependencies": { "@aztec/archiver": "workspace:^", "@aztec/bb-prover": "workspace:^", + "@aztec/bb.js": "workspace:^", "@aztec/blob-client": "workspace:^", "@aztec/blob-lib": "workspace:^", "@aztec/constants": "workspace:^", diff --git a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts index 2e3cd5cca09a..49551b86aff5 100644 --- a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts +++ b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts @@ -3,7 +3,7 @@ import type { L1ContractsConfig } from '@aztec/ethereum/config'; import type { Logger } from '@aztec/foundation/log'; import { type ProverClientConfig, createProverClient } from '@aztec/prover-client'; import { ProverBrokerConfig, createAndStartProvingBroker } from '@aztec/prover-client/broker'; -import { PublicProcessorFactory } from '@aztec/simulator/server'; +import { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import type { GenesisData } from '@aztec/stdlib/world-state'; import { getTelemetryClient } from '@aztec/telemetry-client'; @@ -34,8 +34,13 @@ export async function rerunEpochProvingJob( await using worldState = await createWorldState(config, genesis); const initialBlockHash = await worldState.getInitialHeader().hash(); const archiver = await createArchiverStore(config, initialBlockHash); + const contractDataSource = createContractDataSource(archiver); + + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldState.getIpcPath() }); + const publicProcessorFactory = new PublicProcessorFactory( - createContractDataSource(archiver), + contractDataSource, + avmExecutor, undefined, undefined, log.getBindings(), @@ -48,9 +53,6 @@ export async function rerunEpochProvingJob( const l2BlockSourceForReorgDetection = undefined; const deadline = undefined; - // This starts a local proving broker that does not get exposed as a service. This should be good enough for - // smallish epochs to be proven if we run on a large machine, but as epochs grow larger, we may want to switch - // this out for a live proving broker with multiple agents that we can connect to. const broker = await createAndStartProvingBroker(config, telemetry); const prover = await createProverClient(config, worldState, broker, telemetry); @@ -68,7 +70,13 @@ export async function rerunEpochProvingJob( ); log.info(`Rerunning epoch proving job for epoch ${jobData.epochNumber}`); - await provingJob.run(); - log.info(`Completed job for epoch ${jobData.epochNumber} with status ${provingJob.getState()}`); - return provingJob.getState(); + try { + await provingJob.run(); + log.info(`Completed job for epoch ${jobData.epochNumber} with status ${provingJob.getState()}`); + return provingJob.getState(); + } finally { + await prover.stop(); + await broker.stop(); + await avmExecutor[Symbol.asyncDispose](); + } } diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index c2aa37d230c9..2ca1b1dd44f3 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -19,6 +19,7 @@ import { type ProverTxSenderConfig, getPublisherConfigFromProverConfig, } from '@aztec/sequencer-client'; +import type { AvmExecutor } from '@aztec/simulator/server'; import type { ITxProvider, ProverConfig, @@ -48,6 +49,8 @@ export type ProverNodeDeps = { epochCache: EpochCacheInterface; blobClient: BlobClientInterface; keyStoreManager?: KeystoreManager; + /** AVM execution backend (simulator pool + CDB server) for public simulation. */ + avmExecutor: AvmExecutor; }; /** Creates a new prover node subsystem given a config and dependencies */ @@ -188,6 +191,7 @@ export async function createProverNode( epochMonitor, rollupContract, l1Metrics, + deps.avmExecutor, proverNodeConfig, telemetry, delayer, diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.ts b/yarn-project/prover-node/src/job/epoch-proving-job.ts index 88b8fad06dda..1fd16c026af3 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.ts @@ -4,7 +4,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; import { RunningPromise, promiseWithResolvers } from '@aztec/foundation/promise'; import { Timer } from '@aztec/foundation/timer'; -import { AVM_MAX_CONCURRENT_SIMULATIONS } from '@aztec/native'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import { protocolContractsHash } from '@aztec/protocol-contracts'; import { buildFinalBlobChallenges } from '@aztec/prover-client/helpers'; @@ -29,6 +28,9 @@ import type { ProverNodeJobMetrics } from '../metrics.js'; import type { ProverNodePublisher } from '../prover-node-publisher.js'; import { type EpochProvingJobData, validateEpochProvingJobData } from './epoch-proving-job-data.js'; +/** Default parallelism for processing checkpoints. The AVM pool already limits concurrent processes. */ +const DEFAULT_PARALLEL_CHECKPOINT_LIMIT = parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10); + export type EpochProvingJobOptions = { parallelBlockLimit?: number; skipEpochCheck?: boolean; @@ -167,8 +169,8 @@ export class EpochProvingJob implements Traceable { const parallelism = this.config.parallelBlockLimit ? this.config.parallelBlockLimit - : AVM_MAX_CONCURRENT_SIMULATIONS > 0 - ? AVM_MAX_CONCURRENT_SIMULATIONS + : DEFAULT_PARALLEL_CHECKPOINT_LIMIT > 0 + ? DEFAULT_PARALLEL_CHECKPOINT_LIMIT : this.checkpoints.length; await this.processCheckpoints(parallelism, async checkpoint => { @@ -229,26 +231,25 @@ export class EpochProvingJob implements Traceable { // Process public fns. L1 to L2 messages are only inserted for the first block of a checkpoint, // as the fork for subsequent blocks already includes them from the previous block's synced state. - { - await using db = await this.createFork( - BlockNumber(block.number - 1), - blockIndex === 0 ? l1ToL2Messages : undefined, - ); - this.checkState(); - const config = PublicSimulatorConfig.from({ - proverId: this.prover.getProverId().toField(), - skipFeeEnforcement: false, - collectDebugLogs: false, - collectHints: true, - collectPublicInputs: true, - collectStatistics: false, - }); + const db = await this.createFork( + BlockNumber(block.number - 1), + blockIndex === 0 ? l1ToL2Messages : undefined, + ); + const config = PublicSimulatorConfig.from({ + proverId: this.prover.getProverId().toField(), + skipFeeEnforcement: false, + collectDebugLogs: false, + collectHints: true, + collectPublicInputs: true, + collectStatistics: false, + }); + try { const publicProcessor = this.publicProcessorFactory.create(db, globalVariables, config); const processed = await this.processTxs(publicProcessor, txs); - this.checkState(); await this.prover.addTxs(processed); + } finally { + await db.close(); } - this.checkState(); this.log.verbose(`Processed all ${txs.length} txs for block ${block.number}`, { blockNumber: block.number, blockHash: (await block.hash()).toString(), diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 768cf02cced6..ac8e0c1e8a91 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -6,7 +6,7 @@ import { promiseWithResolvers } from '@aztec/foundation/promise'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { P2PClient, TxProvider } from '@aztec/p2p'; -import type { PublicProcessorFactory } from '@aztec/simulator/server'; +import type { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; import { CommitteeAttestation, GENESIS_BLOCK_HEADER_HASH, @@ -52,6 +52,7 @@ describe('prover-node', () => { let rollupContract: MockProxy; let publisherFactory: MockProxy; let l1Metrics: MockProxy; + let avmExecutor: MockProxy; // L1 genesis time let l1GenesisTime: number; @@ -84,6 +85,7 @@ describe('prover-node', () => { epochMonitor, rollupContract, l1Metrics, + avmExecutor, config, ); @@ -104,6 +106,7 @@ describe('prover-node', () => { publisherFactory.create.mockResolvedValue(publisher); l1Metrics = mock(); + avmExecutor = mock(); p2p = mock(); p2p.getTxProvider.mockReturnValue(txProvider); diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 89c26eef75bd..b85cd5a86945 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -7,7 +7,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import { memoize } from '@aztec/foundation/decorators'; import { createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; -import { PublicProcessorFactory } from '@aztec/simulator/server'; +import { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; import type { ChainConfig } from '@aztec/stdlib/config'; @@ -76,6 +76,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable protected readonly epochsMonitor: EpochMonitor, protected readonly rollupContract: RollupContract, protected readonly l1Metrics: L1Metrics, + private readonly avmExecutor: AvmExecutor, config: Partial = {}, protected readonly telemetryClient: TelemetryClient = getTelemetryClient(), private delayer?: Delayer, @@ -337,6 +338,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable // Create a processor factory const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, + this.avmExecutor, this.dateProvider, this.telemetryClient, this.log.getBindings(), diff --git a/yarn-project/simulator/eslint.config.js b/yarn-project/simulator/eslint.config.js index 28178e9148da..1069d0e36402 100644 --- a/yarn-project/simulator/eslint.config.js +++ b/yarn-project/simulator/eslint.config.js @@ -1,6 +1,9 @@ import config from '@aztec/foundation/eslint'; +import { globalIgnores } from 'eslint/config'; + export default [ + globalIgnores(['src/public/cdb/generated/**']), ...config, { files: ['src/public/avm/testing/account_proof_fetcher.ts'], diff --git a/yarn-project/simulator/package.json b/yarn-project/simulator/package.json index cc719eeff6e0..8da3eabbb0d5 100644 --- a/yarn-project/simulator/package.json +++ b/yarn-project/simulator/package.json @@ -19,8 +19,9 @@ "build": "yarn clean && ../scripts/tsc.sh", "build:dev": "../scripts/tsc.sh --watch", "clean": "rm -rf ./dest .tsbuildinfo", + "generate": "node --experimental-strip-types --experimental-transform-types --no-warnings ../../ipc-codegen/src/generate.ts --schema ../../barretenberg/cpp/src/barretenberg/cdb/cdb_schema.json --lang ts --server --out src/public/cdb/generated", "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}", - "build:fuzzer": "yarn clean && ../scripts/tsc.sh" + "build:fuzzer": "yarn clean && yarn generate && ../scripts/tsc.sh" }, "inherits": [ "../package.common.json" @@ -63,8 +64,10 @@ ] }, "dependencies": { + "@aztec/bb-avm-sim": "0.1.0", "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", + "@aztec/ipc-runtime": "0.1.0", "@aztec/native": "workspace:^", "@aztec/noir-acvm_js": "portal:../../noir/packages/acvm_js", "@aztec/noir-noirc_abi": "portal:../../noir/packages/noirc_abi", @@ -77,6 +80,7 @@ "@aztec/world-state": "workspace:^", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.2", + "msgpackr": "^1.11.2", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/yarn-project/simulator/src/public/avm_executor.ts b/yarn-project/simulator/src/public/avm_executor.ts new file mode 100644 index 000000000000..3e66d45cc8d7 --- /dev/null +++ b/yarn-project/simulator/src/public/avm_executor.ts @@ -0,0 +1,66 @@ +import type { AvmSimulator } from './avm_simulator.js'; +import { AvmSimulatorPool, type AvmSimulatorPoolOptions } from './avm_simulator_pool.js'; +import { CdbIpcServer } from './cdb_ipc_server.js'; +import type { PublicContractsDB } from './public_db_sources.js'; + +/** + * An {@link AvmSimulator} bound to a single WSDB fork. Each `simulate` call registers the fork's contracts DB + * on the CDB server so the C++ AVM's contract-data callbacks route to it, and unregisters it once the call + * returns — registration is only needed while the simulation is running. `simulateWithHints` needs no + * registration (the hinted path makes no CDB callbacks). + */ +class ForkedAvmSimulator implements AvmSimulator { + constructor( + private avmSimulator: AvmSimulator, + private cdbServer: CdbIpcServer, + private readonly forkId: number, + private contractsDB: PublicContractsDB, + private timestamp: bigint, + ) {} + + async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise { + this.cdbServer.registerFork(this.forkId, this.contractsDB, this.timestamp); + try { + return await this.avmSimulator.simulate(inputBuffer, signal); + } finally { + this.cdbServer.unregisterFork(this.forkId); + } + } + + simulateWithHints(inputBuffer: Uint8Array): Promise { + return this.avmSimulator.simulateWithHints(inputBuffer); + } +} + +/** Options for {@link AvmExecutor.spawn}; the executor supplies `cdbIpcPath` from the CDB server it creates. */ +export type AvmExecutorOptions = Omit; + +/** + * Owns the public-execution backend: the AVM simulator (a pool of external bb-avm-sim processes) and the CDB + * server that answers those processes' contract-data callbacks. The two are created together (the pool is + * wired to the CDB server's IPC path) and destroyed together, so callers plumb a single `AvmExecutor` rather + * than the pair. Per-fork work goes through {@link forFork}, which hands out an {@link AvmSimulator} and + * keeps the CDB server (and the fork binding) private. + */ +export class AvmExecutor implements AsyncDisposable { + private constructor( + private avmSimulator: AvmSimulator, + private cdbServer: CdbIpcServer, + ) {} + + static async spawn(options: AvmExecutorOptions): Promise { + const cdbServer = new CdbIpcServer(); + const avmSimulator = await AvmSimulatorPool.spawn({ ...options, cdbIpcPath: cdbServer.ipcPath }); + return new AvmExecutor(avmSimulator, cdbServer); + } + + /** Bind to a fork: returns a simulator that registers the fork's contracts DB for the duration of each call. */ + forFork(forkId: number, contractsDB: PublicContractsDB, timestamp: bigint): AvmSimulator { + return new ForkedAvmSimulator(this.avmSimulator, this.cdbServer, forkId, contractsDB, timestamp); + } + + async [Symbol.asyncDispose](): Promise { + await this.avmSimulator.destroy?.(); + await this.cdbServer.close(); + } +} diff --git a/yarn-project/simulator/src/public/avm_simulator.ts b/yarn-project/simulator/src/public/avm_simulator.ts new file mode 100644 index 000000000000..5f85240e234f --- /dev/null +++ b/yarn-project/simulator/src/public/avm_simulator.ts @@ -0,0 +1,15 @@ +/** + * Something that can run AVM simulations, independent of how the work is dispatched (a single external + * process, a pool of them, or an in-process stub). Callers hold this and never see the underlying transport. + */ +export interface AvmSimulator { + /** + * Run a fast simulation and return the msgpack-encoded result. If `signal` aborts, the simulation stops + * at the next cancellation checkpoint. + */ + simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise; + /** Run a simulation collecting proving hints and return the msgpack-encoded result. */ + simulateWithHints(inputBuffer: Uint8Array): Promise; + /** Release any resources held by the underlying implementation. */ + destroy?(): Promise; +} diff --git a/yarn-project/simulator/src/public/avm_simulator_pool.ts b/yarn-project/simulator/src/public/avm_simulator_pool.ts new file mode 100644 index 000000000000..6fc8ef0cc019 --- /dev/null +++ b/yarn-project/simulator/src/public/avm_simulator_pool.ts @@ -0,0 +1,185 @@ +import { AvmService } from '@aztec/bb-avm-sim'; +import { type Logger, createLogger } from '@aztec/foundation/log'; + +import type { AvmSimulator } from './avm_simulator.js'; + +export interface AvmSimulatorPoolOptions { + /** Maximum number of concurrent AVM processes. If not set, defaults to AVM_MAX_CONCURRENT_SIMULATIONS env var or 4. */ + maxSize?: number; + /** Path to the bb-avm-sim binary. If omitted, the generated package resolves it. */ + avmBinaryPath?: string; + /** IPC path for the shared WSDB server. */ + wsdbIpcPath: string; + /** IPC path for the shared CDB server. */ + cdbIpcPath: string; + /** Optional logger function for AVM process output. */ + logger?: (msg: string) => void; +} + +/** Lazily manages local bb-avm-sim processes for parallel AVM simulation. */ +export class AvmSimulatorPool implements AvmSimulator { + private slots: Array = []; + private available: number[] = []; + private waiters: Array<{ resolve: (simulator: AvmSimulator) => void; reject: (error: Error) => void }> = []; + private createdCount = 0; + private log: Logger; + private maxSize: number; + + constructor(private options: AvmSimulatorPoolOptions) { + this.log = createLogger('simulator:avm-pool'); + this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10); + } + + static async spawn(options: AvmSimulatorPoolOptions): Promise { + const pool = new AvmSimulatorPool(options); + // Always start one process up front so the first simulate() doesn't pay process spawn/connect cost. + await pool.prewarm(); + return pool; + } + + async [Symbol.asyncDispose](): Promise { + await this.destroy(); + } + + async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise { + const simulator = await this.checkout(); + try { + return await simulator.simulate(inputBuffer, signal); + } finally { + this.return(simulator); + } + } + + async simulateWithHints(inputBuffer: Uint8Array): Promise { + const simulator = await this.checkout(); + try { + return await simulator.simulateWithHints(inputBuffer); + } finally { + this.return(simulator); + } + } + + /** Destroy all AVM processes in the pool. */ + async destroy(): Promise { + for (const waiter of this.waiters) { + waiter.reject(new Error('AVM simulator pool destroyed')); + } + this.waiters = []; + + const destroyPromises: Promise[] = []; + for (const slot of this.slots) { + if (slot?.destroy) { + destroyPromises.push(slot.destroy()); + } + } + await Promise.all(destroyPromises); + + this.slots = []; + this.available = []; + this.createdCount = 0; + this.log.info('AVM simulator pool destroyed'); + } + + /** + * Eagerly spawn up to `count` AVM processes (capped at maxSize) and leave them available, so the + * first simulate() doesn't pay process spawn/connect cost. Idempotent. + */ + async prewarm(count = 1): Promise { + const target = Math.min(count, this.maxSize); + const created: AvmSimulator[] = []; + while (this.createdCount < target) { + created.push(await this.createSlot()); + } + // Hand the freshly-spawned processes back to the pool so checkout() reuses them. + for (const simulator of created) { + this.return(simulator); + } + } + + /** Check out an AVM simulator from the pool, blocking until one is free. Caller must return() it when done. */ + private async checkout(): Promise { + const idx = this.available.pop(); + if (idx !== undefined && this.slots[idx]) { + return this.slots[idx]!; + } + + if (this.createdCount < this.maxSize || (idx !== undefined && !this.slots[idx])) { + return await this.createSlot(idx); + } + + return new Promise((resolve, reject) => { + this.waiters.push({ resolve, reject }); + }); + } + + /** Return an AVM simulator to the pool after use. */ + private return(simulator: AvmSimulator): void { + const waiter = this.waiters.shift(); + if (waiter) { + waiter.resolve(simulator); + } else { + const idx = this.slots.indexOf(simulator); + if (idx >= 0) { + this.available.push(idx); + } + } + } + + private async createSlot(reuseIdx?: number): Promise { + const simulator = await AvmSimulatorProcess.spawn({ + binaryPath: this.options.avmBinaryPath, + wsdbIpcPath: this.options.wsdbIpcPath, + cdbIpcPath: this.options.cdbIpcPath, + logger: this.options.logger, + }); + if (reuseIdx !== undefined && reuseIdx < this.slots.length) { + this.slots[reuseIdx] = simulator; + } else { + this.slots.push(simulator); + this.createdCount++; + } + this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`); + return simulator; + } +} + +class AvmSimulatorProcess implements AvmSimulator { + private constructor(private service: AvmService) {} + + static async spawn(options: { + binaryPath?: string; + wsdbIpcPath: string; + cdbIpcPath: string; + logger?: (msg: string) => void; + }): Promise { + const service = await AvmService.spawn({ + binaryPath: options.binaryPath, + transport: 'uds', + logger: options.logger, + extraArgs: ['--wsdb', options.wsdbIpcPath, '--cdb', options.cdbIpcPath], + }); + return new AvmSimulatorProcess(service); + } + + public async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise { + // Signal the C++ process to stop at its next cancellation checkpoint when the caller aborts. + const onAbort = () => this.service.sendProcessSignal('SIGUSR1'); + if (signal?.aborted) { + onAbort(); + } + signal?.addEventListener('abort', onAbort, { once: true }); + try { + return (await this.service.simulate({ inputs: inputBuffer })).result; + } finally { + signal?.removeEventListener('abort', onAbort); + } + } + + public async simulateWithHints(inputBuffer: Uint8Array): Promise { + return (await this.service.simulateWithHints({ inputs: inputBuffer })).result; + } + + public async destroy(): Promise { + await this.service.destroy(); + } +} diff --git a/yarn-project/simulator/src/public/cdb_ipc_server.test.ts b/yarn-project/simulator/src/public/cdb_ipc_server.test.ts new file mode 100644 index 000000000000..ad0dd709466b --- /dev/null +++ b/yarn-project/simulator/src/public/cdb_ipc_server.test.ts @@ -0,0 +1,25 @@ +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { SerializableContractInstance } from '@aztec/stdlib/contract'; + +import { serializeContractInstance } from './cdb_ipc_server.js'; + +describe('cdb_ipc_server', () => { + it('serializes the complete public keys shape expected by the C++ AVM', async () => { + const instance = (await SerializableContractInstance.random()).withAddress(await AztecAddress.random()); + + const serialized = serializeContractInstance(instance); + const publicKeys = serialized.publicKeys as Record>; + + expect(publicKeys).toEqual({ + npkMHash: instance.publicKeys.npkMHash.toBuffer(), + ivpkM: { + x: instance.publicKeys.ivpkM.x.toBuffer(), + y: instance.publicKeys.ivpkM.y.toBuffer(), + }, + ovpkMHash: instance.publicKeys.ovpkMHash.toBuffer(), + tpkMHash: instance.publicKeys.tpkMHash.toBuffer(), + mspkMHash: instance.publicKeys.mspkMHash.toBuffer(), + fbpkMHash: instance.publicKeys.fbpkMHash.toBuffer(), + }); + }); +}); diff --git a/yarn-project/simulator/src/public/cdb_ipc_server.ts b/yarn-project/simulator/src/public/cdb_ipc_server.ts new file mode 100644 index 000000000000..72116bd746e7 --- /dev/null +++ b/yarn-project/simulator/src/public/cdb_ipc_server.ts @@ -0,0 +1,197 @@ +/** + * UDS server for AVM CDB requests. + * + * Transport (socket, framing, per-connection response ordering) is handled by the shared + * `UdsIpcServer` from `@aztec/ipc-runtime`; message dispatch comes from the generated CDB + * server. This class only implements the generated handler interface and routes requests to + * PublicContractsDB instances by fork ID. + */ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { UdsIpcServer } from '@aztec/ipc-runtime'; +import { FunctionSelector } from '@aztec/stdlib/abi'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { ContractDeploymentData, type ContractInstanceWithAddress } from '@aztec/stdlib/contract'; + +import { Decoder, Encoder } from 'msgpackr'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { threadId } from 'node:worker_threads'; + +import type { + CdbAddContracts, + CdbAddContractsResponse, + CdbCommitCheckpoint, + CdbCommitCheckpointResponse, + CdbCreateCheckpoint, + CdbCreateCheckpointResponse, + CdbGetBytecodeCommitment, + CdbGetBytecodeCommitmentResponse, + CdbGetContractClass, + CdbGetContractClassResponse, + CdbGetContractInstance, + CdbGetContractInstanceResponse, + CdbGetDebugFunctionName, + CdbGetDebugFunctionNameResponse, + CdbRevertCheckpoint, + CdbRevertCheckpointResponse, +} from './cdb/generated/api_types.js'; +import { type Handler as CdbHandler, handleRequest } from './cdb/generated/server.js'; +import type { PublicContractsDB } from './public_db_sources.js'; + +const encoder = new Encoder({ useRecords: false }); +const decoder = new Decoder({ useRecords: false }); + +let instanceCounter = 0; + +function toFieldBuffer(field: Fr | AztecAddress): Buffer { + return field.toBuffer(); +} + +/** Serializes contract instances to the AVM CDB wire shape. */ +export function serializeContractInstance(instance: ContractInstanceWithAddress): Record { + return { + salt: toFieldBuffer(instance.salt), + deployer: toFieldBuffer(instance.deployer), + currentContractClassId: toFieldBuffer(instance.currentContractClassId), + originalContractClassId: toFieldBuffer(instance.originalContractClassId), + initializationHash: toFieldBuffer(instance.initializationHash), + immutablesHash: toFieldBuffer(instance.immutablesHash), + publicKeys: { + npkMHash: toFieldBuffer(instance.publicKeys.npkMHash), + ivpkM: { + x: toFieldBuffer(instance.publicKeys.ivpkM.x), + y: toFieldBuffer(instance.publicKeys.ivpkM.y), + }, + ovpkMHash: toFieldBuffer(instance.publicKeys.ovpkMHash), + tpkMHash: toFieldBuffer(instance.publicKeys.tpkMHash), + mspkMHash: toFieldBuffer(instance.publicKeys.mspkMHash), + fbpkMHash: toFieldBuffer(instance.publicKeys.fbpkMHash), + }, + }; +} + +/** Serializes contract classes to the AVM CDB wire shape. */ +function serializeContractClass(contractClass: { + id: Fr; + artifactHash: Fr; + privateFunctionsRoot: Fr; + packedBytecode: Buffer; +}): Record { + return { + id: toFieldBuffer(contractClass.id), + artifactHash: toFieldBuffer(contractClass.artifactHash), + privateFunctionsRoot: toFieldBuffer(contractClass.privateFunctionsRoot), + packedBytecode: contractClass.packedBytecode, + }; +} + +/** Routes AVM CDB IPC requests to registered PublicContractsDB forks. */ +export class CdbIpcServer implements CdbHandler { + public readonly ipcPath: string; + private server: Promise; + private log: Logger; + /** Maps WSDB fork IDs to contracts DB/timestamp pairs for concurrent simulations. */ + private forks = new Map(); + + constructor() { + this.log = createLogger('cdb-ipc-server'); + this.ipcPath = path.join(os.tmpdir(), `cdb-ts-${process.pid}-${threadId}-${instanceCounter++}.sock`); + + // Listen asynchronously; the C++ AVM retries connecting until the socket is up. + this.server = UdsIpcServer.listen(this.ipcPath, (_clientId, request) => handleRequest(this, request)); + this.server.then( + () => this.log.debug(`CDB IPC server listening on ${this.ipcPath}`), + err => this.log.error(`CDB IPC server failed to listen on ${this.ipcPath}`, { err }), + ); + } + + /** Register a PublicContractsDB for a given WSDB fork ID. */ + registerFork(forkId: number, contractsDB: PublicContractsDB, timestamp: bigint): void { + this.forks.set(forkId, { db: contractsDB, timestamp }); + } + + /** Unregister a fork's contracts DB (call after simulation completes). */ + unregisterFork(forkId: number): void { + this.forks.delete(forkId); + } + + /** Close the server and all active connections. */ + async close(): Promise { + const server = await this.server.catch(() => undefined); + await server?.close(); + } + + async [Symbol.asyncDispose](): Promise { + await this.close(); + } + + /** Look up the contracts DB for a given fork ID, throwing if not registered. */ + private getFork(forkId: number): { db: PublicContractsDB; timestamp: bigint } { + const fork = this.forks.get(forkId); + if (!fork) { + const registered = Array.from(this.forks.keys()).sort((a, b) => a - b); + throw new Error( + `CDB server: no contracts DB registered for forkId ${forkId} (registered=[${registered.join(',')}])`, + ); + } + return fork; + } + + async getContractInstance(command: CdbGetContractInstance): Promise { + const { db, timestamp } = this.getFork(command.forkId); + const address = AztecAddress.fromBuffer(Buffer.from(command.address)); + const instance = await db.getContractInstance(address, timestamp); + return { instance: instance ? encoder.encode(serializeContractInstance(instance)) : null }; + } + + async getContractClass(command: CdbGetContractClass): Promise { + const { db } = this.getFork(command.forkId); + const classId = Fr.fromBuffer(Buffer.from(command.classId)); + const contractClass = await db.getContractClass(classId); + return { contractClass: contractClass ? encoder.encode(serializeContractClass(contractClass)) : null }; + } + + async getBytecodeCommitment(command: CdbGetBytecodeCommitment): Promise { + const { db } = this.getFork(command.forkId); + const classId = Fr.fromBuffer(Buffer.from(command.classId)); + const commitment = await db.getBytecodeCommitment(classId); + return { commitment: commitment ? toFieldBuffer(commitment) : null }; + } + + async getDebugFunctionName(command: CdbGetDebugFunctionName): Promise { + const { db } = this.getFork(command.forkId); + const address = AztecAddress.fromBuffer(Buffer.from(command.address)); + const selectorField = Fr.fromBuffer(Buffer.from(command.selector)); + const selector = FunctionSelector.fromFieldOrUndefined(selectorField); + const name = selector ? await db.getDebugFunctionName(address, selector) : undefined; + return { name: name ?? null }; + } + + addContracts(command: CdbAddContracts): Promise { + const { db } = this.getFork(command.forkId); + const contractDeploymentData = ContractDeploymentData.fromPlainObject( + decoder.decode(command.contractDeploymentData), + ); + db.addContractsFromLogs(contractDeploymentData); + return Promise.resolve({}); + } + + createCheckpoint(command: CdbCreateCheckpoint): Promise { + const { db } = this.getFork(command.forkId); + db.createCheckpoint(); + return Promise.resolve({}); + } + + commitCheckpoint(command: CdbCommitCheckpoint): Promise { + const { db } = this.getFork(command.forkId); + db.commitCheckpoint(); + return Promise.resolve({}); + } + + revertCheckpoint(command: CdbRevertCheckpoint): Promise { + const { db } = this.getFork(command.forkId); + db.revertCheckpoint(); + return Promise.resolve({}); + } +} diff --git a/yarn-project/simulator/src/public/fixtures/index.ts b/yarn-project/simulator/src/public/fixtures/index.ts index d215d13d15cc..48b6c1369bc9 100644 --- a/yarn-project/simulator/src/public/fixtures/index.ts +++ b/yarn-project/simulator/src/public/fixtures/index.ts @@ -1,4 +1,5 @@ export * from './public_tx_simulation_tester.js'; +export * from './public_processor_test_env.js'; export * from './utils.js'; export * from './simple_contract_data_source.js'; export { TestExecutorMetrics } from '../test_executor_metrics.js'; diff --git a/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts b/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts new file mode 100644 index 000000000000..26097f336a0a --- /dev/null +++ b/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@aztec/foundation/log'; +import { TestDateProvider } from '@aztec/foundation/timer'; +import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; +import { GasFees } from '@aztec/stdlib/gas'; +import { GlobalVariables } from '@aztec/stdlib/tx'; +import { getTelemetryClient } from '@aztec/telemetry-client'; +import { NativeWorldStateService } from '@aztec/world-state'; + +import { AvmExecutor } from '../avm_executor.js'; +import { PublicContractsDB } from '../public_db_sources.js'; +import { GuardedMerkleTreeOperations } from '../public_processor/guarded_merkle_tree.js'; +import { PublicProcessor } from '../public_processor/public_processor.js'; +import { PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; +import { PublicTxSimulationTester } from './public_tx_simulation_tester.js'; +import { SimpleContractDataSource } from './simple_contract_data_source.js'; + +/** Options for {@link PublicProcessorTestEnv.create}. */ +export interface PublicProcessorTestEnvOptions { + /** Global variables shared by the processor and the tester. Defaults to empty with nonzero gas fees. */ + globals?: GlobalVariables; + /** Simulator config for the processor's PublicTxSimulator. */ + config?: Partial; +} + +function defaultGlobals(): GlobalVariables { + const globals = GlobalVariables.empty(); + globals.gasFees = new GasFees(2, 3); + return globals; +} + +const defaultConfig = PublicSimulatorConfig.from({ + skipFeeEnforcement: false, + collectDebugLogs: true, + collectHints: false, + collectStatistics: false, + collectCallMetadata: true, +}); + +/** + * Bundles the real-AVM wiring shared by public-processor app tests: a live WSDB world state, a CDB IPC + * server, a spawned AVM simulator, a {@link PublicProcessor} backed by all three, and a setup-only + * {@link PublicTxSimulationTester} sharing the same fork and contract data source for building txs and + * seeding balances. Own it with `await using`, or call {@link Symbol.asyncDispose} from `afterEach`. + */ +export class PublicProcessorTestEnv implements AsyncDisposable { + private constructor( + public readonly processor: PublicProcessor, + public readonly tester: PublicTxSimulationTester, + public readonly contractsDB: PublicContractsDB, + public readonly globals: GlobalVariables, + private readonly worldStateService: NativeWorldStateService, + private readonly avmExecutor: AvmExecutor, + ) {} + + static async create(opts: PublicProcessorTestEnvOptions = {}): Promise { + const globals = opts.globals ?? defaultGlobals(); + const config = opts.config ? PublicSimulatorConfig.from({ ...defaultConfig, ...opts.config }) : defaultConfig; + + const contractDataSource = new SimpleContractDataSource(); + const worldStateService = await NativeWorldStateService.tmp(); + const merkleTrees = await worldStateService.fork(); + const contractsDB = new PublicContractsDB(contractDataSource); + + const forkId = merkleTrees.getRevision().forkId; + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const forkedSimulator = avmExecutor.forFork(forkId, contractsDB, globals.timestamp); + + const simulator = new PublicTxSimulator(forkedSimulator, globals, config, undefined, forkId); + const processor = new PublicProcessor( + globals, + new GuardedMerkleTreeOperations(merkleTrees), + contractsDB, + simulator, + new TestDateProvider(), + getTelemetryClient(), + createLogger('simulator:public-processor'), + ); + + const tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); + + return new PublicProcessorTestEnv(processor, tester, contractsDB, globals, worldStateService, avmExecutor); + } + + async [Symbol.asyncDispose](): Promise { + await this.avmExecutor[Symbol.asyncDispose](); + await this.tester.close(); + await this.worldStateService.close(); + } +} diff --git a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index 32127e5752b4..c75dfebea5fa 100644 --- a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -18,6 +18,7 @@ import { getContractFunctionAbi, getFunctionSelector, } from '../avm/testing/utils.js'; +import { AvmExecutor } from '../avm_executor.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { MeasuredPublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; import type { MeasuredPublicTxSimulatorInterface } from '../public_tx_simulator/public_tx_simulator_interface.js'; @@ -63,8 +64,9 @@ export type MeasuredSimulatorFactory = ( */ export class PublicTxSimulationTester extends BaseAvmSimulationTester { protected txCount: number = 0; - private simulator: MeasuredPublicTxSimulatorInterface; + private simulator: MeasuredPublicTxSimulatorInterface | undefined; private metricsPrefix?: string; + protected avmExecutor?: AvmExecutor; constructor( merkleTree: MerkleTreeWriteOperations, @@ -80,7 +82,9 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { if (simulatorFactory) { this.simulator = simulatorFactory(merkleTree, contractsDB, globals, this.metrics, config); } else { - this.simulator = new MeasuredPublicTxSimulator(merkleTree, contractsDB, globals, this.metrics, config); + // No simulator — this tester can only be used for setup (setFeePayerBalance, createTx, etc.) + // To simulate, use PublicTxSimulationTester.create() or pass a simulatorFactory. + this.simulator = undefined; } } @@ -92,9 +96,23 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { ): Promise { const contractDataSource = new SimpleContractDataSource(); const merkleTree = await worldStateService.fork(); - const simulatorFactory: MeasuredSimulatorFactory = (mt, cdb, g, m, c) => - new MeasuredPublicTxSimulator(mt, cdb, g, m, c); - return new PublicTxSimulationTester(merkleTree, contractDataSource, globals, metrics, simulatorFactory, config); + + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const forkId = merkleTree.getRevision().forkId; + const forkedSimulator = avmExecutor.forFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredPublicTxSimulator(forkedSimulator, g, m, c, undefined, forkId); + + const tester = new PublicTxSimulationTester( + merkleTree, + contractDataSource, + globals, + metrics, + simulatorFactory, + config, + ); + tester.avmExecutor = avmExecutor; + return tester; } public setMetricsPrefix(prefix: string) { @@ -193,18 +211,8 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall?: TestEnqueuedCall, feePayer?: AztecAddress, privateInsertions?: TestPrivateInsertions, - gasLimits?: Gas, ): Promise { - return await this.simulateTx( - sender, - setupCalls, - appCalls, - teardownCall, - feePayer, - privateInsertions, - txLabel, - gasLimits, - ); + return await this.simulateTx(sender, setupCalls, appCalls, teardownCall, feePayer, privateInsertions, txLabel); } /** @@ -222,7 +230,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall?: TestEnqueuedCall, feePayer?: AztecAddress, privateInsertions?: TestPrivateInsertions, - gasLimits?: Gas, ): Promise { return await this.simulateTxWithLabel( txLabel, @@ -232,7 +239,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall, feePayer, privateInsertions, - gasLimits, ); } @@ -240,22 +246,31 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { this.metrics.prettyPrint(); } + /** Clean up IPC resources (AVM executor and merkle tree fork) created by create(). */ + public async close(): Promise { + await this.avmExecutor?.[Symbol.asyncDispose](); + // Close the merkle tree fork to release IPC resources before the wsdb process is killed. + if (this.merkleTrees?.close) { + await this.merkleTrees.close().catch(() => {}); + } + } + /** * Cancel the current simulation if one is in progress. - * This signals the underlying simulator (e.g., C++) to stop at the next safe point. + * This signals the underlying simulator to stop at the next safe point. * Safe to call even if no simulation is in progress. * * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop. */ public async cancel(waitTimeoutMs?: number): Promise { - await this.simulator.cancel?.(waitTimeoutMs); + await this.simulator?.cancel?.(waitTimeoutMs); } /** * Get the underlying simulator for advanced test scenarios. * Use this when you need direct control over simulation (e.g., for testing cancellation). */ - public getSimulator(): MeasuredPublicTxSimulatorInterface { + public getSimulator(): MeasuredPublicTxSimulatorInterface | undefined { return this.simulator; } diff --git a/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts b/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts index dfbab677dd83..2e56980be087 100644 --- a/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts +++ b/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts @@ -34,6 +34,8 @@ import { import type { NativeWorldStateService } from '@aztec/world-state'; import { BaseAvmSimulationTester } from '../avm/testing/base_avm_simulation_tester.js'; +import { AvmExecutor } from '../avm_executor.js'; +import type { AvmSimulator } from '../avm_simulator.js'; import { SimpleContractDataSource } from '../fixtures/simple_contract_data_source.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; @@ -199,19 +201,27 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester { merkleTrees: MerkleTreeWriteOperations, contractDataSource: SimpleContractDataSource, globals: GlobalVariables, + forkedSimulator: AvmSimulator, + private avmExecutor: AvmExecutor, + forkId: number, ) { super(contractDataSource, merkleTrees); - const contractsDb = new PublicContractsDB(contractDataSource); // collectPublicInputs and collectCallMetadata are required so the C++ result carries the end // tree snapshots and app-logic return values that the fuzzer bin reports back to the harness. - this.simulator = new PublicTxSimulator(merkleTrees, contractsDb, globals, { - skipFeeEnforcement: false, - collectDebugLogs: false, - collectHints: false, - collectPublicInputs: true, - collectStatistics: false, - collectCallMetadata: true, - }); + this.simulator = new PublicTxSimulator( + forkedSimulator, + globals, + { + skipFeeEnforcement: false, + collectDebugLogs: false, + collectHints: false, + collectPublicInputs: true, + collectStatistics: false, + collectCallMetadata: true, + }, + undefined, + forkId, + ); } /** @@ -223,7 +233,15 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester { ): Promise { const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await worldStateService.fork(); - return new AvmFuzzerSimulator(merkleTrees, contractDataSource, globals); + const forkId = merkleTrees.getRevision().forkId; + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const forkedSimulator = avmExecutor.forFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + return new AvmFuzzerSimulator(merkleTrees, contractDataSource, globals, forkedSimulator, avmExecutor, forkId); + } + + /** Tear down the AVM executor (AVM process pool and CDB server). */ + public async close(): Promise { + await this.avmExecutor[Symbol.asyncDispose](); } /** diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 2047f95a2fb5..8dcbffca8ca0 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,8 +1,13 @@ +export { AvmSimulatorPool, type AvmSimulatorPoolOptions } from './avm_simulator_pool.js'; +export { AvmExecutor, type AvmExecutorOptions } from './avm_executor.js'; +export type { PublicContractsDBInterface } from './db_interfaces.js'; export { PublicContractsDB } from './public_db_sources.js'; export { GuardedMerkleTreeOperations } from './public_processor/guarded_merkle_tree.js'; export { PublicProcessor, PublicProcessorFactory } from './public_processor/public_processor.js'; +export type { AvmSimulator } from './avm_simulator.js'; export { PublicTxSimulator, + MeasuredPublicTxSimulator, createPublicTxSimulatorForBlockBuilding, DumpingPublicTxSimulator, type PublicTxSimulatorInterface, diff --git a/yarn-project/simulator/src/public/public_db_sources.ts b/yarn-project/simulator/src/public/public_db_sources.ts index 6852bf23473d..a4d6b27de502 100644 --- a/yarn-project/simulator/src/public/public_db_sources.ts +++ b/yarn-project/simulator/src/public/public_db_sources.ts @@ -55,7 +55,7 @@ export class PublicContractsDB implements PublicContractsDBInterface { this.log = createLogger('simulator:contracts-data-source', bindings); } - /** Parses raw log data from the C++/NAPI bridge and inserts the resulting contracts into the current checkpoint. */ + /** Parses raw contract deployment data and inserts the resulting contracts into the current checkpoint. */ public addContractsFromLogs(contractDeploymentData: ContractDeploymentData): void { const currentState = this.getCurrentState(); diff --git a/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts index ba3eb9fa28cc..0d430d6d3c92 100644 --- a/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts +++ b/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts @@ -1,62 +1,24 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { createLogger } from '@aztec/foundation/log'; -import { TestDateProvider } from '@aztec/foundation/timer'; import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; import { AvmTestContractArtifact } from '@aztec/noir-test-contracts.js/AvmTest'; -import { PublicSimulatorConfig, RevertCode } from '@aztec/stdlib/avm'; +import { RevertCode } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { GasFees } from '@aztec/stdlib/gas'; -import { GlobalVariables } from '@aztec/stdlib/tx'; -import { getTelemetryClient } from '@aztec/telemetry-client'; -import { NativeWorldStateService } from '@aztec/world-state'; -import { PublicContractsDB } from '../../../server.js'; import { createContractClassAndInstance } from '../../avm/testing/utils.js'; -import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; +import { PublicProcessorTestEnv } from '../../fixtures/index.js'; import { addNewContractClassToTx, addNewContractInstanceToTx, createTxForPrivateOnly } from '../../fixtures/utils.js'; -import { PublicTxSimulator } from '../../public_tx_simulator/public_tx_simulator.js'; -import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; -import { PublicProcessor } from '../public_processor.js'; describe('Public processor contract registration/deployment tests', () => { const admin = AztecAddress.fromNumber(42); const sender = AztecAddress.fromNumber(111); - let worldStateService: NativeWorldStateService; - let contractsDB: PublicContractsDB; - let tester: PublicTxSimulationTester; - let processor: PublicProcessor; + let env: PublicProcessorTestEnv; + let tester: PublicProcessorTestEnv['tester']; + let processor: PublicProcessorTestEnv['processor']; beforeEach(async () => { - const globals = GlobalVariables.empty(); - // apply some nonzero default gas fees - globals.gasFees = new GasFees(2, 3); - - const contractDataSource = new SimpleContractDataSource(); - worldStateService = await NativeWorldStateService.tmp(); - const merkleTrees = await worldStateService.fork(); - const guardedMerkleTrees = new GuardedMerkleTreeOperations(merkleTrees); - contractsDB = new PublicContractsDB(contractDataSource); - const config = PublicSimulatorConfig.from({ - skipFeeEnforcement: false, - collectDebugLogs: true, - collectHints: false, - collectStatistics: false, - collectCallMetadata: true, - }); - const simulator = new PublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config); - - processor = new PublicProcessor( - globals, - guardedMerkleTrees, - contractsDB, - simulator, - new TestDateProvider(), - getTelemetryClient(), - createLogger('simulator:public-processor'), - ); - - tester = new PublicTxSimulationTester(merkleTrees, contractDataSource); + env = await PublicProcessorTestEnv.create(); + ({ tester, processor } = env); // make sure tx senders have fee balance await tester.setFeePayerBalance(admin); @@ -64,7 +26,7 @@ describe('Public processor contract registration/deployment tests', () => { }); afterEach(async () => { - await worldStateService.close(); + await env[Symbol.asyncDispose](); }); it('can deploy in a private-only tx and call a public function later in the block', async () => { diff --git a/yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts deleted file mode 100644 index da5cb39cc8ad..000000000000 --- a/yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts +++ /dev/null @@ -1,378 +0,0 @@ -/** - * Test to reproduce the C++ simulation timeout race condition. - * - * Root Cause: When a timeout fires during C++ AVM simulation: - * 1. The C++ simulation continues running on a libuv worker thread - * 2. It directly accesses WorldState via the native handle - * 3. TypeScript calls checkpoint revert operations - * 4. Both paths operate on the same WorldState concurrently - * - * The key issues were: - * - GuardedMerkleTreeOperations does not guard C++ access - * - Nothing stops C++ simulation on PublicProcessor deadline - */ -import { createLogger } from '@aztec/foundation/log'; -import { sleep } from '@aztec/foundation/sleep'; -import { TestDateProvider } from '@aztec/foundation/timer'; -import { AvmTestContractArtifact } from '@aztec/noir-test-contracts.js/AvmTest'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { GasFees } from '@aztec/stdlib/gas'; -import { MerkleTreeId, merkleTreeIds } from '@aztec/stdlib/trees'; -import { GlobalVariables } from '@aztec/stdlib/tx'; -import { getTelemetryClient } from '@aztec/telemetry-client'; -import { ForkCheckpoint, NativeWorldStateService } from '@aztec/world-state'; - -import { jest } from '@jest/globals'; - -import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; -import { PublicContractsDB } from '../../public_db_sources.js'; -import { PublicTxSimulator } from '../../public_tx_simulator/public_tx_simulator.js'; -import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; -import { PublicProcessor } from '../public_processor.js'; - -// AvmTest's `n_storage_writes_to_same_slot` writes the same public-data slot in a loop. Same-slot -// writes are squashed so they never hit the per-tx public-data-write limit: the call keeps writing -// to PUBLIC_DATA_TREE continuously (no gaps) until it runs out of gas, which is exactly what we -// need to reliably catch the C++ simulation mid-write. The count is far larger than any tx can -// afford, so the call always runs to out-of-gas. -const STORAGE_WRITE_SPAM_FN = 'n_storage_writes_to_same_slot'; -const STORAGE_WRITE_SPAM_COUNT = 1_000_000_000; - -jest.setTimeout(120_000); - -describe('PublicProcessor C++ Timeout Race Condition', () => { - // BUG PROOF tests - this is the race condition and is flaky so we run more iterations - const MAX_BUG_PROOF_ITERATIONS = 10; - // FIX PROOF tests - just confirm that the fix always works - const FIX_PROOF_ITERATIONS = 5; - - const logger = createLogger('public-processor-timeout-race'); - - const admin = AztecAddress.fromNumber(42); - - let worldStateService: NativeWorldStateService; - - beforeEach(async () => { - worldStateService = await NativeWorldStateService.tmp(); - }); - - afterEach(async () => { - await worldStateService.close(); - }); - - /** - * Helper function to run the race condition test at the PublicTxSimulator level. - * Both BUG PROOF and FIX PROOF use IDENTICAL code - the ONLY difference is - * whether cancellation is signaled and waited for. - * - * Uses same-slot storage spamming to keep C++ constantly writing to PUBLIC_DATA_TREE without gaps. - * - * For the BUG proof: Don't call cancel() → C++ continues during reverts → corruption - * For the FIX proof: Call cancel(100) → wait for C++ to stop → then revert → no corruption - * - * @param useCancellation - Whether to call cancel() before reverts - * @param numIterations - Number of iterations to run - */ - async function runRaceConditionTest(useCancellation: boolean, numIterations: number): Promise { - let raceObservedCount = 0; - - const globals = GlobalVariables.empty(); - globals.gasFees = new GasFees(2, 3); - - const contractDataSource = new SimpleContractDataSource(); - const merkleTrees = await worldStateService.fork(); - const contractsDB = new PublicContractsDB(contractDataSource); - - const simulator = new PublicTxSimulator(merkleTrees, contractsDB, globals); - - const tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); - await tester.setFeePayerBalance(admin); - - // Deploy the AvmTest contract; its `n_storage_writes_to_same_slot` loops storage writes until OOG. - const contract = await tester.registerAndDeployContract(/*constructorArgs=*/ [], admin, AvmTestContractArtifact); - const contractAddress = contract.address; - - for (let iteration = 0; iteration < numIterations; iteration++) { - // Ensure any previous simulation is fully stopped before starting a new one - await simulator.cancel(1000); - - // Get initial state for trees we need to check - const initialTreeInfo = new Map(); - for (const treeId of merkleTreeIds()) { - const info = await merkleTrees.getTreeInfo(treeId); - initialTreeInfo.set(treeId, { size: info.size, root: info.root }); - } - - // Create checkpoint BEFORE simulation (like PublicProcessor does) - const forkCheckpoint = await ForkCheckpoint.new(merkleTrees); - - // Create transaction that calls the spammer contract - const tx = await tester.createTx( - admin, - [], - [{ address: contractAddress, fnName: STORAGE_WRITE_SPAM_FN, args: [STORAGE_WRITE_SPAM_COUNT] }], - ); - - // Start C++ simulation (not awaiting - like production timeout behavior!) - const simulationPromise = simulator.simulate(tx); - // Eagerly add catch to prevent unhandled promise rejection warnings - simulationPromise.catch(() => {}); - - // No delay - immediately try to catch C++ mid-write - // This maximizes the chance of hitting the race condition - - // THE ONLY DIFFERENCE: signal cancellation AND WAIT, or not - if (useCancellation) { - // FIX - Signal cancellation and WAIT for C++ to actually stop (up to 100ms) - // This ensures C++ has finished before we proceed with reverts. - await simulator.cancel(100); - } - // BUG - No cancel, C++ continues running during reverts below - - // Clean up - revert all changes - await forkCheckpoint.revertToCheckpoint(); - - // Wait for simulation promise for cleanup - await Promise.race([simulationPromise.catch(() => {}), sleep(100)]); - - // Check state after everything is cleaned up - let anyTreeCorrupted = false; - for (const treeId of merkleTreeIds()) { - const finalInfo = await merkleTrees.getTreeInfo(treeId); - const initialInfo = initialTreeInfo.get(treeId)!; - const changed = finalInfo.size !== initialInfo.size || !finalInfo.root.equals(initialInfo.root); - if (changed) { - anyTreeCorrupted = true; - break; - } - } - - if (anyTreeCorrupted) { - raceObservedCount++; - // Early exit - bug exists, no need to continue - // Always cancel simulation for clean test shutdown (prevent crash during afterEach) - await simulator.cancel(1000); - logger.verbose(`Early exit`); - return raceObservedCount; - } - } - - // Always cancel simulation for clean test shutdown (prevent crash during afterEach) - await simulator.cancel(1000); - return raceObservedCount; - } - - /** - * PublicTxSimulation BUG - Demonstrate the race condition WITHOUT cancellation. - * - * This test proves the bug exists by showing that without cancellation: - * - C++ simulation continues running after we call revertCheckpoint - * - C++ makes writes AFTER the revert, corrupting state - * - * The race is non-deterministic, so we run multiple iterations. - * This test PASSES if we observe corruption (proving the bug exists). - */ - it('PublicTxSimulator BUG PROOF: race condition exists WITHOUT cancellation', async () => { - const raceObservedCount = await runRaceConditionTest(false, MAX_BUG_PROOF_ITERATIONS); - logger.info(`Race condition observed in >0/${MAX_BUG_PROOF_ITERATIONS} iterations (expected: >0)`); - expect(raceObservedCount).toBeGreaterThan(0); - }); - - /** - * PublicTxSimulation FIX - Demonstrate the fix WITH cancellation. - * - * This test proves the fix works by showing that with cancellation: - * - We signal C++ to stop before it makes more writes - * - C++ checks the token before each write and stops - * - No corruption occurs even though we revert while C++ is "running" - * - * This test PASSES if we observe NO corruption (proving the fix works). - */ - it('PublicTxSimulator FIX PROOF: no race condition WITH cancellation', async () => { - const raceObservedCount = await runRaceConditionTest(true, FIX_PROOF_ITERATIONS); - logger.info(`Race condition observed in ${raceObservedCount}/${FIX_PROOF_ITERATIONS} iterations (expected: 0)`); - expect(raceObservedCount).toBe(0); - }); - - /** - * Helper to run PublicProcessor timeout test (Level 3). - * Both BUG and FIX tests use IDENTICAL code - the ONLY difference is whether - * cancel() method exists on the simulator. - * - * Uses same-slot storage spamming to keep C++ constantly writing to PUBLIC_DATA_TREE without gaps. - * - * For the BUG proof: cancel is undefined → PublicProcessor can't wait for C++ → corruption - * For the FIX proof: cancel exists → PublicProcessor awaits cancel(100) → C++ stops → no corruption - * - * Returns the number of times state corruption was observed. - * - * @param useCancellation - Whether to provide cancel() method to PublicProcessor - * @param numIterations - Number of iterations to run - */ - async function runPublicProcessorTimeoutTest(useCancellation: boolean, numIterations: number): Promise { - let corruptionCount = 0; - - const globals = GlobalVariables.empty(); - globals.gasFees = new GasFees(2, 3); - - const contractDataSource = new SimpleContractDataSource(); - const merkleTrees = await worldStateService.fork(); - const contractsDB = new PublicContractsDB(contractDataSource); - - // Set up contracts and balances using a tester on the unguarded fork - const tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); - await tester.setFeePayerBalance(admin); - - // Deploy the AvmTest contract; its `n_storage_writes_to_same_slot` loops storage writes until OOG. - const contract = await tester.registerAndDeployContract(/*constructorArgs=*/ [], admin, AvmTestContractArtifact); - const contractAddress = contract.address; - - for (let iteration = 0; iteration < numIterations; iteration++) { - // Create fresh guarded tree and processor for each iteration because - // GuardedMerkleTreeOperations.stop() is called on timeout and can't be reused. - const guardedMerkleTrees = new GuardedMerkleTreeOperations(merkleTrees); - - // Create the real C++ simulator - const realSimulator = new PublicTxSimulator(guardedMerkleTrees, contractsDB, globals); - - // Track the simulation promise so we can await it for cleanup. - // Use an object wrapper to avoid TypeScript control flow analysis issues. - const simState = { promise: null as Promise | null }; - - // Both tests use IDENTICAL code - the ONLY difference is whether cancel() exists. - // PublicProcessor now calls: await this.publicTxSimulator.cancel?.(100) - // - FIX - cancel exists, waits for C++ to stop before reverts - // - BUG - cancel is undefined, reverts proceed while C++ is still running - const simulator = { - simulate: (tx: any) => { - simState.promise = realSimulator.simulate(tx); - return simState.promise; - }, - cancel: useCancellation ? (waitTimeoutMs?: number) => realSimulator.cancel(waitTimeoutMs) : undefined, // No cancel method - PublicProcessor can't wait for C++ to stop - }; - - // Use TestDateProvider to control time - const dateProvider = new TestDateProvider(); - - // Create PublicProcessor with the simulator - const processor = new PublicProcessor( - globals, - guardedMerkleTrees, - contractsDB, - simulator, - dateProvider, - getTelemetryClient(), - createLogger('simulator:public-processor'), - ); - - // Get initial state for trees we need to check - const initialTreeInfo = new Map(); - for (const treeId of merkleTreeIds()) { - const info = await merkleTrees.getTreeInfo(treeId); - initialTreeInfo.set(treeId, { size: info.size, root: info.root }); - } - - // Create transaction that calls the spammer contract - const tx = await tester.createTx( - admin, - [], - [{ address: contractAddress, fnName: STORAGE_WRITE_SPAM_FN, args: [STORAGE_WRITE_SPAM_COUNT] }], - ); - - // Calculate deadline RIGHT BEFORE process() to ensure we get the full timeout. - // Use a 20ms deadline - enough for C++ to start but short enough to timeout mid-simulation. - const deadline = new Date(dateProvider.now() + 20); - - // Process the transaction with the short deadline - // PublicProcessor flow on timeout: - // 1. await this.publicTxSimulator.cancel?.(100) - // - FIX - waits up to 100ms for C++ to stop - // - BUG - cancel is undefined, immediately proceeds - // 2. reverts run, then checkWorldStateUnchanged() - // 3. process() returns - let checkWorldStateUnchangedCaughtIt = false; - try { - await processor.process([tx], { deadline }); - } catch (err: any) { - // checkWorldStateUnchanged() throws if it detects corruption - if (err.message?.includes('state reference changed')) { - checkWorldStateUnchangedCaughtIt = true; - } - // Continue - we'll check state ourselves too - } - - // Give C++ time to make corrupting writes, but don't wait for full completion (OOG). - // In BUG case: C++ continues running, we wait 100ms for it to corrupt state. - // In FIX case: C++ already stopped, this is just a short sleep. - await Promise.race([ - simState.promise?.catch(() => {}), - sleep(100), // Enough time for corruption, but don't wait for full OOG - ]); - - // Check state after everything is cleaned up - let anyTreeCorrupted = false; - for (const treeId of merkleTreeIds()) { - const finalInfo = await merkleTrees.getTreeInfo(treeId); - const initialInfo = initialTreeInfo.get(treeId)!; - const changed = finalInfo.size !== initialInfo.size || !finalInfo.root.equals(initialInfo.root); - if (changed) { - anyTreeCorrupted = true; - break; - } - } - - // Log the comparison: did checkWorldStateUnchanged catch it vs. our check after C++ finished - if (checkWorldStateUnchangedCaughtIt || anyTreeCorrupted) { - const caughtBy = checkWorldStateUnchangedCaughtIt - ? anyTreeCorrupted - ? 'BOTH' - : 'checkWorldStateUnchanged only' - : 'our check only (C++ corrupted AFTER checkWorldStateUnchanged)'; - logger.verbose(`Iteration ${iteration}: corruption detected by ${caughtBy}`); - } - - if (anyTreeCorrupted) { - corruptionCount++; - // Early exit - bug exists, no need to continue - // Always cancel simulation for clean test shutdown (prevent crash during afterEach) - await realSimulator.cancel(1000); - logger.verbose( - `Early exit: checkWorldStateUnchanged caught=${checkWorldStateUnchangedCaughtIt}, our check caught=true`, - ); - return corruptionCount; - } - - // Cancel simulation before next iteration or function end - await realSimulator.cancel(1000); - } - - return corruptionCount; - } - - /** - * PublicProcessor BUG - state corruption without cancellation. - * - * This demonstrates that without cancellation, C++ continues making writes after - * PublicProcessor's timeout handling completes, corrupting state. This is the root - * cause of CI failures like "Fork state reference changed by tx after error". - */ - it('PublicProcessor BUG PROOF: state corruption occurs WITHOUT cancellation', async () => { - const corruptionCount = await runPublicProcessorTimeoutTest(false, MAX_BUG_PROOF_ITERATIONS); - logger.info(`State corruption detected in >0/${MAX_BUG_PROOF_ITERATIONS} iterations (expected: >0)`); - // BUG - Without cancellation, C++ corrupts state after process() completes - expect(corruptionCount).toBeGreaterThan(0); - }); - - /** - * PublicProcessor FIX - no state corruption with cancellation. - * - * With cancellation, C++ stops before making corrupting writes. - * State remains unchanged after process() returns. - */ - it('PublicProcessor FIX PROOF: no state corruption WITH cancellation', async () => { - const corruptionCount = await runPublicProcessorTimeoutTest(true, FIX_PROOF_ITERATIONS); - logger.info(`State corruption detected in ${corruptionCount}/${FIX_PROOF_ITERATIONS} iterations (expected: 0)`); - // FIX - With cancellation, state should remain unchanged - expect(corruptionCount).toBe(0); - }); -}); diff --git a/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts index c0dca3642fd3..1cff9acd376f 100644 --- a/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts +++ b/yarn-project/simulator/src/public/public_processor/apps_tests/token.test.ts @@ -1,20 +1,11 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; -import { TestDateProvider, Timer } from '@aztec/foundation/timer'; +import { Timer } from '@aztec/foundation/timer'; import { TokenContractArtifact } from '@aztec/noir-contracts.js/Token'; -import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; -import { GasFees } from '@aztec/stdlib/gas'; -import { GlobalVariables } from '@aztec/stdlib/tx'; -import { getTelemetryClient } from '@aztec/telemetry-client'; -import { NativeWorldStateService } from '@aztec/world-state'; -import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; -import { PublicContractsDB } from '../../public_db_sources.js'; -import { PublicTxSimulator } from '../../public_tx_simulator/public_tx_simulator.js'; -import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; -import { PublicProcessor } from '../public_processor.js'; +import { PublicProcessorTestEnv } from '../../fixtures/index.js'; describe('Public Processor app tests: TokenContract', () => { const logger = createLogger('public-processor-apps-tests-token'); @@ -24,41 +15,13 @@ describe('Public Processor app tests: TokenContract', () => { const sender = AztecAddress.fromNumber(111); let token: ContractInstanceWithAddress; - let worldStateService: NativeWorldStateService; - let contractsDB: PublicContractsDB; - let tester: PublicTxSimulationTester; - let processor: PublicProcessor; + let env: PublicProcessorTestEnv; + let tester: PublicProcessorTestEnv['tester']; + let processor: PublicProcessorTestEnv['processor']; beforeEach(async () => { - const globals = GlobalVariables.empty(); - // apply some nonzero default gas fees - globals.gasFees = new GasFees(2, 3); - - const contractDataSource = new SimpleContractDataSource(); - worldStateService = await NativeWorldStateService.tmp(); - const merkleTrees = await worldStateService.fork(); - const guardedMerkleTrees = new GuardedMerkleTreeOperations(merkleTrees); - contractsDB = new PublicContractsDB(contractDataSource); - const config = PublicSimulatorConfig.from({ - skipFeeEnforcement: false, - collectDebugLogs: true, - collectHints: false, - collectStatistics: false, - collectCallMetadata: true, - }); - const simulator = new PublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config); - - processor = new PublicProcessor( - globals, - guardedMerkleTrees, - contractsDB, - simulator, - new TestDateProvider(), - getTelemetryClient(), - createLogger('simulator:public-processor'), - ); - - tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); + env = await PublicProcessorTestEnv.create(); + ({ tester, processor } = env); // make sure tx senders have fee balance await tester.setFeePayerBalance(admin); @@ -66,7 +29,7 @@ describe('Public Processor app tests: TokenContract', () => { }); afterEach(async () => { - await worldStateService.close(); + await env[Symbol.asyncDispose](); }); it('token constructor, mint, many transfers', async () => { diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index 58123cf47340..a34fc2fbe7cf 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -50,6 +50,8 @@ import { ForkCheckpoint } from '@aztec/world-state/native'; import { AssertionError } from 'assert'; +import { AvmExecutor } from '../avm_executor.js'; +import type { AvmSimulator } from '../avm_simulator.js'; import { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; import { type PublicTxSimulatorConfig, @@ -66,6 +68,7 @@ export class PublicProcessorFactory { private log: Logger; constructor( private contractDataSource: ContractDataSource, + private avmExecutor: AvmExecutor, private dateProvider: DateProvider = new DateProvider(), protected telemetryClient: TelemetryClient = getTelemetryClient(), bindings?: LoggerBindings, @@ -74,7 +77,10 @@ export class PublicProcessorFactory { } /** - * Creates a new instance of a PublicProcessor. + * Creates a new instance of a PublicProcessor. The simulator it drives is bound to the fork's contracts DB + * via {@link AvmExecutor.forFork}, so AVM requests carrying this forkId route to the right contracts DB; + * that binding is scoped to each simulation, so there is nothing to unregister when the fork is closed. + * * @param globalVariables - The global variables for the block being processed. * @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted. * @returns A new instance of a PublicProcessor. @@ -85,8 +91,11 @@ export class PublicProcessorFactory { config: PublicSimulatorConfig, contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()), ): PublicProcessor { + const forkId = merkleTree.getRevision().forkId; + const forkedSimulator = this.avmExecutor.forFork(forkId, contractsDB, globalVariables.timestamp); + const guardedFork = new GuardedMerkleTreeOperations(merkleTree); - const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config); + const publicTxSimulator = this.createPublicTxSimulator(forkedSimulator, forkId, globalVariables, config); return new PublicProcessor( globalVariables, @@ -100,18 +109,18 @@ export class PublicProcessorFactory { } protected createPublicTxSimulator( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, + forkId: number, globalVariables: GlobalVariables, config?: Partial, ): PublicTxSimulatorInterface { return new TelemetryPublicTxSimulator( - merkleTree, - contractsDB, + avmSimulator, globalVariables, this.telemetryClient, config, this.log.getBindings(), + forkId, ); } } @@ -130,10 +139,6 @@ class PublicProcessorAbortError extends Error { } } -function isPublicProcessorInterruptError(err: any) { - return err?.name === 'PublicProcessorTimeoutError' || err?.name === 'PublicProcessorAbortError'; -} - /** * Converts Txs lifted from the P2P module into ProcessedTx objects by executing * any public function calls in them. Txs with private calls only are unaffected. @@ -326,11 +331,15 @@ export class PublicProcessor implements Traceable { // Commit the tx-level contracts checkpoint on success this.contractsDB.commitCheckpoint(); } catch (err: any) { - if (isPublicProcessorInterruptError(err)) { - const interruptReason = err.name === 'PublicProcessorTimeoutError' ? 'timeout' : 'abort signal'; - this.log.warn(`Stopping tx processing due to ${interruptReason}.`); - // The tx may still be executing on a worker thread (C++ via NAPI). - // Signal cancellation AND WAIT for the simulation to actually stop before touching fork checkpoints. + if (err?.name === 'PublicProcessorTimeoutError' || err?.name === 'PublicProcessorAbortError') { + this.log.warn( + `Stopping tx processing due to ${err.name === 'PublicProcessorTimeoutError' ? 'timeout' : 'abort'}.`, + ); + // We hit the transaction execution deadline or an external abort signal. + // There may still be a simulation in progress. + // Signal cancellation AND WAIT for the simulation to actually stop. Cancellation is + // cooperative: the simulator may be mid slow-operation and won't observe the flag until it + // completes, and without waiting we'd revert checkpoints while it's still writing to state. await this.publicTxSimulator.cancel?.(); // Now stop the guarded fork to prevent any further TS-side access to the world state. diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts index 2831072b74d9..d720c082d2cf 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/amm.test.ts @@ -18,6 +18,7 @@ describe('Public TX simulator apps tests: AMM Contract', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_gadgets.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_gadgets.test.ts index 57f80bc40910..93f3831436d8 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_gadgets.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_gadgets.test.ts @@ -8,7 +8,7 @@ import { NativeWorldStateService } from '@aztec/world-state'; import { PublicTxSimulationTester, defaultGlobals } from '../../fixtures/public_tx_simulation_tester.js'; describe('Public TX simulator apps tests: gadgets', () => { - describe('Public TX simulator apps tests: gadgets (via Cpp Simulator)', () => { + describe('Public TX simulator apps tests: gadgets', () => { const deployer = AztecAddress.fromNumber(42); let worldStateService: NativeWorldStateService; @@ -26,6 +26,7 @@ describe('Public TX simulator apps tests: gadgets', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts index 0aa08974de8a..d5f2078f930e 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_test.test.ts @@ -26,6 +26,7 @@ describe('Public TX simulator apps tests: AvmTestContract', () => { }); afterEach(async () => { + await simTester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts index 00a2b2642dc5..ccfe80eb25b1 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/bench.test.ts @@ -18,15 +18,9 @@ import { fileURLToPath } from 'url'; import { ammTest } from '../../fixtures/amm_test.js'; import { bulkTest, megaBulkTest } from '../../fixtures/bulk_test.js'; -import { - type MeasuredSimulatorFactory, - PublicTxSimulationTester, - defaultGlobals, -} from '../../fixtures/public_tx_simulation_tester.js'; -import { SimpleContractDataSource } from '../../fixtures/simple_contract_data_source.js'; +import { PublicTxSimulationTester, defaultGlobals } from '../../fixtures/public_tx_simulation_tester.js'; import { tokenTest } from '../../fixtures/token_test.js'; import { TestExecutorMetrics } from '../../test_executor_metrics.js'; -import { MeasuredPublicTxSimulator } from '../public_tx_simulator.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -61,22 +55,11 @@ describe('Public TX simulator apps tests: benchmarks', () => { beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - const contractDataSource = new SimpleContractDataSource(); - const merkleTree = await worldStateService.fork(); - // For benchmarking, use pure simulators (no CppVsTs comparison overhead) - const simulatorFactory: MeasuredSimulatorFactory = (mt, cdb, g, m, c) => - new MeasuredPublicTxSimulator(mt, cdb, g, m, c); - tester = new PublicTxSimulationTester( - merkleTree, - contractDataSource, - defaultGlobals(), - metrics, - simulatorFactory, - config, - ); + tester = await PublicTxSimulationTester.create(worldStateService, defaultGlobals(), metrics, config); }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -201,18 +184,7 @@ describe('Public TX simulator apps tests: benchmarks', () => { beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - const contractDataSource = new SimpleContractDataSource(); - const merkleTree = await worldStateService.fork(); - // For benchmarking, use pure simulators (no CppVsTs comparison overhead) - const simulatorFactory: MeasuredSimulatorFactory = (mt, cdb, g, m, c) => - new MeasuredPublicTxSimulator(mt, cdb, g, m, c); - tester = new PublicTxSimulationTester( - merkleTree, - contractDataSource, - defaultGlobals(), - metrics, - simulatorFactory, - ); + tester = await PublicTxSimulationTester.create(worldStateService, defaultGlobals(), metrics); avmGadgetsTestContract = await tester.registerAndDeployContract( /*constructorArgs=*/ [], deployer, @@ -222,6 +194,7 @@ describe('Public TX simulator apps tests: benchmarks', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/cpp_exception_handling.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/cpp_exception_handling.test.ts index 173f20a162d2..8b8ad5a4679f 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/cpp_exception_handling.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/cpp_exception_handling.test.ts @@ -5,7 +5,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -describe('C++ Exception Handling during Public Tx Simulation', () => { +describe('AVM Error Propagation during Public Tx Simulation', () => { const sender = AztecAddress.fromNumber(42); let avmTestContractInstance: ContractInstanceWithAddress; let tester: PublicTxSimulationTester; @@ -22,14 +22,15 @@ describe('C++ Exception Handling during Public Tx Simulation', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); /** - * Call assertion_failure function during setup, and expect C++ simulator to throw. + * Call assertion_failure function during setup. The AVM should detect the assertion + * failure, revert the setup phase, and propagate the error back through IPC. */ - it('assertion failure during setup - C++ simulator should throw and TS should handle gracefully', async () => { - // expect reject with SimulationError + it('assertion failure during setup propagates as simulation error', async () => { await expect( tester.simulateTx( sender, @@ -42,6 +43,6 @@ describe('C++ Exception Handling during Public Tx Simulation', () => { ], /*appCalls=*/ [], ), - ).rejects.toThrow(/C\+\+ simulation failed.*SETUP/); + ).rejects.toThrow(/simulation failed|AVM error|assertion/i); }, 30_000); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts index ace50bece3d9..a4d300b122ae 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/token.test.ts @@ -17,6 +17,7 @@ describe('Public TX simulator apps tests: TokenContract', () => { }); afterAll(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts b/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts deleted file mode 100644 index 21b195484d66..000000000000 --- a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Fr } from '@aztec/foundation/curves/bn254'; -import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; -import type { ContractProvider } from '@aztec/native'; -import { FunctionSelector } from '@aztec/stdlib/abi'; -import { deserializeFromMessagePack, serializeWithMessagePack } from '@aztec/stdlib/avm'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { ContractDeploymentData } from '@aztec/stdlib/contract'; -import type { GlobalVariables } from '@aztec/stdlib/tx'; - -import type { PublicContractsDB } from '../public_db_sources.js'; - -export class ContractProviderForCpp implements ContractProvider { - private log: Logger; - - constructor( - private contractsDB: PublicContractsDB, - private globalVariables: GlobalVariables, - bindings?: LoggerBindings, - ) { - this.log = createLogger('simulator:contract_provider_for_cpp', bindings); - } - - public getContractInstance = async (address: string): Promise => { - this.log.trace(`Contract provider callback: getContractInstance(${address})`); - - const aztecAddr = AztecAddress.fromString(address); - - const instance = await this.contractsDB.getContractInstance(aztecAddr, this.globalVariables.timestamp); - - if (!instance) { - this.log.debug(`Contract instance not found: ${address}`); - return undefined; - } - - return serializeWithMessagePack(instance); - }; - - public getContractClass = async (classId: string): Promise => { - this.log.trace(`Contract provider callback: getContractClass(${classId})`); - - // Parse classId string to Fr - const classIdFr = Fr.fromString(classId); - - // Fetch contract class from the contracts DB - const contractClass = await this.contractsDB.getContractClass(classIdFr); - - if (!contractClass) { - this.log.debug(`Contract class not found: ${classId}`); - return undefined; - } - - return serializeWithMessagePack(contractClass); - }; - - // eslint-disable-next-line require-await - public addContracts = async (contractDeploymentDataBuffer: Buffer): Promise => { - this.log.trace(`Contract provider callback: addContracts`); - - const rawData: any = deserializeFromMessagePack(contractDeploymentDataBuffer); - - // Construct ContractDeploymentData from plain object. - const contractDeploymentData = ContractDeploymentData.fromPlainObject(rawData); - - // Add contracts to the contracts DB - this.log.trace(`Calling contractsDB.addContractsFromLogs`); - this.contractsDB.addContractsFromLogs(contractDeploymentData); - }; - - public getBytecodeCommitment = async (classId: string): Promise => { - this.log.trace(`Contract provider callback: getBytecodeCommitment(${classId})`); - - // Parse classId string to Fr - const classIdFr = Fr.fromString(classId); - - // Fetch bytecode commitment from the contracts DB - const commitment = await this.contractsDB.getBytecodeCommitment(classIdFr); - - if (!commitment) { - this.log.debug(`Bytecode commitment not found: ${classId}`); - return undefined; - } - - // Serialize the Fr to buffer - return serializeWithMessagePack(commitment); - }; - - public getDebugFunctionName = async (address: string, selector: string): Promise => { - this.log.trace(`Contract provider callback: getDebugFunctionName(${address}, ${selector})`); - - // Parse address and selector strings - const aztecAddr = AztecAddress.fromString(address); - const selectorFr = Fr.fromString(selector); - const functionSelector = FunctionSelector.fromFieldOrUndefined(selectorFr); - - if (!functionSelector) { - this.log.trace(`calldata[0] is not a function selector: ${selector}`); - return undefined; - } - - // Fetch debug function name from the contracts DB - const name = await this.contractsDB.getDebugFunctionName(aztecAddr, functionSelector); - - if (!name) { - this.log.trace(`Debug function name not found for ${address}:${selector}`); - return undefined; - } - - return name; - }; - - public createCheckpoint = (): Promise => { - this.log.trace(`Contract provider callback: createCheckpoint`); - return Promise.resolve(this.contractsDB.createCheckpoint()); - }; - - public commitCheckpoint = (): Promise => { - this.log.trace(`Contract provider callback: commitCheckpoint`); - return Promise.resolve(this.contractsDB.commitCheckpoint()); - }; - - public revertCheckpoint = (): Promise => { - this.log.trace(`Contract provider callback: revertCheckpoint`); - return Promise.resolve(this.contractsDB.revertCheckpoint()); - }; -} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts index a7cd138d6c49..642db8e64f96 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts @@ -4,35 +4,34 @@ import { AvmCircuitPublicInputs, AvmExecutionHints, type PublicSimulatorConfig, - PublicTxResult, + type PublicTxResult, serializeWithMessagePack, } from '@aztec/stdlib/avm'; -import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; -import type { GlobalVariables, Tx, TxHash } from '@aztec/stdlib/tx'; +import type { GlobalVariables, Tx } from '@aztec/stdlib/tx'; import { strict as assert } from 'assert'; import { mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; -import type { PublicContractsDB } from '../public_db_sources.js'; +import type { AvmSimulator } from '../avm_simulator.js'; import { PublicTxSimulator } from './public_tx_simulator.js'; /** - * A C++ public tx simulator that dumps AVM circuit inputs to disk after simulation. - * Used during nightly CI runs to collect circuit inputs for benchmarking. + * A {@link PublicTxSimulator} that dumps AVM circuit inputs to disk after simulation. + * Used during nightly CI runs to collect circuit inputs for AVM proving benchmarks. */ export class DumpingPublicTxSimulator extends PublicTxSimulator { private readonly outputDir: string; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, globalVariables: GlobalVariables, config: Partial, outputDir: string, bindings?: LoggerBindings, + forkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); + super(avmSimulator, globalVariables, config, bindings, forkId); assert(config.collectHints === true, 'collectHints must be enabled to dump AVM circuit inputs'); assert(config.collectPublicInputs === true, 'collectPublicInputs must be enabled to dump AVM circuit inputs'); this.outputDir = outputDir; @@ -40,44 +39,27 @@ export class DumpingPublicTxSimulator extends PublicTxSimulator { public override async simulate(tx: Tx): Promise { const result = await super.simulate(tx); - - // Dump the circuit inputs after successful simulation - const txHash = this.computeTxHash(tx); - this.dumpAvmCircuitInputs(result, txHash); - + this.dumpAvmCircuitInputs(result, tx.getTxHash().toString()); return result; } - /** - * Dumps AVM circuit inputs to disk. - * - * @param result - The simulation result containing hints and public inputs - * @param txHash - The transaction hash to use in the filename - */ - private dumpAvmCircuitInputs(result: PublicTxResult, txHash: TxHash): void { + private dumpAvmCircuitInputs(result: PublicTxResult, txHash: string): void { try { - // Ensure the output directory exists mkdirSync(this.outputDir, { recursive: true }); - // Generate filename using transaction hash - const filename = `avm-circuit-inputs-tx-${txHash.toString()}.bin`; + const filename = `avm-circuit-inputs-tx-${txHash}.bin`; const filepath = join(this.outputDir, filename); - // Create circuit inputs from the result const hints = result.hints ?? AvmExecutionHints.empty(); const publicInputs = result.publicInputs ?? AvmCircuitPublicInputs.empty(); const avmCircuitInputs = new AvmCircuitInputs(hints, publicInputs); - // Serialize the circuit inputs using MessagePack const serialized = serializeWithMessagePack(avmCircuitInputs); - - // Write to disk writeFileSync(filepath, serialized); this.log.debug(`Dumped AVM circuit inputs to ${filepath}`); } catch (error) { - // Non-blocking error handling - log but don't interrupt processing - this.log.warn(`Failed to dump AVM circuit inputs for tx ${txHash.toString()}: ${error}`); + this.log.warn(`Failed to dump AVM circuit inputs for tx ${txHash}: ${error}`); } } } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts index 9f0827a8d7c2..7048525a61a4 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts @@ -1,24 +1,23 @@ import type { LoggerBindings } from '@aztec/foundation/log'; import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; -import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import type { GlobalVariables } from '@aztec/stdlib/tx'; import type { TelemetryClient } from '@aztec/telemetry-client'; -import type { PublicContractsDB } from '../public_db_sources.js'; +import type { AvmSimulator } from '../avm_simulator.js'; import { DumpingPublicTxSimulator } from './dumping_public_tx_simulator.js'; import { TelemetryPublicTxSimulator } from './public_tx_simulator.js'; /** - * Creates a public tx simulator for block building. - * Uses DumpingPublicTxSimulator if DUMP_AVM_INPUTS_TO_DIR env var is set (for CI/testing avm circuit), + * Creates an IPC-based public tx simulator for block building. + * Uses DumpingPublicTxSimulator if DUMP_AVM_INPUTS_TO_DIR env var is set (for CI/testing the AVM circuit), * otherwise uses TelemetryPublicTxSimulator (for production). */ export function createPublicTxSimulatorForBlockBuilding( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, globalVariables: GlobalVariables, telemetryClient: TelemetryClient, bindings?: LoggerBindings, + forkId?: number, collectDebugLogs = false, ) { const config = PublicSimulatorConfig.from({ @@ -38,7 +37,7 @@ export function createPublicTxSimulatorForBlockBuilding( collectHints: true, collectPublicInputs: true, }; - return new DumpingPublicTxSimulator(merkleTree, contractsDB, globalVariables, dumpingConfig, dumpDir, bindings); + return new DumpingPublicTxSimulator(avmSimulator, globalVariables, dumpingConfig, dumpDir, bindings, forkId); } - return new TelemetryPublicTxSimulator(merkleTree, contractsDB, globalVariables, telemetryClient, config, bindings); + return new TelemetryPublicTxSimulator(avmSimulator, globalVariables, telemetryClient, config, bindings, forkId); } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/index.ts b/yarn-project/simulator/src/public/public_tx_simulator/index.ts index c391da2a3d7b..cfa3d840919a 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/index.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/index.ts @@ -1,5 +1,8 @@ -export { PublicTxSimulator, TelemetryPublicTxSimulator } from './public_tx_simulator.js'; +export { PublicTxSimulator, MeasuredPublicTxSimulator, TelemetryPublicTxSimulator } from './public_tx_simulator.js'; export { DumpingPublicTxSimulator } from './dumping_public_tx_simulator.js'; export { createPublicTxSimulatorForBlockBuilding } from './factories.js'; -export type { PublicTxSimulatorInterface } from './public_tx_simulator_interface.js'; +export type { + PublicTxSimulatorInterface, + MeasuredPublicTxSimulatorInterface, +} from './public_tx_simulator_interface.js'; export type { PublicTxResult, PublicSimulatorConfig as PublicTxSimulatorConfig } from '@aztec/stdlib/avm'; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 2af1341f9c15..2ac21065a3f6 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -1,7 +1,5 @@ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; -import { type CancellationToken, avmSimulate, cancelSimulation, createCancellationToken } from '@aztec/native'; -import { ProtocolContractsList } from '@aztec/protocol-contracts'; import { AvmFastSimulationInputs, AvmTxHint, @@ -10,14 +8,13 @@ import { deserializeFromMessagePack, } from '@aztec/stdlib/avm'; import { SimulationError } from '@aztec/stdlib/errors'; -import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import type { GlobalVariables, Tx } from '@aztec/stdlib/tx'; +import { WorldStateRevision } from '@aztec/stdlib/world-state'; import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client'; +import type { AvmSimulator } from '../avm_simulator.js'; import { ExecutorMetrics } from '../executor_metrics.js'; import type { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; -import type { PublicContractsDB } from '../public_db_sources.js'; -import { ContractProviderForCpp } from './contract_provider_for_cpp.js'; import { PublicTxSimulatorBase } from './public_tx_simulator_base.js'; import type { MeasuredPublicTxSimulatorInterface, @@ -25,146 +22,129 @@ import type { } from './public_tx_simulator_interface.js'; /** - * Simulates a transaction's public portion using the C++ AVM simulator. - * The C++ simulator accesses the world state directly/natively within C++. - * For contract DB accesses, it makes callbacks through NAPI back to the TS PublicContractsDB cache. + * Simulates a transaction's public portion by running its public calls through an {@link AvmSimulator}. + * `forkId` selects the fork whose contract data the simulation reads. */ export class PublicTxSimulator extends PublicTxSimulatorBase implements PublicTxSimulatorInterface { protected override log: Logger; - /** Current cancellation token for in-flight simulation. */ - private cancellationToken?: CancellationToken; + /** Aborts the in-flight simulation. */ + private abortController?: AbortController; /** Current simulation promise, used to wait for completion after cancellation. */ - private simulationPromise?: Promise; + private simulationPromise?: Promise; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, globalVariables: GlobalVariables, config?: Partial, bindings?: LoggerBindings, + forkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, undefined, bindings); + super(avmSimulator, globalVariables, config, undefined, bindings, forkId); this.log = createLogger(`simulator:public_tx_simulator`, bindings); } /** - * Simulate a transaction's public portion using the C++ avvm simulator. + * Simulate a transaction's public portion. * * @param tx - The transaction to simulate. * @returns The result of the transaction's public execution. */ public async simulate(tx: Tx): Promise { - const txHash = this.computeTxHash(tx); - this.log.debug(`C++ simulation of ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}`, { - txHash, - }); - - const wsRevision = this.merkleTree.getRevision(); - const wsdbIpcPath = this.merkleTree.getIpcPath(); + this.abortController = new AbortController(); + this.simulationPromise = this.doSimulate(tx, this.abortController.signal); + try { + return await this.simulationPromise; + } finally { + this.abortController = undefined; + this.simulationPromise = undefined; + } + } - this.log.trace(`Running C++ simulation with world state revision ${JSON.stringify(wsRevision)}`); + protected async doSimulate(tx: Tx, signal: AbortSignal): Promise { + const txHash = this.computeTxHash(tx); + this.log.debug( + `Simulating ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}, forkId=${this.forkId ?? 0}`, + { txHash }, + ); - // Create the fast simulation inputs + // Create the fast simulation inputs. const txHint = AvmTxHint.fromTx(tx, this.globalVariables.gasFees); - const protocolContracts = ProtocolContractsList; const fastSimInputs = new AvmFastSimulationInputs( - wsRevision, + // blockNumber: WorldStateRevision.LATEST sentinel so the WSDB walks the fork's current + // uncommitted state. Using 0 here makes WSDB treat this as a historical query against + // the empty genesis tree and miss any in-fork uncommitted leaves (deployed contracts, + // etc.) from earlier txs in the same block. + { forkId: this.forkId ?? 0, blockNumber: WorldStateRevision.LATEST, includeUncommitted: true }, this.config, txHint, this.globalVariables, - protocolContracts, + this.protocolContracts, ); - // Create contract provider for callbacks to TypeScript PublicContractsDB from C++ - const contractProvider = new ContractProviderForCpp(this.contractsDB, this.globalVariables, this.bindings); - - // Serialize to msgpack and call the C++ simulator this.log.trace(`Serializing fast simulation inputs to msgpack...`); const inputBuffer = fastSimInputs.serializeWithMessagePack(); - // Create cancellation token for this simulation - this.cancellationToken = createCancellationToken(); - - // Store the promise so cancel() can wait for it - this.log.debug(`Calling C++ simulator for tx ${txHash}`); - this.simulationPromise = avmSimulate( - inputBuffer, - contractProvider, - wsdbIpcPath, - this.log.level, - undefined, - this.cancellationToken, - ); - - let resultBuffer: Buffer; + this.log.debug(`Running AVM simulation for tx ${txHash}`); + let resultBuffer: Uint8Array; try { - resultBuffer = await this.simulationPromise; + resultBuffer = await this.avmSimulator.simulate(inputBuffer, signal); } catch (error: any) { - // Check if this was a cancellation - if (error.message?.includes('Simulation cancelled')) { - throw new SimulationError(`C++ simulation cancelled`, []); + if (error.message?.includes('cancelled')) { + throw new SimulationError(`AVM simulation cancelled`, []); } - throw new SimulationError(`C++ simulation failed: ${error.message}`, []); - } finally { - this.cancellationToken = undefined; - this.simulationPromise = undefined; + throw new SimulationError(`AVM simulation failed: ${error.message}`, []); } - // If we've reached this point, C++ succeeded during simulation, + this.log.trace(`Deserializing simulation result from buffer (size: ${resultBuffer.length})...`); + const resultJSON: object = deserializeFromMessagePack(Buffer.from(resultBuffer)); + const result = PublicTxResult.fromPlainObject(resultJSON); - // Deserialize the msgpack result - this.log.trace(`Deserializing C++ from buffer (size: ${resultBuffer.length})...`); - const cppResultJSON: object = deserializeFromMessagePack(resultBuffer); - this.log.trace(`Deserializing C++ result to PublicTxResult...`); - const cppResult = PublicTxResult.fromPlainObject(cppResultJSON); - - this.log.trace(`C++ simulation completed for tx ${txHash}`, { + this.log.trace(`AVM simulation completed for tx ${txHash}`, { txHash, - reverted: !cppResult.revertCode.isOK(), - cppGasUsed: cppResult.gasUsed.totalGas.l2Gas, + reverted: !result.revertCode.isOK(), + l2GasUsed: result.gasUsed.totalGas.l2Gas, }); - return cppResult; + return result; } /** * Cancel the current simulation if one is in progress. - * This signals the C++ simulator to stop at the next opcode or before the next WorldState write. - * Safe to call even if no simulation is in progress. + * This signals the underlying simulator to stop at the next safe point (between opcodes, before a + * world-state write). Safe to call even if no simulation is in progress. * * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop. - * This is important because C++ might be in the middle of a slow operation - * (e.g., pad_trees) and won't check the cancellation flag until it completes. + * Cancellation is cooperative: the simulator may be mid slow-operation and + * won't observe the cancellation until it completes. * Default timeout of 100ms after cancellation. */ public async cancel(waitTimeoutMs: number = 100): Promise { - if (this.cancellationToken) { - this.log.debug('Cancelling C++ simulation'); - cancelSimulation(this.cancellationToken); + if (this.abortController) { + this.log.debug('Cancelling AVM simulation'); + this.abortController.abort(); } - // Wait for the simulation to actually complete if not already done if (this.simulationPromise) { - this.log.debug(`Waiting up to ${waitTimeoutMs}ms for C++ simulation to stop`); + this.log.debug(`Waiting up to ${waitTimeoutMs}ms for AVM simulation to stop`); await Promise.race([ this.simulationPromise.catch(() => {}), // Ignore rejection, just wait for completion sleep(waitTimeoutMs), ]); - this.log.debug('C++ simulation stopped or wait timed out'); + this.log.debug('AVM simulation stopped or wait timed out'); } } } export class MeasuredPublicTxSimulator extends PublicTxSimulator implements MeasuredPublicTxSimulatorInterface { constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, globalVariables: GlobalVariables, protected readonly metrics: ExecutorMetricsInterface, config?: Partial, bindings?: LoggerBindings, + forkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); + super(avmSimulator, globalVariables, config, bindings, forkId); } public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { @@ -180,22 +160,22 @@ export class MeasuredPublicTxSimulator extends PublicTxSimulator implements Meas } /** - * A C++ public tx simulator that tracks runtime/production metrics with telemetry. + * A public tx simulator that tracks runtime/production metrics with telemetry. */ export class TelemetryPublicTxSimulator extends MeasuredPublicTxSimulator { /* tracer needed by trackSpans */ public readonly tracer: Tracer; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmSimulator: AvmSimulator, globalVariables: GlobalVariables, telemetryClient: TelemetryClient = getTelemetryClient(), config?: Partial, bindings?: LoggerBindings, + forkId?: number, ) { const metrics = new ExecutorMetrics(telemetryClient, 'PublicTxSimulator'); - super(merkleTree, contractsDB, globalVariables, metrics, config, bindings); + super(avmSimulator, globalVariables, metrics, config, bindings, forkId); this.tracer = metrics.tracer; } } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts index 3da1d9481402..96cd55e51c7f 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts @@ -1,15 +1,15 @@ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; import { ProtocolContractsList } from '@aztec/protocol-contracts'; import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; -import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import type { GlobalVariables, ProtocolContracts, Tx } from '@aztec/stdlib/tx'; -import type { PublicContractsDB } from '../public_db_sources.js'; +import type { AvmSimulator } from '../avm_simulator.js'; /** - * Shared base for public tx simulators. Holds the common configuration, world-state and contract DB - * handles, logger, and the tx-hash helper. Concrete simulators (e.g. the C++ simulator) extend this - * and implement `simulate`. + * Shared base for public tx simulators: holds the common configuration, the {@link AvmSimulator} used + * to run the transaction's public calls, the fork id that routes the simulation's contract-data + * lookups, the logger, and the tx-hash helper. Concrete simulators extend this and implement + * `simulate`. */ export abstract class PublicTxSimulatorBase { protected log: Logger; @@ -17,12 +17,12 @@ export abstract class PublicTxSimulatorBase { protected readonly bindings?: LoggerBindings; constructor( - protected merkleTree: MerkleTreeWriteOperations, - protected contractsDB: PublicContractsDB, + protected avmSimulator: AvmSimulator, protected globalVariables: GlobalVariables, config?: Partial, protected protocolContracts: ProtocolContracts = ProtocolContractsList, bindings?: LoggerBindings, + protected forkId?: number, ) { this.config = PublicSimulatorConfig.from(config ?? {}); this.bindings = bindings; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_interface.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_interface.ts index 9e4785e826dd..781375bee6bc 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_interface.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_interface.ts @@ -5,14 +5,14 @@ export interface PublicTxSimulatorInterface { simulate(tx: Tx): Promise; /** * Cancel the current simulation if one is in progress. - * This signals the underlying simulator (e.g., C++) to stop at the next safe point. + * This signals the underlying simulator to stop at the next safe point. * Safe to call even if no simulation is in progress. * Optional - not all implementations support cancellation. * * @param waitTimeoutMs - If provided, wait up to this many ms for the simulation to actually stop. - * This is important because signaling cancellation doesn't immediately stop C++ - - * it only sets a flag that C++ checks at certain points. If C++ is in the middle - * of a slow operation (e.g., pad_trees), it won't stop until that completes. + * Cancellation is cooperative: signaling it only sets a flag the simulator + * checks at certain points, so if it's mid slow-operation it won't stop until + * that completes. * @returns Promise that resolves when cancellation is signaled (and optionally when simulation stops) */ cancel?(waitTimeoutMs?: number): Promise; @@ -22,7 +22,7 @@ export interface MeasuredPublicTxSimulatorInterface { simulate(tx: Tx, txLabel: string): Promise; /** * Cancel the current simulation if one is in progress. - * This signals the underlying simulator (e.g., C++) to stop at the next safe point. + * This signals the underlying simulator to stop at the next safe point. * Safe to call even if no simulation is in progress. * Optional - not all implementations support cancellation. * diff --git a/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts b/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts index 564ffe90201b..ce7e05e9e1c1 100644 --- a/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts +++ b/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts @@ -141,9 +141,8 @@ export interface MerkleTreeReadOperations { getRevision(): WorldStateRevision; /** - * Returns the IPC path of the underlying aztec-wsdb process. The C++ AVM (NAPI) uses this to - * connect to the same world state instance that the TS layer is using; the merkle tree fork and - * the AVM must point at the same WSDB process for the simulation to see consistent state. + * Returns the IPC path of the underlying aztec-wsdb process. External AVM simulators use this to + * connect to the same world state instance that the TS layer is using. */ getIpcPath(): string; diff --git a/yarn-project/txe/esbuild/stubs/world_state_stub.ts b/yarn-project/txe/esbuild/stubs/world_state_stub.ts index d5bdab6381f8..6bebbbef050b 100644 --- a/yarn-project/txe/esbuild/stubs/world_state_stub.ts +++ b/yarn-project/txe/esbuild/stubs/world_state_stub.ts @@ -7,3 +7,19 @@ export function createWorldState(..._args: unknown[]): never { export function createWorldStateSynchronizer(..._args: unknown[]): never { throwStub('createWorldStateSynchronizer'); } + +export class IpcWorldState { + constructor(..._args: unknown[]) { + throwStub('IpcWorldState'); + } +} + +export class WorldStateInstrumentation { + constructor(..._args: unknown[]) { + throwStub('WorldStateInstrumentation'); + } +} + +export function getWsdbOptions(..._args: unknown[]): never { + throwStub('getWsdbOptions'); +} diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index ef940357232e..694450d1bdbe 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -524,7 +524,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl globals.version = this.version; globals.gasFees = GasFees.empty(); - const forkedWorldTrees = await this.stateMachine.synchronizer.nativeWorldStateService.fork(); + await using forkedWorldTrees = await this.stateMachine.synchronizer.nativeWorldStateService.fork(); const bindings = this.logger.getBindings(); const contractsDB = new PublicContractsDB( @@ -539,11 +539,14 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); + // Bind the AVM simulator to this fork's contracts DB for the duration of each simulation. + const forkId = forkedWorldTrees.getRevision().forkId; + const forkedSimulator = this.stateMachine.synchronizer.avmExecutor.forFork(forkId, contractsDB, globals.timestamp); const processor = new PublicProcessor( globals, guardedMerkleTrees, contractsDB, - new PublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config, bindings), + new PublicTxSimulator(forkedSimulator, globals, config, bindings, forkId), new TestDateProvider(), undefined, createLogger('simulator:public-processor', bindings), @@ -588,8 +591,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl if (isStaticCall) { await checkpoint!.revert(); - - await forkedWorldTrees.close(); return { returnValues: executionResult.returnValues ?? [], offchainEffects }; } @@ -610,8 +611,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl await this.stateMachine.handleL2Block(l2Block); - await forkedWorldTrees.close(); - return { returnValues: executionResult.returnValues ?? [], offchainEffects }; } @@ -642,7 +641,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl globals.version = this.version; globals.gasFees = GasFees.empty(); - const forkedWorldTrees = await this.stateMachine.synchronizer.nativeWorldStateService.fork(); + await using forkedWorldTrees = await this.stateMachine.synchronizer.nativeWorldStateService.fork(); const bindings2 = this.logger.getBindings(); const contractsDB = new PublicContractsDB( @@ -657,7 +656,14 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); - const simulator = new PublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config, bindings2); + // Bind the AVM simulator to this fork's contracts DB for the duration of each simulation. + const forkId2 = forkedWorldTrees.getRevision().forkId; + const forkedSimulator2 = this.stateMachine.synchronizer.avmExecutor.forFork( + forkId2, + contractsDB, + globals.timestamp, + ); + const simulator = new PublicTxSimulator(forkedSimulator2, globals, config, bindings2, forkId2); const processor = new PublicProcessor( globals, guardedMerkleTrees, @@ -737,9 +743,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl if (isStaticCall) { await checkpoint!.revert(); - - await forkedWorldTrees.close(); - return returnValues ?? []; } @@ -760,8 +763,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl await this.stateMachine.handleL2Block(l2Block); - await forkedWorldTrees.close(); - return returnValues ?? []; } diff --git a/yarn-project/txe/src/state_machine/synchronizer.ts b/yarn-project/txe/src/state_machine/synchronizer.ts index f6598d2ce8fe..56a81b23179d 100644 --- a/yarn-project/txe/src/state_machine/synchronizer.ts +++ b/yarn-project/txe/src/state_machine/synchronizer.ts @@ -1,6 +1,7 @@ import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { AvmExecutor } from '@aztec/simulator/server'; import type { BlockHash, L2Block } from '@aztec/stdlib/block'; import type { MerkleTreeReadOperations, @@ -15,12 +16,21 @@ export class TXESynchronizer implements WorldStateSynchronizer { // This works when set to 1 as well. private blockNumber = BlockNumber.ZERO; + /** AVM execution backend (simulator pool + CDB server) shared across all public simulations. */ + public avmExecutor!: AvmExecutor; + constructor(public nativeWorldStateService: NativeWorldStateService) {} static async create() { const nativeWorldStateService = await NativeWorldStateService.ephemeral(); - return new this(nativeWorldStateService); + const synchronizer = new this(nativeWorldStateService); + + synchronizer.avmExecutor = await AvmExecutor.spawn({ + wsdbIpcPath: nativeWorldStateService.getIpcPath(), + }); + + return synchronizer; } public async handleL2Block(block: L2Block) { @@ -70,8 +80,8 @@ export class TXESynchronizer implements WorldStateSynchronizer { throw new Error('TXE Synchronizer does not implement "status"'); } - public stop(): Promise { - throw new Error('TXE Synchronizer does not implement "stop"'); + public async stop(): Promise { + await this.closeIpc(); } public stopSync(): Promise { @@ -85,4 +95,9 @@ export class TXESynchronizer implements WorldStateSynchronizer { public clear(): Promise { throw new Error('TXE Synchronizer does not implement "clear"'); } + + /** Clean up IPC resources. */ + public async closeIpc(): Promise { + await this.avmExecutor?.[Symbol.asyncDispose](); + } } diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index e5151c230e6e..1e1f1abfca5e 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -10,7 +10,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { TestDateProvider } from '@aztec/foundation/timer'; import type { LightweightCheckpointBuilder } from '@aztec/prover-client/light'; -import type { PublicContractsDB, PublicProcessor } from '@aztec/simulator/server'; +import type { AvmExecutor, PublicContractsDB, PublicProcessor } from '@aztec/simulator/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2Block } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -66,7 +66,7 @@ describe('CheckpointBuilder', () => { declare public contractsDB: PublicContractsDB; public override makeBlockBuilderDeps(_globalVariables: GlobalVariables, _fork: MerkleTreeWriteOperations) { - return Promise.resolve({ processor, validator }); + return Promise.resolve({ processor, validator, wsdbForkId: 0 }); } /** Expose for testing */ @@ -104,6 +104,8 @@ describe('CheckpointBuilder', () => { contractDataSource, dateProvider, telemetryClient, + // TestCheckpointBuilder overrides makeBlockBuilderDeps, so this is never exercised. + mock(), ); } diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 05489c21e809..4ec4caba6b2a 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -9,6 +9,7 @@ import { DateProvider, elapsed } from '@aztec/foundation/timer'; import { createTxValidatorForBlockBuilding, getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators'; import { LightweightCheckpointBuilder } from '@aztec/prover-client/light'; import { + AvmExecutor, GuardedMerkleTreeOperations, PublicContractsDB, PublicProcessor, @@ -57,6 +58,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { private contractDataSource: ContractDataSource, private dateProvider: DateProvider, private telemetryClient: TelemetryClient, + private avmExecutor: AvmExecutor, bindings?: LoggerBindings, private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { @@ -241,16 +243,18 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { const contractsDB = this.contractsDB; const guardedFork = new GuardedMerkleTreeOperations(fork); - const collectDebugLogs = this.debugLogStore.isEnabled; - const bindings = this.log.getBindings(); + // Extract the WSDB fork ID so the C++ AVM can modify the same fork in-place. The forked simulator + // registers the contracts DB on the CDB server for the duration of each simulation. + const wsdbForkId = fork.getRevision().forkId; + const forkedSimulator = this.avmExecutor.forFork(wsdbForkId, contractsDB, globalVariables.timestamp); const publicTxSimulator = createPublicTxSimulatorForBlockBuilding( - guardedFork, - contractsDB, + forkedSimulator, globalVariables, this.telemetryClient, bindings, - collectDebugLogs, + wsdbForkId, + this.debugLogStore?.isEnabled ?? false, ); const processor = new PublicProcessor( @@ -289,6 +293,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { private worldState: WorldStateSynchronizer, private contractDataSource: ContractDataSource, private dateProvider: DateProvider, + private avmExecutor: AvmExecutor, private telemetryClient: TelemetryClient = getTelemetryClient(), private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { @@ -344,6 +349,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.contractDataSource, this.dateProvider, this.telemetryClient, + this.avmExecutor, bindings, this.debugLogStore, ); @@ -405,6 +411,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.contractDataSource, this.dateProvider, this.telemetryClient, + this.avmExecutor, bindings, this.debugLogStore, ); diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index 52aaffb8deba..c5dd49acb53b 100644 --- a/yarn-project/validator-client/src/validator.integration.test.ts +++ b/yarn-project/validator-client/src/validator.integration.test.ts @@ -19,6 +19,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import type { P2P, PeerId } from '@aztec/p2p'; import { TestTxProvider } from '@aztec/p2p/test-helpers'; import { protocolContractsHash } from '@aztec/protocol-contracts'; +import type { AvmExecutor } from '@aztec/simulator/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CommitteeAttestation, GENESIS_BLOCK_HEADER_HASH, L2Block } from '@aztec/stdlib/block'; import { CheckpointReexecutionTracker, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; @@ -67,6 +68,7 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder: FullNodeCheckpointsBuilder; p2pClient: MockProxy; validator: ValidatorClient; + avmExecutor?: AvmExecutor; }; let slotNumber: SlotNumber; @@ -138,6 +140,9 @@ describe('ValidatorClient Integration', () => { const synchronizer = new ServerWorldStateSynchronizer(worldStateDb, archiver, wsConfig); await synchronizer.start(); + const { AvmExecutor } = await import('@aztec/simulator/server'); + const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateDb.getIpcPath() }); + // Create real checkpoints builder const checkpointsBuilder = new FullNodeCheckpointsBuilder( { @@ -151,6 +156,9 @@ describe('ValidatorClient Integration', () => { synchronizer, archiver, dateProvider, + avmExecutor, + /*telemetryClient=*/ undefined, + /*debugLogStore=*/ undefined, ); // Create mock p2p client @@ -222,6 +230,7 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder, p2pClient, validator, + avmExecutor, }; }; @@ -352,6 +361,7 @@ describe('ValidatorClient Integration', () => { /** Validates blocks by calling the validator client in the attestor. */ const attestorValidateBlocks = async (blocks: BlockProposalResult[]) => { for (const block of blocks) { + setBuildTimeForSlot(block.proposal.slotNumber); logger.warn(`Validating block proposal ${block.proposal.blockNumber}`); expect(await attestor.validator.validateBlockProposal(block.proposal, mockPeerId)).toBe(true); } @@ -406,11 +416,12 @@ describe('ValidatorClient Integration', () => { afterEach(async () => { logger.warn(`Stopping validator contexts`); - for (const { validator, synchronizer, archiver, worldStateDb } of [attestor, proposer]) { + for (const { validator, synchronizer, archiver, worldStateDb, avmExecutor } of [attestor, proposer]) { await tryStop(validator); await tryStop(synchronizer); await tryStop(archiver); await tryStop(worldStateDb); + await avmExecutor?.[Symbol.asyncDispose](); } }); diff --git a/yarn-project/world-state/package.json b/yarn-project/world-state/package.json index 2736a41e74ae..129915d24654 100644 --- a/yarn-project/world-state/package.json +++ b/yarn-project/world-state/package.json @@ -68,7 +68,6 @@ "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/kv-store": "workspace:^", - "@aztec/native": "workspace:^", "@aztec/protocol-contracts": "workspace:^", "@aztec/stdlib": "workspace:^", "@aztec/telemetry-client": "workspace:^", diff --git a/yarn-project/world-state/src/index.ts b/yarn-project/world-state/src/index.ts index 63f92765c7e3..d5d28f550309 100644 --- a/yarn-project/world-state/src/index.ts +++ b/yarn-project/world-state/src/index.ts @@ -2,3 +2,4 @@ export * from './synchronizer/index.js'; export * from './world-state-db/index.js'; export * from './synchronizer/config.js'; export * from './native/index.js'; +export { WorldStateInstrumentation } from './instrumentation/instrumentation.js'; diff --git a/yarn-project/world-state/src/native/index.ts b/yarn-project/world-state/src/native/index.ts index 133319956c9d..fc8aaf795cca 100644 --- a/yarn-project/world-state/src/native/index.ts +++ b/yarn-project/world-state/src/native/index.ts @@ -1,2 +1,3 @@ export * from './native_world_state.js'; export * from './fork_checkpoint.js'; +export { IpcWorldState, getWsdbOptions } from './ipc_world_state_instance.js'; diff --git a/yarn-project/world-state/src/native/native_world_state_instance.ts b/yarn-project/world-state/src/native/native_world_state_instance.ts index e4abeebf065c..a4d9fe2d041c 100644 --- a/yarn-project/world-state/src/native/native_world_state_instance.ts +++ b/yarn-project/world-state/src/native/native_world_state_instance.ts @@ -16,12 +16,7 @@ import type { WorldStateStatusSummary, } from './message.js'; -/** - * Backend-agnostic handle to a running aztec-wsdb world state, accessed by the TS layer. - * - * The legacy in-process NAPI implementation has been removed; the C++ AVM (NAPI) now connects to - * the same aztec-wsdb process using the IPC path returned by {@link getIpcPath}. - */ +/** Backend-agnostic handle to a running aztec-wsdb world state, accessed by the TS layer. */ export interface NativeWorldStateInstance { getTreeInfo(treeId: MerkleTreeId, revision: WorldStateRevision): Promise; getStateReference(revision: WorldStateRevision): Promise>; diff --git a/yarn-project/world-state/tsconfig.json b/yarn-project/world-state/tsconfig.json index ad5c5ae3de92..e9d4ae98be59 100644 --- a/yarn-project/world-state/tsconfig.json +++ b/yarn-project/world-state/tsconfig.json @@ -15,9 +15,6 @@ { "path": "../kv-store" }, - { - "path": "../native" - }, { "path": "../protocol-contracts" }, diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 5b9e58902010..88c39a627b5f 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -754,6 +754,7 @@ __metadata: dependencies: "@aztec/archiver": "workspace:^" "@aztec/bb-prover": "workspace:^" + "@aztec/bb.js": "workspace:^" "@aztec/blob-client": "workspace:^" "@aztec/blob-lib": "workspace:^" "@aztec/constants": "workspace:^" @@ -907,6 +908,59 @@ __metadata: languageName: unknown linkType: soft +"@aztec/bb-avm-sim-darwin-arm64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-arm64::locator=%40aztec%2Faztec3-packages%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/bb-avm-sim-darwin-arm64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-arm64::locator=%40aztec%2Faztec3-packages%40workspace%3A." + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: soft + +"@aztec/bb-avm-sim-darwin-x64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-x64::locator=%40aztec%2Faztec3-packages%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/bb-avm-sim-darwin-x64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-darwin-x64::locator=%40aztec%2Faztec3-packages%40workspace%3A." + conditions: os=darwin & cpu=x64 + languageName: node + linkType: soft + +"@aztec/bb-avm-sim-linux-arm64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-arm64::locator=%40aztec%2Faztec3-packages%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/bb-avm-sim-linux-arm64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-arm64::locator=%40aztec%2Faztec3-packages%40workspace%3A." + conditions: os=linux & cpu=arm64 + languageName: node + linkType: soft + +"@aztec/bb-avm-sim-linux-x64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-x64::locator=%40aztec%2Faztec3-packages%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/bb-avm-sim-linux-x64@portal:../barretenberg/ts/bb-avm-sim/packages/bb-avm-sim-linux-x64::locator=%40aztec%2Faztec3-packages%40workspace%3A." + conditions: os=linux & cpu=x64 + languageName: node + linkType: soft + +"@aztec/bb-avm-sim@portal:../barretenberg/ts/bb-avm-sim::locator=%40aztec%2Faztec3-packages%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@aztec/bb-avm-sim@portal:../barretenberg/ts/bb-avm-sim::locator=%40aztec%2Faztec3-packages%40workspace%3A." + dependencies: + "@aztec/bb-avm-sim-darwin-arm64": "npm:0.1.0" + "@aztec/bb-avm-sim-darwin-x64": "npm:0.1.0" + "@aztec/bb-avm-sim-linux-arm64": "npm:0.1.0" + "@aztec/bb-avm-sim-linux-x64": "npm:0.1.0" + "@aztec/ipc-runtime": "@aztec/ipc-runtime" + msgpackr: "npm:^1.11.2" + tslib: "npm:^2.4.0" + dependenciesMeta: + "@aztec/bb-avm-sim-darwin-arm64": + optional: true + "@aztec/bb-avm-sim-darwin-x64": + optional: true + "@aztec/bb-avm-sim-linux-arm64": + optional: true + "@aztec/bb-avm-sim-linux-x64": + optional: true + bin: + bb-avm-sim: ./dest/bin.js + languageName: node + linkType: soft + "@aztec/bb-prover@workspace:^, @aztec/bb-prover@workspace:bb-prover": version: 0.0.0-use.local resolution: "@aztec/bb-prover@workspace:bb-prover" @@ -1942,6 +1996,7 @@ __metadata: dependencies: "@aztec/archiver": "workspace:^" "@aztec/bb-prover": "workspace:^" + "@aztec/bb.js": "workspace:^" "@aztec/blob-client": "workspace:^" "@aztec/blob-lib": "workspace:^" "@aztec/constants": "workspace:^" @@ -2096,8 +2151,10 @@ __metadata: version: 0.0.0-use.local resolution: "@aztec/simulator@workspace:simulator" dependencies: + "@aztec/bb-avm-sim": "npm:0.1.0" "@aztec/constants": "workspace:^" "@aztec/foundation": "workspace:^" + "@aztec/ipc-runtime": "npm:0.1.0" "@aztec/kv-store": "workspace:^" "@aztec/native": "workspace:^" "@aztec/noir-acvm_js": "portal:../../noir/packages/acvm_js" @@ -2123,6 +2180,7 @@ __metadata: jest-mock-extended: "npm:^4.0.0" lodash.clonedeep: "npm:^4.5.0" lodash.merge: "npm:^4.6.2" + msgpackr: "npm:^1.11.2" ts-node: "npm:^10.9.1" tslib: "npm:^2.4.0" typescript: "npm:^5.3.3" @@ -2428,7 +2486,6 @@ __metadata: "@aztec/foundation": "workspace:^" "@aztec/ipc-runtime": "npm:0.1.0" "@aztec/kv-store": "workspace:^" - "@aztec/native": "workspace:^" "@aztec/protocol-contracts": "workspace:^" "@aztec/stdlib": "workspace:^" "@aztec/telemetry-client": "workspace:^" From 9ab457e702e1d6c16828b2a3fb5aa5d80396a243 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:35:09 +0000 Subject: [PATCH 2/4] refactor: thread AvmSimulator interface, drop AvmExecutor/forFork The public-execution wiring threaded a concrete `AvmExecutor` (pool + CDB server) through the node, prover-node, checkpoint builder, and TXE, and each call site called `forFork(forkId, contractsDB, timestamp)` to get a fork-bound `ForkedAvmSimulator`. That leaked the IPC transport (the CDB server, its fork-id routing key, and the register/unregister dance) into every consumer of public simulation. Thread a single transport-agnostic `AvmSimulator` interface instead, mirroring the pre-IPC shape where the factory threaded `(fork, contractsDB)` and called an in-process simulate. `simulate` now takes an `AvmContractsDBContext` ({contractsDB, forkId, timestamp}); the implementation owns the CDB register -> run -> unregister lifecycle. The pool (`AvmSimulatorPool`) folds in the CDB server and is the sole concrete `AvmSimulator`, spawned only at program entrypoints. `AvmExecutor`, `forFork`, and `ForkedAvmSimulator` are gone. `PublicTxSimulatorBase` gains `contractsDB`; consumers (PublicProcessorFactory, prover-node, checkpoint builder, TXE synchronizer) hold the `AvmSimulator` interface and pass the DBs, deriving forkId from the fork's revision. --- .../aztec-node/src/aztec-node/server.ts | 29 ++++---- .../avm_proving_tests/avm_proving_tester.ts | 16 ++--- .../src/actions/rerun-epoch-proving-job.ts | 8 +-- yarn-project/prover-node/src/factory.ts | 6 +- .../prover-node/src/prover-node.test.ts | 8 +-- yarn-project/prover-node/src/prover-node.ts | 6 +- .../simulator/src/public/avm_executor.ts | 66 ------------------ .../simulator/src/public/avm_simulator.ts | 20 +++++- .../src/public/avm_simulator_pool.ts | 69 +++++++++++++------ .../fixtures/public_processor_test_env.ts | 13 ++-- .../fixtures/public_tx_simulation_tester.ts | 18 +++-- .../public/fuzzing/avm_fuzzer_simulator.ts | 20 +++--- yarn-project/simulator/src/public/index.ts | 3 +- .../public_processor/public_processor.ts | 16 ++--- .../dumping_public_tx_simulator.ts | 4 +- .../public/public_tx_simulator/factories.ts | 22 +++++- .../public_tx_simulator.ts | 19 +++-- .../public_tx_simulator_base.ts | 8 ++- .../oracle/txe_oracle_top_level_context.ts | 24 ++++--- .../txe/src/state_machine/synchronizer.ts | 8 +-- .../src/checkpoint_builder.test.ts | 4 +- .../src/checkpoint_builder.ts | 18 ++--- .../src/validator.integration.test.ts | 16 ++--- 23 files changed, 214 insertions(+), 207 deletions(-) delete mode 100644 yarn-project/simulator/src/public/avm_executor.ts diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 6c5dda35bb95..bc3578d72ab0 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -42,7 +42,12 @@ import { type SequencerPublisher, createAutomineSequencer, } from '@aztec/sequencer-client'; -import { AvmExecutor, PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; +import { + type AvmSimulator, + AvmSimulatorPool, + PublicContractsDB, + PublicProcessorFactory, +} from '@aztec/simulator/server'; import { AttestationsBlockWatcher, AttestedInvalidProposalWatcher, @@ -220,7 +225,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb private readonly automineSequencer?: AutomineSequencer, // AVM execution backend for public simulation. Wired in production (createAndSync); absent in unit/TXE // nodes that don't drive public execution, hence optional and asserted at the simulation call site. - private avmExecutor?: AvmExecutor, + private avmSimulator?: AvmSimulator, ) { this.metrics = new NodeMetrics(telemetry, 'AztecNodeService'); this.tracer = telemetry.getTracer('AztecNodeService'); @@ -605,12 +610,12 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const initialHeader = nativeWs.getInitialHeader(); const initialBlockHash = await initialHeader.hash(); - log.info('WSDB ready, creating AVM executor'); - const avmExecutor = await AvmExecutor.spawn({ + log.info('WSDB ready, creating AVM simulator pool'); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: nativeWs.getIpcPath(), logger: (msg: string) => log.debug(msg), }); - started.push({ stop: () => avmExecutor[Symbol.asyncDispose]() }); + started.push({ stop: () => avmSimulator[Symbol.asyncDispose]() }); const archiver = await createArchiver( config, @@ -703,7 +708,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb worldStateSynchronizer, archiver, dateProvider, - avmExecutor, + avmSimulator, telemetry, undefined, // debugLogStore ); @@ -910,7 +915,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb worldStateSynchronizer, archiver, dateProvider, - avmExecutor, + avmSimulator, telemetry, debugLogStore, ); @@ -994,7 +999,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb epochCache, blobClient, keyStoreManager, - avmExecutor, + avmSimulator, }); if (!options.dontStartProverNode) { @@ -1034,11 +1039,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb keyStoreManager, debugLogStore, automineSequencer, - avmExecutor, + avmSimulator, ); - // Register the AVM executor for cleanup on stop (it owns the AVM pool + CDB server). - node.ipcBackends.push({ destroy: () => avmExecutor[Symbol.asyncDispose]() }); + // Register the AVM simulator pool for cleanup on stop (it owns the AVM processes + CDB server). + node.ipcBackends.push({ destroy: () => avmSimulator[Symbol.asyncDispose]() }); return node; } catch (err) { @@ -1626,7 +1631,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, - this.avmExecutor!, + this.avmSimulator!, new DateProvider(), this.telemetry, this.log.getBindings(), diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts index 8053bf60b2f3..56dcc255b75b 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_proving_tester.ts @@ -8,7 +8,7 @@ import { type TestExecutorMetrics, type TestPrivateInsertions, } from '@aztec/simulator/public/fixtures'; -import { AvmExecutor, MeasuredPublicTxSimulator, PublicContractsDB } from '@aztec/simulator/server'; +import { AvmSimulatorPool, MeasuredPublicTxSimulator } from '@aztec/simulator/server'; import type { PublicTxResult } from '@aztec/simulator/server'; import { AvmCircuitInputs, AvmCircuitPublicInputs, PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -56,15 +56,9 @@ export class AvmProvingTester extends PublicTxSimulationTester { const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await worldStateService.fork(); - const forkId = merkleTrees.getRevision().forkId; - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); - const forkedSimulator = avmExecutor.forFork( - forkId, - new PublicContractsDB(contractDataSource), - globals?.timestamp ?? 0n, - ); - const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => - new MeasuredPublicTxSimulator(forkedSimulator, g, m, c, undefined, forkId); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const simulatorFactory: MeasuredSimulatorFactory = (mt, cdb, g, m, c) => + new MeasuredPublicTxSimulator(avmSimulator, g, cdb, m, c, undefined, mt.getRevision().forkId); const tester = new AvmProvingTester( checkCircuitOnly, @@ -74,7 +68,7 @@ export class AvmProvingTester extends PublicTxSimulationTester { metrics, simulatorFactory, ); - tester.avmExecutor = avmExecutor; + tester.avmSimulator = avmSimulator; return tester; } diff --git a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts index 49551b86aff5..1b04d1c52d8b 100644 --- a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts +++ b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts @@ -3,7 +3,7 @@ import type { L1ContractsConfig } from '@aztec/ethereum/config'; import type { Logger } from '@aztec/foundation/log'; import { type ProverClientConfig, createProverClient } from '@aztec/prover-client'; import { ProverBrokerConfig, createAndStartProvingBroker } from '@aztec/prover-client/broker'; -import { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; +import { AvmSimulatorPool, PublicProcessorFactory } from '@aztec/simulator/server'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import type { GenesisData } from '@aztec/stdlib/world-state'; import { getTelemetryClient } from '@aztec/telemetry-client'; @@ -36,11 +36,11 @@ export async function rerunEpochProvingJob( const archiver = await createArchiverStore(config, initialBlockHash); const contractDataSource = createContractDataSource(archiver); - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldState.getIpcPath() }); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldState.getIpcPath() }); const publicProcessorFactory = new PublicProcessorFactory( contractDataSource, - avmExecutor, + avmSimulator, undefined, undefined, log.getBindings(), @@ -77,6 +77,6 @@ export async function rerunEpochProvingJob( } finally { await prover.stop(); await broker.stop(); - await avmExecutor[Symbol.asyncDispose](); + await avmSimulator[Symbol.asyncDispose](); } } diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index 2ca1b1dd44f3..9ac2bb6e8df7 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -19,7 +19,7 @@ import { type ProverTxSenderConfig, getPublisherConfigFromProverConfig, } from '@aztec/sequencer-client'; -import type { AvmExecutor } from '@aztec/simulator/server'; +import type { AvmSimulator } from '@aztec/simulator/server'; import type { ITxProvider, ProverConfig, @@ -50,7 +50,7 @@ export type ProverNodeDeps = { blobClient: BlobClientInterface; keyStoreManager?: KeystoreManager; /** AVM execution backend (simulator pool + CDB server) for public simulation. */ - avmExecutor: AvmExecutor; + avmSimulator: AvmSimulator; }; /** Creates a new prover node subsystem given a config and dependencies */ @@ -191,7 +191,7 @@ export async function createProverNode( epochMonitor, rollupContract, l1Metrics, - deps.avmExecutor, + deps.avmSimulator, proverNodeConfig, telemetry, delayer, diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index ac8e0c1e8a91..618c0be2c638 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -6,7 +6,7 @@ import { promiseWithResolvers } from '@aztec/foundation/promise'; import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import type { P2PClient, TxProvider } from '@aztec/p2p'; -import type { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; +import type { AvmSimulator, PublicProcessorFactory } from '@aztec/simulator/server'; import { CommitteeAttestation, GENESIS_BLOCK_HEADER_HASH, @@ -52,7 +52,7 @@ describe('prover-node', () => { let rollupContract: MockProxy; let publisherFactory: MockProxy; let l1Metrics: MockProxy; - let avmExecutor: MockProxy; + let avmSimulator: MockProxy; // L1 genesis time let l1GenesisTime: number; @@ -85,7 +85,7 @@ describe('prover-node', () => { epochMonitor, rollupContract, l1Metrics, - avmExecutor, + avmSimulator, config, ); @@ -106,7 +106,7 @@ describe('prover-node', () => { publisherFactory.create.mockResolvedValue(publisher); l1Metrics = mock(); - avmExecutor = mock(); + avmSimulator = mock(); p2p = mock(); p2p.getTxProvider.mockReturnValue(txProvider); diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index b85cd5a86945..0ca1bc1ecfdc 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -7,7 +7,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import { memoize } from '@aztec/foundation/decorators'; import { createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; -import { AvmExecutor, PublicProcessorFactory } from '@aztec/simulator/server'; +import { type AvmSimulator, PublicProcessorFactory } from '@aztec/simulator/server'; import type { L2BlockSource } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; import type { ChainConfig } from '@aztec/stdlib/config'; @@ -76,7 +76,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable protected readonly epochsMonitor: EpochMonitor, protected readonly rollupContract: RollupContract, protected readonly l1Metrics: L1Metrics, - private readonly avmExecutor: AvmExecutor, + private readonly avmSimulator: AvmSimulator, config: Partial = {}, protected readonly telemetryClient: TelemetryClient = getTelemetryClient(), private delayer?: Delayer, @@ -338,7 +338,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable // Create a processor factory const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, - this.avmExecutor, + this.avmSimulator, this.dateProvider, this.telemetryClient, this.log.getBindings(), diff --git a/yarn-project/simulator/src/public/avm_executor.ts b/yarn-project/simulator/src/public/avm_executor.ts deleted file mode 100644 index 3e66d45cc8d7..000000000000 --- a/yarn-project/simulator/src/public/avm_executor.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { AvmSimulator } from './avm_simulator.js'; -import { AvmSimulatorPool, type AvmSimulatorPoolOptions } from './avm_simulator_pool.js'; -import { CdbIpcServer } from './cdb_ipc_server.js'; -import type { PublicContractsDB } from './public_db_sources.js'; - -/** - * An {@link AvmSimulator} bound to a single WSDB fork. Each `simulate` call registers the fork's contracts DB - * on the CDB server so the C++ AVM's contract-data callbacks route to it, and unregisters it once the call - * returns — registration is only needed while the simulation is running. `simulateWithHints` needs no - * registration (the hinted path makes no CDB callbacks). - */ -class ForkedAvmSimulator implements AvmSimulator { - constructor( - private avmSimulator: AvmSimulator, - private cdbServer: CdbIpcServer, - private readonly forkId: number, - private contractsDB: PublicContractsDB, - private timestamp: bigint, - ) {} - - async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise { - this.cdbServer.registerFork(this.forkId, this.contractsDB, this.timestamp); - try { - return await this.avmSimulator.simulate(inputBuffer, signal); - } finally { - this.cdbServer.unregisterFork(this.forkId); - } - } - - simulateWithHints(inputBuffer: Uint8Array): Promise { - return this.avmSimulator.simulateWithHints(inputBuffer); - } -} - -/** Options for {@link AvmExecutor.spawn}; the executor supplies `cdbIpcPath` from the CDB server it creates. */ -export type AvmExecutorOptions = Omit; - -/** - * Owns the public-execution backend: the AVM simulator (a pool of external bb-avm-sim processes) and the CDB - * server that answers those processes' contract-data callbacks. The two are created together (the pool is - * wired to the CDB server's IPC path) and destroyed together, so callers plumb a single `AvmExecutor` rather - * than the pair. Per-fork work goes through {@link forFork}, which hands out an {@link AvmSimulator} and - * keeps the CDB server (and the fork binding) private. - */ -export class AvmExecutor implements AsyncDisposable { - private constructor( - private avmSimulator: AvmSimulator, - private cdbServer: CdbIpcServer, - ) {} - - static async spawn(options: AvmExecutorOptions): Promise { - const cdbServer = new CdbIpcServer(); - const avmSimulator = await AvmSimulatorPool.spawn({ ...options, cdbIpcPath: cdbServer.ipcPath }); - return new AvmExecutor(avmSimulator, cdbServer); - } - - /** Bind to a fork: returns a simulator that registers the fork's contracts DB for the duration of each call. */ - forFork(forkId: number, contractsDB: PublicContractsDB, timestamp: bigint): AvmSimulator { - return new ForkedAvmSimulator(this.avmSimulator, this.cdbServer, forkId, contractsDB, timestamp); - } - - async [Symbol.asyncDispose](): Promise { - await this.avmSimulator.destroy?.(); - await this.cdbServer.close(); - } -} diff --git a/yarn-project/simulator/src/public/avm_simulator.ts b/yarn-project/simulator/src/public/avm_simulator.ts index 5f85240e234f..69f010c7a290 100644 --- a/yarn-project/simulator/src/public/avm_simulator.ts +++ b/yarn-project/simulator/src/public/avm_simulator.ts @@ -1,13 +1,27 @@ +import type { PublicContractsDB } from './public_db_sources.js'; + +/** + * Identifies the contract data a single simulation reads. The AVM's contract-data lookups during a + * `simulate` call are answered from `contractsDB`; `forkId` and `timestamp` scope those lookups to the + * world-state fork being simulated against. How this reaches the AVM (in-process call, IPC callback + * server, ...) is the implementation's concern — callers only supply the context. + */ +export interface AvmContractsDBContext { + contractsDB: PublicContractsDB; + forkId: number; + timestamp: bigint; +} + /** * Something that can run AVM simulations, independent of how the work is dispatched (a single external * process, a pool of them, or an in-process stub). Callers hold this and never see the underlying transport. */ export interface AvmSimulator { /** - * Run a fast simulation and return the msgpack-encoded result. If `signal` aborts, the simulation stops - * at the next cancellation checkpoint. + * Run a fast simulation and return the msgpack-encoded result. `context` supplies the contracts DB whose + * data the simulation reads. If `signal` aborts, the simulation stops at the next cancellation checkpoint. */ - simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise; + simulate(inputBuffer: Uint8Array, context: AvmContractsDBContext, signal?: AbortSignal): Promise; /** Run a simulation collecting proving hints and return the msgpack-encoded result. */ simulateWithHints(inputBuffer: Uint8Array): Promise; /** Release any resources held by the underlying implementation. */ diff --git a/yarn-project/simulator/src/public/avm_simulator_pool.ts b/yarn-project/simulator/src/public/avm_simulator_pool.ts index 6fc8ef0cc019..b17f47c43f32 100644 --- a/yarn-project/simulator/src/public/avm_simulator_pool.ts +++ b/yarn-project/simulator/src/public/avm_simulator_pool.ts @@ -1,7 +1,8 @@ import { AvmService } from '@aztec/bb-avm-sim'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import type { AvmSimulator } from './avm_simulator.js'; +import type { AvmContractsDBContext, AvmSimulator } from './avm_simulator.js'; +import { CdbIpcServer } from './cdb_ipc_server.js'; export interface AvmSimulatorPoolOptions { /** Maximum number of concurrent AVM processes. If not set, defaults to AVM_MAX_CONCURRENT_SIMULATIONS env var or 4. */ @@ -10,24 +11,41 @@ export interface AvmSimulatorPoolOptions { avmBinaryPath?: string; /** IPC path for the shared WSDB server. */ wsdbIpcPath: string; - /** IPC path for the shared CDB server. */ - cdbIpcPath: string; /** Optional logger function for AVM process output. */ logger?: (msg: string) => void; } -/** Lazily manages local bb-avm-sim processes for parallel AVM simulation. */ +/** + * A raw handle to a single bb-avm-sim process: it runs a serialized simulation and connects back to the + * shared CDB/WSDB servers for state, but is unaware of which fork's contract data it is reading — that is + * routed by the fork id baked into the input buffer. + */ +interface AvmProcessHandle { + simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise; + simulateWithHints(inputBuffer: Uint8Array): Promise; + destroy(): Promise; +} + +/** + * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim processes plus the CDB server that + * answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool, + * the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate` + * registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id + * so concurrent simulations on different forks don't collide) and unregisters it once the call returns. + */ export class AvmSimulatorPool implements AvmSimulator { - private slots: Array = []; + private slots: Array = []; private available: number[] = []; - private waiters: Array<{ resolve: (simulator: AvmSimulator) => void; reject: (error: Error) => void }> = []; + private waiters: Array<{ resolve: (simulator: AvmProcessHandle) => void; reject: (error: Error) => void }> = []; private createdCount = 0; private log: Logger; private maxSize: number; + private cdbServer: CdbIpcServer; constructor(private options: AvmSimulatorPoolOptions) { this.log = createLogger('simulator:avm-pool'); this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10); + this.cdbServer = new CdbIpcServer(); } static async spawn(options: AvmSimulatorPoolOptions): Promise { @@ -41,16 +59,24 @@ export class AvmSimulatorPool implements AvmSimulator { await this.destroy(); } - async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise { - const simulator = await this.checkout(); + async simulate(inputBuffer: Uint8Array, context: AvmContractsDBContext, signal?: AbortSignal): Promise { + // Register the fork's contracts DB so the C++ AVM's callbacks (which carry this fork id) route to it, + // and unregister once the simulation returns — registration is only needed while the call is running. + this.cdbServer.registerFork(context.forkId, context.contractsDB, context.timestamp); try { - return await simulator.simulate(inputBuffer, signal); + const simulator = await this.checkout(); + try { + return await simulator.simulate(inputBuffer, signal); + } finally { + this.return(simulator); + } } finally { - this.return(simulator); + this.cdbServer.unregisterFork(context.forkId); } } async simulateWithHints(inputBuffer: Uint8Array): Promise { + // The hinted path makes no contract-data callbacks, so no CDB registration is needed. const simulator = await this.checkout(); try { return await simulator.simulateWithHints(inputBuffer); @@ -59,7 +85,7 @@ export class AvmSimulatorPool implements AvmSimulator { } } - /** Destroy all AVM processes in the pool. */ + /** Destroy all AVM processes in the pool and close the CDB server. */ async destroy(): Promise { for (const waiter of this.waiters) { waiter.reject(new Error('AVM simulator pool destroyed')); @@ -68,7 +94,7 @@ export class AvmSimulatorPool implements AvmSimulator { const destroyPromises: Promise[] = []; for (const slot of this.slots) { - if (slot?.destroy) { + if (slot) { destroyPromises.push(slot.destroy()); } } @@ -77,6 +103,7 @@ export class AvmSimulatorPool implements AvmSimulator { this.slots = []; this.available = []; this.createdCount = 0; + await this.cdbServer.close(); this.log.info('AVM simulator pool destroyed'); } @@ -86,7 +113,7 @@ export class AvmSimulatorPool implements AvmSimulator { */ async prewarm(count = 1): Promise { const target = Math.min(count, this.maxSize); - const created: AvmSimulator[] = []; + const created: AvmProcessHandle[] = []; while (this.createdCount < target) { created.push(await this.createSlot()); } @@ -96,8 +123,8 @@ export class AvmSimulatorPool implements AvmSimulator { } } - /** Check out an AVM simulator from the pool, blocking until one is free. Caller must return() it when done. */ - private async checkout(): Promise { + /** Check out an AVM process from the pool, blocking until one is free. Caller must return() it when done. */ + private async checkout(): Promise { const idx = this.available.pop(); if (idx !== undefined && this.slots[idx]) { return this.slots[idx]!; @@ -107,13 +134,13 @@ export class AvmSimulatorPool implements AvmSimulator { return await this.createSlot(idx); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.waiters.push({ resolve, reject }); }); } - /** Return an AVM simulator to the pool after use. */ - private return(simulator: AvmSimulator): void { + /** Return an AVM process to the pool after use. */ + private return(simulator: AvmProcessHandle): void { const waiter = this.waiters.shift(); if (waiter) { waiter.resolve(simulator); @@ -125,11 +152,11 @@ export class AvmSimulatorPool implements AvmSimulator { } } - private async createSlot(reuseIdx?: number): Promise { + private async createSlot(reuseIdx?: number): Promise { const simulator = await AvmSimulatorProcess.spawn({ binaryPath: this.options.avmBinaryPath, wsdbIpcPath: this.options.wsdbIpcPath, - cdbIpcPath: this.options.cdbIpcPath, + cdbIpcPath: this.cdbServer.ipcPath, logger: this.options.logger, }); if (reuseIdx !== undefined && reuseIdx < this.slots.length) { @@ -143,7 +170,7 @@ export class AvmSimulatorPool implements AvmSimulator { } } -class AvmSimulatorProcess implements AvmSimulator { +class AvmSimulatorProcess implements AvmProcessHandle { private constructor(private service: AvmService) {} static async spawn(options: { diff --git a/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts b/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts index 26097f336a0a..9b71000d7856 100644 --- a/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts +++ b/yarn-project/simulator/src/public/fixtures/public_processor_test_env.ts @@ -6,7 +6,7 @@ import { GlobalVariables } from '@aztec/stdlib/tx'; import { getTelemetryClient } from '@aztec/telemetry-client'; import { NativeWorldStateService } from '@aztec/world-state'; -import { AvmExecutor } from '../avm_executor.js'; +import { AvmSimulatorPool } from '../avm_simulator_pool.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { GuardedMerkleTreeOperations } from '../public_processor/guarded_merkle_tree.js'; import { PublicProcessor } from '../public_processor/public_processor.js'; @@ -49,7 +49,7 @@ export class PublicProcessorTestEnv implements AsyncDisposable { public readonly contractsDB: PublicContractsDB, public readonly globals: GlobalVariables, private readonly worldStateService: NativeWorldStateService, - private readonly avmExecutor: AvmExecutor, + private readonly avmSimulator: AvmSimulatorPool, ) {} static async create(opts: PublicProcessorTestEnvOptions = {}): Promise { @@ -62,10 +62,9 @@ export class PublicProcessorTestEnv implements AsyncDisposable { const contractsDB = new PublicContractsDB(contractDataSource); const forkId = merkleTrees.getRevision().forkId; - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); - const forkedSimulator = avmExecutor.forFork(forkId, contractsDB, globals.timestamp); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); - const simulator = new PublicTxSimulator(forkedSimulator, globals, config, undefined, forkId); + const simulator = new PublicTxSimulator(avmSimulator, globals, contractsDB, config, undefined, forkId); const processor = new PublicProcessor( globals, new GuardedMerkleTreeOperations(merkleTrees), @@ -78,11 +77,11 @@ export class PublicProcessorTestEnv implements AsyncDisposable { const tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); - return new PublicProcessorTestEnv(processor, tester, contractsDB, globals, worldStateService, avmExecutor); + return new PublicProcessorTestEnv(processor, tester, contractsDB, globals, worldStateService, avmSimulator); } async [Symbol.asyncDispose](): Promise { - await this.avmExecutor[Symbol.asyncDispose](); + await this.avmSimulator[Symbol.asyncDispose](); await this.tester.close(); await this.worldStateService.close(); } diff --git a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index c75dfebea5fa..4aee3b56367a 100644 --- a/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts +++ b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts @@ -18,7 +18,7 @@ import { getContractFunctionAbi, getFunctionSelector, } from '../avm/testing/utils.js'; -import { AvmExecutor } from '../avm_executor.js'; +import { AvmSimulatorPool } from '../avm_simulator_pool.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { MeasuredPublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; import type { MeasuredPublicTxSimulatorInterface } from '../public_tx_simulator/public_tx_simulator_interface.js'; @@ -66,7 +66,7 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { protected txCount: number = 0; private simulator: MeasuredPublicTxSimulatorInterface | undefined; private metricsPrefix?: string; - protected avmExecutor?: AvmExecutor; + protected avmSimulator?: AvmSimulatorPool; constructor( merkleTree: MerkleTreeWriteOperations, @@ -97,11 +97,9 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { const contractDataSource = new SimpleContractDataSource(); const merkleTree = await worldStateService.fork(); - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); - const forkId = merkleTree.getRevision().forkId; - const forkedSimulator = avmExecutor.forFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); - const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => - new MeasuredPublicTxSimulator(forkedSimulator, g, m, c, undefined, forkId); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const simulatorFactory: MeasuredSimulatorFactory = (mt, cdb, g, m, c) => + new MeasuredPublicTxSimulator(avmSimulator, g, cdb, m, c, undefined, mt.getRevision().forkId); const tester = new PublicTxSimulationTester( merkleTree, @@ -111,7 +109,7 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { simulatorFactory, config, ); - tester.avmExecutor = avmExecutor; + tester.avmSimulator = avmSimulator; return tester; } @@ -246,9 +244,9 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { this.metrics.prettyPrint(); } - /** Clean up IPC resources (AVM executor and merkle tree fork) created by create(). */ + /** Clean up IPC resources (AVM simulator pool and merkle tree fork) created by create(). */ public async close(): Promise { - await this.avmExecutor?.[Symbol.asyncDispose](); + await this.avmSimulator?.[Symbol.asyncDispose](); // Close the merkle tree fork to release IPC resources before the wsdb process is killed. if (this.merkleTrees?.close) { await this.merkleTrees.close().catch(() => {}); diff --git a/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts b/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts index 2e56980be087..e5826f60f78a 100644 --- a/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts +++ b/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts @@ -34,8 +34,7 @@ import { import type { NativeWorldStateService } from '@aztec/world-state'; import { BaseAvmSimulationTester } from '../avm/testing/base_avm_simulation_tester.js'; -import { AvmExecutor } from '../avm_executor.js'; -import type { AvmSimulator } from '../avm_simulator.js'; +import { AvmSimulatorPool } from '../avm_simulator_pool.js'; import { SimpleContractDataSource } from '../fixtures/simple_contract_data_source.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { PublicTxSimulator } from '../public_tx_simulator/public_tx_simulator.js'; @@ -201,16 +200,17 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester { merkleTrees: MerkleTreeWriteOperations, contractDataSource: SimpleContractDataSource, globals: GlobalVariables, - forkedSimulator: AvmSimulator, - private avmExecutor: AvmExecutor, + private avmSimulator: AvmSimulatorPool, + contractsDB: PublicContractsDB, forkId: number, ) { super(contractDataSource, merkleTrees); // collectPublicInputs and collectCallMetadata are required so the C++ result carries the end // tree snapshots and app-logic return values that the fuzzer bin reports back to the harness. this.simulator = new PublicTxSimulator( - forkedSimulator, + avmSimulator, globals, + contractsDB, { skipFeeEnforcement: false, collectDebugLogs: false, @@ -234,14 +234,14 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester { const contractDataSource = new SimpleContractDataSource(); const merkleTrees = await worldStateService.fork(); const forkId = merkleTrees.getRevision().forkId; - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); - const forkedSimulator = avmExecutor.forFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); - return new AvmFuzzerSimulator(merkleTrees, contractDataSource, globals, forkedSimulator, avmExecutor, forkId); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldStateService.getIpcPath() }); + const contractsDB = new PublicContractsDB(contractDataSource); + return new AvmFuzzerSimulator(merkleTrees, contractDataSource, globals, avmSimulator, contractsDB, forkId); } - /** Tear down the AVM executor (AVM process pool and CDB server). */ + /** Tear down the AVM simulator pool (AVM processes and CDB server). */ public async close(): Promise { - await this.avmExecutor[Symbol.asyncDispose](); + await this.avmSimulator[Symbol.asyncDispose](); } /** diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 8dcbffca8ca0..5a3c0d81e23b 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,10 +1,9 @@ export { AvmSimulatorPool, type AvmSimulatorPoolOptions } from './avm_simulator_pool.js'; -export { AvmExecutor, type AvmExecutorOptions } from './avm_executor.js'; export type { PublicContractsDBInterface } from './db_interfaces.js'; export { PublicContractsDB } from './public_db_sources.js'; export { GuardedMerkleTreeOperations } from './public_processor/guarded_merkle_tree.js'; export { PublicProcessor, PublicProcessorFactory } from './public_processor/public_processor.js'; -export type { AvmSimulator } from './avm_simulator.js'; +export type { AvmContractsDBContext, AvmSimulator } from './avm_simulator.js'; export { PublicTxSimulator, MeasuredPublicTxSimulator, diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index a34fc2fbe7cf..abe3aa634df3 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -50,7 +50,6 @@ import { ForkCheckpoint } from '@aztec/world-state/native'; import { AssertionError } from 'assert'; -import { AvmExecutor } from '../avm_executor.js'; import type { AvmSimulator } from '../avm_simulator.js'; import { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; import { @@ -68,7 +67,7 @@ export class PublicProcessorFactory { private log: Logger; constructor( private contractDataSource: ContractDataSource, - private avmExecutor: AvmExecutor, + private avmSimulator: AvmSimulator, private dateProvider: DateProvider = new DateProvider(), protected telemetryClient: TelemetryClient = getTelemetryClient(), bindings?: LoggerBindings, @@ -77,9 +76,8 @@ export class PublicProcessorFactory { } /** - * Creates a new instance of a PublicProcessor. The simulator it drives is bound to the fork's contracts DB - * via {@link AvmExecutor.forFork}, so AVM requests carrying this forkId route to the right contracts DB; - * that binding is scoped to each simulation, so there is nothing to unregister when the fork is closed. + * Creates a new instance of a PublicProcessor. The simulator it drives reads contract data from the fork's + * contracts DB, scoped by the fork id so AVM requests route to the right state. * * @param globalVariables - The global variables for the block being processed. * @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted. @@ -92,10 +90,9 @@ export class PublicProcessorFactory { contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()), ): PublicProcessor { const forkId = merkleTree.getRevision().forkId; - const forkedSimulator = this.avmExecutor.forFork(forkId, contractsDB, globalVariables.timestamp); const guardedFork = new GuardedMerkleTreeOperations(merkleTree); - const publicTxSimulator = this.createPublicTxSimulator(forkedSimulator, forkId, globalVariables, config); + const publicTxSimulator = this.createPublicTxSimulator(forkId, globalVariables, contractsDB, config); return new PublicProcessor( globalVariables, @@ -109,14 +106,15 @@ export class PublicProcessorFactory { } protected createPublicTxSimulator( - avmSimulator: AvmSimulator, forkId: number, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, config?: Partial, ): PublicTxSimulatorInterface { return new TelemetryPublicTxSimulator( - avmSimulator, + this.avmSimulator, globalVariables, + contractsDB, this.telemetryClient, config, this.log.getBindings(), diff --git a/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts index 642db8e64f96..f7d16a5b96f9 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/dumping_public_tx_simulator.ts @@ -14,6 +14,7 @@ import { mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; import type { AvmSimulator } from '../avm_simulator.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; import { PublicTxSimulator } from './public_tx_simulator.js'; /** @@ -26,12 +27,13 @@ export class DumpingPublicTxSimulator extends PublicTxSimulator { constructor( avmSimulator: AvmSimulator, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, config: Partial, outputDir: string, bindings?: LoggerBindings, forkId?: number, ) { - super(avmSimulator, globalVariables, config, bindings, forkId); + super(avmSimulator, globalVariables, contractsDB, config, bindings, forkId); assert(config.collectHints === true, 'collectHints must be enabled to dump AVM circuit inputs'); assert(config.collectPublicInputs === true, 'collectPublicInputs must be enabled to dump AVM circuit inputs'); this.outputDir = outputDir; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts index 7048525a61a4..117b9b9af4cf 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/factories.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/factories.ts @@ -4,6 +4,7 @@ import type { GlobalVariables } from '@aztec/stdlib/tx'; import type { TelemetryClient } from '@aztec/telemetry-client'; import type { AvmSimulator } from '../avm_simulator.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; import { DumpingPublicTxSimulator } from './dumping_public_tx_simulator.js'; import { TelemetryPublicTxSimulator } from './public_tx_simulator.js'; @@ -15,6 +16,7 @@ import { TelemetryPublicTxSimulator } from './public_tx_simulator.js'; export function createPublicTxSimulatorForBlockBuilding( avmSimulator: AvmSimulator, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, telemetryClient: TelemetryClient, bindings?: LoggerBindings, forkId?: number, @@ -37,7 +39,23 @@ export function createPublicTxSimulatorForBlockBuilding( collectHints: true, collectPublicInputs: true, }; - return new DumpingPublicTxSimulator(avmSimulator, globalVariables, dumpingConfig, dumpDir, bindings, forkId); + return new DumpingPublicTxSimulator( + avmSimulator, + globalVariables, + contractsDB, + dumpingConfig, + dumpDir, + bindings, + forkId, + ); } - return new TelemetryPublicTxSimulator(avmSimulator, globalVariables, telemetryClient, config, bindings, forkId); + return new TelemetryPublicTxSimulator( + avmSimulator, + globalVariables, + contractsDB, + telemetryClient, + config, + bindings, + forkId, + ); } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 2ac21065a3f6..10588cae5aab 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -12,9 +12,10 @@ import type { GlobalVariables, Tx } from '@aztec/stdlib/tx'; import { WorldStateRevision } from '@aztec/stdlib/world-state'; import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client'; -import type { AvmSimulator } from '../avm_simulator.js'; +import type { AvmContractsDBContext, AvmSimulator } from '../avm_simulator.js'; import { ExecutorMetrics } from '../executor_metrics.js'; import type { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; import { PublicTxSimulatorBase } from './public_tx_simulator_base.js'; import type { MeasuredPublicTxSimulatorInterface, @@ -35,11 +36,12 @@ export class PublicTxSimulator extends PublicTxSimulatorBase implements PublicTx constructor( avmSimulator: AvmSimulator, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, config?: Partial, bindings?: LoggerBindings, forkId?: number, ) { - super(avmSimulator, globalVariables, config, undefined, bindings, forkId); + super(avmSimulator, globalVariables, contractsDB, config, undefined, bindings, forkId); this.log = createLogger(`simulator:public_tx_simulator`, bindings); } @@ -85,9 +87,14 @@ export class PublicTxSimulator extends PublicTxSimulatorBase implements PublicTx const inputBuffer = fastSimInputs.serializeWithMessagePack(); this.log.debug(`Running AVM simulation for tx ${txHash}`); + const context: AvmContractsDBContext = { + contractsDB: this.contractsDB, + forkId: this.forkId ?? 0, + timestamp: this.globalVariables.timestamp, + }; let resultBuffer: Uint8Array; try { - resultBuffer = await this.avmSimulator.simulate(inputBuffer, signal); + resultBuffer = await this.avmSimulator.simulate(inputBuffer, context, signal); } catch (error: any) { if (error.message?.includes('cancelled')) { throw new SimulationError(`AVM simulation cancelled`, []); @@ -139,12 +146,13 @@ export class MeasuredPublicTxSimulator extends PublicTxSimulator implements Meas constructor( avmSimulator: AvmSimulator, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, protected readonly metrics: ExecutorMetricsInterface, config?: Partial, bindings?: LoggerBindings, forkId?: number, ) { - super(avmSimulator, globalVariables, config, bindings, forkId); + super(avmSimulator, globalVariables, contractsDB, config, bindings, forkId); } public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { @@ -169,13 +177,14 @@ export class TelemetryPublicTxSimulator extends MeasuredPublicTxSimulator { constructor( avmSimulator: AvmSimulator, globalVariables: GlobalVariables, + contractsDB: PublicContractsDB, telemetryClient: TelemetryClient = getTelemetryClient(), config?: Partial, bindings?: LoggerBindings, forkId?: number, ) { const metrics = new ExecutorMetrics(telemetryClient, 'PublicTxSimulator'); - super(avmSimulator, globalVariables, metrics, config, bindings, forkId); + super(avmSimulator, globalVariables, contractsDB, metrics, config, bindings, forkId); this.tracer = metrics.tracer; } } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts index 96cd55e51c7f..35e41632b98b 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator_base.ts @@ -4,12 +4,13 @@ import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; import type { GlobalVariables, ProtocolContracts, Tx } from '@aztec/stdlib/tx'; import type { AvmSimulator } from '../avm_simulator.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; /** * Shared base for public tx simulators: holds the common configuration, the {@link AvmSimulator} used - * to run the transaction's public calls, the fork id that routes the simulation's contract-data - * lookups, the logger, and the tx-hash helper. Concrete simulators extend this and implement - * `simulate`. + * to run the transaction's public calls, the contracts DB and fork id that scope the simulation's + * contract-data lookups, the logger, and the tx-hash helper. Concrete simulators extend this and + * implement `simulate`. */ export abstract class PublicTxSimulatorBase { protected log: Logger; @@ -19,6 +20,7 @@ export abstract class PublicTxSimulatorBase { constructor( protected avmSimulator: AvmSimulator, protected globalVariables: GlobalVariables, + protected contractsDB: PublicContractsDB, config?: Partial, protected protocolContracts: ProtocolContracts = ProtocolContractsList, bindings?: LoggerBindings, diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 694450d1bdbe..dc107dacbc80 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -539,14 +539,20 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); - // Bind the AVM simulator to this fork's contracts DB for the duration of each simulation. + // The AVM simulator reads this fork's contracts DB, scoped by fork id, for each simulation. const forkId = forkedWorldTrees.getRevision().forkId; - const forkedSimulator = this.stateMachine.synchronizer.avmExecutor.forFork(forkId, contractsDB, globals.timestamp); const processor = new PublicProcessor( globals, guardedMerkleTrees, contractsDB, - new PublicTxSimulator(forkedSimulator, globals, config, bindings, forkId), + new PublicTxSimulator( + this.stateMachine.synchronizer.avmSimulator, + globals, + contractsDB, + config, + bindings, + forkId, + ), new TestDateProvider(), undefined, createLogger('simulator:public-processor', bindings), @@ -656,14 +662,16 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); - // Bind the AVM simulator to this fork's contracts DB for the duration of each simulation. + // The AVM simulator reads this fork's contracts DB, scoped by fork id, for each simulation. const forkId2 = forkedWorldTrees.getRevision().forkId; - const forkedSimulator2 = this.stateMachine.synchronizer.avmExecutor.forFork( - forkId2, + const simulator = new PublicTxSimulator( + this.stateMachine.synchronizer.avmSimulator, + globals, contractsDB, - globals.timestamp, + config, + bindings2, + forkId2, ); - const simulator = new PublicTxSimulator(forkedSimulator2, globals, config, bindings2, forkId2); const processor = new PublicProcessor( globals, guardedMerkleTrees, diff --git a/yarn-project/txe/src/state_machine/synchronizer.ts b/yarn-project/txe/src/state_machine/synchronizer.ts index 56a81b23179d..6f9916247bf3 100644 --- a/yarn-project/txe/src/state_machine/synchronizer.ts +++ b/yarn-project/txe/src/state_machine/synchronizer.ts @@ -1,7 +1,7 @@ import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { AvmExecutor } from '@aztec/simulator/server'; +import { AvmSimulatorPool } from '@aztec/simulator/server'; import type { BlockHash, L2Block } from '@aztec/stdlib/block'; import type { MerkleTreeReadOperations, @@ -17,7 +17,7 @@ export class TXESynchronizer implements WorldStateSynchronizer { private blockNumber = BlockNumber.ZERO; /** AVM execution backend (simulator pool + CDB server) shared across all public simulations. */ - public avmExecutor!: AvmExecutor; + public avmSimulator!: AvmSimulatorPool; constructor(public nativeWorldStateService: NativeWorldStateService) {} @@ -26,7 +26,7 @@ export class TXESynchronizer implements WorldStateSynchronizer { const synchronizer = new this(nativeWorldStateService); - synchronizer.avmExecutor = await AvmExecutor.spawn({ + synchronizer.avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: nativeWorldStateService.getIpcPath(), }); @@ -98,6 +98,6 @@ export class TXESynchronizer implements WorldStateSynchronizer { /** Clean up IPC resources. */ public async closeIpc(): Promise { - await this.avmExecutor?.[Symbol.asyncDispose](); + await this.avmSimulator?.[Symbol.asyncDispose](); } } diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index 1e1f1abfca5e..a43359f7338a 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -10,7 +10,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { TestDateProvider } from '@aztec/foundation/timer'; import type { LightweightCheckpointBuilder } from '@aztec/prover-client/light'; -import type { AvmExecutor, PublicContractsDB, PublicProcessor } from '@aztec/simulator/server'; +import type { AvmSimulator, PublicContractsDB, PublicProcessor } from '@aztec/simulator/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2Block } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -105,7 +105,7 @@ describe('CheckpointBuilder', () => { dateProvider, telemetryClient, // TestCheckpointBuilder overrides makeBlockBuilderDeps, so this is never exercised. - mock(), + mock(), ); } diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 4ec4caba6b2a..665b28d14a89 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -9,7 +9,7 @@ import { DateProvider, elapsed } from '@aztec/foundation/timer'; import { createTxValidatorForBlockBuilding, getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators'; import { LightweightCheckpointBuilder } from '@aztec/prover-client/light'; import { - AvmExecutor, + type AvmSimulator, GuardedMerkleTreeOperations, PublicContractsDB, PublicProcessor, @@ -58,7 +58,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { private contractDataSource: ContractDataSource, private dateProvider: DateProvider, private telemetryClient: TelemetryClient, - private avmExecutor: AvmExecutor, + private avmSimulator: AvmSimulator, bindings?: LoggerBindings, private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { @@ -244,13 +244,13 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { const guardedFork = new GuardedMerkleTreeOperations(fork); const bindings = this.log.getBindings(); - // Extract the WSDB fork ID so the C++ AVM can modify the same fork in-place. The forked simulator - // registers the contracts DB on the CDB server for the duration of each simulation. + // Extract the WSDB fork ID so the C++ AVM can modify the same fork in-place; the simulator reads + // contract data from `contractsDB`, scoped to this fork for the duration of each simulation. const wsdbForkId = fork.getRevision().forkId; - const forkedSimulator = this.avmExecutor.forFork(wsdbForkId, contractsDB, globalVariables.timestamp); const publicTxSimulator = createPublicTxSimulatorForBlockBuilding( - forkedSimulator, + this.avmSimulator, globalVariables, + contractsDB, this.telemetryClient, bindings, wsdbForkId, @@ -293,7 +293,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { private worldState: WorldStateSynchronizer, private contractDataSource: ContractDataSource, private dateProvider: DateProvider, - private avmExecutor: AvmExecutor, + private avmSimulator: AvmSimulator, private telemetryClient: TelemetryClient = getTelemetryClient(), private debugLogStore: DebugLogStore = new NullDebugLogStore(), ) { @@ -349,7 +349,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.contractDataSource, this.dateProvider, this.telemetryClient, - this.avmExecutor, + this.avmSimulator, bindings, this.debugLogStore, ); @@ -411,7 +411,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.contractDataSource, this.dateProvider, this.telemetryClient, - this.avmExecutor, + this.avmSimulator, bindings, this.debugLogStore, ); diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index c5dd49acb53b..567095a72245 100644 --- a/yarn-project/validator-client/src/validator.integration.test.ts +++ b/yarn-project/validator-client/src/validator.integration.test.ts @@ -19,7 +19,7 @@ import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree'; import type { P2P, PeerId } from '@aztec/p2p'; import { TestTxProvider } from '@aztec/p2p/test-helpers'; import { protocolContractsHash } from '@aztec/protocol-contracts'; -import type { AvmExecutor } from '@aztec/simulator/server'; +import type { AvmSimulatorPool } from '@aztec/simulator/server'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CommitteeAttestation, GENESIS_BLOCK_HEADER_HASH, L2Block } from '@aztec/stdlib/block'; import { CheckpointReexecutionTracker, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; @@ -68,7 +68,7 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder: FullNodeCheckpointsBuilder; p2pClient: MockProxy; validator: ValidatorClient; - avmExecutor?: AvmExecutor; + avmSimulator?: AvmSimulatorPool; }; let slotNumber: SlotNumber; @@ -140,8 +140,8 @@ describe('ValidatorClient Integration', () => { const synchronizer = new ServerWorldStateSynchronizer(worldStateDb, archiver, wsConfig); await synchronizer.start(); - const { AvmExecutor } = await import('@aztec/simulator/server'); - const avmExecutor = await AvmExecutor.spawn({ wsdbIpcPath: worldStateDb.getIpcPath() }); + const { AvmSimulatorPool } = await import('@aztec/simulator/server'); + const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldStateDb.getIpcPath() }); // Create real checkpoints builder const checkpointsBuilder = new FullNodeCheckpointsBuilder( @@ -156,7 +156,7 @@ describe('ValidatorClient Integration', () => { synchronizer, archiver, dateProvider, - avmExecutor, + avmSimulator, /*telemetryClient=*/ undefined, /*debugLogStore=*/ undefined, ); @@ -230,7 +230,7 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder, p2pClient, validator, - avmExecutor, + avmSimulator, }; }; @@ -416,12 +416,12 @@ describe('ValidatorClient Integration', () => { afterEach(async () => { logger.warn(`Stopping validator contexts`); - for (const { validator, synchronizer, archiver, worldStateDb, avmExecutor } of [attestor, proposer]) { + for (const { validator, synchronizer, archiver, worldStateDb, avmSimulator } of [attestor, proposer]) { await tryStop(validator); await tryStop(synchronizer); await tryStop(archiver); await tryStop(worldStateDb); - await avmExecutor?.[Symbol.asyncDispose](); + await avmSimulator?.[Symbol.asyncDispose](); } }); From 860be789e5a4adeb920443c97a552afcd2f2c727 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:13:12 +0000 Subject: [PATCH 3/4] fix: dispose AVM pool per TXE session and cap pool growth under concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two leaks/bugs surfaced by a full-CI process-tree capture of the AVM cutover: 1. TXE session leak. `TXESession.dispose()` closed the world state and LMDB but never stopped the synchronizer's AVM pool, so every test orphaned its prewarmed bb-avm-sim process — and, because that process kept a live connection to the WSDB server, pinned the WSDB alive too. Over a run this accumulated to hundreds of undisposed process-triples. Dispose the pool (`closeIpc()`) first, so the bb-avm-sim processes exit and release their WSDB connections before the WSDB is closed. 2. Pool overshoot under concurrency. `AvmSimulatorPool` bumped `createdCount` only after the `await` that spawns a process, so concurrent checkouts could all observe `createdCount < maxSize` and each spawn, growing the pool past maxSize (a pool was seen holding maxSize+1 processes in CI). Reserve the count synchronously at the top of `createSlot`, before the spawn, and roll it back if the spawn fails, so the maxSize guard is exclusive. --- .../src/public/avm_simulator_pool.ts | 32 +++++++++++++------ yarn-project/txe/src/txe_session.ts | 12 +++++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/yarn-project/simulator/src/public/avm_simulator_pool.ts b/yarn-project/simulator/src/public/avm_simulator_pool.ts index b17f47c43f32..17e78d812e11 100644 --- a/yarn-project/simulator/src/public/avm_simulator_pool.ts +++ b/yarn-project/simulator/src/public/avm_simulator_pool.ts @@ -153,17 +153,31 @@ export class AvmSimulatorPool implements AvmSimulator { } private async createSlot(reuseIdx?: number): Promise { - const simulator = await AvmSimulatorProcess.spawn({ - binaryPath: this.options.avmBinaryPath, - wsdbIpcPath: this.options.wsdbIpcPath, - cdbIpcPath: this.cdbServer.ipcPath, - logger: this.options.logger, - }); - if (reuseIdx !== undefined && reuseIdx < this.slots.length) { - this.slots[reuseIdx] = simulator; + const reuse = reuseIdx !== undefined && reuseIdx < this.slots.length; + // Reserve the slot count synchronously, before the async spawn, so concurrent checkouts can't all + // observe `createdCount < maxSize` and overshoot the pool (the count is only bumped once control has + // yielded on the await). Roll back the reservation if the spawn itself fails. + if (!reuse) { + this.createdCount++; + } + let simulator: AvmProcessHandle; + try { + simulator = await AvmSimulatorProcess.spawn({ + binaryPath: this.options.avmBinaryPath, + wsdbIpcPath: this.options.wsdbIpcPath, + cdbIpcPath: this.cdbServer.ipcPath, + logger: this.options.logger, + }); + } catch (err) { + if (!reuse) { + this.createdCount--; + } + throw err; + } + if (reuse) { + this.slots[reuseIdx!] = simulator; } else { this.slots.push(simulator); - this.createdCount++; } this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`); return simulator; diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 291c2eee9fab..5aa8d00d2612 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -234,14 +234,22 @@ export class TXESession implements TXESessionStateHandler { ) {} /** - * Closes the per-session `txe-session` LMDB and the `NativeWorldStateService` . - * Called via IPC when the dispatcher detects the end of a test. Idempotent. + * Tears down the per-session AVM simulator (process pool + CDB server), the `NativeWorldStateService`, + * and the `txe-session` LMDB. Called via IPC when the dispatcher detects the end of a test. Idempotent. */ async dispose(): Promise { if (this.disposed) { return; } this.disposed = true; + // Dispose the AVM pool before the world state: it kills the bb-avm-sim processes so they release their + // connection to the WSDB server, letting it shut down cleanly. Skipping this leaks a bb-avm-sim process + // (and keeps its WSDB alive) per session. + try { + await this.stateMachine.synchronizer.closeIpc(); + } catch (err) { + this.logger.warn(`Error closing AVM simulator during session dispose`, err); + } try { await this.stateMachine.synchronizer.nativeWorldStateService.close(); } catch (err) { From aae0983776a7a1bea241da4546696a6e25e084d0 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:31:01 +0000 Subject: [PATCH 4/4] fix: cap TXE ephemeral world-state to minimal wsdb threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each TXE test spins up its own tiny, short-lived world state — dozens concurrently — and each spawned aztec-wsdb was sized to one thread per core (min(16, cpus)), used for BOTH the tree-op pool AND the IPC dispatch pool, so a 16-core box gave ~32 threads per wsdb. The trees are cache-sized (256MB map, tiny state), so those threads buy nothing and multiply into thousands across a test run. Thread a `threads` option through IpcWorldState.spawn -> NativeWorldStateService .tmp (default unchanged: min(16, cpus)), and have `ephemeral()` — used only by the TXE — request 1. That yields a single tree-op thread; the C++ IPC dispatch pool still floors at 2 (max(2, threads)), which is the minimum that avoids the dispatch/tree-pool deadlock. Net: ~32 -> ~3 threads per TXE wsdb, production paths untouched. --- .../src/native/ipc_world_state_instance.ts | 2 +- .../src/native/native_world_state.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/yarn-project/world-state/src/native/ipc_world_state_instance.ts b/yarn-project/world-state/src/native/ipc_world_state_instance.ts index 5eeb58275c4e..b5554ebf3186 100644 --- a/yarn-project/world-state/src/native/ipc_world_state_instance.ts +++ b/yarn-project/world-state/src/native/ipc_world_state_instance.ts @@ -365,8 +365,8 @@ export class IpcWorldState implements NativeWorldStateInstance { genesis: GenesisData, instrumentation: WorldStateInstrumentation, bindings?: LoggerBindings, + threads: number = getWsdbThreadCount(), ): Promise { - const threads = getWsdbThreadCount(); const transport = process.env.WSDB_TRANSPORT === 'shm' ? 'shm' : 'uds'; const wsdb = await WsdbService.spawn({ transport, diff --git a/yarn-project/world-state/src/native/native_world_state.ts b/yarn-project/world-state/src/native/native_world_state.ts index 18b66996b46a..5039e6572da4 100644 --- a/yarn-project/world-state/src/native/native_world_state.ts +++ b/yarn-project/world-state/src/native/native_world_state.ts @@ -107,6 +107,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { genesis: GenesisData = EMPTY_GENESIS_DATA, instrumentation = new WorldStateInstrumentation(getTelemetryClient()), bindings?: LoggerBindings, + threads?: number, ): Promise { const log = createLogger('world-state:database', bindings); const dataDir = await mkdtemp(join(tmpdir(), 'aztec-world-state-')); @@ -124,7 +125,14 @@ export class NativeWorldStateService implements MerkleTreeDatabase { }; log.debug(`Created temporary world state database at: ${dataDir} with tree map size: ${dbMapSizeKb}`); - const instance = await IpcWorldState.spawn(dataDir, worldStateTreeMapSizes, genesis, instrumentation, bindings); + const instance = await IpcWorldState.spawn( + dataDir, + worldStateTreeMapSizes, + genesis, + instrumentation, + bindings, + threads, + ); const cleanup = async () => { if (cleanupTmpDir) { @@ -138,7 +146,7 @@ export class NativeWorldStateService implements MerkleTreeDatabase { const recreateInstance = async () => { await rm(dataDir, { recursive: true, force: true, maxRetries: 3 }); await mkdir(dataDir, { recursive: true }); - return IpcWorldState.spawn(dataDir, worldStateTreeMapSizes, genesis, instrumentation, bindings); + return IpcWorldState.spawn(dataDir, worldStateTreeMapSizes, genesis, instrumentation, bindings, threads); }; const worldState = new this(instance, instrumentation, log, genesis, cleanup, recreateInstance); @@ -156,7 +164,11 @@ export class NativeWorldStateService implements MerkleTreeDatabase { instrumentation = new WorldStateInstrumentation(getTelemetryClient()), bindings?: LoggerBindings, ): Promise { - return this.tmp(/*cleanupTmpDir=*/ true, genesis, instrumentation, bindings); + // The TXE spins up one tiny, short-lived world state per test, many concurrently. Cap the wsdb thread + // usage to the minimum: a single tree-op thread instead of one per core (the C++ IPC dispatcher pool + // still floors at 2). This avoids ~32 threads per wsdb multiplied across dozens of concurrent test + // world states — the trees are cache-sized (see tmp's map-size note), so extra threads buy nothing. + return this.tmp(/*cleanupTmpDir=*/ true, genesis, instrumentation, bindings, /*threads=*/ 1); } static async fromIpc(