Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ One command runs everything — it compiles, resets the stack, runs a quick harn
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.
Every `src/<category>` with tests runs live: `multisig`, `token`, `access`, and `security` carry backend-aware specs (real coins and on-chain identity threaded through the wallet pool; coin-flow assertions split into dry and live blocks), and `crypto` and `utils` — pure hashing / commitment / encoding primitives — run their computation through a real deploy on the node too. There is no separate live-ready allowlist; the runner discovers categories automatically (CI reads the list via `--list`). A category that must not run live is an explicit opt-out (`EXCLUDED_CATEGORIES` in [`scripts/test-live.ts`](./scripts/test-live.ts) — only legacy `archive` today).

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:

Expand Down
65 changes: 42 additions & 23 deletions contracts/src/access/test/ZOwnablePK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ import {
} from '@midnight-ntwrk/compact-runtime';
import { beforeEach, describe, expect, it } from 'vitest';
import * as utils from '#test-utils/fixtures/address.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import type { ZswapCoinPublicKey } from '../../../artifacts/MockOwnable/contract/index.js';
import { ZOwnablePKSimulator } from './simulators/ZOwnablePKSimulator.js';
import { ZOwnablePKPrivateState } from './witnesses/ZOwnablePKWitnesses.js';

// PKs
const [, Z_OWNER] = utils.generatePubKeyPair('OWNER');
const [, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER');
// The three owner identities this spec authorizes by. ZOwnablePK is the one
// access contract that authorizes by the caller's REAL coin public key:
// `assertOnlyOwner` recomputes `id = SHA256(ownPublicKey(), nonce)` and matches
// its commitment against the stored one. So the PK used to build the expected
// commitment off-chain MUST equal the key `.as(alias)` makes `ownPublicKey()`
// return, and the three roles must resolve to distinct wallets or the negative
// "caller is not the owner" checks false-pass. The live wallet pool exposes
// fixed slots (`SIGNER1`–`SIGNER3`, within the 3-signer cap); any other alias
// collapses to the deployer wallet. So bind each role to its own slot here and
// refer to it by these names throughout.
const OWNER = 'SIGNER1';
const NEW_OWNER = 'SIGNER2';
const UNAUTHORIZED = 'SIGNER3';

// The owner / new-owner coin public keys — the `.left` (user) arm of the shared
// signer fixture, i.e. exactly what `ownPublicKey()` returns for that alias. On
// live each is the pooled wallet's own key (published as
// `MIDNIGHT_<ALIAS>_COIN_PK`); on dry it is the deterministic synthetic key the
// dry backend also derives for `.as(alias)`.
const Z_OWNER = shieldedTestKey(OWNER).left;
const Z_NEW_OWNER = shieldedTestKey(NEW_OWNER).left;

const INSTANCE_SALT = new Uint8Array(32).fill(8675309);
const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE'));
Expand Down Expand Up @@ -196,36 +215,36 @@ describe('ZOwnablePK', () => {
});

it('should transfer ownership', async () => {
await ownable.as('OWNER').transferOwnership(newIdHash);
await ownable.as(OWNER).transferOwnership(newIdHash);
expect(await ownable.owner()).toEqual(newOwnerCommitment);

// Old owner
await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow(
await expect(ownable.as(OWNER).assertOnlyOwner()).rejects.toThrow(
'ZOwnablePK: caller is not the owner',
);

// Unauthorized
await expect(
ownable.as('UNAUTHORIZED').assertOnlyOwner(),
ownable.as(UNAUTHORIZED).assertOnlyOwner(),
).rejects.toThrow('ZOwnablePK: caller is not the owner');

// New owner
await ownable.privateState.injectSecretNonce(
Buffer.from(newOwnerNonce),
);
await ownable.as('NEW_OWNER').assertOnlyOwner();
await ownable.as(NEW_OWNER).assertOnlyOwner();
});

it('should fail when transferring to id zero', async () => {
const badId = new Uint8Array(32).fill(0);
await expect(
ownable.as('OWNER').transferOwnership(badId),
ownable.as(OWNER).transferOwnership(badId),
).rejects.toThrow('ZOwnablePK: invalid id');
});

it('should fail when unauthorized transfers ownership', async () => {
await expect(
ownable.as('UNAUTHORIZED').transferOwnership(newIdHash),
ownable.as(UNAUTHORIZED).transferOwnership(newIdHash),
).rejects.toThrow('ZOwnablePK: caller is not the owner');
});

Expand All @@ -237,7 +256,7 @@ describe('ZOwnablePK', () => {
.ZOwnablePK__counter;

// Transfer
await ownable.as('OWNER').transferOwnership(newIdHash);
await ownable.as(OWNER).transferOwnership(newIdHash);

// Check counter
const afterInstance = (await ownable.getPublicState())
Expand All @@ -257,7 +276,7 @@ describe('ZOwnablePK', () => {
expect(initCommitment).toEqual(expInitCommitment);

// Transfer ownership to self with the same id -> `H(pk, nonce)`
await ownable.as('OWNER').transferOwnership(repeatedId);
await ownable.as(OWNER).transferOwnership(repeatedId);

// Check commitments don't match
const newCommitment = await ownable.owner();
Expand All @@ -273,40 +292,40 @@ describe('ZOwnablePK', () => {
expect(newCommitment).toEqual(expNewCommitment);

// Check same owner maintains permissions after transfer
await ownable.as('OWNER').assertOnlyOwner();
await ownable.as(OWNER).assertOnlyOwner();
});
});

describe('renounceOwnership', () => {
it('should renounce ownership', async () => {
await ownable.as('OWNER').renounceOwnership();
await ownable.as(OWNER).renounceOwnership();

// Check owner is reset
expect(await ownable.owner()).toEqual(new Uint8Array(32).fill(0));

// Check revoked permissions
await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow(
await expect(ownable.as(OWNER).assertOnlyOwner()).rejects.toThrow(
'ZOwnablePK: caller is not the owner',
);
});

it('should fail when renouncing from unauthorized', async () => {
await expect(
ownable.as('UNAUTHORIZED').renounceOwnership(),
ownable.as(UNAUTHORIZED).renounceOwnership(),
).rejects.toThrow('ZOwnablePK: caller is not the owner');
});

it('should fail when renouncing from authorized with bad nonce', async () => {
await ownable.privateState.injectSecretNonce(BAD_NONCE);
await expect(ownable.as('OWNER').renounceOwnership()).rejects.toThrow(
await expect(ownable.as(OWNER).renounceOwnership()).rejects.toThrow(
'ZOwnablePK: caller is not the owner',
);
});

it('should fail when renouncing from unauthorized with bad nonce', async () => {
await ownable.privateState.injectSecretNonce(BAD_NONCE);
await expect(
ownable.as('UNAUTHORIZED').renounceOwnership(),
ownable.as(UNAUTHORIZED).renounceOwnership(),
).rejects.toThrow('ZOwnablePK: caller is not the owner');
});
});
Expand All @@ -318,7 +337,7 @@ describe('ZOwnablePK', () => {
secretNonce,
);

await ownable.as('OWNER').assertOnlyOwner();
await ownable.as(OWNER).assertOnlyOwner();
});

it('should fail when the authorized caller has the wrong nonce', async () => {
Expand All @@ -331,7 +350,7 @@ describe('ZOwnablePK', () => {
);

// Set caller and call circuit
await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow(
await expect(ownable.as(OWNER).assertOnlyOwner()).rejects.toThrow(
'ZOwnablePK: caller is not the owner',
);
});
Expand All @@ -343,7 +362,7 @@ describe('ZOwnablePK', () => {
);

await expect(
ownable.as('UNAUTHORIZED').assertOnlyOwner(),
ownable.as(UNAUTHORIZED).assertOnlyOwner(),
).rejects.toThrow('ZOwnablePK: caller is not the owner');
});

Expand All @@ -358,7 +377,7 @@ describe('ZOwnablePK', () => {

// Set unauthorized caller and call circuit
await expect(
ownable.as('UNAUTHORIZED').assertOnlyOwner(),
ownable.as(UNAUTHORIZED).assertOnlyOwner(),
).rejects.toThrow('ZOwnablePK: caller is not the owner');
});
});
Expand Down Expand Up @@ -495,9 +514,9 @@ describe('ZOwnablePK', () => {

it('should allow anyone to transfer', async () => {
const id = createIdHash(Z_OWNER, secretNonce);
await ownable.as('OWNER')._transferOwnership(id);
await ownable.as(OWNER)._transferOwnership(id);

await ownable.as('UNAUTHORIZED')._transferOwnership(id);
await ownable.as(UNAUTHORIZED)._transferOwnership(id);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions contracts/src/multisig/test/Forwarder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import { MockForwarderShieldedSimulator } from './simulators/MockForwarderShieldedSimulator.js';
import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnshieldedSimulator.js';

Expand All @@ -22,7 +22,7 @@ import { MockForwarderUnshieldedSimulator } from './simulators/MockForwarderUnsh
// 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_PARENT = shieldedTestKey().left;
const SHIELDED_ZERO = utils.ZERO_KEY.left;
const UNSHIELDED_PARENT = utils.createEitherTestUserAddress('PARENT').right;
const UNSHIELDED_ZERO = utils.ZERO_USER_ADDRESS.right;
Expand Down
4 changes: 2 additions & 2 deletions contracts/src/multisig/test/ForwarderPrivate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import {
bytesToHex,
isNonceSpent,
Expand All @@ -31,7 +31,7 @@ import { MockForwarderPrivateSimulator } from './simulators/MockForwarderPrivate
// none). Published by the live setup, so safe to read at module scope. `WRONG`
// stays a synthetic key: it only exercises the commitment gate, which rejects it
// before any send.
const PARENT_BYTES = shieldedTestParentKey().bytes;
const PARENT_BYTES = shieldedTestKey().left.bytes;
const WRONG_BYTES = utils.createEitherTestUser('WRONG').left.bytes;
const OP_SECRET = new Uint8Array(32).fill(0xaa);
const WRONG_OP_SECRET = new Uint8Array(32).fill(0xbb);
Expand Down
23 changes: 10 additions & 13 deletions contracts/src/multisig/test/ShieldedMultiSig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import {
shieldedTestParentKey,
shieldedTestSigner,
} from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import { ShieldedMultiSigSimulator } from './simulators/ShieldedMultiSigSimulator.js';

const ProposalStatus = { Inactive: 0, Active: 1, Executed: 2, Cancelled: 3 };
Expand All @@ -24,16 +21,16 @@ const PROPOSAL_AMOUNT = 400n;
// `ownPublicKey()` matches — the only way to exercise multi-signer authorization
// on a real node. On dry these are the deterministic synthetic keys `.as(...)`
// resolves to. `OTHER` is not a pooled wallet, so it acts as a non-signer.
const Z_SIGNER1 = shieldedTestSigner('SIGNER1');
const Z_SIGNER2 = shieldedTestSigner('SIGNER2');
const Z_SIGNER3 = shieldedTestSigner('SIGNER3');
const Z_SIGNER1 = shieldedTestKey('SIGNER1');
const Z_SIGNER2 = shieldedTestKey('SIGNER2');
const Z_SIGNER3 = shieldedTestKey('SIGNER3');
const SIGNERS = [Z_SIGNER1, Z_SIGNER2, Z_SIGNER3];

const Z_NON_SIGNER = shieldedTestSigner('OTHER');
// Proposal payout recipient. On live `executeShieldedProposal` sends the treasury
// coins here, so it must be a node-resolvable key (the deployer's own); on dry a
// synthetic key.
const Z_RECIPIENT_PK = shieldedTestParentKey('RECIPIENT');
const Z_NON_SIGNER = shieldedTestKey('OTHER');
// Proposal payout recipient (bare coin public key). On live
// `executeShieldedProposal` sends the treasury coins here, so it must be a
// node-resolvable key (the deployer's own); on dry a synthetic key.
const Z_RECIPIENT_PK = shieldedTestKey().left;

function makeRecipient(pk: { bytes: Uint8Array }): {
kind: number;
Expand Down Expand Up @@ -172,7 +169,7 @@ describe('ShieldedMultiSig', () => {
// witness the dry sim sets from `.as('SIGNER1')`). On live each alias is
// backed by its own prefunded wallet (the harness pool), so `.as('SIGNER1')`
// submits from that wallet and `ownPublicKey()` is that signer's key — the
// synthetic set is replaced by the pooled wallets' keys (see `shieldedTestSigner`),
// synthetic set is replaced by the pooled wallets' keys (see `shieldedTestKey`),
// so these run on both backends.
describe('caller-gated proposal flows', () => {
describe('createShieldedProposal', () => {
Expand Down
6 changes: 3 additions & 3 deletions contracts/src/multisig/test/ShieldedMultiSigV3.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isLiveBackend } from '@openzeppelin/compact-simulator';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import * as utils from '#test-utils/fixtures/address.js';
import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import {
calculateSignerId,
ShieldedMultiSigV3Simulator,
Expand Down Expand Up @@ -39,7 +39,7 @@ const CONTRACT_RECIPIENT = utils.createEitherTestContractAddress('TARGET');
// live it resolves to the deployer's own coin public key (whose encryption key
// the node can resolve), so the minted coin is deliverable; dry → a synthetic
// user.
let USER_RECIPIENT: ReturnType<typeof shieldedTestRecipient>;
let USER_RECIPIENT: ReturnType<typeof shieldedTestKey>;

function makeQualifiedCoin(
color: Uint8Array,
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('ShieldedMultiSigV3', () => {
// mutating groups below build their own fresh instance per test.
beforeAll(async () => {
multisig = await freshMultisig();
USER_RECIPIENT = shieldedTestRecipient();
USER_RECIPIENT = shieldedTestKey();
});

describe('view', () => {
Expand Down
6 changes: 3 additions & 3 deletions contracts/src/multisig/test/ShieldedTreasury.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import {
bytesToHex,
isNonceSpent,
Expand All @@ -30,7 +30,7 @@ const TREASURY_ADDRESS = '7a'.repeat(32);
// Assigned in `beforeAll` after `create()` syncs the wallet: on live the harness
// then publishes MIDNIGHT_DEPLOYER_COIN_PK, so this resolves to the deployer's own
// coin public key (an encryption key the node can resolve); dry → a synthetic user.
let Z_RECIPIENT: ReturnType<typeof shieldedTestRecipient>;
let Z_RECIPIENT: ReturnType<typeof shieldedTestKey>;

// Delegates to the backend-aware builder: on live every coin gets a fresh random
// nonce (the local node persists nullifiers across runs, so a fixed nonce would
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('ShieldedTreasury', () => {
// reuses this shared deploy.
beforeAll(async () => {
treasury = await freshTreasury();
Z_RECIPIENT = shieldedTestRecipient();
Z_RECIPIENT = shieldedTestKey();
});

describe('initial state', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
encodeShieldedCoinInfo,
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestRecipient } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import {
bytesToHex,
isNonceSpent,
Expand All @@ -31,7 +31,7 @@ const TREASURY_ADDRESS = '5c'.repeat(32);
// Assigned in `beforeEach` after `create()` syncs the wallet: on live this
// resolves to the deployer's own coin public key (an encryption key the node can
// resolve); dry → a synthetic user.
let Z_RECIPIENT: ReturnType<typeof shieldedTestRecipient>;
let Z_RECIPIENT: ReturnType<typeof shieldedTestKey>;

// Delegates to the backend-aware builder: live gets a fresh random nonce per run
// (the local node persists nullifiers, so a fixed nonce would replay a spent
Expand All @@ -51,7 +51,7 @@ describe('ShieldedTreasuryStateless', () => {
treasury = await MockShieldedTreasuryStatelessSimulator.create({
contractAddress: TREASURY_ADDRESS,
});
Z_RECIPIENT = shieldedTestRecipient();
Z_RECIPIENT = shieldedTestKey();
const deposited = makeCoin(COLOR, AMOUNT);
await treasury._deposit(deposited);
coin = await getQualifiedShieldedCoinInfo(
Expand Down Expand Up @@ -257,7 +257,7 @@ describe('ShieldedTreasuryStateless', () => {
'_send — implementing contract routes the change onward on live',
() => {
it('should let the caller send the change to a different recipient using the send result', async () => {
const changeDest = shieldedTestRecipient();
const changeDest = shieldedTestKey();
const routed = await treasury._sendAndRouteChange(
coin,
Z_RECIPIENT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
GENESIS_NATIVE_SHIELDED_TOKEN_COLORS,
encodeShieldedCoinInfo as makeCoin,
} from '#test-utils/fixtures/nativeShieldedToken.js';
import { shieldedTestParentKey } from '#test-utils/fixtures/shieldedKey.js';
import { shieldedTestKey } from '#test-utils/fixtures/shieldedKey.js';
import {
contractOwner,
getQualifiedShieldedCoinInfo,
Expand All @@ -14,7 +14,7 @@ import { ForwarderPrivateSimulator } from '../simulators/presets/ForwarderPrivat
// over its raw 32 bytes (`calculateParentCommitment(parent.bytes, opSecret)`).
// On live it is the deployer's own key (the drain sends the note to it, so its
// encryption key must resolve on-chain).
const PARENT_BYTES = shieldedTestParentKey().bytes;
const PARENT_BYTES = shieldedTestKey().left.bytes;
const OP_SECRET = new Uint8Array(32).fill(0xaa);
// A shielded token type the deployer wallet holds on live (genesis-minted).
const COLOR = GENESIS_NATIVE_SHIELDED_TOKEN_COLORS.nativeShieldedToken1;
Expand Down
Loading