Skip to content
Merged
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
26 changes: 26 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ maci registry list

# Compare on-chain vkeys against the bundled registry
maci registry check dora1abc...xyz

# Launch the local web UI (visual verification dashboard)
maci ui
```

## Commands
Expand Down Expand Up @@ -147,6 +150,27 @@ maci round dora1abc...xyz --network testnet --recheck

---

### `maci ui`

Starts a local web UI for round verification and registry browsing — the same checks as `maci round` and `maci registry`, rendered as a visual dashboard with live progress.

```bash
maci ui # serve on http://127.0.0.1:7766 and open the browser
maci ui --port 8080 # custom port (auto-increments if busy)
maci ui --no-open # don't open the browser automatically
```

Features:

- Run Layer 1 / Layer 2 round verification with real-time step progress (streamed via SSE)
- Structured verification report: per-check PASS/FAIL, commitment comparisons, result banner
- Export the full verification result as JSON
- Browse bundled circuits (parameters, vkey fingerprints, zkey links) and check a contract's on-chain vkeys against the registry

The server binds to `127.0.0.1` only and performs the same read-only queries as the CLI commands — no private keys, no transactions.

---

## Options Reference

| Option | Commands | Default | Description |
Expand All @@ -155,6 +179,8 @@ maci round dora1abc...xyz --network testnet --recheck
| `--rpc` | `round` | *(see Network Defaults)* | Override the CosmWasm RPC endpoint |
| `--indexer` | `round` | *(see Network Defaults)* | Override the MACI GraphQL indexer endpoint |
| `--recheck` | `round` | `false` | Enable Layer 2 local ZK re-verification |
| `--port` | `ui` | `7766` | Port for the local web UI (auto-increments if busy) |
| `--no-open` | `ui` | — | Don't open the browser automatically |

## Verification Levels

Expand Down
100 changes: 12 additions & 88 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,14 @@
import type { CommandModule, Argv, ArgumentsCamelCase } from 'yargs';
import { getOnChainVkeys, type Groth16VkeyOnChain } from '../core/chain.js';
import {
resolveEndpoints,
NETWORK_CHOICES,
type NetworkName,
} from '../core/network.js';
import {
AMACI_CIRCUITS,
type AmaciCircuitEntry,
type AmaciVkeySet,
} from '../core/circuits.js';
import { NETWORK_CHOICES, type NetworkName } from '../core/network.js';
import { AMACI_CIRCUITS } from '../core/circuits.js';
import { runRegistryCheck } from '../core/pipeline.js';
import {
printRegistryList,
printCircuitDetail,
printVkeyCheckResult,
printError,
} from '../core/report.js';

// ─── Helpers ────────────────────────────────────────────────────────────────

function vkeysMatch(onChain: Groth16VkeyOnChain, registered: AmaciVkeySet): boolean {
const fields = [
'vk_alpha1',
'vk_beta_2',
'vk_gamma_2',
'vk_delta_2',
'vk_ic0',
'vk_ic1',
] as const;
return fields.every((f) => onChain[f] === registered[f]);
}

type VkeyMatchResult = {
power: string;
entry: AmaciCircuitEntry;
processMatch: boolean;
tallyMatch: boolean;
deactivateMatch: boolean | null;
addNewKeyMatch: boolean | null;
};

/**
* Scan all known aMACI circuits to find one whose vkeys match the on-chain values.
*/
function findMatchingCircuit(
processVkey: Groth16VkeyOnChain,
tallyVkey: Groth16VkeyOnChain,
deactivateVkey: Groth16VkeyOnChain | undefined,
addNewKeyVkey: Groth16VkeyOnChain | undefined
): VkeyMatchResult | null {
for (const [power, entry] of Object.entries(AMACI_CIRCUITS)) {
const pm = vkeysMatch(processVkey, entry.vkeys.process);
const tm = vkeysMatch(tallyVkey, entry.vkeys.tally);
if (pm || tm) {
const dm = deactivateVkey !== undefined
? vkeysMatch(deactivateVkey, entry.vkeys.deactivate)
: null;
const am = addNewKeyVkey !== undefined
? vkeysMatch(addNewKeyVkey, entry.vkeys.addNewKey)
: null;
return { power, entry, processMatch: pm, tallyMatch: tm, deactivateMatch: dm, addNewKeyMatch: am };
}
}
return null;
}

// ─── Subcommand handlers ─────────────────────────────────────────────────────

function handleList() {
Expand Down Expand Up @@ -92,40 +36,20 @@ function handleShow(args: ArgumentsCamelCase<{ power: string }>) {
async function handleCheck(
args: ArgumentsCamelCase<{ contract: string; network: NetworkName; rpc?: string }>
) {
const { rpc } = resolveEndpoints(args.network, { rpc: args.rpc });
try {
const vkeys = await getOnChainVkeys(rpc, args.contract);
const match = findMatchingCircuit(
vkeys.processVkey,
vkeys.tallyVkey,
vkeys.deactivateVkey,
vkeys.addNewKeyVkey
);

if (!match) {
printVkeyCheckResult({
power: 'UNKNOWN',
source: 'not found in registry',
production: false,
processMatch: null,
tallyMatch: null,
deactivateMatch: null,
addNewKeyMatch: null,
});
process.exit(1);
}
const result = await runRegistryCheck(args.contract, args.network, args.rpc);

printVkeyCheckResult({
power: match.power,
source: match.entry.source,
production: match.entry.production,
processMatch: match.processMatch,
tallyMatch: match.tallyMatch,
deactivateMatch: match.deactivateMatch,
addNewKeyMatch: match.addNewKeyMatch,
power: result.power,
source: result.source,
production: result.production,
processMatch: result.processMatch,
tallyMatch: result.tallyMatch,
deactivateMatch: result.deactivateMatch,
addNewKeyMatch: result.addNewKeyMatch,
});

if (!match.processMatch || !match.tallyMatch) {
if (!result.passed) {
process.exit(1);
}
} catch (err) {
Expand Down
74 changes: 74 additions & 0 deletions packages/cli/src/commands/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { spawn } from 'node:child_process';
import type { CommandModule, Argv, ArgumentsCamelCase } from 'yargs';
import chalk from 'chalk';
import { startUiServer } from '../ui/server.js';
import { printError } from '../core/report.js';

const DEFAULT_PORT = 7766;

/** Open a URL in the default browser without extra dependencies. */
function openBrowser(url: string) {
const platform = process.platform;
let cmd: string;
let args: string[];

if (platform === 'darwin') {
cmd = 'open';
args = [url];
} else if (platform === 'win32') {
// `start` is a cmd built-in; empty title arg required when URL is quoted
cmd = 'cmd';
args = ['/c', 'start', '', url];
} else {
cmd = 'xdg-open';
args = [url];
}

const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
child.on('error', () => {
// Non-fatal: user can open the printed URL manually
});
child.unref();
}

async function handleUi(args: ArgumentsCamelCase<{ port: number; open: boolean }>) {
try {
const { port, url } = await startUiServer(args.port);

console.log('');
console.log(` ${chalk.bold('maci ui')} — local verification dashboard`);
console.log('');
console.log(` Serving on ${chalk.cyan(url)}`);
if (port !== args.port) {
console.log(chalk.dim(` (port ${args.port} was busy, using ${port})`));
}
console.log(chalk.dim(' Read-only: no keys, no signing. Press Ctrl+C to stop.'));
console.log('');

if (args.open) {
openBrowser(url);
}
} catch (err) {
printError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
}

export const uiCommand: CommandModule = {
command: 'ui',
describe: 'Start a local web UI for round verification and registry browsing',
builder: (yargs: Argv) =>
yargs
.option('port', {
alias: 'p',
type: 'number',
description: 'Port to listen on (auto-increments if busy)',
default: DEFAULT_PORT,
})
.option('open', {
type: 'boolean',
description: 'Open the browser automatically (use --no-open to disable)',
default: true,
}),
handler: (args) => handleUi(args as ArgumentsCamelCase<{ port: number; open: boolean }>),
};
Loading
Loading