@aztec/simulator 6.0.0-nightly.20260809 → 6.0.0-nightly.20260812

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,13 +8,47 @@ export interface AvmSimulatorPoolOptions {
8
8
  wsdbIpcPath: string;
9
9
  /** Optional logger function for AVM process output. */
10
10
  logger?: (msg: string) => void;
11
+ /**
12
+ * Flat delay between environmental spawn failures (default 1s). No backoff: sequencers live on
13
+ * ~6s slots, so sleeping longer than this after a failure costs whole blocks while the machine may
14
+ * have recovered — and a spawn attempt is cheap. Spawning never gives up on its own; the caller's
15
+ * deadline (abort signal) is the bound.
16
+ */
17
+ spawnRetryIntervalMs?: number;
18
+ /** Process spawner override. Test hook; defaults to spawning a real bb-avm-sim via the generated AvmService. */
19
+ spawnProcess?: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
20
+ }
21
+ /** Options handed to the process spawner for each new pool slot. */
22
+ export interface AvmProcessSpawnOptions {
23
+ binaryPath?: string;
24
+ wsdbIpcPath: string;
25
+ cdbIpcPath: string;
26
+ logger?: (msg: string) => void;
27
+ }
28
+ /**
29
+ * A handle to a single bb-avm-sim service: it runs serialized simulations and connects back to the
30
+ * shared CDB/WSDB servers for state, but is unaware of which fork's contract data it is reading — that is
31
+ * routed by the fork id baked into the input buffer. The underlying service owns its process lifecycle
32
+ * (including respawn-on-death), so a handle stays usable for the life of the pool.
33
+ */
34
+ export interface AvmProcessHandle {
35
+ simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;
36
+ simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
37
+ destroy(): Promise<void>;
11
38
  }
12
39
  /**
13
- * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim processes plus the CDB server that
40
+ * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
14
41
  * answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
15
42
  * the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
16
43
  * registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
17
44
  * so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
45
+ *
46
+ * Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
47
+ * either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
48
+ * Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
49
+ * only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
50
+ * process. The one deliberate exception: an input that kills the process twice is treated as a failing
51
+ * transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
18
52
  */
19
53
  export declare class AvmSimulatorPool implements AvmSimulator {
20
54
  private options;
@@ -22,24 +56,29 @@ export declare class AvmSimulatorPool implements AvmSimulator {
22
56
  private available;
23
57
  private waiters;
24
58
  private createdCount;
59
+ private destroyed;
25
60
  private log;
26
61
  private maxSize;
27
62
  private cdbServer;
63
+ private readonly spawnRetryIntervalMs;
64
+ private readonly spawnProcess;
28
65
  constructor(options: AvmSimulatorPoolOptions);
29
66
  static spawn(options: AvmSimulatorPoolOptions): Promise<AvmSimulatorPool>;
30
67
  [Symbol.asyncDispose](): Promise<void>;
31
68
  simulate(inputBuffer: Uint8Array, context: AvmContractsDBContext, signal?: AbortSignal): Promise<Uint8Array>;
32
69
  simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
33
- /** Destroy all AVM processes in the pool and close the CDB server. */
70
+ /** Destroy all AVM services in the pool and close the CDB server. */
34
71
  destroy(): Promise<void>;
35
72
  /**
36
73
  * Eagerly spawn up to `count` AVM processes (capped at maxSize) and leave them available, so the
37
74
  * first simulate() doesn't pay process spawn/connect cost. Idempotent.
38
75
  */
39
76
  prewarm(count?: number): Promise<void>;
77
+ private runOnPool;
40
78
  private checkout;
41
- /** Return an AVM process to the pool after use. */
79
+ /** Return an AVM service to the pool after use. */
42
80
  private return;
43
81
  private createSlot;
82
+ private spawnUntilUp;
44
83
  }
45
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXZtX3NpbXVsYXRvcl9wb29sLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHVibGljL2F2bV9zaW11bGF0b3JfcG9vbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFHQSxPQUFPLEtBQUssRUFBRSxxQkFBcUIsRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUc5RSxNQUFNLFdBQVcsdUJBQXVCO0lBQ3RDLHVIQUF1SDtJQUN2SCxPQUFPLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDakIsb0ZBQW9GO0lBQ3BGLGFBQWEsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUN2QiwyQ0FBMkM7SUFDM0MsV0FBVyxFQUFFLE1BQU0sQ0FBQztJQUNwQix1REFBdUQ7SUFDdkQsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxLQUFLLElBQUksQ0FBQztDQUNoQztBQWFEOzs7Ozs7R0FNRztBQUNILHFCQUFhLGdCQUFpQixZQUFXLFlBQVk7SUFTdkMsT0FBTyxDQUFDLE9BQU87SUFSM0IsT0FBTyxDQUFDLEtBQUssQ0FBc0M7SUFDbkQsT0FBTyxDQUFDLFNBQVMsQ0FBZ0I7SUFDakMsT0FBTyxDQUFDLE9BQU8sQ0FBaUc7SUFDaEgsT0FBTyxDQUFDLFlBQVksQ0FBSztJQUN6QixPQUFPLENBQUMsR0FBRyxDQUFTO0lBQ3BCLE9BQU8sQ0FBQyxPQUFPLENBQVM7SUFDeEIsT0FBTyxDQUFDLFNBQVMsQ0FBZTtJQUVoQyxZQUFvQixPQUFPLEVBQUUsdUJBQXVCLEVBSW5EO0lBRUQsT0FBYSxLQUFLLENBQUMsT0FBTyxFQUFFLHVCQUF1QixHQUFHLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUs5RTtJQUVLLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FFM0M7SUFFSyxRQUFRLENBQUMsV0FBVyxFQUFFLFVBQVUsRUFBRSxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxDQUFDLEVBQUUsV0FBVyxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0Fjakg7SUFFSyxpQkFBaUIsQ0FBQyxXQUFXLEVBQUUsVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FRcEU7SUFFRCxzRUFBc0U7SUFDaEUsT0FBTyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FtQjdCO0lBRUQ7OztPQUdHO0lBQ0csT0FBTyxDQUFDLEtBQUssU0FBSSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FVdEM7WUFHYSxRQUFRO0lBZXRCLG1EQUFtRDtJQUNuRCxPQUFPLENBQUMsTUFBTTtZQVlBLFVBQVU7Q0E4QnpCIn0=
84
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXZtX3NpbXVsYXRvcl9wb29sLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHVibGljL2F2bV9zaW11bGF0b3JfcG9vbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFLQSxPQUFPLEtBQUssRUFBRSxxQkFBcUIsRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUc5RSxNQUFNLFdBQVcsdUJBQXVCO0lBQ3RDLHVIQUF1SDtJQUN2SCxPQUFPLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDakIsb0ZBQW9GO0lBQ3BGLGFBQWEsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUN2QiwyQ0FBMkM7SUFDM0MsV0FBVyxFQUFFLE1BQU0sQ0FBQztJQUNwQix1REFBdUQ7SUFDdkQsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxLQUFLLElBQUksQ0FBQztJQUMvQjs7Ozs7T0FLRztJQUNILG9CQUFvQixDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQzlCLGdIQUFnSDtJQUNoSCxZQUFZLENBQUMsRUFBRSxDQUFDLE9BQU8sRUFBRSxzQkFBc0IsS0FBSyxPQUFPLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztDQUMvRTtBQUVELG9FQUFvRTtBQUNwRSxNQUFNLFdBQVcsc0JBQXNCO0lBQ3JDLFVBQVUsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNwQixXQUFXLEVBQUUsTUFBTSxDQUFDO0lBQ3BCLFVBQVUsRUFBRSxNQUFNLENBQUM7SUFDbkIsTUFBTSxDQUFDLEVBQUUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxLQUFLLElBQUksQ0FBQztDQUNoQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxXQUFXLGdCQUFnQjtJQUMvQixRQUFRLENBQUMsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLENBQUMsRUFBRSxXQUFXLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzdFLGlCQUFpQixDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ2hFLE9BQU8sSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7Q0FDMUI7QUFpQ0Q7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUNILHFCQUFhLGdCQUFpQixZQUFXLFlBQVk7SUFZdkMsT0FBTyxDQUFDLE9BQU87SUFYM0IsT0FBTyxDQUFDLEtBQUssQ0FBMEI7SUFDdkMsT0FBTyxDQUFDLFNBQVMsQ0FBZ0I7SUFDakMsT0FBTyxDQUFDLE9BQU8sQ0FBaUc7SUFDaEgsT0FBTyxDQUFDLFlBQVksQ0FBSztJQUN6QixPQUFPLENBQUMsU0FBUyxDQUFTO0lBQzFCLE9BQU8sQ0FBQyxHQUFHLENBQVM7SUFDcEIsT0FBTyxDQUFDLE9BQU8sQ0FBUztJQUN4QixPQUFPLENBQUMsU0FBUyxDQUFlO0lBQ2hDLE9BQU8sQ0FBQyxRQUFRLENBQUMsb0JBQW9CLENBQVM7SUFDOUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxZQUFZLENBQWlFO0lBRTlGLFlBQW9CLE9BQU8sRUFBRSx1QkFBdUIsRUFNbkQ7SUFFRCxPQUFhLEtBQUssQ0FBQyxPQUFPLEVBQUUsdUJBQXVCLEdBQUcsT0FBTyxDQUFDLGdCQUFnQixDQUFDLENBTTlFO0lBRUssQ0FBQyxNQUFNLENBQUMsWUFBWSxDQUFDLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUUzQztJQUVLLFFBQVEsQ0FBQyxXQUFXLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxNQUFNLENBQUMsRUFBRSxXQUFXLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQVNqSDtJQUVLLGlCQUFpQixDQUFDLFdBQVcsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUdwRTtJQUVELHFFQUFxRTtJQUMvRCxPQUFPLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQWE3QjtJQUVEOzs7T0FHRztJQUNHLE9BQU8sQ0FBQyxLQUFLLFNBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBVXRDO1lBUWEsU0FBUztZQXdCVCxRQUFRO0lBcUN0QixtREFBbUQ7SUFDbkQsT0FBTyxDQUFDLE1BQU07WUFZQSxVQUFVO1lBeUJWLFlBQVk7Q0EyQjNCIn0=
@@ -1 +1 @@
1
- {"version":3,"file":"avm_simulator_pool.d.ts","sourceRoot":"","sources":["../../src/public/avm_simulator_pool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,uHAAuH;IACvH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAaD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IASvC,OAAO,CAAC,OAAO;IAR3B,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,OAAO,CAAiG;IAChH,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAe;IAEhC,YAAoB,OAAO,EAAE,uBAAuB,EAInD;IAED,OAAa,KAAK,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAK9E;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;IAEK,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAcjH;IAEK,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAQpE;IAED,sEAAsE;IAChE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAmB7B;IAED;;;OAGG;IACG,OAAO,CAAC,KAAK,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtC;YAGa,QAAQ;IAetB,mDAAmD;IACnD,OAAO,CAAC,MAAM;YAYA,UAAU;CA8BzB"}
1
+ {"version":3,"file":"avm_simulator_pool.d.ts","sourceRoot":"","sources":["../../src/public/avm_simulator_pool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,uHAAuH;IACvH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gHAAgH;IAChH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC/E;AAED,oEAAoE;AACpE,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7E,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAiCD;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IAYvC,OAAO,CAAC,OAAO;IAX3B,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,OAAO,CAAiG;IAChH,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAE9F,YAAoB,OAAO,EAAE,uBAAuB,EAMnD;IAED,OAAa,KAAK,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM9E;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;IAEK,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CASjH;IAEK,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAGpE;IAED,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAa7B;IAED;;;OAGG;IACG,OAAO,CAAC,KAAK,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtC;YAQa,SAAS;YAwBT,QAAQ;IAqCtB,mDAAmD;IACnD,OAAO,CAAC,MAAM;YAYA,UAAU;YAyBV,YAAY;CA2B3B"}
@@ -1,36 +1,83 @@
1
1
  var _computedKey;
2
2
  import { AvmService } from '@aztec/bb-avm-sim';
3
+ import { AbortError } from '@aztec/foundation/error';
3
4
  import { createLogger } from '@aztec/foundation/log';
5
+ import { sleep } from '@aztec/foundation/sleep';
4
6
  import { CdbIpcServer } from './cdb_ipc_server.js';
7
+ /**
8
+ * The generated service flags errors caused by the death of the underlying process (rather than by the
9
+ * request itself) with `retry: true`. That distinction is interpreted here and goes no further: process
10
+ * lifecycle is invisible to the pool's callers.
11
+ */ function isProcessFailure(err) {
12
+ return err instanceof Error && err.retry === true;
13
+ }
14
+ /** Sleep that wakes early (without throwing) when the signal aborts; callers re-check the signal. */ async function abortableSleep(ms, signal) {
15
+ if (!signal) {
16
+ return sleep(ms);
17
+ }
18
+ let onAbort;
19
+ const aborted = new Promise((resolve)=>{
20
+ onAbort = resolve;
21
+ signal.addEventListener('abort', onAbort, {
22
+ once: true
23
+ });
24
+ });
25
+ try {
26
+ await Promise.race([
27
+ sleep(ms),
28
+ aborted
29
+ ]);
30
+ } finally{
31
+ signal.removeEventListener('abort', onAbort);
32
+ }
33
+ }
34
+ // After a cancellation is requested, how long the C++ process gets to cancel cooperatively (SIGUSR1
35
+ // checkpoint) before it is SIGKILLed. Killing is safe: the service respawns its process lazily, so the
36
+ // pool slot recovers instead of being leaked to a wedged simulation.
37
+ const CANCEL_KILL_GRACE_MS = 5_000;
5
38
  _computedKey = Symbol.asyncDispose;
6
39
  /**
7
- * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim processes plus the CDB server that
40
+ * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
8
41
  * answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
9
42
  * the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
10
43
  * registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
11
44
  * so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
45
+ *
46
+ * Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
47
+ * either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
48
+ * Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
49
+ * only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
50
+ * process. The one deliberate exception: an input that kills the process twice is treated as a failing
51
+ * transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
12
52
  */ export class AvmSimulatorPool {
13
53
  options;
14
54
  slots;
15
55
  available;
16
56
  waiters;
17
57
  createdCount;
58
+ destroyed;
18
59
  log;
19
60
  maxSize;
20
61
  cdbServer;
62
+ spawnRetryIntervalMs;
63
+ spawnProcess;
21
64
  constructor(options){
22
65
  this.options = options;
23
66
  this.slots = [];
24
67
  this.available = [];
25
68
  this.waiters = [];
26
69
  this.createdCount = 0;
70
+ this.destroyed = false;
27
71
  this.log = createLogger('simulator:avm-pool');
28
72
  this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10);
29
73
  this.cdbServer = new CdbIpcServer();
74
+ this.spawnRetryIntervalMs = options.spawnRetryIntervalMs ?? 1_000;
75
+ this.spawnProcess = options.spawnProcess ?? ((spawnOptions)=>AvmSimulatorProcess.spawn(spawnOptions));
30
76
  }
31
77
  static async spawn(options) {
32
78
  const pool = new AvmSimulatorPool(options);
33
79
  // Always start one process up front so the first simulate() doesn't pay process spawn/connect cost.
80
+ // This is also where configuration errors (missing binary) surface, fast and fatally.
34
81
  await pool.prewarm();
35
82
  return pool;
36
83
  }
@@ -42,37 +89,21 @@ _computedKey = Symbol.asyncDispose;
42
89
  // and unregister once the simulation returns — registration is only needed while the call is running.
43
90
  this.cdbServer.registerFork(context.forkId, context.contractsDB, context.timestamp);
44
91
  try {
45
- const simulator = await this.checkout();
46
- try {
47
- return await simulator.simulate(inputBuffer, signal);
48
- } finally{
49
- this.return(simulator);
50
- }
92
+ return await this.runOnPool((simulator)=>simulator.simulate(inputBuffer, signal), signal);
51
93
  } finally{
52
94
  this.cdbServer.unregisterFork(context.forkId);
53
95
  }
54
96
  }
55
97
  async simulateWithHints(inputBuffer) {
56
98
  // The hinted path makes no contract-data callbacks, so no CDB registration is needed.
57
- const simulator = await this.checkout();
58
- try {
59
- return await simulator.simulateWithHints(inputBuffer);
60
- } finally{
61
- this.return(simulator);
62
- }
99
+ return await this.runOnPool((simulator)=>simulator.simulateWithHints(inputBuffer));
63
100
  }
64
- /** Destroy all AVM processes in the pool and close the CDB server. */ async destroy() {
65
- for (const waiter of this.waiters){
101
+ /** Destroy all AVM services in the pool and close the CDB server. */ async destroy() {
102
+ this.destroyed = true;
103
+ for (const waiter of this.waiters.splice(0)){
66
104
  waiter.reject(new Error('AVM simulator pool destroyed'));
67
105
  }
68
- this.waiters = [];
69
- const destroyPromises = [];
70
- for (const slot of this.slots){
71
- if (slot) {
72
- destroyPromises.push(slot.destroy());
73
- }
74
- }
75
- await Promise.all(destroyPromises);
106
+ await Promise.all(this.slots.map((slot)=>slot.destroy()));
76
107
  this.slots = [];
77
108
  this.available = [];
78
109
  this.createdCount = 0;
@@ -86,29 +117,81 @@ _computedKey = Symbol.asyncDispose;
86
117
  const target = Math.min(count, this.maxSize);
87
118
  const created = [];
88
119
  while(this.createdCount < target){
89
- created.push(await this.createSlot());
120
+ created.push(await this.checkout());
90
121
  }
91
- // Hand the freshly-spawned processes back to the pool so checkout() reuses them.
122
+ // Hand the checked-out processes back to the pool so checkout() reuses them.
92
123
  for (const simulator of created){
93
124
  this.return(simulator);
94
125
  }
95
126
  }
96
- /** Check out an AVM process from the pool, blocking until one is free. Caller must return() it when done. */ async checkout() {
127
+ /**
128
+ * Run a call on a pooled service. A call that fails because its process died is re-issued on the
129
+ * respawned process; a second death for the same input is attributed to the input and surfaces as an
130
+ * ordinary error (the pre-IPC equivalent — a native crash — took down the whole node, so a failed tx
131
+ * is strictly gentler). Non-process failures surface as-is: they are the simulation's own verdict.
132
+ */ async runOnPool(fn, signal) {
133
+ for(let attempt = 0;; attempt++){
134
+ const simulator = await this.checkout(signal);
135
+ try {
136
+ return await fn(simulator);
137
+ } catch (err) {
138
+ if (!isProcessFailure(err) || signal?.aborted) {
139
+ throw err;
140
+ }
141
+ if (attempt > 0) {
142
+ const message = err instanceof Error ? err.message : String(err);
143
+ throw new Error(`AVM simulator process died twice running this simulation; attributing the failure to the input: ${message}`, {
144
+ cause: err
145
+ });
146
+ }
147
+ this.log.warn(`AVM process died during simulation; re-issuing once on the respawned process`, {
148
+ err
149
+ });
150
+ } finally{
151
+ this.return(simulator);
152
+ }
153
+ }
154
+ }
155
+ /** Check out an AVM service from the pool, blocking until one is free. Caller must return() it when done. */ async checkout(signal) {
156
+ if (this.destroyed) {
157
+ throw new Error('AVM simulator pool destroyed');
158
+ }
97
159
  const idx = this.available.pop();
98
- if (idx !== undefined && this.slots[idx]) {
160
+ if (idx !== undefined) {
99
161
  return this.slots[idx];
100
162
  }
101
- if (this.createdCount < this.maxSize || idx !== undefined && !this.slots[idx]) {
102
- return await this.createSlot(idx);
163
+ if (this.createdCount < this.maxSize) {
164
+ return await this.createSlot(signal);
103
165
  }
104
166
  return new Promise((resolve, reject)=>{
105
- this.waiters.push({
167
+ const waiter = {
106
168
  resolve,
107
169
  reject
108
- });
170
+ };
171
+ if (signal) {
172
+ const onAbort = ()=>{
173
+ const at = this.waiters.indexOf(waiter);
174
+ if (at >= 0) {
175
+ this.waiters.splice(at, 1);
176
+ reject(new AbortError('AVM checkout aborted'));
177
+ }
178
+ };
179
+ signal.addEventListener('abort', onAbort, {
180
+ once: true
181
+ });
182
+ waiter.resolve = (simulator)=>{
183
+ signal.removeEventListener('abort', onAbort);
184
+ resolve(simulator);
185
+ };
186
+ waiter.reject = (err)=>{
187
+ signal.removeEventListener('abort', onAbort);
188
+ reject(err);
189
+ };
190
+ }
191
+ this.waiters.push(waiter);
109
192
  });
110
193
  }
111
- /** Return an AVM process to the pool after use. */ return(simulator) {
194
+ /** Return an AVM service to the pool after use. */ return(simulator) {
112
195
  const waiter = this.waiters.shift();
113
196
  if (waiter) {
114
197
  waiter.resolve(simulator);
@@ -119,35 +202,54 @@ _computedKey = Symbol.asyncDispose;
119
202
  }
120
203
  }
121
204
  }
122
- async createSlot(reuseIdx) {
123
- const reuse = reuseIdx !== undefined && reuseIdx < this.slots.length;
205
+ async createSlot(signal) {
124
206
  // Reserve the slot count synchronously, before the async spawn, so concurrent checkouts can't all
125
207
  // observe `createdCount < maxSize` and overshoot the pool (the count is only bumped once control has
126
- // yielded on the await). Roll back the reservation if the spawn itself fails.
127
- if (!reuse) {
128
- this.createdCount++;
129
- }
130
- let simulator;
208
+ // yielded on the await). Roll back the reservation if the spawn is abandoned.
209
+ this.createdCount++;
131
210
  try {
132
- simulator = await AvmSimulatorProcess.spawn({
133
- binaryPath: this.options.avmBinaryPath,
134
- wsdbIpcPath: this.options.wsdbIpcPath,
135
- cdbIpcPath: this.cdbServer.ipcPath,
136
- logger: this.options.logger
137
- });
211
+ const simulator = await this.spawnUntilUp(signal);
212
+ this.slots.push(simulator);
213
+ this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
214
+ return simulator;
138
215
  } catch (err) {
139
- if (!reuse) {
140
- this.createdCount--;
141
- }
216
+ this.createdCount--;
142
217
  throw err;
143
218
  }
144
- if (reuse) {
145
- this.slots[reuseIdx] = simulator;
146
- } else {
147
- this.slots.push(simulator);
219
+ }
220
+ /**
221
+ * Spawn a service, retrying environmental failures on a flat cadence indefinitely — the bound is
222
+ * the caller's own deadline (abort signal), matching the pre-IPC contract where a simulation either
223
+ * completed or was deadlined out. The cadence is deliberately fast and constant: an attempt is cheap,
224
+ * a slow one self-paces inside the backend's connect backstop, and backing off would cost a
225
+ * ~6s-slot sequencer whole blocks after the machine has already recovered. Configuration errors
226
+ * (missing binary, flagged non-retryable by the generated service) throw immediately; the boot-time
227
+ * prewarm is where those are meant to surface.
228
+ */ async spawnUntilUp(signal) {
229
+ for(let failures = 0;; failures++){
230
+ if (this.destroyed) {
231
+ throw new Error('AVM simulator pool destroyed');
232
+ }
233
+ if (signal?.aborted) {
234
+ throw new AbortError('AVM process spawn aborted');
235
+ }
236
+ try {
237
+ return await this.spawnProcess({
238
+ binaryPath: this.options.avmBinaryPath,
239
+ wsdbIpcPath: this.options.wsdbIpcPath,
240
+ cdbIpcPath: this.cdbServer.ipcPath,
241
+ logger: this.options.logger
242
+ });
243
+ } catch (err) {
244
+ if (!isProcessFailure(err)) {
245
+ throw err;
246
+ }
247
+ this.log.warn(`Failed to spawn AVM process (attempt ${failures + 1}); retrying in ${this.spawnRetryIntervalMs}ms`, {
248
+ err
249
+ });
250
+ await abortableSleep(this.spawnRetryIntervalMs, signal);
251
+ }
148
252
  }
149
- this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
150
- return simulator;
151
253
  }
152
254
  }
153
255
  class AvmSimulatorProcess {
@@ -165,13 +267,22 @@ class AvmSimulatorProcess {
165
267
  options.wsdbIpcPath,
166
268
  '--cdb',
167
269
  options.cdbIpcPath
168
- ]
270
+ ],
271
+ // Each simulation is self-contained (state comes from the WSDB/CDB servers, routed by fork id),
272
+ // so a fresh process can safely serve the next call after a death.
273
+ respawn: true
169
274
  });
170
275
  return new AvmSimulatorProcess(service);
171
276
  }
172
277
  async simulate(inputBuffer, signal) {
173
- // Signal the C++ process to stop at its next cancellation checkpoint when the caller aborts.
174
- const onAbort = ()=>this.service.sendProcessSignal('SIGUSR1');
278
+ let killTimer;
279
+ // Cooperative cancellation first: SIGUSR1 makes the C++ process stop at its next cancellation
280
+ // checkpoint. If it doesn't respond within the grace (wedged in a slow op), SIGKILL it — the
281
+ // service respawns lazily, so this reclaims the pool slot rather than leaking it.
282
+ const onAbort = ()=>{
283
+ this.service.sendProcessSignal('SIGUSR1');
284
+ killTimer = setTimeout(()=>this.service.sendProcessSignal('SIGKILL'), CANCEL_KILL_GRACE_MS);
285
+ };
175
286
  if (signal?.aborted) {
176
287
  onAbort();
177
288
  }
@@ -184,6 +295,9 @@ class AvmSimulatorProcess {
184
295
  })).result;
185
296
  } finally{
186
297
  signal?.removeEventListener('abort', onAbort);
298
+ if (killTimer !== undefined) {
299
+ clearTimeout(killTimer);
300
+ }
187
301
  }
188
302
  }
189
303
  async simulateWithHints(inputBuffer) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/simulator",
3
- "version": "6.0.0-nightly.20260809",
3
+ "version": "6.0.0-nightly.20260812",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./server": "./dest/server.js",
@@ -63,29 +63,29 @@
63
63
  ]
64
64
  },
65
65
  "dependencies": {
66
- "@aztec/bb-avm-sim": "6.0.0-nightly.20260809",
67
- "@aztec/cdb": "6.0.0-nightly.20260809",
68
- "@aztec/constants": "6.0.0-nightly.20260809",
69
- "@aztec/foundation": "6.0.0-nightly.20260809",
70
- "@aztec/ipc-runtime": "6.0.0-nightly.20260809",
71
- "@aztec/native": "6.0.0-nightly.20260809",
72
- "@aztec/noir-acvm_js": "6.0.0-nightly.20260809",
73
- "@aztec/noir-noirc_abi": "6.0.0-nightly.20260809",
74
- "@aztec/noir-types": "6.0.0-nightly.20260809",
75
- "@aztec/protocol-contracts": "6.0.0-nightly.20260809",
76
- "@aztec/standard-contracts": "6.0.0-nightly.20260809",
77
- "@aztec/stdlib": "6.0.0-nightly.20260809",
78
- "@aztec/telemetry-client": "6.0.0-nightly.20260809",
79
- "@aztec/world-state": "6.0.0-nightly.20260809",
66
+ "@aztec/bb-avm-sim": "6.0.0-nightly.20260812",
67
+ "@aztec/cdb": "6.0.0-nightly.20260812",
68
+ "@aztec/constants": "6.0.0-nightly.20260812",
69
+ "@aztec/foundation": "6.0.0-nightly.20260812",
70
+ "@aztec/ipc-runtime": "6.0.0-nightly.20260812",
71
+ "@aztec/native": "6.0.0-nightly.20260812",
72
+ "@aztec/noir-acvm_js": "6.0.0-nightly.20260812",
73
+ "@aztec/noir-noirc_abi": "6.0.0-nightly.20260812",
74
+ "@aztec/noir-types": "6.0.0-nightly.20260812",
75
+ "@aztec/protocol-contracts": "6.0.0-nightly.20260812",
76
+ "@aztec/standard-contracts": "6.0.0-nightly.20260812",
77
+ "@aztec/stdlib": "6.0.0-nightly.20260812",
78
+ "@aztec/telemetry-client": "6.0.0-nightly.20260812",
79
+ "@aztec/world-state": "6.0.0-nightly.20260812",
80
80
  "lodash.clonedeep": "^4.5.0",
81
81
  "lodash.merge": "^4.6.2",
82
82
  "msgpackr": "^1.11.2",
83
83
  "tslib": "^2.4.0"
84
84
  },
85
85
  "devDependencies": {
86
- "@aztec/kv-store": "6.0.0-nightly.20260809",
87
- "@aztec/noir-contracts.js": "6.0.0-nightly.20260809",
88
- "@aztec/noir-test-contracts.js": "6.0.0-nightly.20260809",
86
+ "@aztec/kv-store": "6.0.0-nightly.20260812",
87
+ "@aztec/noir-contracts.js": "6.0.0-nightly.20260812",
88
+ "@aztec/noir-test-contracts.js": "6.0.0-nightly.20260812",
89
89
  "@jest/globals": "^30.0.0",
90
90
  "@types/jest": "^30.0.0",
91
91
  "@types/lodash.clonedeep": "^4.5.7",
@@ -1,5 +1,7 @@
1
1
  import { AvmService } from '@aztec/bb-avm-sim';
2
+ import { AbortError } from '@aztec/foundation/error';
2
3
  import { type Logger, createLogger } from '@aztec/foundation/log';
4
+ import { sleep } from '@aztec/foundation/sleep';
3
5
 
4
6
  import type { AvmContractsDBContext, AvmSimulator } from './avm_simulator.js';
5
7
  import { CdbIpcServer } from './cdb_ipc_server.js';
@@ -13,44 +15,106 @@ export interface AvmSimulatorPoolOptions {
13
15
  wsdbIpcPath: string;
14
16
  /** Optional logger function for AVM process output. */
15
17
  logger?: (msg: string) => void;
18
+ /**
19
+ * Flat delay between environmental spawn failures (default 1s). No backoff: sequencers live on
20
+ * ~6s slots, so sleeping longer than this after a failure costs whole blocks while the machine may
21
+ * have recovered — and a spawn attempt is cheap. Spawning never gives up on its own; the caller's
22
+ * deadline (abort signal) is the bound.
23
+ */
24
+ spawnRetryIntervalMs?: number;
25
+ /** Process spawner override. Test hook; defaults to spawning a real bb-avm-sim via the generated AvmService. */
26
+ spawnProcess?: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
27
+ }
28
+
29
+ /** Options handed to the process spawner for each new pool slot. */
30
+ export interface AvmProcessSpawnOptions {
31
+ binaryPath?: string;
32
+ wsdbIpcPath: string;
33
+ cdbIpcPath: string;
34
+ logger?: (msg: string) => void;
16
35
  }
17
36
 
18
37
  /**
19
- * A raw handle to a single bb-avm-sim process: it runs a serialized simulation and connects back to the
38
+ * A handle to a single bb-avm-sim service: it runs serialized simulations and connects back to the
20
39
  * shared CDB/WSDB servers for state, but is unaware of which fork's contract data it is reading — that is
21
- * routed by the fork id baked into the input buffer.
40
+ * routed by the fork id baked into the input buffer. The underlying service owns its process lifecycle
41
+ * (including respawn-on-death), so a handle stays usable for the life of the pool.
22
42
  */
23
- interface AvmProcessHandle {
43
+ export interface AvmProcessHandle {
24
44
  simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;
25
45
  simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array>;
26
46
  destroy(): Promise<void>;
27
47
  }
28
48
 
29
49
  /**
30
- * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim processes plus the CDB server that
50
+ * The generated service flags errors caused by the death of the underlying process (rather than by the
51
+ * request itself) with `retry: true`. That distinction is interpreted here and goes no further: process
52
+ * lifecycle is invisible to the pool's callers.
53
+ */
54
+ function isProcessFailure(err: unknown): boolean {
55
+ return err instanceof Error && (err as Error & { retry?: unknown }).retry === true;
56
+ }
57
+
58
+ /** Sleep that wakes early (without throwing) when the signal aborts; callers re-check the signal. */
59
+ async function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
60
+ if (!signal) {
61
+ return sleep(ms);
62
+ }
63
+ let onAbort: () => void;
64
+ const aborted = new Promise<void>(resolve => {
65
+ onAbort = resolve;
66
+ signal.addEventListener('abort', onAbort, { once: true });
67
+ });
68
+ try {
69
+ await Promise.race([sleep(ms), aborted]);
70
+ } finally {
71
+ signal.removeEventListener('abort', onAbort!);
72
+ }
73
+ }
74
+
75
+ // After a cancellation is requested, how long the C++ process gets to cancel cooperatively (SIGUSR1
76
+ // checkpoint) before it is SIGKILLed. Killing is safe: the service respawns its process lazily, so the
77
+ // pool slot recovers instead of being leaked to a wedged simulation.
78
+ const CANCEL_KILL_GRACE_MS = 5_000;
79
+
80
+ /**
81
+ * The public-execution AVM backend: a lazily-grown pool of bb-avm-sim services plus the CDB server that
31
82
  * answers those processes' contract-data callbacks. Callers hold this as an {@link AvmSimulator}; the pool,
32
83
  * the CDB server, its IPC path, and fork-id routing are all hidden behind that interface. Each `simulate`
33
84
  * registers the call's contracts DB on the CDB server for the duration of the simulation (keyed by fork id
34
85
  * so concurrent simulations on different forks don't collide) and unregisters it once the call returns.
86
+ *
87
+ * Process lifecycle is invisible to callers, exactly as when the simulator ran in-process: a simulation
88
+ * either produces a result, fails on its own merits, or runs until the caller's deadline aborts it.
89
+ * Environmental trouble is absorbed here — spawn failures retry indefinitely on a backoff ladder (bounded
90
+ * only by the caller's abort signal), and a simulation whose process dies is re-issued on the respawned
91
+ * process. The one deliberate exception: an input that kills the process twice is treated as a failing
92
+ * transaction, so a simulator-crashing tx gets evicted instead of burning a process per block forever.
35
93
  */
36
94
  export class AvmSimulatorPool implements AvmSimulator {
37
- private slots: Array<AvmProcessHandle | null> = [];
95
+ private slots: AvmProcessHandle[] = [];
38
96
  private available: number[] = [];
39
97
  private waiters: Array<{ resolve: (simulator: AvmProcessHandle) => void; reject: (error: Error) => void }> = [];
40
98
  private createdCount = 0;
99
+ private destroyed = false;
41
100
  private log: Logger;
42
101
  private maxSize: number;
43
102
  private cdbServer: CdbIpcServer;
103
+ private readonly spawnRetryIntervalMs: number;
104
+ private readonly spawnProcess: (options: AvmProcessSpawnOptions) => Promise<AvmProcessHandle>;
44
105
 
45
106
  constructor(private options: AvmSimulatorPoolOptions) {
46
107
  this.log = createLogger('simulator:avm-pool');
47
108
  this.maxSize = options.maxSize ?? parseInt(process.env.AVM_MAX_CONCURRENT_SIMULATIONS ?? '4', 10);
48
109
  this.cdbServer = new CdbIpcServer();
110
+ this.spawnRetryIntervalMs = options.spawnRetryIntervalMs ?? 1_000;
111
+ this.spawnProcess = options.spawnProcess ?? (spawnOptions => AvmSimulatorProcess.spawn(spawnOptions));
49
112
  }
50
113
 
51
114
  static async spawn(options: AvmSimulatorPoolOptions): Promise<AvmSimulatorPool> {
52
115
  const pool = new AvmSimulatorPool(options);
53
116
  // Always start one process up front so the first simulate() doesn't pay process spawn/connect cost.
117
+ // This is also where configuration errors (missing binary) surface, fast and fatally.
54
118
  await pool.prewarm();
55
119
  return pool;
56
120
  }
@@ -64,12 +128,7 @@ export class AvmSimulatorPool implements AvmSimulator {
64
128
  // and unregister once the simulation returns — registration is only needed while the call is running.
65
129
  this.cdbServer.registerFork(context.forkId, context.contractsDB, context.timestamp);
66
130
  try {
67
- const simulator = await this.checkout();
68
- try {
69
- return await simulator.simulate(inputBuffer, signal);
70
- } finally {
71
- this.return(simulator);
72
- }
131
+ return await this.runOnPool(simulator => simulator.simulate(inputBuffer, signal), signal);
73
132
  } finally {
74
133
  this.cdbServer.unregisterFork(context.forkId);
75
134
  }
@@ -77,28 +136,17 @@ export class AvmSimulatorPool implements AvmSimulator {
77
136
 
78
137
  async simulateWithHints(inputBuffer: Uint8Array): Promise<Uint8Array> {
79
138
  // The hinted path makes no contract-data callbacks, so no CDB registration is needed.
80
- const simulator = await this.checkout();
81
- try {
82
- return await simulator.simulateWithHints(inputBuffer);
83
- } finally {
84
- this.return(simulator);
85
- }
139
+ return await this.runOnPool(simulator => simulator.simulateWithHints(inputBuffer));
86
140
  }
87
141
 
88
- /** Destroy all AVM processes in the pool and close the CDB server. */
142
+ /** Destroy all AVM services in the pool and close the CDB server. */
89
143
  async destroy(): Promise<void> {
90
- for (const waiter of this.waiters) {
144
+ this.destroyed = true;
145
+ for (const waiter of this.waiters.splice(0)) {
91
146
  waiter.reject(new Error('AVM simulator pool destroyed'));
92
147
  }
93
- this.waiters = [];
94
148
 
95
- const destroyPromises: Promise<void>[] = [];
96
- for (const slot of this.slots) {
97
- if (slot) {
98
- destroyPromises.push(slot.destroy());
99
- }
100
- }
101
- await Promise.all(destroyPromises);
149
+ await Promise.all(this.slots.map(slot => slot.destroy()));
102
150
 
103
151
  this.slots = [];
104
152
  this.available = [];
@@ -115,31 +163,82 @@ export class AvmSimulatorPool implements AvmSimulator {
115
163
  const target = Math.min(count, this.maxSize);
116
164
  const created: AvmProcessHandle[] = [];
117
165
  while (this.createdCount < target) {
118
- created.push(await this.createSlot());
166
+ created.push(await this.checkout());
119
167
  }
120
- // Hand the freshly-spawned processes back to the pool so checkout() reuses them.
168
+ // Hand the checked-out processes back to the pool so checkout() reuses them.
121
169
  for (const simulator of created) {
122
170
  this.return(simulator);
123
171
  }
124
172
  }
125
173
 
126
- /** Check out an AVM process from the pool, blocking until one is free. Caller must return() it when done. */
127
- private async checkout(): Promise<AvmProcessHandle> {
174
+ /**
175
+ * Run a call on a pooled service. A call that fails because its process died is re-issued on the
176
+ * respawned process; a second death for the same input is attributed to the input and surfaces as an
177
+ * ordinary error (the pre-IPC equivalent — a native crash — took down the whole node, so a failed tx
178
+ * is strictly gentler). Non-process failures surface as-is: they are the simulation's own verdict.
179
+ */
180
+ private async runOnPool<T>(fn: (simulator: AvmProcessHandle) => Promise<T>, signal?: AbortSignal): Promise<T> {
181
+ for (let attempt = 0; ; attempt++) {
182
+ const simulator = await this.checkout(signal);
183
+ try {
184
+ return await fn(simulator);
185
+ } catch (err) {
186
+ if (!isProcessFailure(err) || signal?.aborted) {
187
+ throw err;
188
+ }
189
+ if (attempt > 0) {
190
+ const message = err instanceof Error ? err.message : String(err);
191
+ throw new Error(
192
+ `AVM simulator process died twice running this simulation; attributing the failure to the input: ${message}`,
193
+ { cause: err },
194
+ );
195
+ }
196
+ this.log.warn(`AVM process died during simulation; re-issuing once on the respawned process`, { err });
197
+ } finally {
198
+ this.return(simulator);
199
+ }
200
+ }
201
+ }
202
+
203
+ /** Check out an AVM service from the pool, blocking until one is free. Caller must return() it when done. */
204
+ private async checkout(signal?: AbortSignal): Promise<AvmProcessHandle> {
205
+ if (this.destroyed) {
206
+ throw new Error('AVM simulator pool destroyed');
207
+ }
128
208
  const idx = this.available.pop();
129
- if (idx !== undefined && this.slots[idx]) {
130
- return this.slots[idx]!;
209
+ if (idx !== undefined) {
210
+ return this.slots[idx];
131
211
  }
132
212
 
133
- if (this.createdCount < this.maxSize || (idx !== undefined && !this.slots[idx])) {
134
- return await this.createSlot(idx);
213
+ if (this.createdCount < this.maxSize) {
214
+ return await this.createSlot(signal);
135
215
  }
136
216
 
137
217
  return new Promise<AvmProcessHandle>((resolve, reject) => {
138
- this.waiters.push({ resolve, reject });
218
+ const waiter = { resolve, reject };
219
+ if (signal) {
220
+ const onAbort = () => {
221
+ const at = this.waiters.indexOf(waiter);
222
+ if (at >= 0) {
223
+ this.waiters.splice(at, 1);
224
+ reject(new AbortError('AVM checkout aborted'));
225
+ }
226
+ };
227
+ signal.addEventListener('abort', onAbort, { once: true });
228
+ waiter.resolve = simulator => {
229
+ signal.removeEventListener('abort', onAbort);
230
+ resolve(simulator);
231
+ };
232
+ waiter.reject = err => {
233
+ signal.removeEventListener('abort', onAbort);
234
+ reject(err);
235
+ };
236
+ }
237
+ this.waiters.push(waiter);
139
238
  });
140
239
  }
141
240
 
142
- /** Return an AVM process to the pool after use. */
241
+ /** Return an AVM service to the pool after use. */
143
242
  private return(simulator: AvmProcessHandle): void {
144
243
  const waiter = this.waiters.shift();
145
244
  if (waiter) {
@@ -152,59 +251,85 @@ export class AvmSimulatorPool implements AvmSimulator {
152
251
  }
153
252
  }
154
253
 
155
- private async createSlot(reuseIdx?: number): Promise<AvmProcessHandle> {
156
- const reuse = reuseIdx !== undefined && reuseIdx < this.slots.length;
254
+ private async createSlot(signal?: AbortSignal): Promise<AvmProcessHandle> {
157
255
  // Reserve the slot count synchronously, before the async spawn, so concurrent checkouts can't all
158
256
  // observe `createdCount < maxSize` and overshoot the pool (the count is only bumped once control has
159
- // yielded on the await). Roll back the reservation if the spawn itself fails.
160
- if (!reuse) {
161
- this.createdCount++;
162
- }
163
- let simulator: AvmProcessHandle;
257
+ // yielded on the await). Roll back the reservation if the spawn is abandoned.
258
+ this.createdCount++;
164
259
  try {
165
- simulator = await AvmSimulatorProcess.spawn({
166
- binaryPath: this.options.avmBinaryPath,
167
- wsdbIpcPath: this.options.wsdbIpcPath,
168
- cdbIpcPath: this.cdbServer.ipcPath,
169
- logger: this.options.logger,
170
- });
260
+ const simulator = await this.spawnUntilUp(signal);
261
+ this.slots.push(simulator);
262
+ this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
263
+ return simulator;
171
264
  } catch (err) {
172
- if (!reuse) {
173
- this.createdCount--;
174
- }
265
+ this.createdCount--;
175
266
  throw err;
176
267
  }
177
- if (reuse) {
178
- this.slots[reuseIdx!] = simulator;
179
- } else {
180
- this.slots.push(simulator);
268
+ }
269
+
270
+ /**
271
+ * Spawn a service, retrying environmental failures on a flat cadence indefinitely — the bound is
272
+ * the caller's own deadline (abort signal), matching the pre-IPC contract where a simulation either
273
+ * completed or was deadlined out. The cadence is deliberately fast and constant: an attempt is cheap,
274
+ * a slow one self-paces inside the backend's connect backstop, and backing off would cost a
275
+ * ~6s-slot sequencer whole blocks after the machine has already recovered. Configuration errors
276
+ * (missing binary, flagged non-retryable by the generated service) throw immediately; the boot-time
277
+ * prewarm is where those are meant to surface.
278
+ */
279
+ private async spawnUntilUp(signal?: AbortSignal): Promise<AvmProcessHandle> {
280
+ for (let failures = 0; ; failures++) {
281
+ if (this.destroyed) {
282
+ throw new Error('AVM simulator pool destroyed');
283
+ }
284
+ if (signal?.aborted) {
285
+ throw new AbortError('AVM process spawn aborted');
286
+ }
287
+ try {
288
+ return await this.spawnProcess({
289
+ binaryPath: this.options.avmBinaryPath,
290
+ wsdbIpcPath: this.options.wsdbIpcPath,
291
+ cdbIpcPath: this.cdbServer.ipcPath,
292
+ logger: this.options.logger,
293
+ });
294
+ } catch (err) {
295
+ if (!isProcessFailure(err)) {
296
+ throw err;
297
+ }
298
+ this.log.warn(
299
+ `Failed to spawn AVM process (attempt ${failures + 1}); retrying in ${this.spawnRetryIntervalMs}ms`,
300
+ { err },
301
+ );
302
+ await abortableSleep(this.spawnRetryIntervalMs, signal);
303
+ }
181
304
  }
182
- this.log.debug(`Created AVM pool slot (${this.createdCount}/${this.maxSize})`);
183
- return simulator;
184
305
  }
185
306
  }
186
307
 
187
308
  class AvmSimulatorProcess implements AvmProcessHandle {
188
309
  private constructor(private service: AvmService) {}
189
310
 
190
- static async spawn(options: {
191
- binaryPath?: string;
192
- wsdbIpcPath: string;
193
- cdbIpcPath: string;
194
- logger?: (msg: string) => void;
195
- }): Promise<AvmSimulatorProcess> {
311
+ static async spawn(options: AvmProcessSpawnOptions): Promise<AvmSimulatorProcess> {
196
312
  const service = await AvmService.spawn({
197
313
  binaryPath: options.binaryPath,
198
314
  transport: 'uds',
199
315
  logger: options.logger,
200
316
  extraArgs: ['--wsdb', options.wsdbIpcPath, '--cdb', options.cdbIpcPath],
317
+ // Each simulation is self-contained (state comes from the WSDB/CDB servers, routed by fork id),
318
+ // so a fresh process can safely serve the next call after a death.
319
+ respawn: true,
201
320
  });
202
321
  return new AvmSimulatorProcess(service);
203
322
  }
204
323
 
205
324
  public async simulate(inputBuffer: Uint8Array, signal?: AbortSignal): Promise<Uint8Array> {
206
- // Signal the C++ process to stop at its next cancellation checkpoint when the caller aborts.
207
- const onAbort = () => this.service.sendProcessSignal('SIGUSR1');
325
+ let killTimer: NodeJS.Timeout | undefined;
326
+ // Cooperative cancellation first: SIGUSR1 makes the C++ process stop at its next cancellation
327
+ // checkpoint. If it doesn't respond within the grace (wedged in a slow op), SIGKILL it — the
328
+ // service respawns lazily, so this reclaims the pool slot rather than leaking it.
329
+ const onAbort = () => {
330
+ this.service.sendProcessSignal('SIGUSR1');
331
+ killTimer = setTimeout(() => this.service.sendProcessSignal('SIGKILL'), CANCEL_KILL_GRACE_MS);
332
+ };
208
333
  if (signal?.aborted) {
209
334
  onAbort();
210
335
  }
@@ -213,6 +338,9 @@ class AvmSimulatorProcess implements AvmProcessHandle {
213
338
  return (await this.service.simulate({ inputs: inputBuffer })).result;
214
339
  } finally {
215
340
  signal?.removeEventListener('abort', onAbort);
341
+ if (killTimer !== undefined) {
342
+ clearTimeout(killTimer);
343
+ }
216
344
  }
217
345
  }
218
346