Skip to content

spike: TXE runs world state + AVM fully in-process (Slice C)#24642

Draft
charlielye wants to merge 12 commits into
cl/ipc-5-avm-cutoverfrom
cl/txe-inprocess-spike
Draft

spike: TXE runs world state + AVM fully in-process (Slice C)#24642
charlielye wants to merge 12 commits into
cl/ipc-5-avm-cutoverfrom
cl/txe-inprocess-spike

Conversation

@charlielye

Copy link
Copy Markdown
Contributor

Spike: TXE runs world state + AVM fully in-process (zero child processes)

Draft / spike — proves feasibility and surfaces the edges; productionization is a follow-up.

The AVM cutover made public execution an out-of-process bb-avm-sim service and world state an aztec-wsdb service. Right for production nodes; heavy for the TXE, where each of hundreds of Noir tests spins up its own (aztec-wsdb + bb-avm-sim + cdb) process-triple. Pre-cutover the TXE ran both in-process.

This spike makes where a service runs a backend choice expressed through the codegen service model, and has the TXE choose to run both the AVM and world state in one process, sharing a single WorldState — recovering the pre-cutover topology while production keeps the out-of-process backends unchanged. Set TXE_IN_PROCESS=1.

How it fits the service model

  • Barretenberg stays Node/NAPI-free: it produces plain-C-ABI libraries (wsdb_ffi, avm_sim_ffi). The out-of-process executables (aztec-wsdb, bb-avm-sim) and a small NAPI wrapper are both thin consumers of the same dispatch — the socket server just feeds wire frames into the FFI entry.
  • create / call / destroy is the symmetric backend lifecycle (process for IPC, thread/instance for NAPI).
  • host_call(target, bytes) is the generic reverse channel (same contract as the wasm onHostCall): for NAPI it reaches TS-resident services (the CDB). World state stays C++↔C++ (a synchronous byte transport), so leaf reads never bounce through JS.

Shape

  • wsdb_ffi: wsdb_create / wsdb_call (async) / wsdb_destroy; aztec-wsdb becomes a thin socket adapter over it.
  • avm_create_inprocess: the AVM reaches world state via a sync wsdb byte-callback and contract data via host_call. avm_sim_ffi stays free of world_state, so bb-avm-sim is unaffected.
  • avm_inprocess addon co-hosts InProcessWsdb + a co-hosting InProcessAvm(inProcessWsdb, onHostCall) — one shared WorldState.
  • TS: NativeWorldStateService.fromWsdbBackend + NapiWsdbBackend; NapiAvmSimulator.spawnCoHosted; TXESynchronizer.createInProcess().

Verification

  • C++: aztec-wsdb / bb-avm-sim still build and run.
  • Real Noir TXE public-execution tests pass against the fully in-process TXE (public_immutable read + init, nft transfer_in_public write) with no aztec-wsdb and no bb-avm-sim child processes.

Non-goals (spike)

Production node in-process; MemoryMerkleDB / no-disk world state; folding the hand-written NAPI wrapper into the codegen; browser/wasm backend.

…+ services

Adds `ipc_codegen/in_process.hpp` (`dispatch_sync`) to the C++ template dir the
codegen copies into every generated service. It drives a service's generated
`make_<prefix>_handler(ctx)` synchronously across a direct function-call
boundary (NAPI/FFI) instead of a socket — the in-process counterpart to the
`<prefix>_ipc_server.hpp` socket serve loop.

The concrete entry point (a NAPI method or named FFI symbol) is instantiated by
the host per service, deliberately not a single global `ipc_ffi_entry`, so
multiple services can be co-hosted in one process without symbol collisions.

First building block of the TXE in-process spike (run the AVM + world state in
the TXE's own process, no bb-avm-sim/aztec-wsdb child processes).
Carves the AVM simulator's dispatch + command handlers out of the bb-avm-sim
executable into a transport-agnostic `avm_sim_ffi` library, and adds a plain-C
ABI (`avm_ffi.h`: avm_create_ipc / avm_call / avm_destroy) that runs a
simulation in-process via the codegen `dispatch_sync` primitive — no socket.

The bb-avm-sim executable now just links the library and wraps its dispatch in
the socket serve loop (out-of-process path unchanged). Any in-process host — a
small NAPI wrapper generated up the stack, pointed at this library — is the
other consumer. Barretenberg itself stays free of any Node/NAPI knowledge; the
ABI is plain C.

avm_execute.hpp gains explicit-specialization declarations for the AvmRequest
handlers so both the executable and the library link against them rather than
implicitly instantiating the definition-less primary template.

Builds green (AVM=ON): libavm_sim_ffi.a exports the three C symbols and
bb-avm-sim relinks onto it.
A ~90-line node-addon-api wrapper (`InProcessAvm`) over barretenberg's
avm_sim_ffi C ABI. `new InProcessAvm(wsdbPath, cdbPath)` → `avm_create_ipc`;
`.call(requestFrame)` → `avm_call` (dispatch_sync, in-process, no subprocess);
`.destroy()` → `avm_destroy`. Generic byte-in/byte-out `call`; the only
AVM-specific bit is the constructor's dep wiring.

Builds standalone with barretenberg's zig toolchain (ABI match) and links its
static archives into the .node — no shared lib, no PIC issues (bb is built
-fPIC). Barretenberg stays Node/NAPI-free; this wrapper is the in-process
consumer of its C ABI, mirroring bb-avm-sim as the out-of-process consumer.

Verified: builds, loads in Node, exposes InProcessAvm. This proves the whole
native chain (bb C ABI → linked .node → loadable) — the spike's riskiest part.
The generalized form (codegen emits this given a lib path) is the follow-up;
TS backend + InProcessAvmSimulator + TXE wiring come next.
Wires the in-process AVM through to a working round-trip, behind the existing
AvmSimulator interface so nothing downstream changes.

- NAPI wrapper `call` now runs avm_call on a worker thread and returns a Promise
  (was synchronous). Mandatory even for Slice A: the CDB server runs on the JS
  event loop, so a synchronous AVM call on that thread would block the loop and
  deadlock against its own CDB requests. Off-thread frees the loop to serve CDB.
- `NapiAvmSimulator implements AvmSimulator` — sibling of AvmSimulatorPool: owns
  the same CDB reverse channel (CdbIpcServer + per-simulate fork registration)
  and drives the generated AvmService via a NapiBackend (IpcClientAsync over the
  addon), so the in-process path runs behind the codegen'd API, not bespoke.
  Only the transport differs: an in-process worker thread vs a child process.
- CdbIpcServer.ready() so the addon's synchronous connect waits for the socket
  to bind.
- TXESynchronizer + PublicProcessorTestEnv select the in-process backend behind
  TXE_IN_PROCESS / AVM_INPROCESS_NODE; the pool remains the default.

Verified: the public-processor deployments test (deploy + public calls + CDB
callbacks + block-cache revert) passes with AVM_INPROCESS_NODE set — identical
results to the subprocess pool, no bb-avm-sim child process. Slice A done.
Adds the generic host_call(target, bytes) reverse channel so an in-process AVM
can reach contract data via a direct host callback instead of a CDB socket:

- avm_ffi.h gains `avm_host_call_fn` (the native twin of the wasm host_call
  import — same (target, req, resp) contract) and `avm_create_hostcall`.
- HostCallContractDB implements ContractDBInterface over that proxy, speaking
  the exact same CDB wire protocol/framing as CdbIpcContractDB — only the
  transport differs (host callback vs socket).
- AvmRequest is decoupled from the concrete CDB: it now holds a
  ContractDBInterface& plus a set_fork_id closure (fork routing is a transport
  concern, not part of the DB interface), so the socket and host_call CDBs both
  slot in. bb-avm-sim's out-of-process path is updated to match and still links.

Builds green (AVM=ON). NAPI TSFN delivery of host_call + the TS host router
(→ CdbIpcServer dispatch) come next; wasm reuses the same (target, bytes) shape.
Completes the generic host_call(target, bytes) reverse channel so the in-process
AVM reaches contract data by calling straight into the host, dropping the CDB
socket:

- host_call callback gains a `void* ctx` (standard C-callback pattern) so the
  NAPI wrapper can route it to the owning instance's ThreadSafeFunction.
- NAPI wrapper: `new InProcessAvm(wsdbPath, onHostCall)` builds a TSFN over the
  JS onHostCall; the AVM's worker-thread host_call blocks on a std::promise the
  TSFN callback fulfils — handling onHostCall returning a Promise (the CDB read
  is async). This is the native twin of the wasm host_call import; same
  (target, bytes) contract, so the future wasm build reuses the router.
- CdbIpcServer can run socketless (listenSocket=false) and exposes handle(bytes)
  = its generated dispatch; NapiAvmSimulator routes host_call target CDB→handle.
  registerFork/unregisterFork unchanged.

Verified: the public-processor deployments test (deploy + public calls + CDB
lookups) passes with the AVM in-process AND no CDB socket. Only the wsdb socket
remains between the in-process AVM and the host (Slice C).
…ice C)

The generated C++ IPC client was hardwired to a socket (make_client by path
suffix). Add a `Transport` (byte request→response function) and a constructor
that takes one, routing the per-command `send<>` through it. The path
constructor is unchanged — it now just installs a socket-backed Transport — so
every existing consumer (CdbIpcContractDB, WsdbIpcMerkleDB, bb-avm-sim) is
unaffected.

This is the enabler for an in-process AVM to reach a co-hosted world-state
server directly (C++→C++) instead of over a socket: WsdbIpcMerkleDB can wrap a
WsdbIpcClient built over an in-process transport that drives the wsdb dispatch
of a shared WorldState — the last step to zero child processes for the TXE
(Slice C). Mirrors the zig/rust clients, which are already backend-parameterized.

Builds green (AVM=ON): cdb + wsdb clients regenerate with the injectable
transport and all consumers relink; the socket path is untouched.
WsdbScheduler took an `ipc::IpcServer&` but used it for exactly one thing: the
`has_pending_request()` check that gates the inline fast path. Replace it with an
injected `std::function<bool()>` predicate, so the scheduler (read-batch /
write-barrier per-fork ordering + thread pool) is transport-agnostic — unchanged
behaviour, but no longer tied to the socket server.

This lets the wsdb dispatch run behind a plain C ABI: the socket server passes
its `has_pending_request()`, and an in-process (FFI/NAPI) host passes its own
predicate. First step toward running wsdb in-process (shared WorldState with the
in-process AVM) via the same generalized-backend model as the AVM, rather than
reverting to a pre-wire binding.

aztec-wsdb relinks; the socket path is unchanged.
Mirror avm_sim_ffi: move the WorldState, per-fork scheduler, and generated
dispatch behind a plain-C ABI (wsdb_create / wsdb_call / wsdb_destroy) in a
new wsdb_ffi static library. wsdb_call is async — it decodes and schedules
synchronously but delivers the response through a callback, so the socket
server's concurrency is preserved. aztec-wsdb becomes a thin socket adapter
that feeds wire frames into wsdb_call; an in-process host (NAPI wrapper, or a
co-linked AVM sharing the WorldState) drives the same ABI without a socket.
The Slice C AVM shape: no sockets. World state is reached through a
synchronous wsdb byte transport (avm_wsdb_call_fn — the host drives a
co-hosted WorldState's dispatch, so leaf reads stay C++<->C++ and never
bounce through JS), and contract data through the existing host_call proxy.
The AVM sees a plain WsdbIpcClient built via its Transport ctor and does not
know world state is in-process. avm_sim_ffi stays free of any wsdb_ffi /
world_state dependency, so bb-avm-sim is unaffected.
Add an InProcessWsdb ObjectWrap over the wsdb_ffi C ABI: it owns a WorldState
in-process and exposes call()/destroy() so a generated WsdbService (AsyncApi)
drives it exactly like the spawned aztec-wsdb backend, no child process. Add a
third InProcessAvm constructor — (inProcessWsdb, onHostCall) — that co-hosts
the AVM against the SAME WorldState via avm_create_inprocess: world-state reads
go C++<->C++ through a synchronous wsdb trampoline (never bouncing to JS),
contract data via host_call. The scheduler's inline fast path is disabled
(always-pending predicate) since in-process requests arrive on multiple
threads. Together: a TXE session with zero child processes.
…ce C)

Wire the TS side so TXE_IN_PROCESS runs both the world state and the AVM in
one process sharing a single WorldState, with zero child processes:

- world-state: loosen IpcWorldState to drive any AsyncApi (a spawned
  WsdbService or an in-process NAPI backend); getIpcPath guards on absence.
  Add NativeWorldStateService.fromWsdbBackend(IpcClientAsync) and
  buildInProcessWsdbOptions (the in-process twin of getWsdbExtraArgs).
- simulator: createInProcessWsdb + NapiWsdbBackend (drive the wsdb AsyncApi
  over the addon's InProcessWsdb), and NapiAvmSimulator.spawnCoHosted, which
  hands that same InProcessWsdb to the AVM so both share one WorldState.
- txe: TXESynchronizer.createInProcess() creates the shared InProcessWsdb,
  builds the world state over a NapiWsdbBackend, and co-hosts the AVM. Session
  teardown already disposes the AVM before closing the world state.

TXE_IN_PROCESS now means full in-process (was AVM-only).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant