Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
782d5a9
test(multisig): add live-backend harness for shielded specs
0xisk Jul 9, 2026
a702e20
refactor(multisig): rename shielded presets by capability
0xisk Jul 9, 2026
2bcf9ca
test(multisig): fund fourth signer on live harness
0xisk Jul 9, 2026
9fdc3ee
fix(harness): share private state across signer aliases
0xisk Jul 9, 2026
5f16965
fix(harness): re-sync wallet before balancing tx
0xisk Jul 10, 2026
36fc6eb
test(multisig): raise live timeout for lifecycle specs
0xisk Jul 10, 2026
29330cd
chore(harness): drop WIP coin-tracker plan doc
0xisk Jul 10, 2026
c122ecb
refactor(multisig): use one empty witnesses for test sims
0xisk Jul 10, 2026
d60d1c7
fix(harness): hex-pad the pooled wallet seed helper
0xisk Jul 10, 2026
dfd8dc3
feat(harness): per-worker wallet pools for parallel live runs
0xisk Jul 10, 2026
b080e7b
fix(harness): retry deploy once on submission error
0xisk Jul 10, 2026
5488a82
test(multisig): share deploys in read-only groups
0xisk Jul 10, 2026
b515286
feat(harness): tag live-test output per worker
0xisk Jul 13, 2026
94bc8b3
docs(contributing): document running live tests
0xisk Jul 13, 2026
ab052c0
fix(harness): don't retry 1010 node rejections
0xisk Jul 13, 2026
402b784
feat(harness): guard live runs, fresh node + lock
0xisk Jul 13, 2026
56f3f65
feat(harness): two-round live flake checker
0xisk Jul 13, 2026
468cdcb
docs(contributing): live-run guard and verify flow
0xisk Jul 13, 2026
39097f2
feat(harness): tag live result lines per worker
0xisk Jul 13, 2026
94c60cf
feat(harness): unify live runs into one runner
0xisk Jul 13, 2026
242e3e4
docs(contributing): document unified live runner
0xisk Jul 13, 2026
7b4704a
docs(multisig): fix stale beforeEach reference in test comment
0xisk Jul 14, 2026
c12f1cf
fix(multisig): await treasury total getters sequentially
0xisk Jul 14, 2026
788cfbc
fix(harness): stop provider when wallet init fails
0xisk Jul 14, 2026
444e052
fix(harness): validate MIDNIGHT_*_PORT overrides
0xisk Jul 14, 2026
193df27
test(harness): isolate default-port test from env
0xisk Jul 14, 2026
7a15f98
fix(harness): build pooled wallets sequentially
0xisk Jul 14, 2026
d0477d8
build(turbo): add compile:archive to compile aggregate
0xisk Jul 14, 2026
5edc75b
docs(contributing): document single-file live runs
0xisk Jul 14, 2026
18db498
ci(live): add separate live test suite workflow
0xisk Jul 14, 2026
b2d5ef8
docs(contributing): document live tests in CI
0xisk Jul 14, 2026
92bc078
ci(live): make dispatch category a choice input
0xisk Jul 14, 2026
3bd22fa
feat(harness): add --list flag printing live-ready categories
0xisk Jul 14, 2026
7a79258
ci(live): fan out one parallel job per category
0xisk Jul 14, 2026
8cd046a
docs(contributing): note per-category live CI jobs
0xisk Jul 14, 2026
c49b9ea
feat(harness): surface live verdict in CI job summaries
0xisk Jul 14, 2026
c6984ef
ci(live): split artifacts and track nightly failures
0xisk Jul 14, 2026
bd8bc80
docs(contributing): note live CI summaries and artifacts
0xisk Jul 14, 2026
6ec2940
build(turbo): drop unused test:live task and script
0xisk Jul 14, 2026
f10b4c9
docs(harness): explain why the live runner is not turbo
0xisk Jul 14, 2026
e7545af
build(harness): drop bundled dev-api rename
0xisk Jul 15, 2026
24415bd
chore: update docs
0xisk Jul 15, 2026
e0b0169
ci: defer live-test workflow to a follow-up PR
0xisk Jul 15, 2026
98188c0
ci: drop the live-CI subsection from CONTRIBUTING
0xisk Jul 15, 2026
5f1e04b
fix(turbo): drop archive from aggregate compile
0xisk Jul 15, 2026
3d40dd5
fix(make): scope env-up --wait to node+indexer
0xisk Jul 15, 2026
c70c1d9
refactor(multisig): revert preset rename
0xisk Jul 15, 2026
23501f8
chore: drop corepack prefix from yarn commands
0xisk Jul 15, 2026
7668f20
chore: drop redundant test:live:multisig script
0xisk Jul 15, 2026
031a06a
fix(harness): reset node before each round-2 file
0xisk Jul 15, 2026
fedfa2a
test(multisig): group dry-only burn tests
0xisk Jul 16, 2026
8d4307d
test(multisig): read proposal fields via getProposal
0xisk Jul 16, 2026
b943d63
test(harness): cover coin-tracker indexed resolution
0xisk Jul 16, 2026
3aaf8d5
test(harness): assert port() rejects bad overrides
0xisk Jul 16, 2026
b403758
test(harness): prove pooled wallets build serially
0xisk Jul 16, 2026
8aaaf63
docs(contributing): clarify test:live is a substring match
0xisk Jul 16, 2026
b29abc0
chore: update doc
0xisk Jul 17, 2026
7f6e09e
build: realign with main's compile/lint rename after rebase
0xisk Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ We really appreciate and value contributions to OpenZeppelin Contracts for Compa
* [Pull Requests](#pull-requests)
* [Opening an Issue](#opening-an-issue)

[Running Tests](#running-tests)

* [Unit Tests](#unit-tests)
* [Live Tests](#live-tests)

[Styleguides](#styleguides)

* [Git Commit Messages](#git-commit-messages)
Expand Down Expand Up @@ -152,6 +157,76 @@ A maintainer will re-run the status check for you. If we conclude that the failu

While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.

## Running Tests

Run all commands from the repository root. Enable Corepack once (`corepack enable`) so `yarn` resolves to the version pinned in `package.json`.

### Unit Tests

Unit tests run against an in-process mock backend (no network, ZK proving skipped):

```bash
yarn test
```

### Live Tests

Live tests run against a local Midnight network (node, indexer, and proof server) defined in [`local-env.yml`](./local-env.yml). They require [Docker](https://docs.docker.com/get-docker/) and a completed `yarn install`.

One command runs everything — it compiles, resets the stack, runs a quick harness smoke, then each live-ready category sequentially on a freshly reset node:

```bash
yarn test:live
```

Currently `multisig` is the only live-ready category; the others still assume dry-only semantics and are skipped (listed in the run banner). Each category joins the run — with its own `test:live:<category>` script — as its specs are refactored for the live backend.

If any files fail, a second round re-runs just those files on a fresh node with one worker, to separate a real failure from an environment flake:

* Fails round 1, passes round 2 → **FLAKY** (exit 0, reported loudly).
* Fails both rounds → **REAL** (exit non-zero).

Scope the same mechanism to one category, or to a subset within it. The first
argument names the category; any further argument is a filename substring
(vitest matches it), so pass a spec name to run every file whose name matches it
on the live backend — the fast loop while iterating on one feature, instead of
waiting for the whole category. The match is a substring, not an exact file, so
a name that prefixes others runs all of them:

```bash
yarn test:live multisig # the whole category
yarn test:live multisig ShieldedTreasury # any file matching "ShieldedTreasury"
```

The two-round flake check still applies to a scoped run, so a green result
means the same thing it does for the full suite.

Stop the network when done: `yarn env:down`. (No manual `env:up` is needed — the runner resets the stack itself.)

> **Note:** The live tests all run against one shared node, so state left by an earlier run can make a later one fail. Two rules keep them reliable, both enforced by a guard that fails fast, before any wallet build:
>
> 1. **Start from a fresh node.** State left by a previous run makes shielded spends fail with node `Custom error: 103`. The guard aborts if it finds any shielded coin event beyond genesis. The `test:live*` runner resets for you; reset manually with `yarn env:up`.
> 2. **One live run at a time.** A pid-stamped lock (`contracts/logs/.live-run.lock`) makes a second concurrent run abort.

Environment knobs:

| Variable | Default | Effect |
| --- | --- | --- |
| `MIDNIGHT_LIVE_WORKERS` | 3 | Parallel spec files (max 3 — one genesis-funded deployer each). |
| `MIDNIGHT_LIVE_ALLOW_DIRTY` | unset | `1` skips the freshness check (run against a dirty node). |
| `MIDNIGHT_LIVE_MAX_COIN_EVENTS` | 0 | Coin events beyond genesis tolerated before "not fresh". |
| `MIDNIGHT_LIVE_MAX_SCAN_BLOCKS` | 3600 | Above this indexer head, the guard asks you to `env:up` rather than scan. |

`unit-live` runs up to 3 workers in parallel, so their output interleaves. It is tagged per worker: a `▶ live worker N/3 ready` banner when a worker's wallets are funded, a `[wN] ❯ <file>` line as each spec file starts, and a `[wN] ✓ <test> (<ms>) [done/total]` line per test — showing the worker, the result, and overall progress through the run. Each worker also writes a detailed log to `logs/live-harness-wN.log`.

> **Tip:** to save the run to a colored, readable log, force color and pipe to `tee`. Piping (stdout is no longer a TTY) makes vitest print one clean line per result instead of an animated spinner, and `FORCE_COLOR=1` keeps the color. Write it to a `.ansi` file:
>
> ```bash
> FORCE_COLOR=1 yarn test:live multisig 2>&1 | tee logs/live-multisig.ansi
> ```
>
> The file stores ANSI color codes, so render them rather than reading them raw. In VS Code, an ANSI extension such as [`iliazeus.vscode-ansi`](https://marketplace.visualstudio.com/items?itemName=iliazeus.vscode-ansi) renders a `.ansi` file via **"ANSI Text: Open Preview"**. In a terminal, use `less -R logs/live-multisig.ansi`. On Linux, prefix `systemd-inhibit --why="live tests"` for a long run.

## Styleguides

### TypeScript Styleguide
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ SERVICES := proof-server indexer node

## Start local environment and stream logs to logs/
env-up: env-down
docker compose -f $(COMPOSE_FILE) up -d --wait
docker compose -f $(COMPOSE_FILE) up -d
@# proof-server has no healthcheck, so a blanket `--wait` is not portable
@# across Docker Compose versions; wait only on the services that expose one.
docker compose -f $(COMPOSE_FILE) up -d --wait node indexer
Comment thread
0xisk marked this conversation as resolved.
@mkdir -p $(LOGS_DIR)
@for svc in $(SERVICES); do \
docker compose -f $(COMPOSE_FILE) logs -f --no-log-prefix $$svc > $(LOGS_DIR)/$$svc.log 2>&1 & \
Expand Down
25 changes: 18 additions & 7 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"homepage": "https://docs.openzeppelin.com/contracts-compact/",
"type": "module",
"imports": {
"#test-utils/address.js": "./test-utils/address.js",
"#test-utils/zswap.js": "./test-utils/zswap.js"
"#test-utils/*": "./test-utils/*"
},
"scripts": {
"compile": "compact-compiler --exclude '*/archive/*'",
Expand All @@ -33,12 +32,13 @@
"compile:security": "compact-compiler --dir security",
"compile:token": "compact-compiler --dir token",
"compile:utils": "compact-compiler --dir utils",
"compile:integration": "SKIP_ZK=true compact-compiler --src test/integration/_mocks",
"build": "compact-builder --hierarchical --out dist --clean-dist --exclude '*/archive/*' --exclude 'Mock*' --exclude '*.mock.compact' --copy package.json --copy ../README.md && find dist -type d -empty -delete",
"test": "SKIP_ZK=true yarn run compile && vitest run",
"test:coverage": "SKIP_ZK=true yarn run compile && vitest run --coverage",
"test:live": "yarn run compile && MIDNIGHT_BACKEND=live vitest run --config vitest.live.config.ts",
"compile:integration": "SKIP_ZK=true compact compile test/integration/_mocks/SharedInitCollision.compact artifacts/SharedInitCollision && SKIP_ZK=true compact compile test/integration/_mocks/ComposedTokens.compact artifacts/ComposedTokens",
"test:integration": "yarn run compile:integration && vitest run --config vitest.integration.config.ts",
"test": "vitest run --project unit",
"test:coverage": "vitest run --project unit --coverage",
"test:integration": "vitest run --project integration",
"test:harness": "vitest run --project harness",
"test:harness:live": "MIDNIGHT_BACKEND=live vitest run --project harness-live",
"types": "tsc -p tsconfig.json --noEmit",
"clean": "git clean -fXd"
},
Expand All @@ -49,6 +49,17 @@
"@openzeppelin/compact-cli": "^0.0.2"
},
"devDependencies": {
"@midnight-ntwrk/compact-js": "2.5.1",
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.1.0",
"@midnight-ntwrk/midnight-js-contracts": "4.1.1",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-network-id": "4.1.1",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.1.1",
"@midnight-ntwrk/midnight-js-protocol": "4.1.1",
"@midnight-ntwrk/midnight-js-types": "4.1.1",
"@midnight-ntwrk/testkit-js": "4.1.1",
"@openzeppelin/compact-simulator": "^0.2.0",
"@tsconfig/node24": "^24.0.4",
"@types/node": "26.1.1",
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/access/test/AccessControl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
persistentHash,
} from '@midnight-ntwrk/compact-runtime';
import { beforeEach, describe, expect, it } from 'vitest';
import * as utils from '#test-utils/address.js';
import * as utils from '#test-utils/fixtures/address.js';
import { AccessControlSimulator } from './simulators/AccessControlSimulator.js';

// Helpers
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/access/test/Ownable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
persistentHash,
} from '@midnight-ntwrk/compact-runtime';
import { beforeEach, describe, expect, it } from 'vitest';
import * as utils from '#test-utils/address.js';
import * as utils from '#test-utils/fixtures/address.js';
import { OwnableSimulator } from './simulators/OwnableSimulator.js';

// Helpers
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/access/test/ZOwnablePK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
persistentHash,
} from '@midnight-ntwrk/compact-runtime';
import { beforeEach, describe, expect, it } from 'vitest';
import * as utils from '#test-utils/address.js';
import * as utils from '#test-utils/fixtures/address.js';
import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.js';
import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js';
import { ZOwnablePKPrivateState } from './witnesses/ZOwnablePKWitnesses.js';
Expand Down
21 changes: 6 additions & 15 deletions contracts/src/multisig/presets/ShieldedMultiSig.compact
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,12 @@ export circuit getProposal(id: Uint<64>): Proposal_Proposal {
return Proposal_getProposal(id);
}

export circuit getProposalRecipient(id: Uint<64>): Proposal_Recipient {
return Proposal_getProposalRecipient(id);
}

export circuit getProposalAmount(id: Uint<64>): Uint<128> {
return Proposal_getProposalAmount(id);
}

export circuit getProposalColor(id: Uint<64>): Bytes<32> {
return Proposal_getProposalColor(id);
}
// NOTE: the per-field proposal getters (getProposalRecipient / getProposalAmount
// / getProposalColor) and getReceivedMinusSent were dropped to bring the deploy
// transaction under the block-weight limit (the full 19-circuit deploy is
// rejected with "Transaction would exhaust the block limits"). They are all
// redundant: read the proposal fields via `getProposal(id).to / .amount / .color`,
// and the net balance via `getReceivedTotal(color) - getSentTotal(color)`.
Comment thread
0xisk marked this conversation as resolved.

export circuit getProposalStatus(id: Uint<64>): Proposal_ProposalStatus {
return Proposal_getProposalStatus(id);
Expand All @@ -217,10 +212,6 @@ export circuit getSentTotal(color: Bytes<32>): Uint<128> {
return Treasury_getSentTotal(color);
}

export circuit getReceivedMinusSent(color: Bytes<32>): Uint<128> {
return Treasury_getReceivedMinusSent(color);
}

// ISignerManager

export circuit getSignerCount(): Uint<8> {
Expand Down
6 changes: 3 additions & 3 deletions contracts/src/multisig/test/EmptyWitnesses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/test/EmptyWitnesses.ts)

/**
* Shared empty private state and witnesses for forwarder contracts, none
* of which declare any witnesses. Imported by their simulators in place
* of a per-contract witness module.
* Shared empty private state and witnesses for the multisig test simulators.
* None of the multisig contracts declare witnesses, so every simulator imports
* this in place of a per-contract witness module.
*/
export type EmptyPrivateState = Record<string, never>;
export const EmptyPrivateState: EmptyPrivateState = {};
Expand Down
46 changes: 32 additions & 14 deletions contracts/src/multisig/test/Forwarder.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { isLiveBackend } from '@openzeppelin/compact-simulator';
import { describe, expect, it } from 'vitest';
import * as utils from '#test-utils/address.js';
import * as utils from '#test-utils/fixtures/address.js';
import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js';
import { MockForwarderShieldedSimulator } from './simulators/MockForwarderShieldedSimulator.js';
import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnshieldedSimulator.js';

Expand All @@ -11,19 +17,31 @@ import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnsh
// future CMA circuit upgrade can add contract support without a state
// migration; `initialize` stores the supported arm (shielded → `left`,
// unshielded → `right`), which is what `getParent` reads back.
const SHIELDED_PARENT = utils.createEitherTestUser('PARENT').left;
//
// Live: the shielded parent is the deployer's own key (the forward sends the
// coin to it, so its encryption key must resolve on-chain). The unshielded
// parent stays synthetic — an unshielded recipient is a public address, no
// encryption key needed.
const SHIELDED_PARENT = shieldedTestParentKey();
const SHIELDED_ZERO = utils.ZERO_KEY.left;
const UNSHIELDED_PARENT = utils.createEitherTestUserAddress('PARENT').right;
const UNSHIELDED_ZERO = utils.ZERO_USER_ADDRESS.right;
const COLOR = new Uint8Array(32).fill(1);

// Shielded color: genesis-funded (`0x00…01`) so a live forward has funds to
// draw; `fill(1)` would be unfunded on live. Unshielded color: on live the
// deployer wallet only holds the native unshielded token (`0x00…00`), so the
// forward draws that; on dry any color mints freely.
const SHIELDED_COLOR =
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1;
const UNSHIELDED_COLOR = isLiveBackend()
? new Uint8Array(32)
: new Uint8Array(32).fill(1);
const AMOUNT = 1000n;

// Live gets a fresh random nonce per run (the node persists nullifiers); dry
// uses zero for reproducibility.
function makeCoin(color: Uint8Array, value: bigint, nonce?: Uint8Array) {
return {
nonce: nonce ?? new Uint8Array(32).fill(0),
color,
value,
};
return encodeShieldedCoinInfo(color, value, nonce);
}

describe('ForwarderShielded module', () => {
Expand Down Expand Up @@ -55,9 +73,9 @@ describe('ForwarderShielded module', () => {
SHIELDED_PARENT,
false,
);
await expect(mock.deposit(makeCoin(COLOR, AMOUNT))).rejects.toThrow(
'ForwarderShielded: contract not initialized',
);
await expect(
mock.deposit(makeCoin(SHIELDED_COLOR, AMOUNT)),
).rejects.toThrow('ForwarderShielded: contract not initialized');
});
});

Expand All @@ -67,7 +85,7 @@ describe('ForwarderShielded module', () => {
SHIELDED_PARENT,
true,
);
await mock.deposit(makeCoin(COLOR, AMOUNT));
await mock.deposit(makeCoin(SHIELDED_COLOR, AMOUNT));
});
});
});
Expand Down Expand Up @@ -101,7 +119,7 @@ describe('ForwarderUnshielded module', () => {
UNSHIELDED_PARENT,
false,
);
await expect(mock.deposit(COLOR, AMOUNT)).rejects.toThrow(
await expect(mock.deposit(UNSHIELDED_COLOR, AMOUNT)).rejects.toThrow(
'ForwarderUnshielded: contract not initialized',
);
});
Expand All @@ -113,7 +131,7 @@ describe('ForwarderUnshielded module', () => {
UNSHIELDED_PARENT,
true,
);
await mock.deposit(COLOR, AMOUNT);
await mock.deposit(UNSHIELDED_COLOR, AMOUNT);
});
});
});
Loading