feat: cut simulator over to generated bb-avm-sim IPC service#23697
feat: cut simulator over to generated bb-avm-sim IPC service#23697charlielye wants to merge 3 commits into
Conversation
1c8d4f9 to
a4e2e70
Compare
6867e96 to
65f9aed
Compare
a4e2e70 to
0df283e
Compare
65f9aed to
47b1d3b
Compare
0df283e to
42d8d4f
Compare
47b1d3b to
43f900d
Compare
42d8d4f to
7f602eb
Compare
43f900d to
85b52b4
Compare
7f602eb to
498b1ca
Compare
85b52b4 to
fac9421
Compare
498b1ca to
4597920
Compare
fac9421 to
8b51cfa
Compare
4597920 to
4a53618
Compare
8b51cfa to
cbffb29
Compare
af3dc85 to
8cc9db5
Compare
a9908cb to
665dc82
Compare
8cc9db5 to
3fe908c
Compare
665dc82 to
77496a4
Compare
1ea909f to
e7f5dc1
Compare
77496a4 to
8b2448a
Compare
2358098 to
b248948
Compare
8de6b5d to
389063c
Compare
b248948 to
9835c8c
Compare
3735bd8 to
aae8fc7
Compare
3ab720f to
7c38799
Compare
aae8fc7 to
476ea6b
Compare
7c38799 to
6b222ba
Compare
476ea6b to
f14b0ba
Compare
6b222ba to
a53f9a5
Compare
f14b0ba to
7c17ac3
Compare
a53f9a5 to
4d7c55c
Compare
328033c to
c947bce
Compare
4d7c55c to
6793497
Compare
c947bce to
15daef1
Compare
6793497 to
bf67554
Compare
15daef1 to
3484d44
Compare
bf67554 to
a0707df
Compare
456e460 to
6094676
Compare
1b4352c to
ebe1ab2
Compare
6094676 to
0562481
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| BlockNumber(block.number - 1), | ||
| blockIndex === 0 ? l1ToL2Messages : undefined, | ||
| ); | ||
| this.checkState(); |
There was a problem hiding this comment.
What checkState() is
A cooperative-cancellation poll: it throws HaltExecutionError if this.state is terminal (timed-out/stopped/failed/reorg), set by stop() on deadline/reorg (epoch-proving-job.ts:406-410). It's "have I been told to quit? if so, bail here."
What actually changed
On next, the inner block loop had three of them:
- after createFork, before building the config
- after processTxs, before addTxs
- after the block scope closed
In the new version:
- The one after processTxs moved into processTxs — it's now at epoch-proving-job.ts:511, right after publicProcessor.process(...) returns. Not deleted, relocated.
- The checkpoint-level check survives at line 177, at the top of each processCheckpoints iteration.
- The other two were dropped as redundant. One guarded a stretch of pure synchronous work (building a PublicSimulatorConfig literal — instant, nothing to interrupt); the other was immediately after the block scope, already covered by the next iteration's checks.
Why polling matters less now
The real cancellation path changed. processTxs now passes the abort signal into the processor:
await publicProcessor.process(txs, { deadline, signal: this.abortController.signal }); // :507-509
stop() calls this.abortController.abort() (:428). So a timeout/reorg now interrupts the actual long-running work — publicProcessor.process observes the deadline/abort, and on PublicProcessorAbortError/PublicProcessorTimeoutError it drives the AVM cancel path (public_processor.ts:346-357 → publicTxSimulator.cancel() → the SIGUSR1 mechanism we just discussed).
Under the old NAPI model the simulation was an in-process blocking call that didn't itself honor an abort, so the only way to bail was to poll checkState() at the statement boundaries around it. Now that cancellation reaches into the simulation itself, sprinkling polls between cheap synchronous steps buys nothing — the one check that matters (after the awaitable work returns) is kept, inside processTxs.
One tangential thing in the same diff: await using db became an explicit try/finally with unregisterFork(...) + db.close() (:246-257). That's the IPC/CDB requirement — the CDB server routes callbacks by forkId, so forks must be explicitly unregistered, which await using couldn't express. Unrelated to the checkState removal but part of the same rewrite.
| @@ -29,6 +28,9 @@ import type { ProverNodeJobMetrics } from '../metrics.js'; | |||
| import type { ProverNodePublisher } from '../prover-node-publisher.js'; | |||
| import { type EpochProvingJobData, validateEpochProvingJobData } from './epoch-proving-job-data.js'; | |||
|
|
|||
| /** Default parallelism for processing checkpoints. The AVM pool already limits concurrent processes. */ | |||
| const DEFAULT_PARALLEL_CHECKPOINT_LIMIT = parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10); | |||
There was a problem hiding this comment.
The old code imported the constant AVM_MAX_CONCURRENT_SIMULATIONS from @aztec/native. This PR's legacy-NAPI cleanup deletes AVM simulation from @aztec/native, so that import no longer exists. The prover-node now reads the same env var directly:
const DEFAULT_PARALLEL_CHECKPOINT_LIMIT = parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10);
So it's the same underlying value (AVM_MAX_CONCURRENT_SIMULATIONS, default 4), just sourced from the environment instead of a native constant. The new name describes what the number means at this call site — "how many checkpoints to fan out in parallel" — rather than borrowing the native module's simulation-cap name. The comment (:31) spells out the reasoning: "The AVM pool already limits concurrent processes" — i.e., this is now just an upstream fan-out default, not the actual hard cap on AVM concurrency.
| this.simulator = new MeasuredPublicTxSimulator(merkleTree, contractsDB, globals, this.metrics, config); | ||
| // No simulator — this tester can only be used for setup (setFeePayerBalance, createTx, etc.) | ||
| // To simulate, use PublicTxSimulationTester.create() or pass a simulatorFactory. | ||
| this.simulator = undefined; |
There was a problem hiding this comment.
Post-IPC-cutover the constructor can no longer build a working simulator: MeasuredPublicTxSimulator now requires an already-spawned AvmSimulator (external bb-avm-sim process + WSDB/CDB IPC), which can only be created asynchronously in create(). So the sync constructor degrades to setup-only mode (createTx / setFeePayerBalance); simulateTx throws a clear error if you try to simulate without one. Not a regression — real simulation goes through create() or an explicit simulatorFactory.
| feePayer?: AztecAddress, | ||
| privateInsertions?: TestPrivateInsertions, | ||
| gasLimits?: Gas, | ||
| ): Promise<PublicTxResult> { |
There was a problem hiding this comment.
Dropped the trailing gasLimits? param from this wrapper (and from executeTxWithLabel): no caller of these base-class label wrappers ever passed it. Custom gas limits are still fully supported via createTx / simulateTx, and the AvmProvingTester proving path keeps gas control through its simProveVerify* methods. So this was an unused pass-through, not a loss of functionality.
Replace the in-process NAPI AVM with an out-of-process bb-avm-sim binary reached over IPC. The simulator drives a pool of bb-avm-sim processes (AvmSimulatorPool) behind an AvmExecutor that also runs the CDB server answering the C++ AVM's contract-data callbacks; per-fork work goes through AvmExecutor.forFork. The pool prewarms one process on spawn so the first simulate() runs at steady-state cost. Also adds the bench_compare script + bench-regression skill for PR perf checks.
The public-execution wiring threaded a concrete `AvmExecutor` (pool + CDB
server) through the node, prover-node, checkpoint builder, and TXE, and each
call site called `forFork(forkId, contractsDB, timestamp)` to get a
fork-bound `ForkedAvmSimulator`. That leaked the IPC transport (the CDB
server, its fork-id routing key, and the register/unregister dance) into
every consumer of public simulation.
Thread a single transport-agnostic `AvmSimulator` interface instead, mirroring
the pre-IPC shape where the factory threaded `(fork, contractsDB)` and called
an in-process simulate. `simulate` now takes an `AvmContractsDBContext`
({contractsDB, forkId, timestamp}); the implementation owns the CDB
register -> run -> unregister lifecycle. The pool (`AvmSimulatorPool`) folds in
the CDB server and is the sole concrete `AvmSimulator`, spawned only at
program entrypoints. `AvmExecutor`, `forFork`, and `ForkedAvmSimulator` are
gone.
`PublicTxSimulatorBase` gains `contractsDB`; consumers (PublicProcessorFactory,
prover-node, checkpoint builder, TXE synchronizer) hold the `AvmSimulator`
interface and pass the DBs, deriving forkId from the fork's revision.
fcarreiro
left a comment
There was a problem hiding this comment.
Approved with minor comments.
| * This token is used to signal cancellation from TypeScript to C++ when a simulation | ||
| * times out. The token is created in TypeScript, passed through NAPI, and checked | ||
| * periodically during C++ execution. |
There was a problem hiding this comment.
Not NAPI/TS anymore I suppose? (this whole comment may be stale now)
| /** IPC backends to clean up on stop (CDB, AVM). WSDB is cleaned up by world state. */ | ||
| private ipcBackends: Array<{ destroy?(): Promise<void> }> = []; |
There was a problem hiding this comment.
Every other subservice is explicitly stored and stopped (see stop()). Maybe we should just do that? Also no need to call it IPC, it's just something with a destroy().
| // process and signals only the in-flight one, so this isn't exercised today; | ||
| // hardening it would need a request-scoped cancel channel rather than a signal. | ||
| const avm2::simulation::CancellationTokenPtr g_sim_cancellation_token = | ||
| std::make_shared<avm2::simulation::CancellationToken>(); |
There was a problem hiding this comment.
Does this need to be a shared_ptr now that it's global?
| * Run a fast simulation and return the msgpack-encoded result. If `signal` aborts, the simulation stops | ||
| * at the next cancellation checkpoint. | ||
| */ | ||
| simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>; |
There was a problem hiding this comment.
As you mentioned, this whole interface/IPC impl should probably be autogenerated by the codegen, and should take actual types derived from the schema (that then get messagepacked internally). I understand this will (likely) be done in a later PR so it's ok.
| 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() ?? ''; |
There was a problem hiding this comment.
Is this from your PR? Why was it needed?
| rollupVersion: BigInt(config.rollupVersion), | ||
| l1GenesisTime, | ||
| slotDuration: Number(slotDuration), | ||
| rollupManaLimit, |
| this.return(simulator); | ||
| } | ||
| } finally { | ||
| this.cdbServer.unregisterFork(context.forkId); |
| protected readonly metrics: ExecutorMetricsInterface, | ||
| config?: Partial<PublicSimulatorConfig>, | ||
| bindings?: LoggerBindings, | ||
| forkId?: number, |
There was a problem hiding this comment.
I still find it strange to have this here (and in the base classes)
…rrency Two leaks/bugs surfaced by a full-CI process-tree capture of the AVM cutover: 1. TXE session leak. `TXESession.dispose()` closed the world state and LMDB but never stopped the synchronizer's AVM pool, so every test orphaned its prewarmed bb-avm-sim process — and, because that process kept a live connection to the WSDB server, pinned the WSDB alive too. Over a run this accumulated to hundreds of undisposed process-triples. Dispose the pool (`closeIpc()`) first, so the bb-avm-sim processes exit and release their WSDB connections before the WSDB is closed. 2. Pool overshoot under concurrency. `AvmSimulatorPool` bumped `createdCount` only after the `await` that spawns a process, so concurrent checkouts could all observe `createdCount < maxSize` and each spawn, growing the pool past maxSize (a pool was seen holding maxSize+1 processes in CI). Reserve the count synchronously at the top of `createSlot`, before the spawn, and roll it back if the spawn fails, so the maxSize guard is exclusive.
Flakey Tests🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry. |
Summary
Cuts the public simulator over from the legacy in-process NAPI AVM path to the generated
@aztec/bb-avm-simIPC service package introduced in #23084.The simulator now drives a pool of out-of-process AVM instances over IPC and owns a TypeScript CDB IPC server for contract-data callbacks, while world-state access continues through the generated wsdb package/service from the lower stack.
Stack
Merged into
next: #23610cl/ipc-foundation· #23611cl/ipc-wsdb-migrate· #23036cl/ipc-3-avm-wsdb-cutover· #23084cl/ipc-4-avm-binaryThis chain (bottom → top):
cl/ipc-5-avm-cutover— this PR (basenext)cl/ipc-6-memory-merkle-db— in-memoryMemoryMerkleDBreference; dropsworld_statefrom vm2cl/wsdb-decouple— extract wsdb/lmdblib/kvdb intonative-packages/, decoupled from barretenberg headersParallel IPC stack (independent, also based on
next): #23612cl/ipc-bb-migrate→ #23613cl/ipc-bb-rs-migrate→ #23614cl/ipc-bb-js-migrate.What changes
AVM simulator cutover
AvmSimulatorPool, which owns the out-of-process@aztec/bb-avm-siminstances and implements a smallAvmSimulatorinterface (simulate/simulateWithHints). Checkout/return is fully internal:simulateblocks until an instance is free and releases it when done, so callers never see the pool mechanics or the IPC transport.AvmExecutorfacade.AvmExecutor.forFork(forkId, contractsDB, timestamp)hands out aForkedAvmSimulatorthat registers the fork's contracts DB with the CDB server for the duration of eachsimulatecall. The executor is spawned once and injected into the node, public processor, checkpoint builder, TXE, and the fuzzer.AbortSignal-based cancellation end to end:PublicTxSimulatorcancels via anAbortController, which signals the external C++ AVM process (SIGUSR1) to stop at the next opcode / before the next world-state write, replacing the old in-process cancellation.AsyncDisposable/await using) for the AVM executor, CDB server, wsdb service, and the tests/fixtures that own those resources.CDB server generation
CdbIpcServerwith the sharedUdsIpcServertransport from@aztec/ipc-runtime(socket, framing, per-connection response ordering) rather than hand-rolled framing;CdbIpcServeronly implements the generated handler interface.simulatorgeneration step during bootstrap.Legacy NAPI cleanup
nodejs_module.@aztec/native, leaving the remaining native module focused on the native pieces that still live there.Validation
yarn buildfromyarn-project/simulatormake bb-avm-simcmake --build build --target bb-avm-sim nodejs_modulefrombarretenberg/cppyarn buildfromyarn-project./bootstrap.sh format --check simulatorfromyarn-projectJEST_MAX_WORKERS=1 yarn test public/cdb_ipc_server.test.ts public/public_processor/apps_tests/token.test.ts public/public_tx_simulator/apps_tests/token.test.ts public/public_processor/apps_tests/deployments.test.ts public/public_processor/public_processor.test.tsfromyarn-project/simulator