Skip to content

feat(simulator): backend-aware createSimulator (dry + live)#121

Merged
0xisk merged 4 commits into
mainfrom
feat/simulator-live-backend
Jun 23, 2026
Merged

feat(simulator): backend-aware createSimulator (dry + live)#121
0xisk merged 4 commits into
mainfrom
feat/simulator-live-backend

Conversation

@0xisk

@0xisk 0xisk commented Jun 22, 2026

Copy link
Copy Markdown
Member

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

Refs #75

Adds a live backend to @openzeppelin/compact-simulator so the same
per-module simulator runs against either the in-memory path or a live local
Midnight node, selected by MIDNIGHT_BACKEND=dry|live at construction.

One factory — createSimulator — now returns an async, backend-aware class;
circuits return promises, so a single spec file runs on both backends with
uniform await. MIDNIGHT_BACKEND=dry (default) is the existing in-memory
behaviour; MIDNIGHT_BACKEND=live attaches to a node via a caller-provided
harness.

What's in it

  • Backend seamBackend<P,L> with DryBackend (a thin async facade over
    the unchanged synchronous engine, now createDrySimulator) and a
    dynamically-imported LiveBackend adapter over an injected LiveContext.
    AsyncCircuits<> mapped type for the proxies.
  • Live wiringcreateLiveContext assembler (per-alias handle cache +
    bounded indexer-lag reads) and a registerLiveBackend / isLiveBackend
    registry, so a test:live setup wires the harness once and specs stay
    backend-agnostic (await Sim.create() identical on both; it.skipIf(isLiveBackend())
    guards the documented dry↔live asymmetries).
  • Signers — alias resolver with deterministic dry keys and a 4-signer live cap.
  • Dependency wall — every @midnight-ntwrk/midnight-js import is confined to
    src/live/ and reached only via dynamic import, so a dry import resolves zero
    midnight-js (guarded by a test); declared as optional peer deps.

Breaking change

createSimulator is now async: construct with await Sim.create(args, options)
and await circuits/state getters. The previous synchronous surface is replaced;
the in-memory engine remains available internally as createDrySimulator.
Consumers migrate per-module (mechanical async migration).

Validation

  • Package suite migrated to async — 73/73 green; biome + tsc clean; build clean.
  • Dependency wall verified at build output: dist/index.js resolves 0 real
    midnight-js imports.
  • Exercised against a real consumer (compact-contracts): swapped in, the existing
    1154 passing unit tests are unaffected; the migrated security module runs
    green on dry and green on live against a local node (make env-up
    real deploy → prove → submit tx → indexer read → assert).

PR Checklist

  • I have read the Contributing Guide
  • I have added tests that prove my fix is effective or that my feature works
  • I have added documentation of new methods and any new behavior or changes to existing behavior
  • CI Workflows Are Passing

Further comments

The single-async-factory shape (vs a separate createBackendSimulator) was chosen
so consumers keep one simulator file and one test file per module — no duplicate
*Backend twins — at the cost of a one-time async migration. Design / invariants /
code artifacts are under packages/simulator/docs/.

Summary by CodeRabbit

  • New Features

    • Added live backend support to test contracts against actual Midnight nodes alongside in-memory dry mode.
    • Simulator API now uses async execution with Promise-based circuit operations and factory-pattern construction.
    • Backend selection via MIDNIGHT_BACKEND=dry|live environment variable.
    • Enhanced caller identity management using string aliases.
  • Tests

    • Added unit tests for live backend adapter and signer functionality.

Make the per-module simulator runnable against a live Midnight node as
well as the in-memory path, selected by MIDNIGHT_BACKEND=dry|live at
construction. One factory (createSimulator) now returns an async,
backend-aware class; circuits return promises so a single spec file runs
on both backends.

* core: a Backend<P,L> seam with DryBackend (a thin async facade over the
  existing synchronous engine, now exposed internally as createDrySimulator)
  and a dynamically-imported LiveBackend adapter over an injected
  LiveContext; an AsyncCircuits<> mapped type for the proxies.
* live: a createLiveContext assembler (per-alias handle cache + bounded
  indexer-lag reads) and a registerLiveBackend/isLiveBackend registry, so a
  test:live setup wires the harness once and specs stay backend-agnostic.
* signers: alias resolver with deterministic dry keys and a 4-signer live
  cap.
* dependency wall: every @midnight-ntwrk/midnight-js import is confined to
  src/live/ and reached only via dynamic import, so a dry import resolves
  zero midnight-js (guarded by a test); they are declared optional peers.

BREAKING CHANGE: createSimulator is now async. Construct with
`await Sim.create(args, options)` and await circuits and state getters.
The previous synchronous surface is replaced; the in-memory engine remains
available internally as createDrySimulator.
@0xisk 0xisk requested review from a team as code owners June 22, 2026 21:19
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5197d8fb-bf15-4644-872b-216f8aab749a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces a Backend abstraction layer for the simulator package that routes circuit execution to either an in-memory dry backend or a live Midnight node backend, selected via MIDNIGHT_BACKEND environment variable. The factory createSimulator is refactored to async with static create, circuit proxies return Promises, and three design documents formalize the architecture and invariants.

Changes

Simulator Live-Mode Backend Abstraction

Layer / File(s) Summary
Architecture and invariant design docs
packages/simulator/docs/design/live-backend.md, packages/simulator/docs/design/live-backend-invariants.md, packages/simulator/docs/code/live-backend-code.md
Three new documents specify the backend abstraction design, 23 dry↔live parity invariants (INV-1–INV-23), dependency wall requirements, and open questions.
Backend abstraction contracts and types
packages/simulator/src/backend/Backend.ts, packages/simulator/src/types/Circuit.ts, packages/simulator/src/types/Options.ts, packages/simulator/src/types/index.ts, packages/simulator/src/factory/SimulatorConfig.ts
Defines BackendKind, CircuitKind, and the Backend<P,L> interface; adds AsyncCircuits type; introduces SimulatorOptions extending BaseSimulatorOptions with backend/live/signer fields; adds optional artifactName to SimulatorConfig.
Signers: alias-to-key resolver
packages/simulator/src/signers/Signers.ts
Adds Signers class with dry-mode deterministic hex derivation, live-mode prefunded alias pool enforcement (MAX_LIVE_SIGNERS cap), async keyFor, and circuit-shaped eitherFor. Exports ZswapCoinPublicKey, ContractAddress, Either, and MAX_LIVE_SIGNERS.
LiveContext injection seam and global registry
packages/simulator/src/live/LiveContext.ts, packages/simulator/src/live/registry.ts
Defines FinalizedCallResult, DeployedTxHandle, and LiveContext<P> injection interface. Adds LiveBackendRequest, LiveBackendFactory, and registerLiveBackend/clearLiveBackend/getRegisteredLiveBackend/isLiveBackend registry functions.
DryBackend and createDrySimulator
packages/simulator/src/backend/DryBackend.ts, packages/simulator/src/factory/createDrySimulator.ts
Adds SyncSimulator interface and DryBackend async facade over a sync simulator with caller resolution and circuit dispatch. Extracts createDrySimulator factory with CircuitContextManager, lazy circuit proxies, and witness management.
LiveBackend adapter
packages/simulator/src/live/LiveBackend.ts
Routes pure circuits locally via pureSim and impure circuits through ctx.handleFor(alias).callTx[name], normalizing { private: { result } } to bare result. Hard-errors on setPrivateState, overrideWitness, and setWitnesses. Implements single-shot vs persistent caller lifecycle.
createSimulator factory refactor and barrel expansion
packages/simulator/src/factory/createSimulator.ts, packages/simulator/src/index.ts, packages/simulator/package.json
Refactors createSimulator with resolveBackendKind, prepareBackend (dry vs live dynamic-import path), and buildProxy for async circuit routing. Expands barrel with all new backend/live/signer/type exports. Adds test:live script and optional peer dependencies.
Integration tests migrated to async
packages/simulator/test/integration/SimpleSimulator.ts, packages/simulator/test/integration/Simple.test.ts, packages/simulator/test/integration/WitnessSimulator.ts, packages/simulator/test/integration/Witness.test.ts, packages/simulator/test/integration/SampleZOwnableSimulator.ts, packages/simulator/test/integration/SampleZOwnable.test.ts
All simulators replace constructors with static async create; circuit methods return Promises. Tests await creation, circuit calls, state accessors, and use rejects.toThrow for error paths. Private state helpers converted to async getPrivateState/setPrivateState.
Unit tests: LiveBackend, Signers, dependency wall
packages/simulator/test/unit/LiveBackendAdapter.test.ts, packages/simulator/test/unit/Signers.test.ts, packages/simulator/test/unit/dependency-wall.test.ts
New unit tests validate pure-local execution, impure result normalization, caller lifecycle, live hard-failure guards, dry derivation/overrides, live cap enforcement, and static enforcement that midnight-js imports remain confined to src/live/.

Sequence Diagram(s)

sequenceDiagram
  participant TestSuite
  participant ContractSimulator
  participant createSimulator
  participant DryBackend
  participant LiveBackend
  participant LiveContext

  TestSuite->>ContractSimulator: static create(args, options)
  ContractSimulator->>createSimulator: prepareBackend(config, args, options)

  alt MIDNIGHT_BACKEND=dry
    createSimulator->>DryBackend: new DryBackend(syncSim, signers)
    DryBackend-->>createSimulator: backend (kind='dry')
  else MIDNIGHT_BACKEND=live
    createSimulator->>createSimulator: dynamic import LiveBackend
    createSimulator->>LiveContext: options.live or getRegisteredLiveBackend()(request)
    LiveContext-->>createSimulator: LiveContext<P>
    createSimulator->>LiveBackend: new LiveBackend({ ctx, pureSim, signers, ledgerExtractor })
    LiveBackend-->>createSimulator: backend (kind='live')
  end

  createSimulator-->>ContractSimulator: BackendDeps
  ContractSimulator-->>TestSuite: simulator instance

  TestSuite->>ContractSimulator: circuits.impure.someMethod(args)
  ContractSimulator->>DryBackend: call('impure', 'someMethod', args)
  DryBackend-->>ContractSimulator: Promise<result>
  ContractSimulator-->>TestSuite: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • andrew-fleming

Poem

🐇 Hop, hop, the backend splits in two,
One dry, one live — the tests stay true!
await create() where sync once stood,
Pure runs local, impure runs good.
The dep-wall holds, no midnight leak,
This rabbit's parity is quite unique! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(simulator): backend-aware createSimulator (dry + live)' clearly and concisely describes the main objective: making the simulator backend-aware to support both dry and live modes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/simulator-live-backend

Comment @coderabbitai help to get the list of available commands.

Add the midnight-js optional peer/dev deps to yarn.lock so the immutable
CI install matches package.json. Remove the design/invariants/code
pipeline artifacts under packages/simulator/docs from the PR (kept
locally, out of the published package).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/simulator/docs/design/live-backend.md (1)

68-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify language for fenced code block.

The fenced code block starting at line 68 should specify a language identifier for proper syntax highlighting and tooling support.

Add a language identifier (e.g., text or plaintext):

-```
+```text
 packages/simulator/src/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/simulator/docs/design/live-backend.md` around lines 68 - 85, The
fenced code block displaying the directory structure is missing a language
identifier for proper syntax highlighting. Change the opening fence marker from
``` to ```text at the beginning of the block that shows the
packages/simulator/src/ directory layout to specify the content type and enable
proper rendering.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/simulator/docs/design/live-backend.md`:
- Line 25: The design document references a dual-factory approach with both a
new `createBackendSimulator` and the existing synchronous factory, but the
actual implementation unified these into a single factory. Update the design
document in the live-backend.md file to describe the unified API where
`createSimulator` itself is now the async, backend-aware factory, and remove
references to a separate `createBackendSimulator` factory. Alternatively, add a
note explaining that the dual-factory design was rejected in favor of the single
unified factory approach documented in the code documentation.

In `@packages/simulator/src/live/LiveBackend.ts`:
- Around line 78-105: The call method in LiveBackend currently calls
consumeSingle() after both pure and impure circuit executions, which causes
inconsistent lifecycle management. Remove the consumeSingle() call from the pure
path where it runs after the fn() result is returned. For the impure path, wrap
the txFn execution and result handling in a try-finally block, moving the
consumeSingle() call into the finally block so it executes regardless of whether
the async txFn call succeeds or rejects. This ensures consumption happens
consistently for all impure calls and doesn't occur for pure calls, preventing
leaked single aliases across failed impure calls.

In `@packages/simulator/src/live/registry.ts`:
- Around line 46-48: The registerLiveBackend function currently silently
overwrites an existing registeredFactory without any warning or check, which can
cause non-deterministic behavior in shared test processes. Add a guard check
before assigning the factory parameter to registeredFactory that throws an error
if a factory is already registered, ensuring that accidental or duplicate
registrations are caught and prevent silent context switching. This will make
the function fail loudly rather than silently replacing the backend factory.

In `@packages/simulator/src/signers/Signers.ts`:
- Around line 95-100: The validation block checking if `this.liveAliases.size >
MAX_LIVE_SIGNERS` is executed regardless of the current mode, which causes
shared option objects to fail in dry mode even though liveAliases is documented
as live-only. Gate this validation by wrapping the entire error-throwing block
in a conditional that checks if the mode is 'live' before executing it, so the
signer-cap validation only enforces the limit when actually running in live
mode.

In `@packages/simulator/test/unit/dependency-wall.test.ts`:
- Around line 40-41: The startsWith(LIVE_DIR) check on line 40 uses string
prefix matching which creates false positives for similarly-named directories,
such as treating src/live2/ as being inside src/live/. Replace the startsWith
check with a more robust path boundary check that ensures LIVE_DIR is followed
by a path separator (forward slash) to prevent forbidden imports from bypassing
this guard and ensure only files actually within the LIVE_DIR directory are
filtered out.

---

Nitpick comments:
In `@packages/simulator/docs/design/live-backend.md`:
- Around line 68-85: The fenced code block displaying the directory structure is
missing a language identifier for proper syntax highlighting. Change the opening
fence marker from ``` to ```text at the beginning of the block that shows the
packages/simulator/src/ directory layout to specify the content type and enable
proper rendering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1e1bfb2-ca4b-48d8-847d-e142b296c424

📥 Commits

Reviewing files that changed from the base of the PR and between 22048e3 and b54bac3.

📒 Files selected for processing (27)
  • packages/simulator/docs/code/live-backend-code.md
  • packages/simulator/docs/design/live-backend-invariants.md
  • packages/simulator/docs/design/live-backend.md
  • packages/simulator/package.json
  • packages/simulator/src/backend/Backend.ts
  • packages/simulator/src/backend/DryBackend.ts
  • packages/simulator/src/factory/SimulatorConfig.ts
  • packages/simulator/src/factory/createDrySimulator.ts
  • packages/simulator/src/factory/createSimulator.ts
  • packages/simulator/src/index.ts
  • packages/simulator/src/live/LiveBackend.ts
  • packages/simulator/src/live/LiveContext.ts
  • packages/simulator/src/live/createLiveContext.ts
  • packages/simulator/src/live/registry.ts
  • packages/simulator/src/signers/Signers.ts
  • packages/simulator/src/types/Circuit.ts
  • packages/simulator/src/types/Options.ts
  • packages/simulator/src/types/index.ts
  • packages/simulator/test/integration/SampleZOwnable.test.ts
  • packages/simulator/test/integration/SampleZOwnableSimulator.ts
  • packages/simulator/test/integration/Simple.test.ts
  • packages/simulator/test/integration/SimpleSimulator.ts
  • packages/simulator/test/integration/Witness.test.ts
  • packages/simulator/test/integration/WitnessSimulator.ts
  • packages/simulator/test/unit/LiveBackendAdapter.test.ts
  • packages/simulator/test/unit/Signers.test.ts
  • packages/simulator/test/unit/dependency-wall.test.ts

Comment thread packages/simulator/docs/design/live-backend.md Outdated
Comment thread packages/simulator/src/live/LiveBackend.ts
Comment thread packages/simulator/src/live/registry.ts
Comment thread packages/simulator/src/signers/Signers.ts Outdated
Comment thread packages/simulator/test/unit/dependency-wall.test.ts Outdated
0xisk added 2 commits June 23, 2026 18:18
Remove the INV-N citation tags that referenced the live-backend
invariants design doc from all simulator source and test comments,
reflowing each sentence so it reads naturally on its own. Non-INV
design references (D1, D2, OQ2/4/6) are left intact.

Also normalize a stray NUL byte in createLiveContext.ts to its unicode
escape sequence, so the file is valid UTF-8 text that git diffs normally
instead of being treated as binary. The runtime string value is
unchanged.
* signers: gate the MAX_LIVE_SIGNERS cap check on live mode, so a shared
  options object carrying an oversized `liveAliases` no longer throws at
  construction in dry mode (the field is documented as live-only).
* registry: make `registerLiveBackend` throw when a different factory is
  already registered instead of silently replacing it; a silent swap in
  a shared test process switches harness context and yields
  non-deterministic live runs. Re-registering the same factory is a
  no-op; call `clearLiveBackend` to replace.
* dependency-wall test: match the live directory with a trailing path
  separator so the prefix check can't treat a sibling like `src/live2/`
  as inside `src/live/`.

@andrew-fleming andrew-fleming left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed the PR with @0xisk offline. LGTM for an alpha release, will provide a full review thereafter

@0xisk 0xisk merged commit 6dfe353 into main Jun 23, 2026
10 checks passed
@0xisk 0xisk deleted the feat/simulator-live-backend branch June 23, 2026 22:03
0xisk added a commit to OpenZeppelin/compact-contracts that referenced this pull request Jun 24, 2026
The async backend-aware simulator (OpenZeppelin/compact-tools#121) is
now published as @openzeppelin/compact-simulator@0.2.0. Replace the
placeholder `portal:` dependency on a sibling compact-tools checkout
with the published package, so the default `test` CI can install the
dependency and run the dry suite (the `portal:` link could not resolve
in CI).

Validated dry on the utils, access, token, and security modules
(298 tests green) against the published API; the imported surface
(createSimulator, registerLiveBackend, LiveBackendRequest, LiveContext,
SimulatorOptions) is unchanged from the portal build.
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.

2 participants