OpenZeppelin / OpenZeppelin/compact-contracts

dev: run the concurrency harness on the live backend

Open
#749 0 comments 0 reactions 1 assignee View on GitHub

@0xisk is already working on this.

Since Jul 29, 2026.

2-low enhancement
Dominant language
TypeScript
Stars
55
Forks
29
Avg merge
5d 7h
Merged PRs (30d)
25

Description

🧐 Motivation

The concurrency harness landing with the note-token core (#743) runs dry only. Its cases are already backend-neutral and the seam is in place, but createConcurrencyHarness throws on live and each spec is gated with describe.skipIf(isLiveBackend()).

Dry replay is the real verifying path, not a simulation — but it covers one of the two couplings between a transcript and the state it was built on, and none of the node's behaviour. A concurrency verdict is not fully validated until it is observed on chain.

📝 What exists today

contracts/test-utils/concurrency/:

  • types.tsConcurrencyHarness with four ops: snapshot / build / land / attempt, plus Call, Pending, Attempt, Outcome
  • DryReplayHarness.ts — replays transcripts via QueryContext.runTranscript
  • race.ts — build both calls on one snapshot, land the first, apply the second
  • backend.ts — the seam; throws on live
  • parties.tscreateParties / labelledSecret, contract-agnostic

First consumer: ConfidentialNoteFungibleToken.concurrency.test.ts, 7 cases (4 commuting, 3 conflicting).

📝 What needs doing

Implement LiveConcurrencyHarness behind the existing seam so the same cases run unchanged in both backends, then drop the skipIf.

The design needs no same-block trickery. A conflict is a divergence between the state a transcript was built on and the state it is applied to, not a wall-clock race. So: pin two builds to one snapshot, submit sequentially, and the second is evaluated against a state that no longer exists. Deterministic, no flake.

APIs verified to exist (so this doesn't get rediscovered)
need API
the snapshot triple publicDataProvider.queryZSwapAndContractState(addr)[ZswapChainState, ContractState, LedgerParameters]
build without submitting createUnprovenCallTxFromInitialStates(zkConfigProvider, options, walletEncPubKey).private.unprovenTx
pin the build to a snapshot initialContractState / initialZswapChainState / ledgerParameters are explicit options
submit, blocking submitTx(providers, { unprovenTx, circuitId })FinalizedTxData (waits indefinitely)
submit, bounded submitTxAsync(...)txId, then publicDataProvider.watchForTxData(txId)
caller identity walletProvider.getCoinPublicKey() / .getEncryptionPublicKey()
Classify from typed signals, never from error text

The runtime's error strings are static data inside the .wasm binary; OnchainProgramError is a Rust enum that wasm-bindgen erases to a plain Error. There is nothing to import and nothing worth pattern-matching. Use instead:

  • CallTxFailedError / TxFailedError (midnight-js-contracts) — carry finalizedTxData + circuitId
  • SucceedEntirely / FailEntirely / FailFallible, SegmentSuccess / SegmentFail, TxStatus (midnight-js-types)
  • TransactionResult (ledger-v8) — type: 'success' | 'partialSuccess' | 'failure' and error?: string, i.e. the ledger's own message read at runtime
  • isDeterministicRejection (RPC 1010 "Invalid Transaction"), already in LiveSimulatorBackend.ts
Plumbing
  • Extract the provider + compiled-contract helpers out of LiveSimulatorBackend.ts into a shared module. The concurrency harness needs the raw unproven-tx path, not the simulator's fused call, and should not rebuild five providers.
  • Publish the worker's WalletPool + LocalTestConfiguration from live.setup.ts. They are module-locals there today and a spec cannot import that file without re-running the whole funding setup. Same singleton-per-worker shape as registerLiveBackend.
  • Give each party its own wallet alias (SIGNER1, SIGNER2, …) so fee coins never collide and produce a false conflict unrelated to the contract.
  • Unify HarnessOptions on { artifactName, contractConstructor, actors: { witnesses, walletAlias } }. Both backends then build what they need from one description: dry does new Ctor(witnesses), live does compileArtifact(name, Ctor, witnesses). Note two parties means two compiled contracts (different witnesses) pointed at one deployed address.

⚠️ Open question, only settled by running it

A guaranteed-segment failure keeps the transaction out of any block. Every conflict in the note core is guaranteed-segment (no Kernel.checkpoint anywhere), so there may be no FinalizedTxData and no TransactionResult to read — the signal would be a submit-time rejection or a timeout instead. FailEntirely + TransactionResult.error is the shape a fallible-segment failure produces.

attempt() needs a bounded wait either way. submitTx waiting indefinitely is not usable for the rejected path.

📝 Also worth adding once live exists

An effects-divergence case. run_transcript forwards only program + gas to query and ignores transcript.effects; the declared-vs-recomputed effects check runs in the ledger after the transcript does. So that coupling is structurally invisible to the dry backend and is live-only.

✅ Acceptance criteria

  • createConcurrencyHarness returns a working harness on MIDNIGHT_BACKEND=live
  • ConfidentialNoteFungibleToken.concurrency.test.ts runs green in both backends with no per-backend branching in the cases
  • skipIf(isLiveBackend()) removed from the spec
  • rejections classified from typed signals only; an infrastructure failure surfaces as an error, never as a scored second-rejected
  • attempt() bounded; the guaranteed-vs-fallible behaviour documented from an actual run
  • provider plumbing shared with LiveSimulatorBackend, not duplicated
  • test:harness still passes on a clean checkout (it has no dependsOn: ["compile"], so no harness test may import a compiled artifact)

🚫 Out of scope

  • Mempool / sequencer ordering behaviour (P2 in the Concurrency on Midnight design doc) — a separate question about the node, not about a contract's concurrency properties.
  • Concurrency suites for the other token families. Those follow per contract, once this harness runs in both backends.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.