diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/nodejs_module/CMakeLists.txt index d8ba38229f61..a7259db60b44 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 wsdb_client wsdb_ipc_client ipc vm2_sim) +target_link_libraries(nodejs_module PRIVATE world_state ipc) # 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 97ed8370a104..000000000000 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp +++ /dev/null @@ -1,438 +0,0 @@ -#include "barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp" - -#include -#include -#include - -#include "barretenberg/common/log.hpp" -#include "barretenberg/ipc/ipc_client.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/wsdb/wsdb_ipc_client_generated.hpp" -#include "barretenberg/wsdb_client/wsdb_ipc_merkle_db.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 socket path (required) *** - ***************************************/ - if (!cb_info[2].IsString()) { - throw Napi::TypeError::New(env, "Third argument must be a WSDB socket path (string)"); - } - std::string wsdb_socket_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); - // Create threaded operation that runs on a dedicated std::thread (not libuv pool). - // This prevents libuv thread pool exhaustion when callbacks need libuv threads for I/O. - auto* op = new ThreadedAsyncOperation( - env, - deferred, - [data, tsfns, logger_tsfn, wsdb_socket_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 over UDS 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_socket_path); - bb::wsdb_client::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"); - } - }); - - op->Queue(); - - 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); - - // Create threaded operation that runs on a dedicated std::thread (not libuv pool) - auto* op = new ThreadedAsyncOperation(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"); - } - }); - - op->Queue(); - - 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 7b7e309e5bae..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 UDS socket 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/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index ef0347475f3d..8f80d7d66582 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 855139bf8569..21b5e558fa91 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -55,10 +55,11 @@ import { getPackageVersion } from '@aztec/stdlib/update-checker'; import type { ValidatorClient } from '@aztec/validator-client'; import { jest } from '@jest/globals'; -import { mkdtempSync, rmSync, writeFileSync } from 'fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'; import { type MockProxy, mock } from 'jest-mock-extended'; import { tmpdir } from 'os'; -import { join } from 'path'; +import { dirname, join, resolve } from 'path'; +import { fileURLToPath } from 'url'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { type AztecNodeConfig, getConfigEnvVars } from './config.js'; @@ -222,7 +223,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), ); @@ -351,8 +352,13 @@ describe('aztec node', () => { describe('node info', () => { it('returns the correct node version', async () => { + const releasePleaseVersionFile = readFileSync( + resolve(dirname(fileURLToPath(import.meta.url)), '../../../../.release-please-manifest.json'), + ).toString(); + const releasePleaseVersion = JSON.parse(releasePleaseVersionFile)['.']; + const nodeInfo = await node.getNodeInfo(); - expect(nodeInfo.nodeVersion).toBe(getPackageVersion()); + expect(nodeInfo.nodeVersion).toBe(releasePleaseVersion); }); }); @@ -785,7 +791,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), undefined, @@ -976,7 +982,7 @@ describe('aztec node', () => { globalVariablesBuilder, feeProvider, epochCache, - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), undefined, @@ -1048,7 +1054,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 804569df9d55..7cbedec2f88e 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -39,7 +39,7 @@ import { SequencerClient, type SequencerPublisher, } from '@aztec/sequencer-client'; -import { PublicProcessorFactory } from '@aztec/simulator/server'; +import { type AvmIpcBackend, AvmSimulatorPool, CdbIpcServer, PublicProcessorFactory } from '@aztec/simulator/server'; import { AttestationsBlockWatcher, EpochPruneWatcher, @@ -132,8 +132,17 @@ import { createValidatorClient, } from '@aztec/validator-client'; import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types'; -import { createWorldState, createWorldStateSynchronizer } from '@aztec/world-state'; - +import { + IpcWorldState, + WorldStateInstrumentation, + createWorldState, + createWorldStateSynchronizer, + getWsdbOptions, +} from '@aztec/world-state'; + +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { createPublicClient } from 'viem'; import { createSentinel } from '../sentinel/factory.js'; @@ -157,6 +166,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, @@ -473,7 +489,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); @@ -562,11 +578,89 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config, { dateProvider }); + // Set up IPC backends for world state and AVM simulation. + const { WsdbBackend } = await import('@aztec/bb.js/aztec-wsdb'); + const { findWsdbBinary, findAvmBinary } = await import('@aztec/bb.js/platform'); + + const wsdbBinaryPath = findWsdbBinary(); + const avmBinaryPath = findAvmBinary(); + + if (!wsdbBinaryPath || !avmBinaryPath) { + throw new Error(`Missing required binaries: wsdb=${wsdbBinaryPath}, avm=${avmBinaryPath}`); + } + + const configuredDataDir = config.worldStateDataDirectory ?? config.dataDirectory; + const dataDirectory = configuredDataDir ?? (await mkdtemp(join(tmpdir(), 'aztec-world-state-'))); + const dataStoreMapSizeKb = config.worldStateDbMapSizeKb ?? config.dataStoreMapSizeKb; + const wsTreeMapSizes = { + archiveTreeMapSizeKb: config.archiveTreeMapSizeKb ?? dataStoreMapSizeKb, + nullifierTreeMapSizeKb: config.nullifierTreeMapSizeKb ?? dataStoreMapSizeKb, + noteHashTreeMapSizeKb: config.noteHashTreeMapSizeKb ?? dataStoreMapSizeKb, + messageTreeMapSizeKb: config.messageTreeMapSizeKb ?? dataStoreMapSizeKb, + publicDataTreeMapSizeKb: config.publicDataTreeMapSizeKb ?? dataStoreMapSizeKb, + }; + const wsdbOpts = getWsdbOptions(dataDirectory, wsTreeMapSizes); + const prefilledData = (options.genesis?.prefilledPublicData ?? []).map( + d => [d.slot.toBuffer(), d.value.toBuffer()] as [Buffer, Buffer], + ); + + log.info('Starting IPC backends', { + wsdbBinary: wsdbBinaryPath, + avmBinary: avmBinaryPath, + dataDir: dataDirectory, + }); + + const wsdbBackend = new WsdbBackend({ + binaryPath: wsdbBinaryPath, + dataDir: join(dataDirectory, 'world_state'), + ...wsdbOpts, + prefilledPublicData: prefilledData, + genesisTimestamp: Number(options.genesis?.genesisTimestamp ?? 0), + logger: (msg: string) => log.debug(msg), + useShm: false, + }); + + const cdbServer = new CdbIpcServer(); + + log.info('Waiting for WSDB backend to be ready...'); + await wsdbBackend.waitUntilReady(); + + log.info('WSDB ready, creating AVM simulator pool'); + const avmPool = new AvmSimulatorPool({ + avmBinaryPath, + wsdbSocketPath: wsdbBackend.getSocketPath(), + cdbSocketPath: cdbServer.socketPath, + logger: (msg: string) => log.debug(msg), + }); + + const wsdbDir = join(dataDirectory, 'world_state'); + const recreateIpcInstance = async () => { + await rm(wsdbDir, { recursive: true, force: true, maxRetries: 3 }); + await mkdir(wsdbDir, { recursive: true }); + const freshBackend = new WsdbBackend({ + binaryPath: wsdbBinaryPath, + dataDir: wsdbDir, + ...wsdbOpts, + prefilledPublicData: prefilledData, + logger: (msg: string) => log.debug(msg), + useShm: false, + }); + await freshBackend.waitUntilReady(); + return new IpcWorldState(freshBackend, new WorldStateInstrumentation(telemetry)); + }; + // Track started resources so we can clean up on partial failure during node creation. const started: { stop?(): Promise | void }[] = []; try { // Create world-state first so we can retrieve the initial header before constructing the archiver. - const nativeWs = await createWorldState(config, options.genesis); + const nativeWs = await createWorldState( + config, + options.genesis, + undefined, + undefined, + wsdbBackend, + recreateIpcInstance, + ); const initialHeader = nativeWs.getInitialHeader(); const initialBlockHash = await initialHeader.hash(); const archiver = await createArchiver( @@ -612,6 +706,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb rollupVersion: BigInt(config.rollupVersion), l1GenesisTime, slotDuration: Number(slotDuration), + rollupManaLimit, }; const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig); @@ -655,6 +750,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb archiver, dateProvider, telemetry, + undefined, // debugLogStore + avmPool, + cdbServer, ); let validatorClient: ValidatorClient | undefined; @@ -825,6 +923,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb dateProvider, telemetry, debugLogStore, + avmPool, + cdbServer, ); sequencer = await SequencerClient.new(config, { @@ -868,6 +968,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb epochCache, blobClient, keyStoreManager, + avmBackend: avmPool, + cdbServer, }); if (!options.dontStartProverNode) { @@ -909,6 +1011,18 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb debugLogStore, ); + // 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); @@ -1179,6 +1293,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`); } @@ -1474,6 +1596,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(), @@ -1519,6 +1643,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb debugLogs, ); } finally { + try { + publicProcessorFactory.unregisterFork(merkleTreeFork.getRevision().forkId); + } catch { + // Fork may be in an error state after simulation failure + } await merkleTreeFork.close(); } } 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 47f81edb57d4..6b9a93dfaf73 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'; @@ -164,7 +164,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); @@ -192,19 +192,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 3f4464bc95a0..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, - wsdbSocketPath: 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 wsdbSocketPath - UDS 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, - wsdbSocketPath: string, - logLevel: LogLevel = 'info', - logger?: Logger, - cancellationToken?: CancellationToken, -): Promise { - return withAvmConcurrencyLimit(() => - nativeAvmSimulate( - inputs, - contractProvider, - wsdbSocketPath, - 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/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 cb208eb891f9..aa2464f7773a 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'; @@ -32,46 +32,69 @@ export async function rerunEpochProvingJob( const telemetry = getTelemetryClient(); const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job')); const worldState = await createWorldState(config, genesis); - try { - const archiver = await createArchiverStore(config); - const publicProcessorFactory = new PublicProcessorFactory( - createContractDataSource(archiver), - undefined, - undefined, - log.getBindings(), - ); + const archiver = await createArchiverStore(config); + + // Spawn IPC backends for C++ simulation + const wsdbSocketPath = worldState.getSocketPath(); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + const contractDataSource = createContractDataSource(archiver); + const cdbServer = new CdbIpcServer(); + const contractsDB = new PublicContractsDB(contractDataSource); + cdbServer.registerFork(0, contractsDB, 0n); - const publisher = { - submitEpochProof: () => Promise.resolve(true), - analyzeEpochProofSubmission: () => Promise.resolve(), - }; - const l2BlockSourceForReorgDetection = undefined; - const deadline = undefined; + const avmPool = new AvmSimulatorPool({ + avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); - // 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); + const publicProcessorFactory = new PublicProcessorFactory( + contractDataSource, + avmPool, + cdbServer, + undefined, + undefined, + log.getBindings(), + ); - const provingJob = new EpochProvingJob( - jobData, - worldState, - prover.createEpochProver(), - publicProcessorFactory, - publisher, - l2BlockSourceForReorgDetection, - metrics, - deadline, - { skipEpochCheck: true }, - log.getBindings(), - ); + const publisher = { + submitEpochProof: () => Promise.resolve(true), + analyzeEpochProofSubmission: () => Promise.resolve(), + }; + const l2BlockSourceForReorgDetection = undefined; + const deadline = undefined; - log.info(`Rerunning epoch proving job for epoch ${jobData.epochNumber}`); + const broker = await createAndStartProvingBroker(config, telemetry); + const prover = await createProverClient(config, worldState, broker, telemetry); + + const provingJob = new EpochProvingJob( + jobData, + worldState, + prover.createEpochProver(), + publicProcessorFactory, + publisher, + l2BlockSourceForReorgDetection, + metrics, + deadline, + { skipEpochCheck: true }, + log.getBindings(), + ); + + log.info(`Rerunning epoch proving job for epoch ${jobData.epochNumber}`); + 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(); await worldState.close(); } } diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index 6f2ceda0926d..a895160971ef 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 022fcc02289e..e902556c5800 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.ts @@ -6,7 +6,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'; @@ -31,6 +30,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; @@ -168,8 +170,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 asyncPool(parallelism, this.checkpoints, async checkpoint => { @@ -244,6 +246,11 @@ export class EpochProvingJob implements Traceable { const publicProcessor = this.publicProcessorFactory.create(db, globalVariables, config); const processed = await this.processTxs(publicProcessor, txs); await this.prover.addTxs(processed); + 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.log.verbose(`Processed all ${txs.length} txs for block ${block.number}`, { blockNumber: block.number, diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 45b09dc511a0..8e0332911821 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; @@ -82,6 +83,8 @@ describe('prover-node', () => { epochMonitor, rollupContract, l1Metrics, + avmBackend, + undefined, // cdbServer config, ); @@ -102,6 +105,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 9d7a4336054e..d5f680d70b18 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, @@ -292,6 +294,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/src/public/avm_simulator_pool.ts b/yarn-project/simulator/src/public/avm_simulator_pool.ts index 02f582f7f142..968f08e89fd0 100644 --- a/yarn-project/simulator/src/public/avm_simulator_pool.ts +++ b/yarn-project/simulator/src/public/avm_simulator_pool.ts @@ -1,18 +1,6 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; -/** - * Minimal interface for an out-of-process AVM simulator that speaks msgpack over IPC. - * - * Intentionally aligned with `IMsgpackBackendAsync` from bb.js — `AvmBackend` (which spawns - * `aztec-avm` and routes msgpack via UDS) and `AvmSimulatorPool` (a worker pool of those backends) - * both implement this. Anything that wants to run an AVM simulation can take this interface and - * not care which it got. - */ -export interface AvmIpcBackend { - call(inputBuffer: Uint8Array): Promise; - cancel?(): Promise; - destroy?(): Promise; -} +import type { AvmIpcBackend } from './public_tx_simulator/cpp_public_tx_simulator.js'; export interface AvmSimulatorPoolOptions { /** Maximum number of concurrent AVM processes. If not set, defaults to AVM_MAX_CONCURRENT_SIMULATIONS env var or 4. */ 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..5a1c4be37b8f 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,9 @@ import { getContractFunctionAbi, getFunctionSelector, } from '../avm/fixtures/utils.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 AvmIpcBackend, MeasuredCppPublicTxSimulator } from '../public_tx_simulator/cpp_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 +65,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 +84,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 +94,45 @@ 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); + + // Spawn AVM backend for IPC simulation + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + // Create CDB server backed by the test contract data source + const cdbServer = new CdbIpcServer(); + const contractsDB = new PublicContractsDB(contractDataSource); + const forkId = merkleTree.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + + const avmBackend: AvmIpcBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + 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 +200,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 +235,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 +254,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall?: TestEnqueuedCall, feePayer?: AztecAddress, privateInsertions?: TestPrivateInsertions, - gasLimits?: Gas, ): Promise { return await this.simulateTxWithLabel( txLabel, @@ -237,7 +263,6 @@ export class PublicTxSimulationTester extends BaseAvmSimulationTester { teardownCall, feePayer, privateInsertions, - gasLimits, ); } @@ -245,6 +270,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 +292,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/hinting_db_sources.ts b/yarn-project/simulator/src/public/hinting_db_sources.ts index 816a0ec96a3c..85f8ab422ccf 100644 --- a/yarn-project/simulator/src/public/hinting_db_sources.ts +++ b/yarn-project/simulator/src/public/hinting_db_sources.ts @@ -554,10 +554,6 @@ export class HintingMerkleWriteOperations implements MerkleTreeWriteOperations { return this.db.getRevision(); } - public getSocketPath(): string { - return this.db.getSocketPath(); - } - public async updateArchive(header: any): Promise { return await this.db.updateArchive(header); } 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_processor/apps_tests/deployments.test.ts b/yarn-project/simulator/src/public/public_processor/apps_tests/deployments.test.ts index 1183b99f0cfd..92beb9ab377a 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,11 @@ import { NativeWorldStateService } from '@aztec/world-state'; import { PublicContractsDB } from '../../../server.js'; import { createContractClassAndInstance } from '../../avm/fixtures/utils.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 { type AvmIpcBackend, CppPublicTxSimulator } from '../../public_tx_simulator/cpp_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 +31,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 +51,60 @@ 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); + + let simulator; + if (useCppSimulator) { + // IPC: spawn aztec-avm + CDB server + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + simulator = new CppPublicTxSimulator(avmBackend, globals, config, undefined, forkId); + } else { + // TS mode: use IpcVsTs to compare TS and IPC C++ results + // Both paths need IPC setup since IpcVsTsPublicTxSimulator uses CppPublicTxSimulator internally + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + simulator = new IpcVsTsPublicTxSimulator( + guardedMerkleTrees, + contractsDB, + globals, + avmBackend, + config, + undefined, + forkId, + ); + } processor = new PublicProcessor( globals, @@ -72,6 +124,14 @@ describe.each([ }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; 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..437e285e7e60 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,11 @@ import { GlobalVariables } from '@aztec/stdlib/tx'; import { getTelemetryClient } from '@aztec/telemetry-client'; import { NativeWorldStateService } from '@aztec/world-state'; +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 { type AvmIpcBackend, CppPublicTxSimulator } from '../../public_tx_simulator/cpp_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 +33,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 +53,59 @@ 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); + + let simulator; + if (useCppSimulator) { + // IPC: spawn aztec-avm + CDB server + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + simulator = new CppPublicTxSimulator(avmBackend, globals, config, undefined, forkId); + } else { + // TS mode: use IpcVsTs to compare TS and IPC C++ results + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTrees.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + simulator = new IpcVsTsPublicTxSimulator( + guardedMerkleTrees, + contractsDB, + globals, + avmBackend, + config, + undefined, + forkId, + ); + } processor = new PublicProcessor( globals, @@ -74,6 +125,14 @@ describe.each([ }); afterEach(async () => { + if (avmBackend?.destroy) { + await avmBackend.destroy(); + } + if (cdbServer) { + await cdbServer.close(); + } + avmBackend = undefined; + cdbServer = undefined; await worldStateService.close(); }); diff --git a/yarn-project/simulator/src/public/public_processor/guarded_merkle_tree.ts b/yarn-project/simulator/src/public/public_processor/guarded_merkle_tree.ts index 49fd2b015781..e20ebb5192a5 100644 --- a/yarn-project/simulator/src/public/public_processor/guarded_merkle_tree.ts +++ b/yarn-project/simulator/src/public/public_processor/guarded_merkle_tree.ts @@ -97,9 +97,6 @@ export class GuardedMerkleTreeOperations implements MerkleTreeWriteOperations { public getRevision(): WorldStateRevision { return this.target.getRevision(); } - public getSocketPath(): string { - return this.target.getSocketPath(); - } getSiblingPath(treeId: ID, index: bigint): Promise> { return this.guardAndPush(() => this.target.getSiblingPath(treeId, index)); } 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 abc3aedf918e..12c486345211 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 @@ -93,9 +93,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 +149,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]); @@ -244,10 +251,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); @@ -323,7 +333,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]); @@ -376,4 +392,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 f1922dab36cd..d30e85f3d7db 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,10 +79,10 @@ export class PublicProcessorFactory { } /** - * Creates a new instance of a PublicProcessor. - * @param globalVariables - The global variables for the block being processed. - * @param skipFeeEnforcement - Allows disabling balance checks for fee estimations. - * @returns 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. */ public create( merkleTree: MerkleTreeWriteOperations, @@ -86,9 +91,16 @@ export class PublicProcessorFactory { ): PublicProcessor { const bindings = this.log.getBindings(); const contractsDB = new PublicContractsDB(this.contractDataSource, bindings); + const forkId = merkleTree.getRevision().forkId; + + // 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) { + 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, @@ -101,19 +113,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, ); } } @@ -131,6 +149,8 @@ class PublicProcessorTimeoutError extends Error { */ 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, @@ -327,7 +347,7 @@ export class PublicProcessor implements Traceable { // 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.publicTxSimulator.cancel?.(); + await this.currentSimulationHandle?.cancel(); // Now stop the guarded fork to prevent any further TS-side access to the world state. await this.guardedMerkleTree.stop(); @@ -578,7 +598,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 50ffe0f8cebe..8d6f472b3827 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 @@ -15,6 +15,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'fs'; import path, { dirname, join } from 'path'; import { fileURLToPath } from 'url'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { ammTest } from '../../fixtures/amm_test.js'; import { bulkTest, megaBulkTest } from '../../fixtures/bulk_test.js'; import { @@ -24,8 +25,9 @@ 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 { type AvmIpcBackend, MeasuredCppPublicTxSimulator } from '../cpp_public_tx_simulator.js'; import { MeasuredPublicTxSimulator } from '../measured_public_tx_simulator.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -63,19 +65,48 @@ 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) { + // IPC: spawn aztec-avm + CDB server + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + const contractsDB = new PublicContractsDB(contractDataSource); + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTree.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + simulatorFactory = (_mt, _cdb, g, m, c) => + new MeasuredCppPublicTxSimulator(avmBackend!, g, m, c, undefined, forkId); + } else { + // TS simulator (same as on next) + simulatorFactory = (mt, cdb, g, m, c) => new MeasuredPublicTxSimulator(mt, cdb, g, m, c); + } + tester = new PublicTxSimulationTester( merkleTree, contractDataSource, - defaultGlobals(), + globals, metrics, simulatorFactory, config, @@ -83,6 +114,14 @@ 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 worldStateService.close(); }); @@ -178,22 +217,43 @@ 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 wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + const contractsDB = new PublicContractsDB(contractDataSource); + cdbServer = new CdbIpcServer(); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + + const forkId = merkleTree.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + 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, @@ -203,6 +263,14 @@ 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 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..ba96b1a0d016 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,21 +103,13 @@ 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 () => { 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..541bf484f843 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,7 @@ import { NativeWorldStateService } from '@aztec/world-state/native'; import { mkdirSync, writeFileSync } from 'fs'; import path from 'path'; +import { CdbIpcServer } from '../../cdb_ipc_server.js'; import { getSpamConfigsPerOpcode, testOpcodeSpamCase } from '../../fixtures/opcode_spammer.js'; import { type MeasuredSimulatorFactory, @@ -12,9 +13,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 { type AvmIpcBackend, MeasuredCppPublicTxSimulator } from '../cpp_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 +68,64 @@ 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) { + // IPC: spawn aztec-avm + CDB server + const wsdbSocketPath = worldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + const contractsDB = new PublicContractsDB(contractDataSource); + cdbServer = new CdbIpcServer(); + const forkId = merkleTree.getRevision().forkId; + cdbServer.registerFork(forkId, contractsDB, globals.timestamp); + + avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + 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 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 9077b8676c1e..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.addContracts`); - this.contractsDB.addContracts(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 8239c5a42c64..23f93ef07114 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,170 @@ 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, + serializeWithMessagePack, } 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 { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client'; +import type { 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'; +/** Msgpack IPC backend interface (matches bb.js IMsgpackBackendAsync). */ +export interface AvmIpcBackend { + call(inputBuffer: Uint8Array): Promise; + cancel?(): Promise; + destroy?(): Promise; +} + /** - * 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 avmBackend has checkout/return (pool), use per-simulation cancel. + const pool = this.avmBackend as any; + if (typeof pool.checkout === 'function') { + return this.simulateWithPool(tx, pool as AvmSimulatorPool); + } + // Single backend path + const result = this.doSimulate(tx); + return { result, cancel: async () => {} }; + } - const wsRevision = this.merkleTree.getRevision(); - const wsdbSocketPath = this.merkleTree.getSocketPath(); + 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, + { forkId: this.wsdbForkId ?? 0, blockNumber: 0, 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(); + const wrappedCommand = serializeWithMessagePack([['AvmSimulate', { inputs: inputBuffer }]]); - // 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, - wsdbSocketPath, - this.log.level, - undefined, - this.cancellationToken, - ); - - let resultBuffer: Buffer; + let resultBuffer: Uint8Array; try { - resultBuffer = await this.simulationPromise; + resultBuffer = await effectiveBackend.call(wrappedCommand); } 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; + throw new SimulationError(`IPC AVM simulation failed: ${error.message}`, []); } - // If we've reached this point, C++ succeeded during simulation, + const responseObj: any = deserializeFromMessagePack(Buffer.from(resultBuffer)); - // 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); + if (Array.isArray(responseObj) && responseObj.length === 2) { + const [name, payload] = responseObj; + if (name === 'AvmErrorResponse') { + throw new SimulationError(`AVM error: ${payload.message}`, []); + } + if (name === 'AvmSimulateResponse' && payload.result) { + const resultBytes = Buffer.from(payload.result); + const cppResultJSON: object = deserializeFromMessagePack(resultBytes); + return PublicTxResult.fromPlainObject(cppResultJSON); + } } - // 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'); - } + throw new SimulationError('Unexpected response format from aztec-avm', []); } } +/** 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 3c6a65171414..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 wsdbSocketPath = this.merkleTree.getSocketPath(); - - 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, wsdbSocketPath, 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..bc875f2958c8 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,65 @@ 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 { CppPublicTxSimulator } from './cpp_public_tx_simulator.js'; +import { type AvmIpcBackend, 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..4a864f9449a3 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,22 @@ 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 { TelemetryCppPublicTxSimulator } from './cpp_public_tx_simulator.js'; +import { type AvmIpcBackend, 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 +30,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..24ed4566cce6 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 './cpp_public_tx_simulator.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..b4b7292d317f --- /dev/null +++ b/yarn-project/simulator/src/public/public_tx_simulator/ipc_vs_ts_public_tx_simulator.ts @@ -0,0 +1,180 @@ +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 { ExecutorMetricsInterface } from '../executor_metrics_interface.js'; +import type { PublicContractsDB } from '../public_db_sources.js'; +import { type AvmIpcBackend, 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 aztec-avm process and accesses world state via WSDB IPC. + * The TS simulator runs in-process and accesses world state directly. + * + * This is the IPC replacement for the old NAPI-based CppVsTsPublicTxSimulator. + * Instead of calling avmSimulate() via NAPI, it delegates to CppPublicTxSimulator which + * communicates with the aztec-avm binary over UDS. + */ +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 337bd982431d..73ba10d2cb96 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.APP_LOGIC_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.TEARDOWN_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.BOTH_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.APP_LOGIC_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.APP_LOGIC_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.APP_LOGIC_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 9756205ae9ab..3213927ccf73 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 4d61070e154c..b98cb521d0ec 100644 --- a/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts +++ b/yarn-project/stdlib/src/interfaces/merkle_tree_operations.ts @@ -140,13 +140,6 @@ export interface MerkleTreeReadOperations { */ getRevision(): WorldStateRevision; - /** - * Returns the UDS socket 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. - */ - getSocketPath(): string; - /** * Gets sibling path for a leaf. * @param treeId - The tree to be queried for a sibling path. diff --git a/yarn-project/txe/package.json b/yarn-project/txe/package.json index 9f39b19e10a7..ef01baa666c0 100644 --- a/yarn-project/txe/package.json +++ b/yarn-project/txe/package.json @@ -66,6 +66,7 @@ "@aztec/aztec-node": "workspace:^", "@aztec/aztec.js": "workspace:^", "@aztec/bb-prover": "workspace:^", + "@aztec/bb.js": "workspace:^", "@aztec/constants": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/key-store": "workspace:^", 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 dd9a63f8e36a..3dcca5e66f95 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 @@ -476,11 +476,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), @@ -498,58 +502,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( @@ -599,7 +604,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, @@ -656,55 +665,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/index.ts b/yarn-project/txe/src/state_machine/index.ts index ec00644c007e..2d48c09b8f53 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -59,7 +59,7 @@ export class TXEStateMachine { new TXEGlobalVariablesBuilder(), new TXEFeeProvider(), new MockEpochCache(), - getPackageVersion(), + getPackageVersion() ?? '', new TestCircuitVerifier(), new TestCircuitVerifier(), undefined, diff --git a/yarn-project/txe/src/state_machine/synchronizer.ts b/yarn-project/txe/src/state_machine/synchronizer.ts index 8e217f4785d1..4bc1252fc82f 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, CdbIpcServer } from '@aztec/simulator/server'; import type { BlockHash, L2Block } from '@aztec/stdlib/block'; import type { MerkleTreeReadOperations, @@ -15,12 +16,35 @@ 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.tmp(); - return new this(nativeWorldStateService); + const synchronizer = new this(nativeWorldStateService); + + // Spawn IPC backends for C++ public simulation + const wsdbSocketPath = nativeWorldStateService.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found — required for TXE public simulation'); + } + + synchronizer.cdbServer = new CdbIpcServer(); + synchronizer.avmBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: synchronizer.cdbServer.socketPath, + }); + + return synchronizer; } public async handleL2Block(block: L2Block) { @@ -70,8 +94,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 +109,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..dd6d4d398da3 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 aztec-avm 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 249d6e026043..6dee70bb3ce6 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, L2Block } from '@aztec/stdlib/block'; import { 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; @@ -127,6 +130,26 @@ describe('ValidatorClient Integration', () => { const synchronizer = new ServerWorldStateSynchronizer(worldStateDb, archiver, wsConfig); await synchronizer.start(); + // Spawn AVM backend for IPC simulation + const wsdbSocketPath = worldStateDb.getSocketPath(); + const { AvmBackend } = await import('@aztec/bb.js/aztec-avm'); + const { findAvmBinary } = await import('@aztec/bb.js/platform'); + const avmBinaryPath = findAvmBinary(); + if (!avmBinaryPath) { + throw new Error('aztec-avm binary not found'); + } + + const { CdbIpcServer, PublicContractsDB } = await import('@aztec/simulator/server'); + const cdbServer = new CdbIpcServer(); + const contractsDB = new PublicContractsDB(archiver); + cdbServer.registerFork(0, contractsDB, 0n); + + const avmBackend: AvmIpcBackend = new AvmBackend({ + binaryPath: avmBinaryPath, + wsdbSocketPath, + cdbSocketPath: cdbServer.socketPath, + }); + // Create real checkpoints builder const checkpointsBuilder = new FullNodeCheckpointsBuilder( { @@ -140,6 +163,10 @@ describe('ValidatorClient Integration', () => { synchronizer, archiver, dateProvider, + /*telemetryClient=*/ undefined, + /*debugLogStore=*/ undefined, + avmBackend, + cdbServer, ); // Create mock p2p client @@ -208,6 +235,8 @@ describe('ValidatorClient Integration', () => { checkpointsBuilder, p2pClient, validator, + avmBackend, + cdbServer, }; }; @@ -385,11 +414,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 1e095490f2c9..5721d9b6436c 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..68125bc10a70 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, type WsdbIpcBackend, getWsdbOptions } from './ipc_world_state_instance.js'; diff --git a/yarn-project/world-state/src/native/merkle_trees_facade.ts b/yarn-project/world-state/src/native/merkle_trees_facade.ts index 23ef9b9cc7c3..e48f209f69de 100644 --- a/yarn-project/world-state/src/native/merkle_trees_facade.ts +++ b/yarn-project/world-state/src/native/merkle_trees_facade.ts @@ -50,10 +50,6 @@ export class MerkleTreesFacade implements MerkleTreeReadOperations { return this.revision; } - getSocketPath(): string { - return this.instance.getSocketPath(); - } - findLeafIndices(treeId: MerkleTreeId, values: MerkleTreeLeafType[]): Promise<(bigint | undefined)[]> { return this.findLeafIndicesAfter(treeId, values, 0n); } @@ -223,6 +219,12 @@ export class MerkleTreesForkFacade extends MerkleTreesFacade implements MerkleTr assert.equal(revision.includeUncommitted, true, 'Fork must include uncommitted data'); super(instance, initialHeader, revision); } + + /** Returns the WSDB fork ID for this fork. */ + getForkId(): number { + return this.revision.forkId; + } + async updateArchive(header: BlockHeader): Promise { await this.instance.call(WorldStateMessageType.UPDATE_ARCHIVE, { forkId: this.revision.forkId, 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 a500e19cd16d..3c0856a02853 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 @@ -1,43 +1,16 @@ -import type { +import { WorldStateMessageType, - WorldStateRequest, - WorldStateRequestCategories, - WorldStateResponse, + type WorldStateRequest, + type WorldStateRequestCategories, + type WorldStateResponse, } from './message.js'; -/** - * Backend-agnostic handle to a running aztec-wsdb world state, accessed by the TS layer. - * - * Two implementations exist: - * - {@link IpcWorldState} — talks to a standalone aztec-wsdb process over UDS or shared memory. - * - * The legacy in-process NAPI implementation has been removed; the C++ AVM (NAPI) now connects to - * the same aztec-wsdb process via UDS using the socket path returned by {@link getSocketPath}. - */ export interface NativeWorldStateInstance { - /** - * Send a typed msgpack message to the backing world state and await its response. - * - * @param responseHandler — optional pre-resolution hook executed on the per-fork queue, useful - * for caching responses while the queue still holds the fork lock. - * @param errorHandler — optional pre-rejection hook executed on the per-fork queue. - */ call( messageType: T, body: WorldStateRequest[T] & WorldStateRequestCategories, responseHandler?: (response: WorldStateResponse[T]) => WorldStateResponse[T], errorHandler?: (error: string) => void, ): Promise; - - /** - * UDS path the underlying aztec-wsdb process listens on. The C++ AVM uses this to attach to the - * same world state instance the TS layer is using. - */ - getSocketPath(): string; - - /** - * Shut down the world state instance. Cancels any in-flight queues, closes the IPC channel, and - * terminates the underlying aztec-wsdb process. Idempotent. - */ close(): Promise; } diff --git a/yarn-project/world-state/src/synchronizer/factory.ts b/yarn-project/world-state/src/synchronizer/factory.ts index 5f8c2a5f52f9..e4bac50e75be 100644 --- a/yarn-project/world-state/src/synchronizer/factory.ts +++ b/yarn-project/world-state/src/synchronizer/factory.ts @@ -6,6 +6,7 @@ import { EMPTY_GENESIS_DATA, type GenesisData, isGenesisData } from '@aztec/stdl import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client'; import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js'; +import type { WsdbIpcBackend } from '../native/ipc_world_state_instance.js'; import { NativeWorldStateService } from '../native/native_world_state.js'; import type { WorldStateConfig } from './config.js'; import { ServerWorldStateSynchronizer } from './server_world_state_synchronizer.js'; @@ -24,10 +25,19 @@ export async function createWorldStateSynchronizer( genesisOrNativeWorldState: GenesisData | NativeWorldStateService, client: TelemetryClient = getTelemetryClient(), bindings?: LoggerBindings, + wsdbBackend?: WsdbIpcBackend, + recreateIpcInstance?: () => Promise, ) { const instrumentation = new WorldStateInstrumentation(client); const merkleTrees = isGenesisData(genesisOrNativeWorldState) - ? await createWorldState(config, genesisOrNativeWorldState, instrumentation, bindings) + ? await createWorldState( + config, + genesisOrNativeWorldState, + instrumentation, + bindings, + wsdbBackend, + recreateIpcInstance, + ) : genesisOrNativeWorldState; return new ServerWorldStateSynchronizer(merkleTrees, l2BlockSource, config, instrumentation); } @@ -47,7 +57,21 @@ export async function createWorldState( genesis: GenesisData = EMPTY_GENESIS_DATA, instrumentation: WorldStateInstrumentation = new WorldStateInstrumentation(getTelemetryClient()), bindings?: LoggerBindings, + wsdbBackend?: WsdbIpcBackend, + recreateIpcInstance?: () => Promise, ) { + // If an IPC backend is provided, use it directly (avoids spawning a new wsdb process) + if (wsdbBackend) { + return NativeWorldStateService.fromIpc( + wsdbBackend, + instrumentation, + bindings, + genesis, + undefined, + recreateIpcInstance, + ); + } + const dataDirectory = config.worldStateDataDirectory ?? config.dataDirectory; const dataStoreMapSizeKb = config.worldStateDbMapSizeKb ?? config.dataStoreMapSizeKb; const wsTreeMapSizes: WorldStateTreeMapSizes = { 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 ab9a8679cdc4..4e24681e77eb 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:^" @@ -1847,6 +1848,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:^" @@ -2160,6 +2162,7 @@ __metadata: "@aztec/aztec-node": "workspace:^" "@aztec/aztec.js": "workspace:^" "@aztec/bb-prover": "workspace:^" + "@aztec/bb.js": "workspace:^" "@aztec/constants": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/key-store": "workspace:^" @@ -2188,6 +2191,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:^" @@ -2305,7 +2309,6 @@ __metadata: "@aztec/constants": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/kv-store": "workspace:^" - "@aztec/native": "workspace:^" "@aztec/protocol-contracts": "workspace:^" "@aztec/stdlib": "workspace:^" "@aztec/telemetry-client": "workspace:^"