OpenZeppelin / OpenZeppelin/compact-contracts
dev: run the concurrency harness on the live backend
@0xisk is already working on this.
Since Jul 29, 2026.
- 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.ts—ConcurrencyHarnesswith four ops:snapshot/build/land/attempt, plusCall,Pending,Attempt,OutcomeDryReplayHarness.ts— replays transcripts viaQueryContext.runTranscriptrace.ts— build both calls on one snapshot, land the first, apply the secondbackend.ts— the seam; throws on liveparties.ts—createParties/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) — carryfinalizedTxData+circuitIdSucceedEntirely/FailEntirely/FailFallible,SegmentSuccess/SegmentFail,TxStatus(midnight-js-types)TransactionResult(ledger-v8) —type: 'success' | 'partialSuccess' | 'failure'anderror?: string, i.e. the ledger's own message read at runtimeisDeterministicRejection(RPC 1010 "Invalid Transaction"), already inLiveSimulatorBackend.ts
Plumbing
- Extract the provider + compiled-contract helpers out of
LiveSimulatorBackend.tsinto 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+LocalTestConfigurationfromlive.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 asregisterLiveBackend. - Give each party its own wallet alias (
SIGNER1,SIGNER2, …) so fee coins never collide and produce a false conflict unrelated to the contract. - Unify
HarnessOptionson{ artifactName, contractConstructor, actors: { witnesses, walletAlias } }. Both backends then build what they need from one description: dry doesnew Ctor(witnesses), live doescompileArtifact(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
-
createConcurrencyHarnessreturns a working harness onMIDNIGHT_BACKEND=live -
ConfidentialNoteFungibleToken.concurrency.test.tsruns 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:harnessstill passes on a clean checkout (it has nodependsOn: ["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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.