Skip to content

Commit dd42d7e

Browse files
authored
fix(avm): Add cancellation token to prevent C++ simulation race condition on timeout (#19219)
# fix(avm): Add cancellation token to prevent C++ simulation race condition on timeout ## Summary Fix race condition where C++ AVM simulation corrupts WorldState after TypeScript timeout. This was done as follows: - Add `CancellationToken` mechanism to safely stop C++ simulation before reverting checkpoints - **Primary Fix**: PublicProcessor cancels public simulation and **waits** for it to die before proceeding and reverting checkpoints - C++ simulation polls this cancellation token regularly so that we know it will die quickly after cancellation - **Secondary Fix**: Add mutex locking to checkpoint operations for thread safety - Add paired tests proving both the bug exists (without fix) and the fix works (with cancellation) ## The Bug When a C++ AVM simulation times out via `Promise.race()` in `PublicProcessor`, a race condition occurs: 1. TypeScript's timeout fires and calls `revertCheckpoint()` on WorldState 2. **But C++ simulation continues running** on a libuv worker thread 3. C++ holds a native handle to WorldState obtained before the timeout 4. C++ makes writes to WorldState **after** the checkpoint was reverted 5. This corrupts WorldState, causing `checkWorldStateUnchanged()` to fail The root causes are: 1. `GuardedMerkleTreeOperations` only guards TS merkle operations. The C++ code interacts with the same WorldState without guards. 2. Nothing stops C++ from finishing simulation after PublicProcessor reaches deadline. ``` Timeline of the race (BEFORE fix): TypeScript C++ (libuv worker thread) ---------- ------------------------- Start simulation -----------------> Begins executing opcodes | | v v Promise.race timeout fires Still running (e.g., in pad_trees()) | | v | | | v | IMMEDIATELY revertCheckpoint() | | v | Finishes pad_trees() | Makes write AFTER revert! (CORRUPTION) v | checkWorldStateUnchanged() FAILS! ``` ## The Fix The key insight: **signaling cancellation is not enough** - we must **wait** for C++ to actually stop. ``` Timeline (AFTER fix): TypeScript C++ (libuv worker thread) ---------- ------------------------- Start simulation -----------------> Begins executing opcodes | | v v Promise.race timeout fires Still running (e.g., in pad_trees()) | | v | cancel(100) sets atomic flag | (doesn't check flag yet) | | v v WAIT for simulation promise Finishes pad_trees(), checks flag | Throws CancelledException |<-----------------------------Promise rejects v NOW revertCheckpoint() (C++ is done) | v checkWorldStateUnchanged() ✓ clean state ``` ### C++ Side 1. **`CancellationToken` class** (`cancellation_token.hpp`): - Thread-safe `std::atomic<bool>` flag - `cancel()` - signals cancellation (called from TS thread) - `check()` - throws `CancelledException` if cancelled (called from worker thread) 3. **Check at each opcode** (`execution.cpp`, `hybrid_execution.cpp`): ```cpp while (!external_call_stack.empty()) { if (cancellation_token_) { cancellation_token_->check(); } // ... execute opcode } ``` 4. **Check before every WorldState write** (`raw_data_dbs.cpp`): ```cpp void PureRawMerkleDB::pad_tree(...) { check_cancellation(); // Throws if cancelled // ... proceed with write } ``` 5. **Mutex locking for checkpoint operations** (`cached_content_addressed_tree_store.hpp`): ```cpp void ContentAddressedCachedTreeStore::checkpoint() { std::unique_lock lock(mtx_); // Prevent races with C++ writes cache_.checkpoint(); } ``` ### TypeScript Side 1. **`CppPublicTxSimulator.cancel(waitTimeoutMs)`** - Signal AND wait: ```typescript public async cancel(waitTimeoutMs?: number): Promise<void> { if (this.cancellationToken) { cancelSimulation(this.cancellationToken); } // Wait for simulation to actually complete if (waitTimeoutMs !== undefined && this.simulationPromise) { await Promise.race([ this.simulationPromise.catch(() => {}), sleep(waitTimeoutMs), ]); } } ``` > Note: ideally we'd like to have no timeout here since we really want to wait for C++ to recognize cancellation. The timeout is really just a safeguard against some cancellation bug. 2. **`PublicProcessor`** timeout handler: ```typescript if (err?.name === 'PublicProcessorTimeoutError') { // Signal cancellation AND WAIT for C++ to stop (up to 100ms) await this.publicTxSimulator.cancel?.(); // NOW safe to stop the guarded fork await this.guardedMerkleTree.stop(); } ``` ## Test Plan Four paired tests at two levels verify the bug and fix: ### Replace bug and prove fix at TxSimulator level Tests using `CppPublicTxSimulator` directly - **identical code**, only difference is whether `cancel()` is called: ```typescript async function runRaceConditionTest(useCancellation: boolean): Promise<number> { // ... setup ... const simulationPromise = simulator.simulate(tx); if (useCancellation) { await simulator.cancel(100); // FIX: Signal AND wait } // BUG: No cancel, C++ continues during reverts await merkleTrees.revertCheckpoint(); // Check for corruption... } it('BUG PROOF: race condition exists WITHOUT cancellation'); // Expects >0 corruptions it('FIX PROOF: no race condition WITH cancellation'); // Expects 0 corruptions ``` ### Replicate bug and prove fix at PublicProcessor level Tests using full `PublicProcessor.process()` with deadline timeout: ```typescript it('PublicProcessor BUG PROOF: state corruption occurs WITHOUT cancellation'); it('PublicProcessor FIX PROOF: no state corruption WITH cancellation'); ``` ### Running the tests ```bash cd yarn-project/simulator yarn test src/public/public_processor/apps_tests/timeout_race.test.ts ``` ## Files Changed ### C++ (barretenberg) | File | Change | |------|--------| | `vm2/simulation/lib/cancellation_token.hpp` | **New**: Thread-safe cancellation token class | | `vm2/simulation/lib/raw_data_dbs.hpp` | Add token to `PureRawMerkleDB` | | `vm2/simulation/lib/raw_data_dbs.cpp` | Check cancellation before all writes | | `vm2/simulation/gadgets/execution.hpp` | Add token to `Execution` | | `vm2/simulation/gadgets/execution.cpp` | Check cancellation at each opcode | | `vm2/simulation/standalone/hybrid_execution.cpp` | Check cancellation at each opcode | | `vm2/avm_sim_api.hpp` | Thread token through API | | `vm2/avm_sim_api.cpp` | Thread token through API | | `vm2/simulation_helper.hpp` | Thread token to execution | | `vm2/simulation_helper.cpp` | Thread token to execution | | `nodejs_module/avm_simulate/avm_simulate_napi.hpp` | NAPI bindings for token | | `nodejs_module/avm_simulate/avm_simulate_napi.cpp` | NAPI bindings for token | | `nodejs_module/init_module.cpp` | Register NAPI functions | | `crypto/merkle_tree/.../cached_content_addressed_tree_store.hpp` | Add mutex to checkpoint ops | ### TypeScript (yarn-project) | File | Change | |------|--------| | `native/src/native_module.ts` | Export `createCancellationToken`, `cancelSimulation` | | `simulator/.../public_tx_simulator_interface.ts` | Add `cancel?(waitTimeoutMs?)` method | | `simulator/.../cpp_public_tx_simulator.ts` | Track promise, implement `cancel()` with wait | | `simulator/.../public_processor.ts` | `await cancel(100)` before reverts | | `simulator/.../public_tx_simulation_tester.ts` | Add `cancel()` and `getSimulator()` | | `simulator/.../timeout_race.test.ts` | **New**: Paired proof tests | ## Why This Approach 1. **Wait, don't just signal**: The key fix is awaiting the simulation promise after signaling 2. **Bounded wait**: 100ms timeout prevents indefinite blocking if C++ is stuck 3. **Check before writes**: Prevents partial/corrupted state from cancelled operations 4. **Mutex on checkpoints**: Prevents races between C++ writes and TS checkpoint ops 6. **Minimal overhead**: `std::atomic<bool>` check is very cheap (single memory read) 7. **Backward compatible**: Token is optional, existing code works unchanged 8. **Testable**: Paired tests definitively prove both the bug and the fix ## Future Work ### **Move merkle tree guarding into C++** (out of scope for this PR) Currently, `GuardedMerkleTreeOperations` in TypeScript wraps merkle tree operations to prevent access after `stop()` is called. However, this guard is ineffective for C++ because: 1. C++ obtains the native WorldState and bypasses TS guarding 2. C++ can still make writes after TS calls `stop()` ### Guard getRevision I think this is lower-impact, but ideally getRevision should fail if called on a stopped instance. --- 🤖 Description generated with [Claude Code](https://claude.com/claude-code)
1 parent 4a06547 commit dd42d7e

File tree

20 files changed

+792
-65
lines changed

20 files changed

+792
-65
lines changed

barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,31 +273,36 @@ ContentAddressedCachedTreeStore<LeafValueType>::ContentAddressedCachedTreeStore(
273273
}
274274

275275
// Much Like the commit/rollback/set finalized/remove historic blocks apis
276-
// These 3 apis (checkpoint/revert_checkpoint/commit_checkpoint) all assume they are not called
277-
// during the process of reading/writing uncommitted state
278-
// This is reasonable, they intended for use by forks at the point of starting/ending a function call
276+
// These checkpoint apis modify the cache's internal state.
277+
// They acquire the mutex to prevent races with concurrent read/write operations (e.g., when C++ AVM simulation
278+
// runs on a worker thread while TypeScript calls revert_checkpoint from a timeout handler).
279279
template <typename LeafValueType> void ContentAddressedCachedTreeStore<LeafValueType>::checkpoint()
280280
{
281+
std::unique_lock lock(mtx_);
281282
cache_.checkpoint();
282283
}
283284

284285
template <typename LeafValueType> void ContentAddressedCachedTreeStore<LeafValueType>::revert_checkpoint()
285286
{
287+
std::unique_lock lock(mtx_);
286288
cache_.revert();
287289
}
288290

289291
template <typename LeafValueType> void ContentAddressedCachedTreeStore<LeafValueType>::commit_checkpoint()
290292
{
293+
std::unique_lock lock(mtx_);
291294
cache_.commit();
292295
}
293296

294297
template <typename LeafValueType> void ContentAddressedCachedTreeStore<LeafValueType>::revert_all_checkpoints()
295298
{
299+
std::unique_lock lock(mtx_);
296300
cache_.revert_all();
297301
}
298302

299303
template <typename LeafValueType> void ContentAddressedCachedTreeStore<LeafValueType>::commit_all_checkpoints()
300304
{
305+
std::unique_lock lock(mtx_);
301306
cache_.commit_all();
302307
}
303308

barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "barretenberg/serialize/msgpack_impl/msgpack_impl.hpp"
1111
#include "barretenberg/vm2/avm_sim_api.hpp"
1212
#include "barretenberg/vm2/common/avm_io.hpp"
13+
#include "barretenberg/vm2/simulation/lib/cancellation_token.hpp"
1314

1415
namespace bb::nodejs {
1516

@@ -116,15 +117,16 @@ Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info)
116117
{
117118
Napi::Env env = cb_info.Env();
118119

119-
// Validate arguments - expects 4 arguments
120+
// Validate arguments - expects 4-5 arguments
120121
// arg[0]: inputs Buffer (required)
121122
// arg[1]: contractProvider object (required)
122123
// arg[2]: worldStateHandle external (required)
123124
// arg[3]: logLevel number (required) - index into TS LogLevels array
125+
// arg[4]: cancellationToken external (optional)
124126
if (cb_info.Length() < 4) {
125127
throw Napi::TypeError::New(env,
126-
"Wrong number of arguments. Expected 4 arguments: inputs Buffer, contractProvider "
127-
"object, worldStateHandle, and logLevel.");
128+
"Wrong number of arguments. Expected 4-5 arguments: inputs Buffer, contractProvider "
129+
"object, worldStateHandle, logLevel, and optional cancellationToken.");
128130
}
129131

130132
if (!cb_info[0].IsBuffer()) {
@@ -144,6 +146,19 @@ Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info)
144146
throw Napi::TypeError::New(env, "Fourth argument must be a log level number (0-7)");
145147
}
146148

149+
// Extract optional cancellation token (5th argument)
150+
avm2::simulation::CancellationTokenPtr cancellation_token = nullptr;
151+
if (cb_info.Length() > 4 && cb_info[4].IsExternal()) {
152+
auto token_external = cb_info[4].As<Napi::External<avm2::simulation::CancellationToken>>();
153+
// Wrap the raw pointer in a shared_ptr that does NOT delete (since the External owns it)
154+
cancellation_token = std::shared_ptr<avm2::simulation::CancellationToken>(
155+
token_external.Data(), [](avm2::simulation::CancellationToken*) {
156+
// No-op deleter: the External
157+
// (via shared_ptr destructor
158+
// callback) owns the token
159+
});
160+
}
161+
147162
// Extract log level and set logging flags
148163
int log_level = cb_info[3].As<Napi::Number>().Int32Value();
149164
set_logging_from_level(log_level);
@@ -189,42 +204,46 @@ Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info)
189204
auto deferred = std::make_shared<Napi::Promise::Deferred>(env);
190205

191206
// Create async operation that will run on a worker thread
192-
auto* op = new AsyncOperation(env, deferred, [data, tsfns, ws_ptr](msgpack::sbuffer& result_buffer) {
193-
// Ensure all thread-safe functions are released in all code paths
194-
TsfnReleaser releaser = TsfnReleaser(tsfns.to_vector());
195-
196-
try {
197-
// Deserialize inputs from msgpack
198-
avm2::AvmFastSimulationInputs inputs;
199-
msgpack::object_handle obj_handle =
200-
msgpack::unpack(reinterpret_cast<const char*>(data->data()), data->size());
201-
msgpack::object obj = obj_handle.get();
202-
obj.convert(inputs);
203-
204-
// Create TsCallbackContractDB with TypeScript callbacks
205-
TsCallbackContractDB contract_db(*tsfns.instance,
206-
*tsfns.class_,
207-
*tsfns.add_contracts,
208-
*tsfns.bytecode,
209-
*tsfns.debug_name,
210-
*tsfns.create_checkpoint,
211-
*tsfns.commit_checkpoint,
212-
*tsfns.revert_checkpoint);
213-
214-
// Create AVM API and run simulation with the callback-based contracts DB and
215-
// WorldState reference
216-
avm2::AvmSimAPI avm;
217-
avm2::TxSimulationResult result = avm.simulate(inputs, contract_db, *ws_ptr);
218-
219-
// Serialize the simulation result with msgpack into the return buffer to TS.
220-
msgpack::pack(result_buffer, result);
221-
} catch (const std::exception& e) {
222-
// Rethrow with context (RAII wrappers will clean up automatically)
223-
throw std::runtime_error(std::string("AVM simulation failed: ") + e.what());
224-
} catch (...) {
225-
throw std::runtime_error("AVM simulation failed with unknown exception");
226-
}
227-
});
207+
auto* op =
208+
new AsyncOperation(env, deferred, [data, tsfns, ws_ptr, cancellation_token](msgpack::sbuffer& result_buffer) {
209+
// Ensure all thread-safe functions are released in all code paths
210+
TsfnReleaser releaser = TsfnReleaser(tsfns.to_vector());
211+
212+
try {
213+
// Deserialize inputs from msgpack
214+
avm2::AvmFastSimulationInputs inputs;
215+
msgpack::object_handle obj_handle =
216+
msgpack::unpack(reinterpret_cast<const char*>(data->data()), data->size());
217+
msgpack::object obj = obj_handle.get();
218+
obj.convert(inputs);
219+
220+
// Create TsCallbackContractDB with TypeScript callbacks
221+
TsCallbackContractDB contract_db(*tsfns.instance,
222+
*tsfns.class_,
223+
*tsfns.add_contracts,
224+
*tsfns.bytecode,
225+
*tsfns.debug_name,
226+
*tsfns.create_checkpoint,
227+
*tsfns.commit_checkpoint,
228+
*tsfns.revert_checkpoint);
229+
230+
// Create AVM API and run simulation with the callback-based contracts DB,
231+
// WorldState reference, and optional cancellation token
232+
avm2::AvmSimAPI avm;
233+
avm2::TxSimulationResult result = avm.simulate(inputs, contract_db, *ws_ptr, cancellation_token);
234+
235+
// Serialize the simulation result with msgpack into the return buffer to TS.
236+
msgpack::pack(result_buffer, result);
237+
} catch (const avm2::simulation::CancelledException& e) {
238+
// Cancellation is an expected condition, rethrow with context
239+
throw std::runtime_error("Simulation cancelled");
240+
} catch (const std::exception& e) {
241+
// Rethrow with context (RAII wrappers will clean up automatically)
242+
throw std::runtime_error(std::string("AVM simulation failed: ") + e.what());
243+
} catch (...) {
244+
throw std::runtime_error("AVM simulation failed with unknown exception");
245+
}
246+
});
228247

229248
// Napi is now responsible for destroying this object
230249
op->Queue();
@@ -299,4 +318,34 @@ Napi::Value AvmSimulateNapi::simulateWithHintedDbs(const Napi::CallbackInfo& cb_
299318
return deferred->Promise();
300319
}
301320

321+
Napi::Value AvmSimulateNapi::createCancellationToken(const Napi::CallbackInfo& cb_info)
322+
{
323+
Napi::Env env = cb_info.Env();
324+
325+
// Create a new CancellationToken. We use a shared_ptr to manage the lifetime,
326+
// and the destructor callback in the External will clean it up when GC runs.
327+
auto* token = new avm2::simulation::CancellationToken();
328+
329+
// Create an External with a destructor callback that deletes the token
330+
return Napi::External<avm2::simulation::CancellationToken>::New(
331+
env, token, [](Napi::Env /*env*/, avm2::simulation::CancellationToken* t) { delete t; });
332+
}
333+
334+
Napi::Value AvmSimulateNapi::cancelSimulation(const Napi::CallbackInfo& cb_info)
335+
{
336+
Napi::Env env = cb_info.Env();
337+
338+
if (cb_info.Length() < 1 || !cb_info[0].IsExternal()) {
339+
throw Napi::TypeError::New(env, "Expected a CancellationToken External as argument");
340+
}
341+
342+
auto token_external = cb_info[0].As<Napi::External<avm2::simulation::CancellationToken>>();
343+
avm2::simulation::CancellationToken* token = token_external.Data();
344+
345+
// Signal cancellation - this is thread-safe (atomic store)
346+
token->cancel();
347+
348+
return env.Undefined();
349+
}
350+
302351
} // namespace bb::nodejs

barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ class AvmSimulateNapi {
2626
* - getContractInstance(address: string): Promise<Buffer | undefined>
2727
* - getContractClass(classId: string): Promise<Buffer | undefined>
2828
* - info[2]: External WorldState handle (pointer to world_state::WorldState)
29+
* - info[3]: Log level number (0-7)
30+
* - info[4]: External CancellationToken handle (optional)
2931
*
3032
* Returns: Promise<Buffer> containing serialized simulation results
3133
*
3234
* @param info NAPI callback info containing arguments
3335
* @return Napi::Value Promise that resolves with simulation results
3436
*/
3537
static Napi::Value simulate(const Napi::CallbackInfo& info);
38+
3639
/**
3740
* @brief NAPI function to simulate AVM execution with pre-collected hints
3841
*
@@ -43,6 +46,27 @@ class AvmSimulateNapi {
4346
* @return Napi::Value Promise that resolves with simulation results
4447
*/
4548
static Napi::Value simulateWithHintedDbs(const Napi::CallbackInfo& info);
49+
50+
/**
51+
* @brief Create a cancellation token that can be used to cancel a simulation.
52+
*
53+
* Returns: External<CancellationToken> - a handle to a new cancellation token
54+
*
55+
* @param info NAPI callback info (no arguments expected)
56+
* @return Napi::Value External handle to the cancellation token
57+
*/
58+
static Napi::Value createCancellationToken(const Napi::CallbackInfo& info);
59+
60+
/**
61+
* @brief Cancel a simulation by signaling the provided cancellation token.
62+
*
63+
* Expected arguments:
64+
* - info[0]: External CancellationToken handle
65+
*
66+
* @param info NAPI callback info containing the token
67+
* @return Napi::Value undefined
68+
*/
69+
static Napi::Value cancelSimulation(const Napi::CallbackInfo& info);
4670
};
4771

4872
} // namespace bb::nodejs

barretenberg/cpp/src/barretenberg/nodejs_module/init_module.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports)
1616
exports.Set(Napi::String::New(env, "avmSimulate"), Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::simulate));
1717
exports.Set(Napi::String::New(env, "avmSimulateWithHintedDbs"),
1818
Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::simulateWithHintedDbs));
19+
exports.Set(Napi::String::New(env, "createCancellationToken"),
20+
Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::createCancellationToken));
21+
exports.Set(Napi::String::New(env, "cancelSimulation"),
22+
Napi::Function::New(env, bb::nodejs::AvmSimulateNapi::cancelSimulation));
1923
return exports;
2024
}
2125

barretenberg/cpp/src/barretenberg/vm2/avm_sim_api.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ using namespace bb::avm2::simulation;
1010

1111
TxSimulationResult AvmSimAPI::simulate(const FastSimulationInputs& inputs,
1212
simulation::ContractDBInterface& contract_db,
13-
world_state::WorldState& ws)
13+
world_state::WorldState& ws,
14+
simulation::CancellationTokenPtr cancellation_token)
1415
{
1516
vinfo("Simulating...");
1617
AvmSimulationHelper simulation_helper;
@@ -23,7 +24,8 @@ TxSimulationResult AvmSimAPI::simulate(const FastSimulationInputs& inputs,
2324
inputs.config,
2425
inputs.tx,
2526
inputs.global_variables,
26-
inputs.protocol_contracts));
27+
inputs.protocol_contracts,
28+
cancellation_token));
2729
} else {
2830
return AVM_TRACK_TIME_V("simulation/all",
2931
simulation_helper.simulate_fast_with_existing_ws(contract_db,
@@ -32,7 +34,8 @@ TxSimulationResult AvmSimAPI::simulate(const FastSimulationInputs& inputs,
3234
inputs.config,
3335
inputs.tx,
3436
inputs.global_variables,
35-
inputs.protocol_contracts));
37+
inputs.protocol_contracts,
38+
cancellation_token));
3639
}
3740
}
3841

barretenberg/cpp/src/barretenberg/vm2/avm_sim_api.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "barretenberg/vm2/common/avm_io.hpp"
44
#include "barretenberg/vm2/simulation/interfaces/db.hpp"
5+
#include "barretenberg/vm2/simulation/lib/cancellation_token.hpp"
56

67
namespace bb::avm2 {
78

@@ -14,7 +15,8 @@ class AvmSimAPI {
1415

1516
TxSimulationResult simulate(const FastSimulationInputs& inputs,
1617
simulation::ContractDBInterface& contract_db,
17-
world_state::WorldState& ws);
18+
world_state::WorldState& ws,
19+
simulation::CancellationTokenPtr cancellation_token = nullptr);
1820
TxSimulationResult simulate_with_hinted_dbs(const AvmProvingInputs& inputs);
1921
};
2022

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/execution.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,11 @@ EnqueuedCallResult Execution::execute(std::unique_ptr<ContextInterface> enqueued
17241724
external_call_stack.push(std::move(enqueued_call_context));
17251725

17261726
while (!external_call_stack.empty()) {
1727+
// Throws CancelledException if cancelled. No-op when cancellation_token_ is nullptr (non-NAPI paths).
1728+
if (cancellation_token_) {
1729+
cancellation_token_->check_and_throw();
1730+
}
1731+
17271732
// We fix the context at this point. Even if the opcode changes the stack
17281733
// we'll always use this in the loop.
17291734
auto& context = *external_call_stack.top();

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/execution.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "barretenberg/vm2/simulation/interfaces/poseidon2.hpp"
3131
#include "barretenberg/vm2/simulation/interfaces/sha256.hpp"
3232
#include "barretenberg/vm2/simulation/interfaces/to_radix.hpp"
33+
#include "barretenberg/vm2/simulation/lib/cancellation_token.hpp"
3334
#include "barretenberg/vm2/simulation/lib/execution_id_manager.hpp"
3435
#include "barretenberg/vm2/simulation/lib/instruction_info.hpp"
3536
#include "barretenberg/vm2/simulation/lib/serialization.hpp"
@@ -79,7 +80,8 @@ class Execution : public ExecutionInterface {
7980
EmitUnencryptedLogInterface& emit_unencrypted_log_component,
8081
DebugLoggerInterface& debug_log_component,
8182
HighLevelMerkleDBInterface& merkle_db,
82-
CallStackMetadataCollectorInterface& call_stack_metadata_collector)
83+
CallStackMetadataCollectorInterface& call_stack_metadata_collector,
84+
CancellationTokenPtr cancellation_token = nullptr)
8385
: execution_components(execution_components)
8486
, instruction_info_db(instruction_info_db)
8587
, alu(alu)
@@ -100,6 +102,7 @@ class Execution : public ExecutionInterface {
100102
, events(event_emitter)
101103
, ctx_stack_events(ctx_stack_emitter)
102104
, call_stack_metadata_collector(call_stack_metadata_collector)
105+
, cancellation_token_(std::move(cancellation_token))
103106
{}
104107

105108
EnqueuedCallResult execute(std::unique_ptr<ContextInterface> enqueued_call_context) override;
@@ -265,6 +268,10 @@ class Execution : public ExecutionInterface {
265268
std::vector<MemoryValue> inputs;
266269
MemoryValue output;
267270
std::unique_ptr<GasTrackerInterface> gas_tracker;
271+
272+
// Optional cancellation token for stopping simulation on timeout.
273+
// When nullptr, cancellation checks are skipped (no overhead for non-NAPI paths).
274+
CancellationTokenPtr cancellation_token_;
268275
};
269276

270277
} // namespace bb::avm2::simulation

0 commit comments

Comments
 (0)