From d7662c1b477354f44d97fdcd11765db2c17b2688 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:57:32 +0000 Subject: [PATCH 1/2] feat: cut simulator over to generated bb-avm-sim IPC service --- .../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 + barretenberg/ts/bb.js/bootstrap.sh | 0 barretenberg/ts/bootstrap.sh | 0 ipc-codegen/src/typescript_codegen.ts | 18 +- ipc-codegen/src/typescript_package_codegen.ts | 10 + 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 | 61 ++- .../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_custom_bc.test.ts | 3 + .../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_minimal_proving.test.ts | 1 + .../avm_proving_tests/avm_opcode_spam.test.ts | 1 + .../avm_proven_gadgets.test.ts | 2 + .../avm_proving_tests/avm_proving_tester.ts | 42 +- .../avm_public_fee_payment.test.ts | 1 + yarn-project/bootstrap.sh | 2 + yarn-project/end-to-end/.gitignore | 1 + .../end-to-end/src/e2e_p2p/reex.test.ts | 26 +- .../src/avm_integration.test.ts | 2 +- .../src/rollup_ivc_integration.test.ts | 2 +- 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 | 31 +- yarn-project/prover-node/src/factory.ts | 7 + .../prover-node/src/job/epoch-proving-job.ts | 44 +- .../prover-node/src/prover-node.test.ts | 6 +- yarn-project/prover-node/src/prover-node.ts | 6 +- yarn-project/simulator/eslint.config.js | 3 + yarn-project/simulator/package.json | 5 +- .../src/public/avm_simulator_pool.ts | 166 +++++++ .../src/public/cdb_ipc_server.test.ts | 25 + .../simulator/src/public/cdb_ipc_server.ts | 287 ++++++++++++ .../fixtures/public_tx_simulation_tester.ts | 71 ++- .../public/fuzzing/avm_fuzzer_simulator.ts | 2 +- yarn-project/simulator/src/public/index.ts | 8 +- .../simulator/src/public/public_db_sources.ts | 2 +- .../apps_tests/deployments.test.ts | 44 +- .../apps_tests/timeout_race.test.ts | 391 ---------------- .../public_processor/apps_tests/token.test.ts | 44 +- .../public_processor/public_processor.test.ts | 83 +++- .../public_processor/public_processor.ts | 67 ++- .../apps_tests/amm.test.ts | 13 +- .../apps_tests/avm_gadgets.test.ts | 13 +- .../apps_tests/avm_minimal.test.ts | 10 +- .../apps_tests/avm_test.test.ts | 7 +- .../apps_tests/bench.test.ts | 77 +++- .../apps_tests/cpp_exception_handling.test.ts | 18 +- .../apps_tests/custom_bc.test.ts | 39 +- .../apps_tests/opcode_spam.test.ts | 49 +- .../apps_tests/token.test.ts | 13 +- .../contract_provider_for_cpp.ts | 125 ----- .../cpp_public_tx_simulator.ts | 205 ++++----- ...cpp_public_tx_simulator_with_hinted_dbs.ts | 134 ------ .../cpp_vs_ts_public_tx_simulator.ts | 232 ---------- .../dumping_cpp_public_tx_simulator.ts | 52 +-- .../public/public_tx_simulator/factories.ts | 23 +- .../src/public/public_tx_simulator/index.ts | 10 +- .../ipc_vs_ts_public_tx_simulator.ts | 177 ++++++++ .../measured_public_tx_simulator.ts | 24 +- .../public_tx_simulator.test.ts | 63 +-- .../public_tx_simulator.ts | 11 +- .../public_tx_simulator_interface.ts | 30 +- .../src/interfaces/merkle_tree_operations.ts | 5 +- .../txe/esbuild/stubs/world_state_stub.ts | 16 + .../oracle/txe_oracle_top_level_context.ts | 175 +++---- .../txe/src/state_machine/synchronizer.ts | 30 +- yarn-project/validator-client/package.json | 1 + .../src/checkpoint_builder.test.ts | 2 +- .../src/checkpoint_builder.ts | 35 +- .../src/validator.integration.test.ts | 22 +- 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 ++- 96 files changed, 1757 insertions(+), 2998 deletions(-) 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 mode change 100755 => 100644 barretenberg/ts/bb.js/bootstrap.sh mode change 100755 => 100644 barretenberg/ts/bootstrap.sh 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 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 delete mode 100644 yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator_with_hinted_dbs.ts delete mode 100644 yarn-project/simulator/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts create mode 100644 yarn-project/simulator/src/public/public_tx_simulator/ipc_vs_ts_public_tx_simulator.ts 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/barretenberg/ts/bb.js/bootstrap.sh b/barretenberg/ts/bb.js/bootstrap.sh old mode 100755 new mode 100644 diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh old mode 100755 new mode 100644 diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts index cf57283df55f..100b0e1330d4 100644 --- a/ipc-codegen/src/typescript_codegen.ts +++ b/ipc-codegen/src/typescript_codegen.ts @@ -614,23 +614,25 @@ ${methods} }) .join("\n"); - // Collect imports - const importTypes = new Set(); + const typeImports = new Set(); + const valueImports = new Set(); for (const cmd of schema.commands) { const cmdType = toPascalCase(cmd.name); const respType = toPascalCase(cmd.responseType); - importTypes.add(cmdType); - importTypes.add(respType); - importTypes.add(`to${cmdType}`); - importTypes.add(`from${respType}`); + typeImports.add(cmdType); + typeImports.add(respType); + valueImports.add(`to${cmdType}`); + valueImports.add(`from${respType}`); } - const sortedImports = Array.from(importTypes).sort(); + const sortedTypeImports = Array.from(typeImports).sort(); + const sortedValueImports = Array.from(valueImports).sort(); return `// AUTOGENERATED FILE - DO NOT EDIT // Server-side dispatch for IPC protocol import { Decoder, Encoder } from 'msgpackr'; -import { ${sortedImports.join(", ")} } from './api_types.js'; +import type { ${sortedTypeImports.join(", ")} } from './api_types.js'; +import { ${sortedValueImports.join(", ")} } from './api_types.js'; /** Handler interface — implement this to serve commands. */ export interface Handler { diff --git a/ipc-codegen/src/typescript_package_codegen.ts b/ipc-codegen/src/typescript_package_codegen.ts index e4e2d4d69077..32f4238447f3 100644 --- a/ipc-codegen/src/typescript_package_codegen.ts +++ b/ipc-codegen/src/typescript_package_codegen.ts @@ -322,6 +322,12 @@ ${supportsShm return this.client.call(input); } + sendProcessSignal(signal: NodeJS.Signals): void { + if (this.child.exitCode === null) { + this.child.kill(signal); + } + } + async destroy(): Promise { // Mark intentional teardown so the exit handler doesn't report it as an // unexpected death. @@ -405,6 +411,10 @@ export class ${serviceClass} extends AsyncApi { getIpcPath(): string { return this.spawnedBackend.getIpcPath(); } + + sendProcessSignal(signal: NodeJS.Signals): void { + this.spawnedBackend.sendProcessSignal(signal); + } } `; } 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 8310aca29741..b90004ae2fad 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..67c93d1f6696 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -42,7 +42,13 @@ import { type SequencerPublisher, createAutomineSequencer, } from '@aztec/sequencer-client'; -import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; +import { + type AvmIpcBackend, + AvmSimulatorPool, + CdbIpcServer, + PublicContractsDB, + PublicProcessorFactory, +} from '@aztec/simulator/server'; import { AttestationsBlockWatcher, AttestedInvalidProposalWatcher, @@ -187,6 +193,13 @@ 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 }> = []; + /** AVM IPC backend (pool) for parallel public simulation. */ + private avmPool?: AvmIpcBackend; + /** CDB IPC server for contract data queries during AVM simulation. */ + private cdbServer?: CdbIpcServer; + constructor( protected config: AztecNodeConfig, protected readonly p2pClient: P2P, @@ -498,7 +511,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); @@ -586,9 +599,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const epochCache = await EpochCache.create(config.rollupAddress, config, { dateProvider }); + log.info('Starting IPC backends'); + const cdbServer = new CdbIpcServer(); + // Track started resources so we can clean up on partial failure during node creation. const started: { stop?(): Promise | void }[] = []; try { + started.push({ stop: () => cdbServer.close() }); + // Default the orphan-prune grace window from the block build duration when unset, so the archiver // waits roughly one build slot for a proposed checkpoint to arrive before pruning a block-only tip. config.orphanProposedBlockPruneGraceSeconds ??= @@ -598,6 +616,15 @@ 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 simulator pool'); + const avmPool = await AvmSimulatorPool.spawn({ + wsdbIpcPath: nativeWs.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + logger: (msg: string) => log.debug(msg), + }); + started.push({ stop: () => avmPool.destroy() }); + const archiver = await createArchiver( config, { blobClient, epochCache, telemetry, dateProvider }, @@ -646,6 +673,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb rollupVersion: BigInt(config.rollupVersion), l1GenesisTime, slotDuration: Number(slotDuration), + rollupManaLimit, }; const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig); @@ -689,6 +717,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb archiver, dateProvider, telemetry, + undefined, // debugLogStore + avmPool, + cdbServer, ); let validatorClient: ValidatorClient | undefined; @@ -895,6 +926,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb dateProvider, telemetry, debugLogStore, + avmPool, + cdbServer, ); if (config.useAutomineSequencer) { @@ -976,6 +1009,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb epochCache, blobClient, keyStoreManager, + avmBackend: avmPool, + cdbServer, }); if (!options.dontStartProverNode) { @@ -1017,6 +1052,18 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb automineSequencer, ); + // Register IPC backends for cleanup on stop + if (cdbServer) { + node.ipcBackends.push({ destroy: () => cdbServer!.close() }); + } + if (avmPool) { + node.ipcBackends.push(avmPool); + node.avmPool = avmPool; + } + if (cdbServer) { + node.cdbServer = cdbServer; + } + return node; } catch (err) { log.error('Failed during node creation, stopping started resources', err); @@ -1287,6 +1334,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 +1650,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, + this.avmPool!, + this.cdbServer, 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_custom_bc.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_custom_bc.test.ts index eb86d7596105..b3787fdc2cb1 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_custom_bc.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit_custom_bc.test.ts @@ -27,6 +27,7 @@ describe('AVM custom bytecodes unhappy paths', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -66,6 +67,7 @@ describe('AVM bytecode flow unhappy paths', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -110,6 +112,7 @@ describe('AVM custom bytecodes truncation', () => { }); 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_minimal_proving.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_minimal_proving.test.ts index 13df5be252b8..0e234c7231ff 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_minimal_proving.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_minimal_proving.test.ts @@ -13,6 +13,7 @@ describe('AVM proven minimal tx', () => { }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts b/yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts index bf8fde1630bd..10af0f3decad 100644 --- a/yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts +++ b/yarn-project/bb-prover/src/avm_proving_tests/avm_opcode_spam.test.ts @@ -54,6 +54,7 @@ describeOrSkip('AVM Opcode Spammer Proving Benchmarks', () => { }); 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..9a9395dd02b1 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,19 @@ 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 { + AvmSimulatorPool, + CdbIpcServer, + MeasuredCppPublicTxSimulator, + 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 +47,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 +60,36 @@ 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 cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, new PublicContractsDB(contractDataSource), globals?.timestamp ?? 0n); + const avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend, g, m, c, undefined, forkId); + + const tester = new AvmProvingTester( + checkCircuitOnly, + contractDataSource, + merkleTrees, + globals, + metrics, + simulatorFactory, + ); + tester.avmBackend = avmBackend; + tester.cdbServer = cdbServer; + 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 0a2b88c4eb67..3fb2b6746d48 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) } @@ -139,6 +140,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/end-to-end/src/e2e_p2p/reex.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts index 42f238402bea..3ed01c1c60b1 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reex.test.ts @@ -7,7 +7,7 @@ import { times } from '@aztec/foundation/collection'; import { sleep } from '@aztec/foundation/sleep'; import { unfreeze } from '@aztec/foundation/types'; import type { LibP2PService, P2PClient } from '@aztec/p2p'; -import type { CppPublicTxSimulator, PublicTxResult } from '@aztec/simulator/server'; +import type { CppPublicTxSimulator, SimulationHandle } from '@aztec/simulator/server'; import { BlockProposal } from '@aztec/stdlib/p2p'; import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError } from '@aztec/stdlib/validators'; import type { ValidatorKeyStore } from '@aztec/validator-client'; @@ -165,7 +165,7 @@ describe('e2e_p2p_reex', () => { // We abuse the fact that the proposer will always run before the validators const interceptTxProcessorSimulate = ( node: AztecNodeService, - stub: (tx: Tx, originalSimulate: (tx: Tx) => Promise) => Promise, + stub: (tx: Tx, originalSimulate: (tx: Tx) => SimulationHandle) => SimulationHandle, ) => { const blockBuilder: any = (node as any).sequencer.sequencer.blockBuilder; const originalCreateDeps = blockBuilder.makeBlockBuilderDeps.bind(blockBuilder); @@ -193,19 +193,25 @@ describe('e2e_p2p_reex', () => { // Have the public tx processor take an extra long time to process the tx, so the validator times out const interceptTxProcessorWithTimeout = (node: AztecNodeService) => { - interceptTxProcessorSimulate(node, async (tx: Tx, originalSimulate: (tx: Tx) => Promise) => { - t.logger.warn('Public tx simulator sleeping for 40s to simulate timeout', { txHash: tx.getTxHash() }); - await sleep(40_000); - return originalSimulate(tx); + interceptTxProcessorSimulate(node, (tx: Tx, originalSimulate: (tx: Tx) => SimulationHandle) => { + const result = (async () => { + t.logger.warn('Public tx simulator sleeping for 40s to simulate timeout', { txHash: tx.getTxHash() }); + await sleep(40_000); + return originalSimulate(tx).result; + })(); + return { result, cancel: async () => {} }; }); }; // Have the public tx processor throw when processing a tx const interceptTxProcessorWithFailure = (node: AztecNodeService) => { - interceptTxProcessorSimulate(node, async (tx: Tx, _originalSimulate: (tx: Tx) => Promise) => { - await sleep(1); - t.logger.warn('Public tx simulator failing', { txHash: tx.getTxHash() }); - throw new Error(`Fake tx failure`); + interceptTxProcessorSimulate(node, (tx: Tx, _originalSimulate: (tx: Tx) => SimulationHandle) => { + const result = (async () => { + await sleep(1); + t.logger.warn('Public tx simulator failing', { txHash: tx.getTxHash() }); + throw new Error(`Fake tx failure`); + })(); + return { result, cancel: async () => {} }; }); }; diff --git a/yarn-project/ivc-integration/src/avm_integration.test.ts b/yarn-project/ivc-integration/src/avm_integration.test.ts index 60c16afac7de..9e582d705fa5 100644 --- a/yarn-project/ivc-integration/src/avm_integration.test.ts +++ b/yarn-project/ivc-integration/src/avm_integration.test.ts @@ -114,12 +114,12 @@ describe('AVM Integration', () => { worldStateService, /*globals=*/ undefined, // default /*metrics=*/ undefined, - /*useCppSimulator=*/ true, simConfig, ); }); 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 baf716b7b34d..461a3a43787b 100644 --- a/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts +++ b/yarn-project/ivc-integration/src/rollup_ivc_integration.test.ts @@ -85,10 +85,10 @@ describe('Rollup IVC Integration', () => { worldStateService, /*globals=*/ undefined, // default /*metrics=*/ undefined, - /*useCppSimulator=*/ true, 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 7b9c0dff2589..d58fce70531a 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -85,6 +85,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 ad69c6047308..cfcd8a614a31 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..3c29ab1ef4d5 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 { AvmSimulatorPool, CdbIpcServer, PublicContractsDB, 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,19 @@ 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 cdbServer = new CdbIpcServer(); + cdbServer.registerFork(0, new PublicContractsDB(contractDataSource), 0n); + const avmPool = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldState.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + const publicProcessorFactory = new PublicProcessorFactory( - createContractDataSource(archiver), + contractDataSource, + avmPool, + cdbServer, undefined, undefined, log.getBindings(), @@ -48,9 +59,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 +76,14 @@ 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 avmPool.destroy(); + await cdbServer.close(); + } } diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index ec80add799d9..23d390e4cefd 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 { AvmIpcBackend, CdbIpcServer } from '@aztec/simulator/server'; import type { ITxProvider, ProverConfig, @@ -48,6 +49,10 @@ export type ProverNodeDeps = { epochCache: EpochCacheInterface; blobClient: BlobClientInterface; keyStoreManager?: KeystoreManager; + /** AVM IPC backend for public simulation. */ + avmBackend: AvmIpcBackend; + /** CDB IPC server for contract data queries during AVM simulation. */ + cdbServer?: CdbIpcServer; }; /** Creates a new prover node subsystem given a config and dependencies */ @@ -187,6 +192,8 @@ export async function createProverNode( epochMonitor, rollupContract, l1Metrics, + deps.avmBackend, + deps.cdbServer, 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..655bd6ae9afe 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,30 @@ 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 { + try { + this.publicProcessorFactory.unregisterFork(db.getRevision().forkId); + } catch { + // Fork may not have a revision (e.g., in tests with mocked DBs) + } + 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 dbed4994fac7..9266822d2068 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 { AvmIpcBackend, 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 avmBackend: MockProxy; // L1 genesis time let l1GenesisTime: number; @@ -84,6 +85,8 @@ describe('prover-node', () => { epochMonitor, rollupContract, l1Metrics, + avmBackend, + undefined, // cdbServer config, ); @@ -104,6 +107,7 @@ describe('prover-node', () => { publisherFactory.create.mockResolvedValue(publisher); l1Metrics = mock(); + avmBackend = 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..a79d7c2ee65c 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 { type AvmIpcBackend, type CdbIpcServer, 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,8 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable protected readonly epochsMonitor: EpochMonitor, protected readonly rollupContract: RollupContract, protected readonly l1Metrics: L1Metrics, + private readonly avmBackend: AvmIpcBackend, + private readonly cdbServer: CdbIpcServer | undefined, config: Partial = {}, protected readonly telemetryClient: TelemetryClient = getTelemetryClient(), private delayer?: Delayer, @@ -337,6 +339,8 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable // Create a processor factory const publicProcessorFactory = new PublicProcessorFactory( this.contractDataSource, + this.avmBackend, + this.cdbServer, this.dateProvider, this.telemetryClient, this.log.getBindings(), diff --git a/yarn-project/simulator/eslint.config.js b/yarn-project/simulator/eslint.config.js index a6956c3c3782..e43f2252874f 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/opcodes/*.ts'], diff --git a/yarn-project/simulator/package.json b/yarn-project/simulator/package.json index f743eefa6d8f..00d52de53cc0 100644 --- a/yarn-project/simulator/package.json +++ b/yarn-project/simulator/package.json @@ -20,8 +20,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" @@ -64,6 +65,7 @@ ] }, "dependencies": { + "@aztec/bb-avm-sim": "0.1.0", "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/native": "workspace:^", @@ -78,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_simulator_pool.ts b/yarn-project/simulator/src/public/avm_simulator_pool.ts new file mode 100644 index 000000000000..8125c669d313 --- /dev/null +++ b/yarn-project/simulator/src/public/avm_simulator_pool.ts @@ -0,0 +1,166 @@ +import { AvmService } from '@aztec/bb-avm-sim'; +import { type Logger, createLogger } from '@aztec/foundation/log'; + +export interface AvmIpcBackend { + simulate(inputBuffer: Uint8Array): Promise; + simulateWithHints(inputBuffer: Uint8Array): Promise; + cancel?(): Promise; + destroy?(): Promise; +} + +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 AvmIpcBackend { + private slots: Array = []; + private available: number[] = []; + private waiters: Array<{ resolve: (backend: AvmIpcBackend) => 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 spawn(options: AvmSimulatorPoolOptions): Promise { + return Promise.resolve(new AvmSimulatorPool(options)); + } + + async [Symbol.asyncDispose](): Promise { + await this.destroy(); + } + + async simulate(inputBuffer: Uint8Array): Promise { + const backend = await this.checkout(); + try { + return await backend.simulate(inputBuffer); + } finally { + this.return(backend); + } + } + + async simulateWithHints(inputBuffer: Uint8Array): Promise { + const backend = await this.checkout(); + try { + return await backend.simulateWithHints(inputBuffer); + } finally { + this.return(backend); + } + } + + /** 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'); + } + + /** Check out an AVM backend from the pool. Caller must return() it when done. */ + 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 backend to the pool after use. */ + return(backend: AvmIpcBackend): void { + const waiter = this.waiters.shift(); + if (waiter) { + waiter.resolve(backend); + } else { + const idx = this.slots.indexOf(backend); + if (idx >= 0) { + this.available.push(idx); + } + } + } + + private async createSlot(reuseIdx?: number): Promise { + const backend = 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] = backend; + } else { + this.slots.push(backend); + this.createdCount++; + } + this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`); + return backend; + } +} + +class AvmSimulatorProcess implements AvmIpcBackend { + 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): Promise { + return (await this.service.simulate({ inputs: inputBuffer })).result; + } + + public async simulateWithHints(inputBuffer: Uint8Array): Promise { + return (await this.service.simulateWithHints({ inputs: inputBuffer })).result; + } + + public cancel(): Promise { + this.service.sendProcessSignal('SIGUSR1'); + return Promise.resolve(); + } + + 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..bc847d1c7dae --- /dev/null +++ b/yarn-project/simulator/src/public/cdb_ipc_server.ts @@ -0,0 +1,287 @@ +/** + * UDS server for AVM CDB requests. + * + * Messages use a 4-byte little-endian length prefix followed by msgpack + * payloads. Requests are routed to PublicContractsDB instances by fork ID. + */ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +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 fs from 'node:fs'; +import * as net from 'node:net'; +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: net.Server; + private log: Logger; + /** Maps WSDB fork IDs to contracts DB/timestamp pairs for concurrent simulations. */ + private forks = new Map(); + private connections = new Set(); + + constructor() { + this.log = createLogger('cdb-ipc-server'); + this.ipcPath = path.join(os.tmpdir(), `cdb-ts-${process.pid}-${threadId}-${instanceCounter++}.sock`); + + if (fs.existsSync(this.ipcPath)) { + fs.unlinkSync(this.ipcPath); + } + + this.server = net.createServer(socket => this.handleConnection(socket)); + this.server.listen(this.ipcPath, () => { + this.log.debug(`CDB IPC server listening on ${this.ipcPath}`); + }); + } + + /** 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. */ + close(): Promise { + for (const socket of this.connections) { + socket.destroy(); + } + this.connections.clear(); + + return new Promise(resolve => { + this.server.close(() => { + try { + if (fs.existsSync(this.ipcPath)) { + fs.unlinkSync(this.ipcPath); + } + } catch (err) { + this.log.warn('Failed to remove CDB IPC socket', { err }); + } + resolve(); + }); + }); + } + + async [Symbol.asyncDispose](): Promise { + await this.close(); + } + + private handleConnection(socket: net.Socket) { + this.log.debug('C++ AVM connected to CDB server'); + this.connections.add(socket); + socket.on('close', () => this.connections.delete(socket)); + + let readingLength = true; + const lengthBuffer = Buffer.alloc(4); + let lengthBytesRead = 0; + let messageLength = 0; + let messageBuffer: Buffer | null = null; + let messageBytesRead = 0; + + // Preserve response order even if request dispatch completes out of order. + let responseChain: Promise = Promise.resolve(); + + socket.on('data', (chunk: Buffer) => { + let offset = 0; + + while (offset < chunk.length) { + if (readingLength) { + const bytesNeeded = 4 - lengthBytesRead; + const bytesToCopy = Math.min(bytesNeeded, chunk.length - offset); + chunk.copy(lengthBuffer, lengthBytesRead, offset, offset + bytesToCopy); + lengthBytesRead += bytesToCopy; + offset += bytesToCopy; + + if (lengthBytesRead === 4) { + messageLength = lengthBuffer.readUInt32LE(0); + messageBuffer = Buffer.alloc(messageLength); + messageBytesRead = 0; + readingLength = false; + } + } else { + const bytesNeeded = messageLength - messageBytesRead; + const bytesToCopy = Math.min(bytesNeeded, chunk.length - offset); + chunk.copy(messageBuffer!, messageBytesRead, offset, offset + bytesToCopy); + messageBytesRead += bytesToCopy; + offset += bytesToCopy; + + if (messageBytesRead === messageLength) { + const msg = messageBuffer!; + readingLength = true; + lengthBytesRead = 0; + messageBuffer = null; + + const dispatchResult = handleRequest(this, msg); + const prev = responseChain; + responseChain = (async () => { + await prev; + try { + this.sendResponse(socket, await dispatchResult); + } catch (err: any) { + this.log.error(`CDB command error: ${err.message}`, { err }); + } + })(); + void responseChain.catch(() => {}); + } + } + } + }); + + socket.on('error', (err: Error) => { + this.log.warn('CDB IPC socket error', { err }); + }); + } + + private sendResponse(socket: net.Socket, response: Uint8Array): void { + const lengthBuf = Buffer.alloc(4); + lengthBuf.writeUInt32LE(response.length, 0); + socket.write(lengthBuf); + socket.write(response); + } + + /** 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/public_tx_simulation_tester.ts b/yarn-project/simulator/src/public/fixtures/public_tx_simulation_tester.ts index 11813d24e851..981a2d5f98a5 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,9 +18,10 @@ import { getContractFunctionAbi, getFunctionSelector, } from '../avm/fixtures/utils.js'; +import { type AvmIpcBackend, AvmSimulatorPool } from '../avm_simulator_pool.js'; +import { CdbIpcServer } from '../cdb_ipc_server.js'; import { PublicContractsDB } from '../public_db_sources.js'; import { MeasuredCppPublicTxSimulator } from '../public_tx_simulator/cpp_public_tx_simulator.js'; -import { MeasuredCppVsTsPublicTxSimulator } from '../public_tx_simulator/cpp_vs_ts_public_tx_simulator.js'; import type { MeasuredPublicTxSimulatorInterface } from '../public_tx_simulator/public_tx_simulator_interface.js'; import { TestExecutorMetrics } from '../test_executor_metrics.js'; import { SimpleContractDataSource } from './simple_contract_data_source.js'; @@ -65,7 +66,10 @@ export type MeasuredSimulatorFactory = ( export class PublicTxSimulationTester extends BaseAvmSimulationTester { protected txCount: number = 0; private simulator: MeasuredPublicTxSimulatorInterface; + private currentHandle?: { cancel(waitTimeoutMs?: number): Promise }; private metricsPrefix?: string; + protected avmBackend?: AvmIpcBackend; + protected cdbServer?: CdbIpcServer; constructor( merkleTree: MerkleTreeWriteOperations, @@ -81,7 +85,9 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { if (simulatorFactory) { this.simulator = simulatorFactory(merkleTree, contractsDB, globals, this.metrics, config); } else { - this.simulator = new MeasuredCppPublicTxSimulator(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!; } } @@ -89,15 +95,32 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { worldStateService: NativeWorldStateService, // make sure to close this later globals: GlobalVariables = defaultGlobals(), metrics: TestExecutorMetrics = new TestExecutorMetrics(), - useCppSimulator = false, config: PublicSimulatorConfig = defaultConfig, ): Promise { const contractDataSource = new SimpleContractDataSource(); const merkleTree = await worldStateService.fork(); - const simulatorFactory: MeasuredSimulatorFactory = useCppSimulator - ? (mt, cdb, g, m, c) => new MeasuredCppPublicTxSimulator(mt, cdb, g, m, c) - : (mt, cdb, g, m, c) => new MeasuredCppVsTsPublicTxSimulator(mt, cdb, g, m, c); - return new PublicTxSimulationTester(merkleTree, contractDataSource, globals, metrics, simulatorFactory, config); + + const cdbServer = new CdbIpcServer(); + const forkId = merkleTree.getRevision().forkId; + cdbServer.registerFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + const avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + const simulatorFactory: MeasuredSimulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend, g, m, c, undefined, forkId); + + const tester = new PublicTxSimulationTester( + merkleTree, + contractDataSource, + globals, + metrics, + simulatorFactory, + config, + ); + tester.avmBackend = avmBackend; + tester.cdbServer = cdbServer; + return tester; } public setMetricsPrefix(prefix: string) { @@ -165,7 +188,9 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { 'No simulator configured. Pass a simulatorFactory to the constructor or use PublicTxSimulationTester.create()', ); } - const avmResult = await this.simulator.simulate(tx, fullTxLabel); + const handle = this.simulator.simulate(tx, fullTxLabel); + this.currentHandle = handle; + const avmResult = await handle.result; await this.#recordBytecodeSizes(fullTxLabel, [...setupCalls, ...appCalls, ...(teardownCall ? [teardownCall] : [])]); @@ -198,18 +223,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); } /** @@ -227,7 +242,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall?: TestEnqueuedCall, feePayer?: AztecAddress, privateInsertions?: TestPrivateInsertions, - gasLimits?: Gas, ): Promise { return await this.simulateTxWithLabel( txLabel, @@ -237,7 +251,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall, feePayer, privateInsertions, - gasLimits, ); } @@ -245,6 +258,20 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { this.metrics.prettyPrint(); } + /** Clean up IPC resources (AvmBackend process, CDB server, and merkle tree fork) created by create(). */ + public async close(): Promise { + if (this.avmBackend?.destroy) { + await this.avmBackend.destroy(); + } + if (this.cdbServer) { + await this.cdbServer.close(); + } + // 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. @@ -253,7 +280,7 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { * @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.currentHandle?.cancel(waitTimeoutMs); } /** 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 d55c1c030907..61def61d41a1 100644 --- a/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts +++ b/yarn-project/simulator/src/public/fuzzing/avm_fuzzer_simulator.ts @@ -241,7 +241,7 @@ export class AvmFuzzerSimulator extends BaseAvmSimulationTester { await this.setFeePayerBalance(txHint.feePayer, new Fr(totalFee)); const tx = await createTxFromHint(txHint); - return await this.simulator.simulate(tx); + return await this.simulator.simulate(tx).result; } /** diff --git a/yarn-project/simulator/src/public/index.ts b/yarn-project/simulator/src/public/index.ts index 10182c0eb67f..e688509e855f 100644 --- a/yarn-project/simulator/src/public/index.ts +++ b/yarn-project/simulator/src/public/index.ts @@ -1,11 +1,17 @@ +export { AvmSimulatorPool, type AvmSimulatorPoolOptions } from './avm_simulator_pool.js'; +export { CdbIpcServer } from './cdb_ipc_server.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 AvmIpcBackend, CppPublicTxSimulator, + MeasuredCppPublicTxSimulator, createPublicTxSimulatorForBlockBuilding, - DumpingCppPublicTxSimulator, + PublicTxSimulator, type PublicTxSimulatorInterface, + type SimulationHandle, TelemetryCppPublicTxSimulator, } from './public_tx_simulator/index.js'; export type { PublicTxResult, PublicSimulatorConfig as PublicTxSimulatorConfig } from '@aztec/stdlib/avm'; 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 d8a6353581f3..92e25a9cf00c 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 @@ -12,10 +12,12 @@ import { NativeWorldStateService } from '@aztec/world-state'; import { PublicContractsDB } from '../../../server.js'; import { createContractClassAndInstance } from '../../avm/fixtures/utils.js'; +import { type AvmIpcBackend, AvmSimulatorPool } from '../../avm_simulator_pool.js'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; import { addNewContractClassToTx, addNewContractInstanceToTx, createTxForPrivateOnly } from '../../fixtures/utils.js'; import { CppPublicTxSimulator } from '../../public_tx_simulator/cpp_public_tx_simulator.js'; -import { CppVsTsPublicTxSimulator } from '../../public_tx_simulator/cpp_vs_ts_public_tx_simulator.js'; +import { IpcVsTsPublicTxSimulator } from '../../public_tx_simulator/ipc_vs_ts_public_tx_simulator.js'; import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; import { PublicProcessor } from '../public_processor.js'; @@ -30,6 +32,8 @@ describe.each([ let contractsDB: PublicContractsDB; let tester: PublicTxSimulationTester; let processor: PublicProcessor; + let avmBackend: AvmIpcBackend | undefined; + let cdbServer: CdbIpcServer | undefined; beforeEach(async () => { const globals = GlobalVariables.empty(); @@ -48,11 +52,30 @@ describe.each([ collectStatistics: false, collectCallMetadata: true, }); - // TS mode: use CppVsTs to compare TS and C++ results - // C++ mode: use only C++ (pure Cpp simulator) - const simulator = useCppSimulator - ? new CppPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config) - : new CppVsTsPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + + let simulator; + if (useCppSimulator) { + simulator = new CppPublicTxSimulator(avmBackend, globals, config, undefined, forkId); + } else { + // TS mode: use IpcVsTs to compare TS and IPC C++ results + simulator = new IpcVsTsPublicTxSimulator( + guardedMerkleTrees, + contractsDB, + globals, + avmBackend, + config, + undefined, + forkId, + ); + } processor = new PublicProcessor( globals, @@ -72,6 +95,15 @@ describe.each([ }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; + await tester.close(); await worldStateService.close(); }); 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 2d16e26e602f..000000000000 --- a/yarn-project/simulator/src/public/public_processor/apps_tests/timeout_race.test.ts +++ /dev/null @@ -1,391 +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 { Fr } from '@aztec/foundation/curves/bn254'; -import { createLogger } from '@aztec/foundation/log'; -import { sleep } from '@aztec/foundation/sleep'; -import { TestDateProvider } from '@aztec/foundation/timer'; -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 { Opcode } from '../../avm/serialization/instruction_serialization.js'; -import { deployCustomBytecode } from '../../fixtures/custom_bytecode_tester.js'; -import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; -import { SPAM_CONFIGS, type SpamConfig, createOpcodeSpamBytecode } from '../../fixtures/opcode_spammer.js'; -import { PublicContractsDB } from '../../public_db_sources.js'; -import { CppPublicTxSimulator } from '../../public_tx_simulator/cpp_public_tx_simulator.js'; -import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; -import { PublicProcessor } from '../public_processor.js'; - -/** - * SSTORE spammer - writes to PUBLIC_DATA_TREE. - * Uses single contract with infinite loop (no per-TX limit when writing same slot). - * Provides continuous writes with NO gaps - ideal for race condition detection. - */ -const SSTORE_SPAMMER = SPAM_CONFIGS[Opcode.SSTORE]![0]; // "Same slot (no limit)" variant - -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 SSTORE spamming to keep C++ constantly writing to PUBLIC_DATA_TREE. - * SSTORE "Same slot" has NO per-TX limit, so it writes continuously 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 - * @param spammer - Which spammer config to use (defaults to SSTORE for continuous writes) - */ - async function runRaceConditionTest( - useCancellation: boolean, - numIterations: number, - spamConfig: SpamConfig = SSTORE_SPAMMER, // Default to SSTORE for continuous writes - ): 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 CppPublicTxSimulator(merkleTrees, contractsDB, globals); - - const tester = new PublicTxSimulationTester(merkleTrees, contractDataSource, globals); - await tester.setFeePayerBalance(admin); - - // Deploy spammer contract(s) based on configuration - // Single contract: infinite loop of the target opcode until out of gas - const bytecode = createOpcodeSpamBytecode(spamConfig); - const contract = await deployCustomBytecode(bytecode, tester, `${spamConfig.label!}_Spammer`); - const contractAddress = contract.address; - const callArgs: Fr[] = []; - - 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, args: callArgs }]); - - // 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('CppPublicTxSimulator 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('CppPublicTxSimulator 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 SSTORE spamming to keep C++ constantly writing to PUBLIC_DATA_TREE. - * SSTORE "Same slot" has NO per-TX limit, so it writes continuously without gaps. - * This is more reliable than EMITNULLIFIER which has a cyclic 63-emit-then-REVERT pattern. - * - * 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 - * @param spammer - Which spammer config to use (defaults to SSTORE for continuous writes) - */ - async function runPublicProcessorTimeoutTest( - useCancellation: boolean, - numIterations: number, - spamConfig: SpamConfig = SSTORE_SPAMMER, // Default to SSTORE for continuous writes - ): 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 spammer contract(s) based on configuration - // Single contract: infinite loop of the target opcode until out of gas - const bytecode = createOpcodeSpamBytecode(spamConfig); - const contract = await deployCustomBytecode(bytecode, tester, `${spamConfig.label!}_Spammer`); - const contractAddress = contract.address; - const callArgs: Fr[] = []; - - 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 CppPublicTxSimulator(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, args: callArgs }]); - - // 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 2e10ec87d23e..06076c124190 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 @@ -10,10 +10,12 @@ import { GlobalVariables } from '@aztec/stdlib/tx'; import { getTelemetryClient } from '@aztec/telemetry-client'; import { NativeWorldStateService } from '@aztec/world-state'; +import { type AvmIpcBackend, AvmSimulatorPool } from '../../avm_simulator_pool.js'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { PublicTxSimulationTester, SimpleContractDataSource } from '../../fixtures/index.js'; import { PublicContractsDB } from '../../public_db_sources.js'; import { CppPublicTxSimulator } from '../../public_tx_simulator/cpp_public_tx_simulator.js'; -import { CppVsTsPublicTxSimulator } from '../../public_tx_simulator/cpp_vs_ts_public_tx_simulator.js'; +import { IpcVsTsPublicTxSimulator } from '../../public_tx_simulator/ipc_vs_ts_public_tx_simulator.js'; import { GuardedMerkleTreeOperations } from '../guarded_merkle_tree.js'; import { PublicProcessor } from '../public_processor.js'; @@ -32,6 +34,8 @@ describe.each([ let contractsDB: PublicContractsDB; let tester: PublicTxSimulationTester; let processor: PublicProcessor; + let avmBackend: AvmIpcBackend | undefined; + let cdbServer: CdbIpcServer | undefined; beforeEach(async () => { const globals = GlobalVariables.empty(); @@ -50,11 +54,30 @@ describe.each([ collectStatistics: false, collectCallMetadata: true, }); - // TS mode: use CppVsTs to compare TS and C++ results - // C++ mode: use only C++ (pure Cpp simulator) - const simulator = useCppSimulator - ? new CppPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config) - : new CppVsTsPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + + let simulator; + if (useCppSimulator) { + simulator = new CppPublicTxSimulator(avmBackend, globals, config, undefined, forkId); + } else { + // TS mode: use IpcVsTs to compare TS and IPC C++ results + simulator = new IpcVsTsPublicTxSimulator( + guardedMerkleTrees, + contractsDB, + globals, + avmBackend, + config, + undefined, + forkId, + ); + } processor = new PublicProcessor( globals, @@ -74,6 +97,15 @@ describe.each([ }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts index 8cb98a15451b..bbc78f3d21a7 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.test.ts @@ -17,6 +17,7 @@ import { type MerkleTreeWriteOperations, PublicDataTreeLeaf, PublicDataTreeLeafP import { GlobalVariables, StateReference, Tx, type TxValidator } from '@aztec/stdlib/tx'; import { getTelemetryClient } from '@aztec/telemetry-client'; +import { jest } from '@jest/globals'; import { strict as assert } from 'assert'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -93,9 +94,10 @@ describe('public_processor', () => { merkleTree.getStateReference.mockResolvedValue(stateReference); merkleTree.createCheckpoint.mockResolvedValue(1); - publicTxSimulator.simulate.mockImplementation(() => { - return Promise.resolve(mockedEnqueuedCallsResult); - }); + publicTxSimulator.simulate.mockImplementation(() => ({ + result: Promise.resolve(mockedEnqueuedCallsResult), + cancel: async () => {}, + })); processor = new PublicProcessor( globalVariables, @@ -148,7 +150,13 @@ describe('public_processor', () => { }); it('returns failed txs without aborting entire operation', async function () { - publicTxSimulator.simulate.mockRejectedValue(new Error(`Failed`)); + publicTxSimulator.simulate.mockImplementation(() => { + const result = Promise.resolve().then(() => { + throw new Error(`Failed`); + }); + void result.catch(() => {}); // Prevent unhandled rejection + return { result, cancel: async () => {} }; + }); const tx = await mockTxWithPublicCalls(); const [processed, failed] = await processor.process([tx]); @@ -248,21 +256,24 @@ describe('public_processor', () => { finishSimulation = resolve; }); - publicTxSimulator.simulate.mockImplementation(async () => { - controller.abort(); - await simulationFinished; - return mockedEnqueuedCallsResult; - }); - publicTxSimulator.cancel.mockImplementation(() => { + const cancel = jest.fn(() => { finishSimulation(); return Promise.resolve(); }); + publicTxSimulator.simulate.mockImplementation(() => ({ + result: (async () => { + controller.abort(); + await simulationFinished; + return mockedEnqueuedCallsResult; + })(), + cancel, + })); const [processed, failed] = await processor.process([tx], { signal: controller.signal }); expect(processed).toEqual([]); expect(failed).toEqual([]); - expect(publicTxSimulator.cancel).toHaveBeenCalled(); + expect(cancel).toHaveBeenCalled(); }); // Flakey timing test that's totally dependent on system load/architecture etc. @@ -270,10 +281,13 @@ describe('public_processor', () => { const txs = await timesParallel(3, seed => mockTxWithPublicCalls({ seed })); // The simulator will take 400ms to process each tx - publicTxSimulator.simulate.mockImplementation(async () => { - await sleep(800); - return mockedEnqueuedCallsResult; - }); + publicTxSimulator.simulate.mockImplementation(() => ({ + result: (async () => { + await sleep(800); + return mockedEnqueuedCallsResult; + })(), + cancel: async () => {}, + })); // We allocate a deadline of 2s, so only 2 txs should fit const deadline = new Date(Date.now() + 2000); @@ -349,7 +363,13 @@ describe('public_processor', () => { describe('checkpoint depth', () => { it('calls revertAllCheckpointsTo with depth on tx failure', async function () { merkleTree.createCheckpoint.mockResolvedValue(2); - publicTxSimulator.simulate.mockRejectedValue(new Error('Boom')); + publicTxSimulator.simulate.mockImplementation(() => { + const result = Promise.resolve().then(() => { + throw new Error('Boom'); + }); + void result.catch(() => {}); // Prevent unhandled rejection + return { result, cancel: async () => {} }; + }); const tx = await mockTxWithPublicCalls(); const [processed, failed] = await processor.process([tx]); @@ -402,4 +422,35 @@ describe('public_processor', () => { // On uncaught error, the public processor clears the tx-level cache entirely expect(contractClass).toBeUndefined(); }); + + describe('timeout cancellation', () => { + it('calls cancel on simulation handle when deadline is exceeded', async function () { + let cancelCalled = false; + const cancelFn = () => { + cancelCalled = true; + return Promise.resolve(); + }; + + // Simulate a slow simulation: resolves after 2000ms (will be killed by 100ms timeout) + publicTxSimulator.simulate.mockImplementation(() => { + const result = new Promise(resolve => { + setTimeout(() => resolve(mockedEnqueuedCallsResult), 2000); + }); + return { result, cancel: cancelFn }; + }); + + const tx = await mockTxWithPublicCalls(); + // Set deadline 100ms in the future — simulation takes 2s so timeout wins + const deadline = new Date(Date.now() + 100); + + const [processed] = await processor.process([tx], { deadline }); + + // Simulation should have been cancelled via handle.cancel() + expect(cancelCalled).toBe(true); + // Tx should be dropped (timeout stops processing) + expect(processed).toEqual([]); + // Checkpoints should have been reverted + expect(merkleTree.revertAllCheckpointsTo).toHaveBeenCalled(); + }); + }); }); 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 8a4e9c16c463..80d990d643fb 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -14,6 +14,7 @@ import { type AvmProvingRequest, PublicDataWrite, PublicSimulatorConfig, + type PublicTxResult, } from '@aztec/stdlib/avm'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -50,8 +51,10 @@ import { ForkCheckpoint } from '@aztec/world-state/native'; import { AssertionError } from 'assert'; +import type { CdbIpcServer } from '../cdb_ipc_server.js'; import { PublicContractsDB, PublicTreesDB } from '../public_db_sources.js'; import { + type AvmIpcBackend, type PublicTxSimulatorConfig, type PublicTxSimulatorInterface, TelemetryCppPublicTxSimulator, @@ -66,6 +69,8 @@ export class PublicProcessorFactory { private log: Logger; constructor( private contractDataSource: ContractDataSource, + private avmBackend: AvmIpcBackend, + private cdbServer?: CdbIpcServer, private dateProvider: DateProvider = new DateProvider(), protected telemetryClient: TelemetryClient = getTelemetryClient(), bindings?: LoggerBindings, @@ -74,7 +79,11 @@ export class PublicProcessorFactory { } /** - * Creates a new instance of a PublicProcessor. + * Creates a new instance of a PublicProcessor and registers the fork's contracts DB + * on the CDB server for fork-ID-based request routing. + * + * The caller must call `unregisterFork(forkId)` 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 +94,15 @@ export class PublicProcessorFactory { config: PublicSimulatorConfig, contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()), ): PublicProcessor { + // Register this fork's contracts DB on the CDB server so AVM requests + // carrying this forkId are routed to the correct PublicContractsDB instance. + if (this.cdbServer) { + const forkId = merkleTree.getRevision().forkId; + this.cdbServer.registerFork(forkId, contractsDB, globalVariables.timestamp); + } + const guardedFork = new GuardedMerkleTreeOperations(merkleTree); - const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config); + const publicTxSimulator = this.createPublicTxSimulator(guardedFork, globalVariables, config); return new PublicProcessor( globalVariables, @@ -99,19 +115,25 @@ export class PublicProcessorFactory { ); } + /** Unregister a fork's contracts DB from the CDB server. Call when the fork is closed. */ + unregisterFork(forkId: number): void { + this.cdbServer?.unregisterFork(forkId); + } + protected createPublicTxSimulator( merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, globalVariables: GlobalVariables, config?: Partial, ): PublicTxSimulatorInterface { + const bindings = this.log.getBindings(); + const forkId = merkleTree.getRevision().forkId; return new TelemetryCppPublicTxSimulator( - merkleTree, - contractsDB, + this.avmBackend, globalVariables, this.telemetryClient, config, - this.log.getBindings(), + bindings, + forkId, ); } } @@ -130,16 +152,14 @@ 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. */ export class PublicProcessor implements Traceable { private metrics: PublicProcessorMetrics; + /** Handle for the currently in-flight simulation, used for cancellation on timeout. */ + private currentSimulationHandle?: { cancel(waitTimeoutMs?: number): Promise }; constructor( protected globalVariables: GlobalVariables, private guardedMerkleTree: GuardedMerkleTreeOperations, @@ -326,12 +346,18 @@ 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. - await this.publicTxSimulator.cancel?.(); + 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 transaction executing in an external AVM process. + // Signal cancellation AND WAIT for the simulation to actually stop. + // This is critical because C++ might be in the middle of a slow operation (e.g., pad_trees) + // and won't check the cancellation flag until that operation completes. + // Without waiting, we'd proceed to revert checkpoints while C++ is still writing to state. + // Wait for C++ to stop gracefully. + await this.currentSimulationHandle?.cancel(); // Now stop the guarded fork to prevent any further TS-side access to the world state. await this.guardedMerkleTree.stop(); @@ -595,7 +621,14 @@ export class PublicProcessor implements Traceable { private async processTxWithPublicCalls(tx: Tx): Promise<[ProcessedTx, NestedProcessReturnValues[], DebugLog[]]> { const timer = new Timer(); - const result = await this.publicTxSimulator.simulate(tx); + const handle = this.publicTxSimulator.simulate(tx); + this.currentSimulationHandle = handle; + let result: PublicTxResult; + try { + result = await handle.result; + } finally { + this.currentSimulationHandle = undefined; + } // TODO: use the callStackMetadata here to extract more data about public execution const { hints, publicInputs, publicTxEffect, gasUsed, revertCode /*callStackMetadata*/ } = result; 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 25fd0bef8bf4..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 @@ -6,10 +6,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { ammTest } from '../../fixtures/amm_test.js'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: AMM Contract ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: AMM Contract', () => { const logger = createLogger('public-tx-apps-tests-amm'); let worldStateService: NativeWorldStateService; @@ -17,15 +14,11 @@ describe.each([ beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined); }); 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 0af13f737b10..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,10 +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.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, - ])('Public TX simulator apps tests: gadgets (via $simulatorName)', ({ useCppSimulator }) => { + describe('Public TX simulator apps tests: gadgets', () => { const deployer = AztecAddress.fromNumber(42); let worldStateService: NativeWorldStateService; @@ -20,12 +17,7 @@ describe('Public TX simulator apps tests: gadgets', () => { beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - defaultGlobals(), - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService, defaultGlobals(), /*metrics=*/ undefined); avmGadgetsTestContract = await tester.registerAndDeployContract( /*constructorArgs=*/ [], deployer, @@ -34,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_minimal.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_minimal.test.ts index 4a1e34af1c59..a20c586ee9ef 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_minimal.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/avm_minimal.test.ts @@ -5,13 +5,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { executeAvmMinimalPublicTx } from '../../fixtures/minimal_public_tx.js'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -describe.each([ - // Note: cannot run this for both TS and C++ simulators as they produce different hints! - // TODO(dbanks12): ideally we would TS as well and compare hints to make sure that C++ is strictly a subset of TS hints. - // TS generates extra hints that C++ does not. - //{ useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: AvmMinimalTestContract ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: AvmMinimalTestContract', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; // Make sure we collect hints @@ -30,12 +24,12 @@ describe.each([ worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined, - useCppSimulator, config, ); }); 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 b87691692fd5..b3ce56f1866e 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 @@ -5,10 +5,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { bulkTest } from '../../fixtures/bulk_test.js'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: AvmTestContract ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: AvmTestContract', () => { const logger = createLogger('avm-test-contract-tests'); let worldStateService: NativeWorldStateService; @@ -20,11 +17,11 @@ describe.each([ worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined, - useCppSimulator, ); }); 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 dd3fd8d1ddce..9a8b79d46350 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 @@ -16,6 +16,8 @@ import { mkdirSync, readFileSync, writeFileSync } from 'fs'; import path, { dirname, join } from 'path'; import { fileURLToPath } from 'url'; +import { type AvmIpcBackend, AvmSimulatorPool } from '../../avm_simulator_pool.js'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { ammTest } from '../../fixtures/amm_test.js'; import { bulkTest, megaBulkTest } from '../../fixtures/bulk_test.js'; import { @@ -25,6 +27,7 @@ import { } from '../../fixtures/public_tx_simulation_tester.js'; import { SimpleContractDataSource } from '../../fixtures/simple_contract_data_source.js'; import { tokenTest } from '../../fixtures/token_test.js'; +import { PublicContractsDB } from '../../public_db_sources.js'; import { TestExecutorMetrics } from '../../test_executor_metrics.js'; import { MeasuredCppPublicTxSimulator } from '../cpp_public_tx_simulator.js'; import { MeasuredPublicTxSimulator } from '../measured_public_tx_simulator.js'; @@ -64,19 +67,34 @@ describe('Public TX simulator apps tests: benchmarks', () => { describe('Regular apps and AVM test contract', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; + let avmBackend: AvmIpcBackend | undefined; + let cdbServer: CdbIpcServer | undefined; 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 = useCppSimulator - ? (mt, cdb, g, m, c) => new MeasuredCppPublicTxSimulator(mt, cdb, g, m, c) - : (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); + const globals = defaultGlobals(); + + let simulatorFactory: MeasuredSimulatorFactory; + if (useCppSimulator) { + const forkId = merkleTree.getRevision().forkId; + cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + simulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend!, g, m, c, undefined, forkId); + } else { + simulatorFactory = (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); + } + tester = new PublicTxSimulationTester( merkleTree, contractDataSource, - defaultGlobals(), + globals, metrics, simulatorFactory, config, @@ -84,6 +102,15 @@ describe('Public TX simulator apps tests: benchmarks', () => { }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; + await tester.close(); await worldStateService.close(); }); @@ -205,22 +232,31 @@ describe('Public TX simulator apps tests: benchmarks', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; let avmGadgetsTestContract: ContractInstanceWithAddress; + let avmBackend: AvmIpcBackend | undefined; + let cdbServer: CdbIpcServer | undefined; 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 = useCppSimulator - ? (mt, cdb, g, m, c) => new MeasuredCppPublicTxSimulator(mt, cdb, g, m, c) - : (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); - tester = new PublicTxSimulationTester( - merkleTree, - contractDataSource, - defaultGlobals(), - metrics, - simulatorFactory, - ); + const globals = defaultGlobals(); + + let simulatorFactory: MeasuredSimulatorFactory; + if (useCppSimulator) { + const forkId = merkleTree.getRevision().forkId; + cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + simulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend!, g, m, c, undefined, forkId); + } else { + simulatorFactory = (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); + } + + tester = new PublicTxSimulationTester(merkleTree, contractDataSource, globals, metrics, simulatorFactory); avmGadgetsTestContract = await tester.registerAndDeployContract( /*constructorArgs=*/ [], deployer, @@ -230,6 +266,15 @@ describe('Public TX simulator apps tests: benchmarks', () => { }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; + 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 12cd90a8b4ac..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; @@ -13,12 +13,7 @@ describe('C++ Exception Handling during Public Tx Simulation', () => { beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - /*useCppSimulator=*/ true, // Use C++ simulator - ); + tester = await PublicTxSimulationTester.create(worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined); avmTestContractInstance = await tester.registerAndDeployContract( /*constructorArgs=*/ [], /*deployer=*/ AztecAddress.fromNumber(420), @@ -27,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, @@ -47,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/custom_bc.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/custom_bc.test.ts index b3d61ec8d66e..789cba4207f7 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/custom_bc.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/custom_bc.test.ts @@ -18,24 +18,17 @@ import { } from '../../fixtures/custom_bytecode_tests.js'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: custom bytecodes unhappy paths ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: custom bytecodes unhappy paths', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined); }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -65,24 +58,17 @@ describe.each([ }); }); -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: bytecode flow unhappy paths ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: bytecode flow unhappy paths', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined); }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); @@ -117,24 +103,17 @@ describe.each([ }); }); -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: custom bytecodes truncation ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: custom bytecodes truncation', () => { let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService); }); afterEach(async () => { + await tester.close(); await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts index 9dfe67395332..29fa507ea7c3 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/apps_tests/opcode_spam.test.ts @@ -5,6 +5,8 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { mkdirSync, writeFileSync } from 'fs'; import path from 'path'; +import { type AvmIpcBackend, AvmSimulatorPool } from '../../avm_simulator_pool.js'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { getSpamConfigsPerOpcode, testOpcodeSpamCase } from '../../fixtures/opcode_spammer.js'; import { type MeasuredSimulatorFactory, @@ -12,9 +14,10 @@ import { defaultGlobals, } from '../../fixtures/public_tx_simulation_tester.js'; import { SimpleContractDataSource } from '../../fixtures/simple_contract_data_source.js'; +import { PublicContractsDB } from '../../public_db_sources.js'; import { TestExecutorMetrics } from '../../test_executor_metrics.js'; import { MeasuredCppPublicTxSimulator } from '../cpp_public_tx_simulator.js'; -import { MeasuredCppVsTsPublicTxSimulator } from '../cpp_vs_ts_public_tx_simulator.js'; +import { MeasuredPublicTxSimulator } from '../measured_public_tx_simulator.js'; // NOTE: This test is meant to be run for benchmarking. Set RUN_AVM_OPCODE_SPAM=1 to enable. const describeOrSkip = process.env.RUN_AVM_OPCODE_SPAM ? describe : describe.skip; @@ -66,35 +69,53 @@ describeOrSkip('Opcode Spammer Benchmarks', () => { }); describe.each([ - // NOTE: Cpp vs TS simulation is very slow (because TS is slow), so we skip it by default. + // NOTE: IpcVsTs simulation is very slow (because TS is slow), so we skip it by default. // It is useful to manually run to make sure these tests perform identically between simulators. - //{ useCppSimulator: false, simulatorName: 'CppVsTs' }, + //{ useCppSimulator: false, simulatorName: 'IpcVsTs' }, { useCppSimulator: true, simulatorName: 'Cpp' }, ])('($simulatorName) Simulator', ({ useCppSimulator, simulatorName }) => { const metricsPrefix = simulatorName; let worldStateService: NativeWorldStateService; let tester: PublicTxSimulationTester; + let avmBackend: AvmIpcBackend | undefined; + let cdbServer: CdbIpcServer | undefined; beforeEach(async () => { worldStateService = await NativeWorldStateService.tmp(); const contractDataSource = new SimpleContractDataSource(); const merkleTree = await worldStateService.fork(); - const simulatorFactory: MeasuredSimulatorFactory = useCppSimulator - ? (mt, cdb, g, m, c) => new MeasuredCppPublicTxSimulator(mt, cdb, g, m, c) - : (mt, cdb, g, m, c) => new MeasuredCppVsTsPublicTxSimulator(mt, cdb, g, m, c); - tester = new PublicTxSimulationTester( - merkleTree, - contractDataSource, - defaultGlobals(), - metrics, - simulatorFactory, - config, - ); + const globals = defaultGlobals(); + + let simulatorFactory: MeasuredSimulatorFactory; + if (useCppSimulator) { + const forkId = merkleTree.getRevision().forkId; + cdbServer = new CdbIpcServer(); + cdbServer.registerFork(forkId, new PublicContractsDB(contractDataSource), globals.timestamp); + avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateService.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + simulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend!, g, m, c, undefined, forkId); + } else { + simulatorFactory = (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); + } + + tester = new PublicTxSimulationTester(merkleTree, contractDataSource, globals, metrics, simulatorFactory, config); tester.setMetricsPrefix(`${metricsPrefix} Opcode Spam`); }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; + await tester.close(); await worldStateService.close(); }); 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 2ba3b3d1a006..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 @@ -5,10 +5,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { PublicTxSimulationTester } from '../../fixtures/public_tx_simulation_tester.js'; import { tokenTest } from '../../fixtures/token_test.js'; -describe.each([ - { useCppSimulator: false, simulatorName: 'TS Simulator' }, - { useCppSimulator: true, simulatorName: 'Cpp Simulator' }, -])('Public TX simulator apps tests: TokenContract ($simulatorName)', ({ useCppSimulator }) => { +describe('Public TX simulator apps tests: TokenContract', () => { const logger = createLogger('public-tx-apps-tests-token'); let worldStateService: NativeWorldStateService; @@ -16,15 +13,11 @@ describe.each([ beforeAll(async () => { worldStateService = await NativeWorldStateService.tmp(); - tester = await PublicTxSimulationTester.create( - worldStateService, - /*globals=*/ undefined, - /*metrics=*/ undefined, - useCppSimulator, - ); + tester = await PublicTxSimulationTester.create(worldStateService, /*globals=*/ undefined, /*metrics=*/ undefined); }); 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/cpp_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator.ts index 69c73ee73867..e3e0fa7a5a65 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator.ts @@ -1,201 +1,148 @@ 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, - type PublicSimulatorConfig, + PublicSimulatorConfig, PublicTxResult, 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 AvmIpcBackend, AvmSimulatorPool } from '../avm_simulator_pool.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 { PublicTxSimulator } from './public_tx_simulator.js'; import type { MeasuredPublicTxSimulatorInterface, PublicTxSimulatorInterface, + SimulationHandle, } from './public_tx_simulator_interface.js'; /** - * C++ implementation of PublicTxSimulator using the C++ 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. + * IPC-based C++ implementation of PublicTxSimulator. + * Communicates with an AvmIpcBackend (single process or pool) over IPC. + * The AVM binary connects directly to WSDB and CDB — no merkle tree + * or contract DB references needed here. CDB routing uses the fork ID. */ -export class CppPublicTxSimulator extends PublicTxSimulator implements PublicTxSimulatorInterface { - protected override log: Logger; - /** Current cancellation token for in-flight simulation. */ - private cancellationToken?: CancellationToken; - /** Current simulation promise, used to wait for completion after cancellation. */ - private simulationPromise?: Promise; +export class CppPublicTxSimulator implements PublicTxSimulatorInterface { + protected log: Logger; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, - globalVariables: GlobalVariables, - config?: Partial, + private avmBackend: AvmIpcBackend, + private globalVariables: GlobalVariables, + private config: Partial = {}, bindings?: LoggerBindings, + private wsdbForkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, undefined, bindings); - this.log = createLogger(`simulator:cpp_public_tx_simulator`, bindings); + this.log = createLogger('simulator:cpp_public_tx_simulator', bindings); } - /** - * Simulate a transaction's public portion using the C++ avvm simulator. - * - * @param tx - The transaction to simulate. - * @returns The result of the transaction's public execution. - */ - public override 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, - }); + public simulate(tx: Tx): SimulationHandle { + if (this.avmBackend instanceof AvmSimulatorPool) { + return this.simulateWithPool(tx, this.avmBackend); + } + const result = this.doSimulate(tx); + return { result, cancel: async () => {} }; + } - const wsRevision = this.merkleTree.getRevision(); - const wsdbIpcPath = this.merkleTree.getIpcPath(); + private simulateWithPool(tx: Tx, pool: AvmSimulatorPool): SimulationHandle { + const backendPromise = pool.checkout(); + const result = backendPromise.then(b => this.doSimulate(tx, b)); + // Return slot to pool when done (success or error) + void result + .finally(() => { + void backendPromise.then(b => pool.return(b)).catch(() => {}); + }) + .catch(() => {}); + + return { + result, + cancel: async (waitTimeoutMs = 100) => { + const b = await backendPromise; + await b.cancel?.(); + await Promise.race([result.catch(() => {}), sleep(waitTimeoutMs)]); + }, + }; + } - this.log.trace(`Running C++ simulation with world state revision ${JSON.stringify(wsRevision)}`); + protected async doSimulate(tx: Tx, backend?: AvmIpcBackend): Promise { + const effectiveBackend = backend ?? this.avmBackend; + const txHash = tx.getTxHash(); + this.log.debug(`IPC simulation for tx ${txHash}, wsdbForkId=${this.wsdbForkId ?? 0}`); - // Create the fast simulation inputs const txHint = AvmTxHint.fromTx(tx, this.globalVariables.gasFees); const protocolContracts = ProtocolContractsList; const fastSimInputs = new AvmFastSimulationInputs( - wsRevision, - this.config, + // 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.wsdbForkId ?? 0, blockNumber: WorldStateRevision.LATEST, includeUncommitted: true }, + PublicSimulatorConfig.from(this.config ?? {}), txHint, this.globalVariables, 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; + let resultBuffer: Uint8Array; try { - resultBuffer = await this.simulationPromise; + resultBuffer = await effectiveBackend.simulate(fastSimInputs.serializeWithMessagePack()); } catch (error: any) { - // Check if this was a cancellation - if (error.message?.includes('Simulation cancelled')) { - throw new SimulationError(`C++ simulation cancelled`, []); - } - throw new SimulationError(`C++ simulation failed: ${error.message}`, []); - } finally { - this.cancellationToken = undefined; - this.simulationPromise = undefined; - } - - // If we've reached this point, C++ succeeded during simulation, - - // 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}`, { - txHash, - reverted: !cppResult.revertCode.isOK(), - cppGasUsed: cppResult.gasUsed.totalGas.l2Gas, - }); - - return cppResult; - } - - /** - * 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. - * - * @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. - * 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); + throw new SimulationError(`IPC AVM simulation failed: ${error.message}`, []); } - // 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`); - await Promise.race([ - this.simulationPromise.catch(() => {}), // Ignore rejection, just wait for completion - sleep(waitTimeoutMs), - ]); - this.log.debug('C++ simulation stopped or wait timed out'); - } + const cppResultJSON: object = deserializeFromMessagePack(Buffer.from(resultBuffer)); + return PublicTxResult.fromPlainObject(cppResultJSON); } } +/** C++ public tx simulator with metrics recording. */ export class MeasuredCppPublicTxSimulator extends CppPublicTxSimulator implements MeasuredPublicTxSimulatorInterface { constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmBackend: AvmIpcBackend, globalVariables: GlobalVariables, protected readonly metrics: ExecutorMetricsInterface, config?: Partial, bindings?: LoggerBindings, + wsdbForkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); + super(avmBackend, globalVariables, config, bindings, wsdbForkId); } - public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { + public override simulate(tx: Tx, txLabel: string = 'unlabeledTx'): SimulationHandle { + const handle = super.simulate(tx); this.metrics.startRecordingTxSimulation(txLabel); - let result: PublicTxResult | undefined; - try { - result = await super.simulate(tx); - } finally { - this.metrics.stopRecordingTxSimulation(txLabel, result?.gasUsed, result?.revertCode); - } - return result; + const result = handle.result + .then(r => { + this.metrics.stopRecordingTxSimulation(txLabel, r?.gasUsed, r?.revertCode); + return r; + }) + .catch(err => { + this.metrics.stopRecordingTxSimulation(txLabel, undefined, undefined); + throw err; + }); + return { result, cancel: handle.cancel }; } } -/** - * A C++ public tx simulator that tracks runtime/production metrics with telemetry. - */ +/** C++ public tx simulator with telemetry. */ export class TelemetryCppPublicTxSimulator extends MeasuredCppPublicTxSimulator { - /* tracer needed by trackSpans */ public readonly tracer: Tracer; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmBackend: AvmIpcBackend, globalVariables: GlobalVariables, telemetryClient: TelemetryClient = getTelemetryClient(), config?: Partial, bindings?: LoggerBindings, + wsdbForkId?: number, ) { const metrics = new ExecutorMetrics(telemetryClient, 'CppPublicTxSimulator'); - super(merkleTree, contractsDB, globalVariables, metrics, config, bindings); + super(avmBackend, globalVariables, metrics, config, bindings, wsdbForkId); this.tracer = metrics.tracer; } } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator_with_hinted_dbs.ts b/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator_with_hinted_dbs.ts deleted file mode 100644 index 7b979ffde784..000000000000 --- a/yarn-project/simulator/src/public/public_tx_simulator/cpp_public_tx_simulator_with_hinted_dbs.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; -import { avmSimulateWithHintedDbs } from '@aztec/native'; -import { - AvmCircuitInputs, - type PublicSimulatorConfig, - PublicTxResult, - 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 { strict as assert } from 'assert'; - -import type { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; -import type { PublicContractsDB } from '../public_db_sources.js'; -import { PublicTxSimulator } from './public_tx_simulator.js'; -import type { - MeasuredPublicTxSimulatorInterface, - PublicTxSimulatorInterface, -} from './public_tx_simulator_interface.js'; - -/** - * C++ implementation of PublicTxSimulator using pre-collected hints. - * This implementation runs TS simulation first to collect all hints, - * then passes the complete AvmCircuitInputs (hints + public inputs) - * to C++ to run hinted simulation. - */ -export class CppPublicTxSimulatorHintedDbs extends PublicTxSimulator implements PublicTxSimulatorInterface { - protected override log: Logger; - - constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, - globalVariables: GlobalVariables, - config?: Partial, - bindings?: LoggerBindings, - ) { - super(merkleTree, contractsDB, globalVariables, config, undefined, bindings); - this.log = createLogger(`simulator:cpp_public_tx_simulator_hinted_dbs`, bindings); - } - - /** - * Simulate a transaction's public portion using the C++ vm2 simulator with hinted DBs. - * - * This implementation: - * 1. Runs the full TypeScript simulation to generate AvmCircuitInputs (hints + public inputs) - * 2. Passes the complete AvmCircuitInputs to C++ to run hinted simulation - * - * @param tx - The transaction to simulate. - * @returns The result of the transaction's public execution. - */ - public override async simulate(tx: Tx): Promise { - const txHash = this.computeTxHash(tx); - this.log.debug(`C++ hinted DB simulation of ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}`, { - txHash, - }); - - // First, run TS simulation to generate hints and public inputs - this.log.debug(`Running TS simulation for tx ${txHash}`); - - // Run the full TypeScript simulation using the parent class - // This will modify the merkle tree with the transaction's state changes - const tsResult = await super.simulate(tx); - this.log.debug(`TS simulation succeeded for tx ${txHash}`); - - // Extract the full AvmCircuitInputs from the TS result - const avmCircuitInputs = new AvmCircuitInputs(tsResult.hints!, tsResult.publicInputs!); - - // Second, run C++ simulation with hinted DBs - this.log.debug(`Running C++ simulation with hinted DBs for tx ${txHash}`); - - // Serialize to msgpack and call the C++ simulator - const inputBuffer = avmCircuitInputs.serializeWithMessagePack(); - - let resultBuffer: Buffer; - try { - resultBuffer = await avmSimulateWithHintedDbs(inputBuffer, this.log.level); - } catch (error: any) { - throw new SimulationError(`C++ hinted simulation failed: ${error.message}`, []); - } - - // Deserialize the msgpack result - const cppResultJSON: object = deserializeFromMessagePack(resultBuffer); - const cppResult = PublicTxResult.fromPlainObject(cppResultJSON); - - assert(cppResult.revertCode.equals(tsResult.revertCode)); - assert(cppResult.gasUsed.totalGas.equals(tsResult.gasUsed.totalGas)); - - this.log.debug(`C++ hinted simulation completed for tx ${txHash}`, { - txHash, - reverted: !tsResult.revertCode.isOK(), - tsGasUsed: tsResult.gasUsed.totalGas.l2Gas, - cppGasUsed: tsResult.gasUsed.totalGas.l2Gas, - }); - - // TODO(fcarreiro): complete this. - return tsResult; - } -} - -/** - * Class to record metrics for simulation. - * - * Note(dbanks12): We might not be able to collect all the same metrics in C++ as we do in TS! - * Unless we move some of the metrics collection to C++, we don't have inner functions exposed - * to TS for tracking. - */ -export class MeasuredCppPublicTxSimulatorHintedDbs - extends CppPublicTxSimulatorHintedDbs - implements MeasuredPublicTxSimulatorInterface -{ - constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, - globalVariables: GlobalVariables, - protected readonly metrics: ExecutorMetricsInterface, - config?: Partial, - bindings?: LoggerBindings, - ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); - } - - public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { - this.metrics.startRecordingTxSimulation(txLabel); - let result: PublicTxResult | undefined; - try { - result = await super.simulate(tx); - } finally { - this.metrics.stopRecordingTxSimulation(txLabel, result?.gasUsed, result?.revertCode); - } - return result; - } -} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts deleted file mode 100644 index ba8b620e4afb..000000000000 --- a/yarn-project/simulator/src/public/public_tx_simulator/cpp_vs_ts_public_tx_simulator.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { type Logger, type LoggerBindings, createLogger, logLevel } from '@aztec/foundation/log'; -import { avmSimulate } from '@aztec/native'; -import { ProtocolContractsList } from '@aztec/protocol-contracts'; -import { - AvmFastSimulationInputs, - AvmTxHint, - type PublicSimulatorConfig, - PublicTxResult, - deserializeFromMessagePack, -} from '@aztec/stdlib/avm'; -import { SimulationError } from '@aztec/stdlib/errors'; -import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; -import type { GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx'; - -import { strict as assert } from 'assert'; - -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 { PublicTxSimulator } from './public_tx_simulator.js'; -import type { - MeasuredPublicTxSimulatorInterface, - PublicTxSimulatorInterface, -} from './public_tx_simulator_interface.js'; - -/** - * An implementation of PublicTxSimulator that first simulates in C++, then TS, an compares the results. - * 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. - */ -export class CppVsTsPublicTxSimulator extends PublicTxSimulator implements PublicTxSimulatorInterface { - protected override log: Logger; - - constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, - globalVariables: GlobalVariables, - config?: Partial, - bindings?: LoggerBindings, - ) { - super(merkleTree, contractsDB, globalVariables, config, undefined, bindings); - this.log = createLogger(`simulator:cpp_vs_public_tx_simulator`, bindings); - } - - /** - * Simulate a transaction's public portion using the C++ avvm simulator. - * - * @param tx - The transaction to simulate. - * @returns The result of the transaction's public execution. - */ - public override 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, - }); - - // Run TS simulation to generate hints and public inputs - this.log.debug(`Running TS simulation for tx ${txHash}`); - - // create checkpoint for ws - let tsResult: PublicTxResult | undefined; - let tsStateRef: StateReference | undefined; - await this.merkleTree.createCheckpoint(); - this.contractsDB.createCheckpoint(); - try { - // Run the full TypeScript simulation using the parent class - // This will modify the merkle tree with the transaction's state changes - tsResult = await super.simulate(tx); - this.log.debug(`TS simulation completed for tx ${txHash}`); - - tsStateRef = await this.merkleTree.getStateReference(); // capture tree roots for later comparsion - } catch (error: any) { - this.log.warn(`TS simulation failed, but still continuing with C++ simulation: ${error.message} ${error.stack}`); - } finally { - // revert checkpoint for ws and clear contract db changes - // (cpp should reapply exactly the same changes if there are no bugs) - await this.merkleTree.revertCheckpoint(); - this.contractsDB.revertCheckpoint(); - } - - this.log.debug(`Running C++ simulation for tx ${txHash}`); - - const wsRevision = this.merkleTree.getRevision(); - const wsdbIpcPath = this.merkleTree.getIpcPath(); - - this.log.debug(`Running C++ simulation with world state revision ${JSON.stringify(wsRevision)}`); - - // Create the fast simulation inputs - const txHint = AvmTxHint.fromTx(tx, this.globalVariables.gasFees); - const protocolContracts = ProtocolContractsList; - const fastSimInputs = new AvmFastSimulationInputs( - wsRevision, - this.config, - txHint, - this.globalVariables, - 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.debug(`Serializing fast simulation inputs to msgpack...`); - const inputBuffer = fastSimInputs.serializeWithMessagePack(); - - let resultBuffer: Buffer; - try { - this.log.debug(`Calling C++ simulator for tx ${txHash}`); - resultBuffer = await avmSimulate(inputBuffer, contractProvider, wsdbIpcPath, logLevel); - } catch (error: any) { - throw new SimulationError(`C++ simulation failed: ${error.message}`, []); - } - - // If we've reached this point, C++ succeeded during simulation, - // so we assert that TS also succeeded. - assert(tsResult !== undefined, 'TS simulation should have succeeded if C++ succeeded'); - assert(tsStateRef !== undefined, 'TS state reference should have been captured if C++ succeeded'); - - // Deserialize the msgpack result - this.log.debug(`Deserializing C++ from buffer (size: ${resultBuffer.length})...`); - const cppResultJSON: object = deserializeFromMessagePack(resultBuffer); - this.log.debug(`Deserializing C++ result to PublicTxResult...`); - const cppResult = PublicTxResult.fromPlainObject(cppResultJSON); - this.log.debug(`Done.`); - assert(cppResult.revertCode.equals(tsResult.revertCode)); - assert(cppResult.gasUsed.totalGas.equals(tsResult.gasUsed.totalGas)); - assert(cppResult.gasUsed.publicGas.equals(tsResult.gasUsed.publicGas)); - assert(cppResult.gasUsed.teardownGas.equals(tsResult.gasUsed.teardownGas)); - assert(cppResult.gasUsed.billedGas.equals(tsResult.gasUsed.billedGas)); - assert(cppResult.publicTxEffect.equals(tsResult.publicTxEffect)); - if (cppResult.publicInputs !== undefined) { - assert(cppResult.publicInputs!.toBuffer().equals(tsResult.publicInputs!.toBuffer())); - } - - // TODO(fcarreiro): complete this. - // Check that C++ hints are a strict subset of TS hints. - // Then enable for misc tests and validate hints. - //if (this.config?.collectHints) { - //} - - if (this.config?.collectCallMetadata) { - assert(cppResult.getAppLogicReturnValues().length === tsResult.getAppLogicReturnValues().length); - for (let i = 0; i < cppResult.getAppLogicReturnValues().length; i++) { - assert(cppResult.getAppLogicReturnValues()[i].equals(tsResult.getAppLogicReturnValues()[i])); - } - } - // Messages are still not ok for exceptional halts (they are not plumbed in C++). - const cppRevertReason = cppResult.findRevertReason() || {}; - const tsRevertReason = tsResult.findRevertReason() || {}; - const cppRevertReasonAsObject = JSON.parse(JSON.stringify(cppRevertReason)); - const tsRevertReasonAsObject = JSON.parse(JSON.stringify(tsRevertReason)); - if (JSON.stringify(cppRevertReasonAsObject) !== JSON.stringify(tsRevertReasonAsObject)) { - this.log.debug('cppResult.findRevertReason()', cppRevertReasonAsObject); - this.log.debug('tsResult.findRevertReason()', tsRevertReasonAsObject); - } - // TODO: dont compare the strings since this is not deterministic. - - // Sometimes error messages are different between C++ and TS, so we omit in the default comparison - const cppRevertReasonWithoutMessage = { ...cppRevertReasonAsObject, originalMessage: undefined }; - const tsRevertReasonWithoutMessage = { ...tsRevertReasonAsObject, originalMessage: undefined }; - assert(JSON.stringify(cppRevertReasonWithoutMessage) === JSON.stringify(tsRevertReasonWithoutMessage)); - - const cppHasRevertMessage = - cppRevertReasonAsObject.originalMessage && cppRevertReasonAsObject.originalMessage.length > 0; - const tsHasRevertMessage = - tsRevertReasonAsObject.originalMessage && tsRevertReasonAsObject.originalMessage.length > 0; - // assert that if one of the error messages is non-empty, the other is - assert( - cppHasRevertMessage === tsHasRevertMessage, - 'One of the AVM simulators (C++ or TS) produced a revert message, but the other did not', - ); - // Ideally, we'd love to be able to compare full error messages, but without a lot of work - // the two simulators will always be able to produce some differing errors. - // Commenting out the code below will enforce that the error messages are at least - // similar (one contains the other). Even this is not something we can guarantee. - //if (cppHasRevertMessage) { - // const cppRevertMessageContainsTs = cppRevertReasonAsObject.originalMessage.includes( - // tsRevertReasonAsObject.originalMessage, - // ); - // const tsRevertMessageContainsCpp = tsRevertReasonAsObject.originalMessage.includes( - // cppRevertReasonAsObject.originalMessage, - // ); - // assert( - // cppRevertMessageContainsTs || tsRevertMessageContainsCpp, - // 'The AVM simulators (C++ and TS) produced different revert messages (neither was a substring of the other)', - // ); - //} - - // Confirm that tree roots match - const cppStateRef = await this.merkleTree.getStateReference(); - assert( - cppStateRef.equals(tsStateRef), - `Tree roots mismatch between TS and C++ public simulations for tx ${txHash}`, - ); - - this.log.debug(`C++ simulation completed for tx ${txHash}`, { - txHash, - reverted: !cppResult.revertCode.isOK(), - cppGasUsed: cppResult.gasUsed.totalGas.l2Gas, - }); - - // Return cpp result as it has more detailed metadata / revert reasons - return cppResult; - } -} - -export class MeasuredCppVsTsPublicTxSimulator - extends CppVsTsPublicTxSimulator - implements MeasuredPublicTxSimulatorInterface -{ - constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, - globalVariables: GlobalVariables, - protected readonly metrics: ExecutorMetricsInterface, - config?: Partial, - bindings?: LoggerBindings, - ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); - } - - public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { - this.metrics.startRecordingTxSimulation(txLabel); - let result: PublicTxResult | undefined; - try { - result = await super.simulate(tx); - } finally { - this.metrics.stopRecordingTxSimulation(txLabel, result?.gasUsed, result?.revertCode); - } - return result; - } -} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/dumping_cpp_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/dumping_cpp_public_tx_simulator.ts index 1915575ae26d..696fd9e98324 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/dumping_cpp_public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/dumping_cpp_public_tx_simulator.ts @@ -4,80 +4,66 @@ 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 { AvmIpcBackend } from '../avm_simulator_pool.js'; import { CppPublicTxSimulator } from './cpp_public_tx_simulator.js'; +import type { SimulationHandle } from './public_tx_simulator_interface.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. + * IPC-based C++ public tx simulator that dumps AVM circuit inputs to disk after simulation. + * Used during nightly CI runs to collect circuit inputs for AVM proving benchmarks. */ export class DumpingCppPublicTxSimulator extends CppPublicTxSimulator { private readonly outputDir: string; constructor( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmBackend: AvmIpcBackend, globalVariables: GlobalVariables, config: Partial, outputDir: string, bindings?: LoggerBindings, + wsdbForkId?: number, ) { - super(merkleTree, contractsDB, globalVariables, config, bindings); + super(avmBackend, globalVariables, config, bindings, wsdbForkId); 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; } - 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); - - return result; + public override simulate(tx: Tx): SimulationHandle { + const handle = super.simulate(tx); + const result = handle.result.then(r => { + this.dumpAvmCircuitInputs(r, tx.getTxHash().toString()); + return r; + }); + return { result, cancel: handle.cancel }; } - /** - * 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 8d1c29746334..d6144b9b5976 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 { AvmIpcBackend } from '../avm_simulator_pool.js'; import { TelemetryCppPublicTxSimulator } from './cpp_public_tx_simulator.js'; import { DumpingCppPublicTxSimulator } from './dumping_cpp_public_tx_simulator.js'; /** - * Creates a public tx simulator for block building. - * Uses DumpingCppPublicTxSimulator 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 DumpingCppPublicTxSimulator if DUMP_AVM_INPUTS_TO_DIR env var is set (for CI/testing AVM circuit), * otherwise uses TelemetryCppPublicTxSimulator (for production). */ export function createPublicTxSimulatorForBlockBuilding( - merkleTree: MerkleTreeWriteOperations, - contractsDB: PublicContractsDB, + avmBackend: AvmIpcBackend, globalVariables: GlobalVariables, telemetryClient: TelemetryClient, bindings?: LoggerBindings, + wsdbForkId?: number, collectDebugLogs = false, ) { const config = PublicSimulatorConfig.from({ @@ -32,13 +31,9 @@ export function createPublicTxSimulatorForBlockBuilding( const dumpDir = process.env.DUMP_AVM_INPUTS_TO_DIR; if (dumpDir) { - // must collect hints and PIs for dumping - const dumpingConfig = { - ...config, - collectHints: true, - collectPublicInputs: true, - }; - return new DumpingCppPublicTxSimulator(merkleTree, contractsDB, globalVariables, dumpingConfig, dumpDir, bindings); + const dumpingConfig = { ...config, collectHints: true, collectPublicInputs: true }; + return new DumpingCppPublicTxSimulator(avmBackend, globalVariables, dumpingConfig, dumpDir, bindings, wsdbForkId); } - return new TelemetryCppPublicTxSimulator(merkleTree, contractsDB, globalVariables, telemetryClient, config, bindings); + + return new TelemetryCppPublicTxSimulator(avmBackend, globalVariables, telemetryClient, config, bindings, wsdbForkId); } 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 fa7d44e03264..5ea6b796b20f 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/index.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/index.ts @@ -1,7 +1,13 @@ export * from './public_tx_simulator.js'; -export { CppPublicTxSimulator, TelemetryCppPublicTxSimulator } from './cpp_public_tx_simulator.js'; +export { + CppPublicTxSimulator, + MeasuredCppPublicTxSimulator, + TelemetryCppPublicTxSimulator, +} from './cpp_public_tx_simulator.js'; export { DumpingCppPublicTxSimulator } from './dumping_cpp_public_tx_simulator.js'; +export { IpcVsTsPublicTxSimulator, MeasuredIpcVsTsPublicTxSimulator } from './ipc_vs_ts_public_tx_simulator.js'; export { createPublicTxSimulatorForBlockBuilding } from './factories.js'; -export type { PublicTxSimulatorInterface } from './public_tx_simulator_interface.js'; +export type { AvmIpcBackend } from '../avm_simulator_pool.js'; +export type { PublicTxSimulatorInterface, SimulationHandle } from './public_tx_simulator_interface.js'; export { TelemetryPublicTxSimulator } from './telemetry_public_tx_simulator.js'; export type { PublicTxResult, PublicSimulatorConfig as PublicTxSimulatorConfig } from '@aztec/stdlib/avm'; diff --git a/yarn-project/simulator/src/public/public_tx_simulator/ipc_vs_ts_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/ipc_vs_ts_public_tx_simulator.ts new file mode 100644 index 000000000000..f3bef53a254b --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/ipc_vs_ts_public_tx_simulator.ts @@ -0,0 +1,177 @@ +import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; +import { type PublicSimulatorConfig, PublicTxResult } from '@aztec/stdlib/avm'; +import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; +import type { GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx'; + +import { strict as assert } from 'assert'; + +import type { AvmIpcBackend } from '../avm_simulator_pool.js'; +import type { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; +import { CppPublicTxSimulator } from './cpp_public_tx_simulator.js'; +import { PublicTxSimulator } from './public_tx_simulator.js'; +import type { + MeasuredPublicTxSimulatorInterface, + PublicTxSimulatorInterface, + SimulationHandle, +} from './public_tx_simulator_interface.js'; + +/** + * An implementation of PublicTxSimulator that first simulates in C++ (via IPC), then TS, and compares the results. + * The C++ simulator runs in an external bb-avm-sim process and accesses world state via WSDB IPC. + * The TS simulator runs in-process and accesses world state directly. + */ +export class IpcVsTsPublicTxSimulator extends PublicTxSimulator implements PublicTxSimulatorInterface { + protected override log: Logger; + private cppSimulator: CppPublicTxSimulator; + + constructor( + merkleTree: MerkleTreeWriteOperations, + contractsDB: PublicContractsDB, + globalVariables: GlobalVariables, + avmBackend: AvmIpcBackend, + config?: Partial, + bindings?: LoggerBindings, + wsdbForkId?: number, + ) { + super(merkleTree, contractsDB, globalVariables, config, undefined, bindings); + this.log = createLogger('simulator:ipc_vs_ts_public_tx_simulator', bindings); + this.cppSimulator = new CppPublicTxSimulator(avmBackend, globalVariables, config, bindings, wsdbForkId); + } + + /** + * Simulate a transaction's public portion using both C++ (IPC) and TS simulators, then compare results. + * + * @param tx - The transaction to simulate. + * @returns A SimulationHandle with the result of the C++ simulation (after verifying parity with TS). + */ + public override simulate(tx: Tx): SimulationHandle { + const result = this.doCompare(tx); + return { result, cancel: async () => {} }; + } + + private async doCompare(tx: Tx): Promise { + const txHash = this.computeTxHash(tx); + this.log.debug(`IPC vs TS simulation of ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}`, { + txHash, + }); + + // Run TS simulation first (with checkpoint so we can revert) + this.log.debug(`Running TS simulation for tx ${txHash}`); + let tsResult: PublicTxResult | undefined; + let tsStateRef: StateReference | undefined; + await this.merkleTree.createCheckpoint(); + this.contractsDB.createCheckpoint(); + try { + tsResult = await super.simulate(tx).result; + this.log.debug(`TS simulation completed for tx ${txHash}`); + tsStateRef = await this.merkleTree.getStateReference(); + } catch (error: any) { + this.log.warn(`TS simulation failed, but still continuing with C++ simulation: ${error.message} ${error.stack}`); + } finally { + // Revert checkpoint so C++ can reapply exactly the same changes + await this.merkleTree.revertCheckpoint(); + this.contractsDB.revertCheckpoint(); + } + + // Run C++ simulation via IPC + this.log.debug(`Running C++ (IPC) simulation for tx ${txHash}`); + const cppResult = await this.cppSimulator.simulate(tx).result; + + // If C++ succeeded, TS should have too + assert(tsResult !== undefined, 'TS simulation should have succeeded if C++ succeeded'); + assert(tsStateRef !== undefined, 'TS state reference should have been captured if C++ succeeded'); + + // Compare results + assert(cppResult.revertCode.equals(tsResult.revertCode)); + assert(cppResult.gasUsed.totalGas.equals(tsResult.gasUsed.totalGas)); + assert(cppResult.gasUsed.publicGas.equals(tsResult.gasUsed.publicGas)); + assert(cppResult.gasUsed.teardownGas.equals(tsResult.gasUsed.teardownGas)); + assert(cppResult.gasUsed.billedGas.equals(tsResult.gasUsed.billedGas)); + assert(cppResult.publicTxEffect.equals(tsResult.publicTxEffect)); + if (cppResult.publicInputs !== undefined) { + assert(cppResult.publicInputs!.toBuffer().equals(tsResult.publicInputs!.toBuffer())); + } + + // Compare call metadata (return values) + if (this.config?.collectCallMetadata) { + assert(cppResult.getAppLogicReturnValues().length === tsResult.getAppLogicReturnValues().length); + for (let i = 0; i < cppResult.getAppLogicReturnValues().length; i++) { + assert(cppResult.getAppLogicReturnValues()[i].equals(tsResult.getAppLogicReturnValues()[i])); + } + } + + // Compare revert reasons (messages may differ between C++ and TS, so compare without message) + const cppRevertReason = cppResult.findRevertReason() || {}; + const tsRevertReason = tsResult.findRevertReason() || {}; + const cppRevertReasonAsObject = JSON.parse(JSON.stringify(cppRevertReason)); + const tsRevertReasonAsObject = JSON.parse(JSON.stringify(tsRevertReason)); + if (JSON.stringify(cppRevertReasonAsObject) !== JSON.stringify(tsRevertReasonAsObject)) { + this.log.debug('cppResult.findRevertReason()', cppRevertReasonAsObject); + this.log.debug('tsResult.findRevertReason()', tsRevertReasonAsObject); + } + + const cppRevertReasonWithoutMessage = { ...cppRevertReasonAsObject, originalMessage: undefined }; + const tsRevertReasonWithoutMessage = { ...tsRevertReasonAsObject, originalMessage: undefined }; + assert(JSON.stringify(cppRevertReasonWithoutMessage) === JSON.stringify(tsRevertReasonWithoutMessage)); + + const cppHasRevertMessage = + cppRevertReasonAsObject.originalMessage && cppRevertReasonAsObject.originalMessage.length > 0; + const tsHasRevertMessage = + tsRevertReasonAsObject.originalMessage && tsRevertReasonAsObject.originalMessage.length > 0; + assert( + cppHasRevertMessage === tsHasRevertMessage, + 'One of the AVM simulators (C++ or TS) produced a revert message, but the other did not', + ); + + // Confirm that tree roots match + const cppStateRef = await this.merkleTree.getStateReference(); + assert( + cppStateRef.equals(tsStateRef), + `Tree roots mismatch between TS and C++ public simulations for tx ${txHash}`, + ); + + this.log.debug(`IPC vs TS simulation completed for tx ${txHash}`, { + txHash, + reverted: !cppResult.revertCode.isOK(), + cppGasUsed: cppResult.gasUsed.totalGas.l2Gas, + }); + + // Return cpp result as it has more detailed metadata / revert reasons + return cppResult; + } +} + +/** Measured wrapper that records simulation timing metrics. */ +export class MeasuredIpcVsTsPublicTxSimulator + extends IpcVsTsPublicTxSimulator + implements MeasuredPublicTxSimulatorInterface +{ + constructor( + merkleTree: MerkleTreeWriteOperations, + contractsDB: PublicContractsDB, + globalVariables: GlobalVariables, + avmBackend: AvmIpcBackend, + protected readonly metrics: ExecutorMetricsInterface, + config?: Partial, + bindings?: LoggerBindings, + wsdbForkId?: number, + ) { + super(merkleTree, contractsDB, globalVariables, avmBackend, config, bindings, wsdbForkId); + } + + public override simulate(tx: Tx, txLabel: string = 'unlabeledTx'): SimulationHandle { + const handle = super.simulate(tx); + this.metrics.startRecordingTxSimulation(txLabel); + const result = handle.result + .then(r => { + this.metrics.stopRecordingTxSimulation(txLabel, r?.gasUsed, r?.revertCode); + return r; + }) + .catch(err => { + this.metrics.stopRecordingTxSimulation(txLabel, undefined, undefined); + throw err; + }); + return { result, cancel: handle.cancel }; + } +} diff --git a/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts index 5bb346f42039..1d34736882dd 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/measured_public_tx_simulator.ts @@ -1,6 +1,6 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import { Timer } from '@aztec/foundation/timer'; -import type { PublicSimulatorConfig, PublicTxResult } from '@aztec/stdlib/avm'; +import type { PublicSimulatorConfig } from '@aztec/stdlib/avm'; import type { Gas } from '@aztec/stdlib/gas'; import type { AvmSimulationStats } from '@aztec/stdlib/stats'; import type { MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; @@ -12,7 +12,7 @@ import type { PublicContractsDB } from '../public_db_sources.js'; import type { PublicPersistableStateManager } from '../state_manager/state_manager.js'; import { PublicTxContext } from './public_tx_context.js'; import { PublicTxSimulator } from './public_tx_simulator.js'; -import type { MeasuredPublicTxSimulatorInterface } from './public_tx_simulator_interface.js'; +import type { MeasuredPublicTxSimulatorInterface, SimulationHandle } from './public_tx_simulator_interface.js'; /** * A public tx simulator that tracks miscellaneous simulation metrics without telemetry. @@ -28,15 +28,19 @@ export class MeasuredPublicTxSimulator extends PublicTxSimulator implements Meas super(merkleTree, contractsDB, globalVariables, config); } - public override async simulate(tx: Tx, txLabel: string = 'unlabeledTx'): Promise { + public override simulate(tx: Tx, txLabel: string = 'unlabeledTx'): SimulationHandle { + const handle = super.simulate(tx); this.metrics.startRecordingTxSimulation(txLabel); - let avmResult: PublicTxResult | undefined; - try { - avmResult = await super.simulate(tx); - } finally { - this.metrics.stopRecordingTxSimulation(txLabel, avmResult?.gasUsed, avmResult?.revertCode); - } - return avmResult; + const result = handle.result + .then(r => { + this.metrics.stopRecordingTxSimulation(txLabel, r?.gasUsed, r?.revertCode); + return r; + }) + .catch(err => { + this.metrics.stopRecordingTxSimulation(txLabel, undefined, undefined); + throw err; + }); + return { result, cancel: handle.cancel }; } protected override async insertNonRevertiblesFromPrivate(context: PublicTxContext) { diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts index 64a6cf8b585b..7b9eacbb2aca 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.test.ts @@ -270,6 +270,9 @@ describe('public_tx_simulator', () => { }, 30_000); afterEach(async () => { + // Close forks before closing the service to avoid IPC shutdown races + await merkleTrees.close(); + await merkleTreesCopy.close(); await worldStateService.close(); }); @@ -278,7 +281,7 @@ describe('public_tx_simulator', () => { numberOfSetupCalls: 2, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -312,7 +315,7 @@ describe('public_tx_simulator', () => { numberOfAppLogicCalls: 2, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -346,7 +349,7 @@ describe('public_tx_simulator', () => { hasPublicTeardownCall: true, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -381,7 +384,7 @@ describe('public_tx_simulator', () => { hasPublicTeardownCall: true, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -457,7 +460,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(simulateInternal).toHaveBeenCalledTimes(3); @@ -491,7 +494,7 @@ describe('public_tx_simulator', () => { ), ); - await expect(simulator.simulate(tx)).rejects.toThrow(setupFailureMsg); + await expect(simulator.simulate(tx).result).rejects.toThrow(setupFailureMsg); expect(simulateInternal).toHaveBeenCalledTimes(1); }); @@ -505,7 +508,7 @@ describe('public_tx_simulator', () => { numberOfAppLogicCalls: 1, }); - await expect(simulator.simulate(tx)).rejects.toThrow( + await expect(simulator.simulate(tx).result).rejects.toThrow( `exceeds the maximum processable gas of ${MAX_PROCESSABLE_L2_GAS}`, ); @@ -559,7 +562,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -689,7 +692,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); // tx reports app logic failure @@ -810,7 +813,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); expect(txResult.findRevertReason()).toEqual(teardownFailure); @@ -919,7 +922,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); // tx reports app logic failure @@ -999,7 +1002,7 @@ describe('public_tx_simulator', () => { }, ]); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; await checkNullifierRoot(txResult); }); @@ -1016,7 +1019,7 @@ describe('public_tx_simulator', () => { hasPublicTeardownCall: true, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); @@ -1054,7 +1057,7 @@ describe('public_tx_simulator', () => { feePayer, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); }); @@ -1071,7 +1074,7 @@ describe('public_tx_simulator', () => { hasPublicTeardownCall: true, feePayer, }), - ), + ).result, ).rejects.toThrow(/Not enough balance for fee payer to pay for transaction/); }); @@ -1086,7 +1089,7 @@ describe('public_tx_simulator', () => { hasPublicTeardownCall: true, feePayer, }), - ); + ).result; expect(txResult.revertCode).toEqual(RevertCode.OK); expect(txResult.findRevertReason()).toBeUndefined(); }); @@ -1105,7 +1108,7 @@ describe('public_tx_simulator', () => { tx.data.forPublic!.revertibleAccumulatedData.nullifiers[0] = duplicateNullifier; tx.data.forPublic!.revertibleAccumulatedData.nullifiers[1] = duplicateNullifier; - await expect(simulator.simulate(tx)).rejects.toThrow(/Nullifier collision/); + await expect(simulator.simulate(tx).result).rejects.toThrow(/Nullifier collision/); }); describe('prover id', () => { @@ -1114,7 +1117,7 @@ describe('public_tx_simulator', () => { numberOfAppLogicCalls: 1, }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.publicInputs?.proverId).toEqual(Fr.ZERO); }); @@ -1128,7 +1131,7 @@ describe('public_tx_simulator', () => { simulator = createSimulator({ skipFeeEnforcement: true, proverId }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.publicInputs?.proverId).toEqual(proverId); }); @@ -1147,7 +1150,7 @@ describe('public_tx_simulator', () => { const msg = 'This is an unchecked error during enqueued call'; simulateInternal.mockRejectedValue(new Error(msg)); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible nullifier insertion should NOT be caught', async () => { @@ -1171,7 +1174,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible note hash insertion should NOT be caught', async () => { @@ -1192,7 +1195,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible l2 to l1 message insertion should NOT be caught', async () => { @@ -1216,7 +1219,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); }); @@ -1245,7 +1248,7 @@ describe('public_tx_simulator', () => { throw new NullifierLimitReachedError(); }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); const revertReason = txResult.findRevertReason(); expect(revertReason).toBeDefined(); @@ -1268,7 +1271,7 @@ describe('public_tx_simulator', () => { jest.spyOn(PublicPersistableStateManager.prototype, 'writeSiloedNoteHash').mockImplementation(() => { throw new NoteHashLimitReachedError(); }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); const revertReason = txResult.findRevertReason(); expect(revertReason).toBeDefined(); @@ -1295,7 +1298,7 @@ describe('public_tx_simulator', () => { throw new L2ToL1MessageLimitReachedError(); }); - const txResult = await simulator.simulate(tx); + const txResult = await simulator.simulate(tx).result; expect(txResult.revertCode).toEqual(RevertCode.REVERTED); const revertReason = txResult.findRevertReason(); expect(revertReason).toBeDefined(); @@ -1323,7 +1326,7 @@ describe('public_tx_simulator', () => { const msg = 'Error uncaught by AvmSimulator'; simulateInternal.mockRejectedValue(new CheckedError(msg)); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible nullifier insertion should NOT be caught', async () => { @@ -1347,7 +1350,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible note hash insertion should NOT be caught', async () => { @@ -1368,7 +1371,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); it('Unchecked error during revertible l2 to l1 message insertion should NOT be caught', async () => { @@ -1392,7 +1395,7 @@ describe('public_tx_simulator', () => { throw new Error(msg); }); - await expect(simulator.simulate(tx)).rejects.toThrow(msg); + await expect(simulator.simulate(tx).result).rejects.toThrow(msg); }); }); }); 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 85fe0129fd04..e0b0f14efe99 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 @@ -31,7 +31,7 @@ import { } from '../side_effect_errors.js'; import type { PublicPersistableStateManager } from '../state_manager/state_manager.js'; import { PublicTxContext } from './public_tx_context.js'; -import type { PublicTxSimulatorInterface } from './public_tx_simulator_interface.js'; +import type { PublicTxSimulatorInterface, SimulationHandle } from './public_tx_simulator_interface.js'; // The errors below are only thrown here in the public tx simulator, // and only during revertible phases (revertible insertions, app logic and teardown). @@ -98,9 +98,14 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface { /** * Simulate a transaction's public portion including all of its phases. * @param tx - The transaction to simulate. - * @returns The result of the transaction's public execution. + * @returns A SimulationHandle with the result promise and a no-op cancel. */ - public async simulate(tx: Tx): Promise { + public simulate(tx: Tx): SimulationHandle { + const result = this.doSimulate(tx); + return { result, cancel: async () => {} }; + } + + protected async doSimulate(tx: Tx): Promise { const txHash = this.computeTxHash(tx); this.log.debug(`Simulating ${tx.publicFunctionCalldata.length} public calls for tx ${txHash}`, { txHash }); 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..819d99151e9b 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 @@ -1,33 +1,25 @@ import type { PublicTxResult } from '@aztec/stdlib/avm'; import type { Tx } from '@aztec/stdlib/tx'; -export interface PublicTxSimulatorInterface { - simulate(tx: Tx): Promise; +/** Handle returned by simulate(), allowing the caller to await the result or cancel. */ +export interface SimulationHandle { + /** The promise that resolves with the simulation result. */ + result: Promise; /** - * Cancel the current simulation if one is in progress. + * Cancel the simulation if one is in progress. * This signals the underlying simulator (e.g., C++) 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. * @returns Promise that resolves when cancellation is signaled (and optionally when simulation stops) */ - cancel?(waitTimeoutMs?: number): Promise; + cancel(waitTimeoutMs?: number): Promise; +} + +export interface PublicTxSimulatorInterface { + simulate(tx: Tx): SimulationHandle; } 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. - * 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. - * @returns Promise that resolves when cancellation is signaled (and optionally when simulation stops) - */ - cancel?(waitTimeoutMs?: number): Promise; + simulate(tx: Tx, txLabel: string): SimulationHandle; } 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 4bfdaa9a64ea..3c3fb1388d28 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,11 +539,15 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); + // Update CDB server with current contract data source for this simulation + const { cdbServer, avmBackend } = this.stateMachine.synchronizer; + const forkId = forkedWorldTrees.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); const processor = new PublicProcessor( globals, guardedMerkleTrees, contractsDB, - new CppPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config, bindings), + new CppPublicTxSimulator(avmBackend, globals, config, bindings, forkId), new TestDateProvider(), undefined, createLogger('simulator:public-processor', bindings), @@ -561,58 +565,59 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl checkpoint = await ForkCheckpoint.new(forkedWorldTrees); } - const results = await processor.process([tx]); - - const [processedTx] = results[0]; - const failedTxs = results[1]; - - if (failedTxs.length !== 0) { - throw new Error(`Public execution has failed: ${failedTxs[0].error}`); - } else if (!processedTx.revertCode.isOK()) { - if (processedTx.revertReason) { - try { - await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); - // eslint-disable-next-line no-empty - } catch {} - throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); - } else { - throw new Error('Contract execution has reverted'); + try { + const results = await processor.process([tx]); + + const [processedTx] = results[0]; + const failedTxs = results[1]; + + if (failedTxs.length !== 0) { + throw new Error(`Public execution has failed: ${failedTxs[0].error}`); + } else if (!processedTx.revertCode.isOK()) { + if (processedTx.revertReason) { + try { + await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); + // eslint-disable-next-line no-empty + } catch {} + throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); + } else { + throw new Error('Contract execution has reverted'); + } } - } - - // Walk the nested private-call tree and collect every offchain effect the transaction emitted. - // PXE stores these on each `PrivateCallExecutionResult` and they never reach TXE via the - // `aztec_utl_emitOffchainEffect` foreign-call path (that path only fires at the top-level), so - // we pull them out here and the RPC wrapper will hand them to `TXESession` for buffering. - const offchainEffects = collectNested([executionResult], r => r.offchainEffects.map(e => e.data)); - - if (isStaticCall) { - await checkpoint!.revert(); - await forkedWorldTrees.close(); - return { returnValues: executionResult.returnValues ?? [], offchainEffects }; - } + // Walk the nested private-call tree and collect every offchain effect the transaction emitted. + // PXE stores these on each `PrivateCallExecutionResult` and they never reach TXE via the + // `aztec_utl_emitOffchainEffect` foreign-call path (that path only fires at the top-level), so + // we pull them out here and the RPC wrapper will hand them to `TXESession` for buffering. + const offchainEffects = collectNested([executionResult], r => r.offchainEffects.map(e => e.data)); - const txEffect = TxEffect.empty(); + if (isStaticCall) { + await checkpoint!.revert(); + return { returnValues: executionResult.returnValues ?? [], offchainEffects }; + } - txEffect.noteHashes = processedTx!.txEffect.noteHashes; - txEffect.nullifiers = processedTx!.txEffect.nullifiers; - txEffect.privateLogs = processedTx!.txEffect.privateLogs; - txEffect.publicLogs = processedTx!.txEffect.publicLogs; - txEffect.publicDataWrites = processedTx!.txEffect.publicDataWrites; + const txEffect = TxEffect.empty(); - txEffect.txHash = new TxHash(new Fr(blockNumber)); + txEffect.noteHashes = processedTx!.txEffect.noteHashes; + txEffect.nullifiers = processedTx!.txEffect.nullifiers; + txEffect.privateLogs = processedTx!.txEffect.privateLogs; + txEffect.publicLogs = processedTx!.txEffect.publicLogs; + txEffect.publicDataWrites = processedTx!.txEffect.publicDataWrites; - const l1ToL2Messages = Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(0).map(Fr.zero); - await forkedWorldTrees.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2Messages); + txEffect.txHash = new TxHash(new Fr(blockNumber)); - const l2Block = await makeTXEBlock(forkedWorldTrees, globals, [txEffect]); + const l1ToL2Messages = Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(0).map(Fr.zero); + await forkedWorldTrees.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2Messages); - await this.stateMachine.handleL2Block(l2Block); + const l2Block = await makeTXEBlock(forkedWorldTrees, globals, [txEffect]); - await forkedWorldTrees.close(); + await this.stateMachine.handleL2Block(l2Block); - return { returnValues: executionResult.returnValues ?? [], offchainEffects }; + return { returnValues: executionResult.returnValues ?? [], offchainEffects }; + } finally { + cdbServer.unregisterFork(forkId); + await forkedWorldTrees.close(); + } } async publicCallNewFlow( @@ -657,7 +662,11 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl collectStatistics: false, collectCallMetadata: true, }); - const simulator = new CppPublicTxSimulator(guardedMerkleTrees, contractsDB, globals, config, bindings2); + // Update CDB server with current contract data source for this simulation + const { cdbServer: cdbServer2, avmBackend: avmBackend2 } = this.stateMachine.synchronizer; + const forkId2 = forkedWorldTrees.getRevision().forkId; + cdbServer2.registerFork(forkId2, contractsDB, globals.timestamp); + const simulator = new CppPublicTxSimulator(avmBackend2, globals, config, bindings2, forkId2); const processor = new PublicProcessor( globals, guardedMerkleTrees, @@ -714,55 +723,55 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl checkpoint = await ForkCheckpoint.new(forkedWorldTrees); } - const results = await processor.process([tx]); - - const [processedTx] = results[0]; - const failedTxs = results[1]; - - if (failedTxs.length !== 0) { - throw new Error(`Public execution has failed: ${failedTxs[0].error}`); - } else if (!processedTx.revertCode.isOK()) { - if (processedTx.revertReason) { - try { - await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); - // eslint-disable-next-line no-empty - } catch {} - throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); - } else { - throw new Error('Contract execution has reverted'); + try { + const results = await processor.process([tx]); + + const [processedTx] = results[0]; + const failedTxs = results[1]; + + if (failedTxs.length !== 0) { + throw new Error(`Public execution has failed: ${failedTxs[0].error}`); + } else if (!processedTx.revertCode.isOK()) { + if (processedTx.revertReason) { + try { + await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); + // eslint-disable-next-line no-empty + } catch {} + throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); + } else { + throw new Error('Contract execution has reverted'); + } } - } - - const returnValues = results[3][0].values; - - if (isStaticCall) { - await checkpoint!.revert(); - await forkedWorldTrees.close(); - - return returnValues ?? []; - } + const returnValues = results[3][0].values; - const txEffect = TxEffect.empty(); + if (isStaticCall) { + await checkpoint!.revert(); + return returnValues ?? []; + } - txEffect.noteHashes = processedTx!.txEffect.noteHashes; - txEffect.nullifiers = processedTx!.txEffect.nullifiers; - txEffect.privateLogs = processedTx!.txEffect.privateLogs; - txEffect.publicLogs = processedTx!.txEffect.publicLogs; - txEffect.publicDataWrites = processedTx!.txEffect.publicDataWrites; + const txEffect = TxEffect.empty(); - txEffect.txHash = new TxHash(new Fr(blockNumber)); + txEffect.noteHashes = processedTx!.txEffect.noteHashes; + txEffect.nullifiers = processedTx!.txEffect.nullifiers; + txEffect.privateLogs = processedTx!.txEffect.privateLogs; + txEffect.publicLogs = processedTx!.txEffect.publicLogs; + txEffect.publicDataWrites = processedTx!.txEffect.publicDataWrites; - const l1ToL2Messages = Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(0).map(Fr.zero); - await forkedWorldTrees.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2Messages); + txEffect.txHash = new TxHash(new Fr(blockNumber)); - const l2Block = await makeTXEBlock(forkedWorldTrees, globals, [txEffect]); + const l1ToL2Messages = Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(0).map(Fr.zero); + await forkedWorldTrees.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2Messages); - await this.stateMachine.handleL2Block(l2Block); + const l2Block = await makeTXEBlock(forkedWorldTrees, globals, [txEffect]); - await forkedWorldTrees.close(); + await this.stateMachine.handleL2Block(l2Block); - return returnValues ?? []; + return returnValues ?? []; + } finally { + cdbServer2.unregisterFork(forkId2); + await forkedWorldTrees.close(); + } } async executeUtilityFunction( diff --git a/yarn-project/txe/src/state_machine/synchronizer.ts b/yarn-project/txe/src/state_machine/synchronizer.ts index f6598d2ce8fe..1fa90d166c19 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 { type AvmIpcBackend, AvmSimulatorPool, CdbIpcServer } from '@aztec/simulator/server'; import type { BlockHash, L2Block } from '@aztec/stdlib/block'; import type { MerkleTreeReadOperations, @@ -15,12 +16,25 @@ export class TXESynchronizer implements WorldStateSynchronizer { // This works when set to 1 as well. private blockNumber = BlockNumber.ZERO; + /** AVM IPC backend shared across all public simulations. */ + public avmBackend!: AvmIpcBackend; + /** CDB IPC server shared across all public simulations. */ + public cdbServer!: CdbIpcServer; + constructor(public nativeWorldStateService: NativeWorldStateService) {} static async create() { const nativeWorldStateService = await NativeWorldStateService.ephemeral(); - return new this(nativeWorldStateService); + const synchronizer = new this(nativeWorldStateService); + + synchronizer.cdbServer = new CdbIpcServer(); + synchronizer.avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: nativeWorldStateService.getIpcPath(), + cdbIpcPath: synchronizer.cdbServer.ipcPath, + }); + + return synchronizer; } public async handleL2Block(block: L2Block) { @@ -70,8 +84,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 +99,14 @@ 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 { + if (this.avmBackend?.destroy) { + await this.avmBackend.destroy(); + } + if (this.cdbServer) { + await this.cdbServer.close(); + } + } } diff --git a/yarn-project/validator-client/package.json b/yarn-project/validator-client/package.json index 4734b0bbdfc3..8b269831799b 100644 --- a/yarn-project/validator-client/package.json +++ b/yarn-project/validator-client/package.json @@ -87,6 +87,7 @@ }, "devDependencies": { "@aztec/archiver": "workspace:^", + "@aztec/bb.js": "workspace:^", "@aztec/world-state": "workspace:^", "@electric-sql/pglite": "^0.3.14", "@jest/globals": "^30.0.0", diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index e5151c230e6e..b5d17c94acb8 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -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 */ diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 05489c21e809..4398df856f9d 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -9,6 +9,8 @@ import { DateProvider, elapsed } from '@aztec/foundation/timer'; import { createTxValidatorForBlockBuilding, getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators'; import { LightweightCheckpointBuilder } from '@aztec/prover-client/light'; import { + type AvmIpcBackend, + type CdbIpcServer, GuardedMerkleTreeOperations, PublicContractsDB, PublicProcessor, @@ -59,6 +61,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { private telemetryClient: TelemetryClient, bindings?: LoggerBindings, private debugLogStore: DebugLogStore = new NullDebugLogStore(), + private avmBackend?: AvmIpcBackend, + private cdbServer?: CdbIpcServer, ) { this.log = createLogger('checkpoint-builder', { ...bindings, @@ -101,7 +105,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { feeRecipient: constants.feeRecipient, gasFees: constants.gasFees, }); - const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork); + const { processor, validator, wsdbForkId } = await this.makeBlockBuilderDeps(globalVariables, this.fork); // Cap gas limits amd available blob fields by remaining checkpoint-level budgets const cappedOpts: PublicProcessorLimits & { expectedEndState?: StateReference } = { @@ -156,6 +160,11 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { // Otherwise it reverts any changes made to the fork for this failed block await forkCheckpoint.revert(); throw err; + } finally { + // Unregister the fork's contracts DB from the CDB server to prevent leaks. + if (wsdbForkId !== undefined) { + this.cdbServer?.unregisterFork(wsdbForkId); + } } } @@ -241,16 +250,23 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { const contractsDB = this.contractsDB; const guardedFork = new GuardedMerkleTreeOperations(fork); - const collectDebugLogs = this.debugLogStore.isEnabled; - const bindings = this.log.getBindings(); + if (!this.avmBackend) { + throw new Error('AVM IPC backend is required for block building. Ensure bb-avm-sim is running.'); + } + // Extract the WSDB fork ID so the C++ AVM can modify the same fork in-place. + const wsdbForkId = fork.getRevision().forkId; + // Register this fork's contracts DB on the CDB server for fork-ID routing. + if (this.cdbServer) { + this.cdbServer.registerFork(wsdbForkId, contractsDB, globalVariables.timestamp); + } const publicTxSimulator = createPublicTxSimulatorForBlockBuilding( - guardedFork, - contractsDB, + this.avmBackend, globalVariables, this.telemetryClient, bindings, - collectDebugLogs, + wsdbForkId, + this.debugLogStore?.isEnabled ?? false, ); const processor = new PublicProcessor( @@ -276,6 +292,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder { return { processor, validator, + wsdbForkId, }; } } @@ -291,6 +308,8 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { private dateProvider: DateProvider, private telemetryClient: TelemetryClient = getTelemetryClient(), private debugLogStore: DebugLogStore = new NullDebugLogStore(), + private avmBackend?: AvmIpcBackend, + private cdbServer?: CdbIpcServer, ) { this.log = createLogger('checkpoint-builder'); } @@ -346,6 +365,8 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.telemetryClient, bindings, this.debugLogStore, + this.avmBackend, + this.cdbServer, ); } @@ -407,6 +428,8 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder { this.telemetryClient, bindings, this.debugLogStore, + this.avmBackend, + this.cdbServer, ); } diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index 52aaffb8deba..39ffbd1800f8 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 { AvmIpcBackend, CdbIpcServer } 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,8 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder: FullNodeCheckpointsBuilder; p2pClient: MockProxy; validator: ValidatorClient; + avmBackend?: AvmIpcBackend; + cdbServer?: CdbIpcServer; }; let slotNumber: SlotNumber; @@ -138,6 +141,14 @@ describe('ValidatorClient Integration', () => { const synchronizer = new ServerWorldStateSynchronizer(worldStateDb, archiver, wsConfig); await synchronizer.start(); + const { AvmSimulatorPool, CdbIpcServer, PublicContractsDB } = await import('@aztec/simulator/server'); + const cdbServer = new CdbIpcServer(); + cdbServer.registerFork(0, new PublicContractsDB(archiver), 0n); + const avmBackend = await AvmSimulatorPool.spawn({ + wsdbIpcPath: worldStateDb.getIpcPath(), + cdbIpcPath: cdbServer.ipcPath, + }); + // Create real checkpoints builder const checkpointsBuilder = new FullNodeCheckpointsBuilder( { @@ -151,6 +162,10 @@ describe('ValidatorClient Integration', () => { synchronizer, archiver, dateProvider, + /*telemetryClient=*/ undefined, + /*debugLogStore=*/ undefined, + avmBackend, + cdbServer, ); // Create mock p2p client @@ -222,6 +237,8 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder, p2pClient, validator, + avmBackend, + cdbServer, }; }; @@ -352,6 +369,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 +424,13 @@ 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, avmBackend, cdbServer } of [attestor, proposer]) { await tryStop(validator); await tryStop(synchronizer); await tryStop(archiver); await tryStop(worldStateDb); + await avmBackend?.destroy?.(); + await cdbServer?.close(); } }); 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 2f41fa30802a..c2ac0d9a0c4c 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:^" @@ -793,6 +794,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/aztec.js@workspace:^, @aztec/aztec.js@workspace:aztec.js": version: 0.0.0-use.local resolution: "@aztec/aztec.js@workspace:aztec.js" @@ -1850,6 +1904,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:^" @@ -2003,6 +2058,7 @@ __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/kv-store": "workspace:^" @@ -2030,6 +2086,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" @@ -2216,6 +2273,7 @@ __metadata: resolution: "@aztec/validator-client@workspace:validator-client" dependencies: "@aztec/archiver": "workspace:^" + "@aztec/bb.js": "workspace:^" "@aztec/blob-client": "workspace:^" "@aztec/blob-lib": "workspace:^" "@aztec/constants": "workspace:^" @@ -2335,7 +2393,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 beff275603ec46b17e19e02c0fc6924d71a24196 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:10:53 +0000 Subject: [PATCH 2/2] chore: restore executable bit on bootstrap scripts Several bootstrap.sh scripts on this IPC stack were non-executable, causing CI to abort with 'Permission denied' (exit 126) at the barretenberg/ts bootstrap step. Restores +x so the whole stack inherits it. --- aztec-up/bootstrap.sh | 0 barretenberg/ts/bb.js/bootstrap.sh | 0 barretenberg/ts/bootstrap.sh | 0 release-image/bootstrap.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 aztec-up/bootstrap.sh mode change 100644 => 100755 barretenberg/ts/bb.js/bootstrap.sh mode change 100644 => 100755 barretenberg/ts/bootstrap.sh mode change 100644 => 100755 release-image/bootstrap.sh diff --git a/aztec-up/bootstrap.sh b/aztec-up/bootstrap.sh old mode 100644 new mode 100755 diff --git a/barretenberg/ts/bb.js/bootstrap.sh b/barretenberg/ts/bb.js/bootstrap.sh old mode 100644 new mode 100755 diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh old mode 100644 new mode 100755 diff --git a/release-image/bootstrap.sh b/release-image/bootstrap.sh old mode 100644 new mode 100755