Skip to content

Simulator supports a live-mode backend #75

@0xisk

Description

@0xisk

🧐 Motivation

Today every simulator in @openzeppelin-compact/contracts-simulator runs purely in-memory against a CircuitContext. That's fast and deterministic, which is the right default for unit tests, but it only exercises circuit logic in isolation — it never touches a real Midnight node, real provers, real wallets, or real ledger state transitions.

Several things we care about cannot be covered by an in-memory simulator alone:

  • Contract upgradability. Deploying v1, exercising it, upgrading to v2, and verifying state + behavior across the boundary only makes sense against a live node that actually persists ledger state between transactions.
  • End-to-end prover / wallet integration. The in-memory path bypasses proof generation and wallet signing, so regressions in those layers go unnoticed until integration time.
  • Multi-caller flows. Scenarios with several funded accounts interacting with the same deployed contract (role grants, transfers, access checks across principals) are awkward to model in memory and trivial on a live node with a real wallet pool.
  • Confidence that unit-test assertions reflect on-chain reality. Running the same spec against both backends catches divergences between the simulator's model of a circuit and what the node actually executes.

Right now the only way to get any of this is to write a separate, live-node-flavored test file per module, with its own API surface that mirrors the simulator's. That duplication is the root problem: the public API of each contract ends up encoded in two places, and the two drift.

📝 Details

Make the existing per-module simulator classes the single home for each contract's test-facing API, and let them run against either an in-memory context or a real deployed contract on a local Midnight node — same methods, same spec files, selected at fixture-construction time.

Proposal

Introduce a Backend abstraction in @openzeppelin-compact/contracts-simulator:

  • DryBackend — wraps the existing in-memory CircuitContext path.
  • LiveBackend<C> — wraps a DeployedContract<C>, MidnightProviders, and a wallet (plus an optional address-keyed wallet pool for .as(alias) / setPersistentCaller(alias) support).

Each simulator method returns Promise<T> in both modes (dry mode wraps sync logic in async, live mode awaits network calls). Tests write await simulator.foo() uniformly; no conditional types, no runtime branching in spec code. Backend selection happens at fixture-construction time, driven by MIDNIGHT_BACKEND=dry|live.

A new factory createBackendSimulator(...) lives alongside the existing createSimulator(...); modules opt in one at a time by swapping the factory call and wrapping their method return types in Promise<T>. No flag day.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status

Backlog

Relationships

None yet

Development

No branches or pull requests

Issue actions