8000 Human-readable constraint display by numtel · Pull Request #105 · erhant/circomkit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Human-readable constraint display #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ npx circomkit vkey <circuit> [pkey-path]

# Automatically download PTAU (for BN128)
npx circomkit ptau <circuit>

# Display circuit info (e.g. constraint count)
npx circomkit info <circuit>

# Display human-readable constraint formulas
npx circomkit constraints <circuit>
```

> [!NOTE]
Expand Down Expand Up @@ -207,6 +213,17 @@ it('should have correct number of constraints', async () => {
});
```

> <picture>
> <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/light-theme/tip.svg">
> <img alt="Warning" src="https://raw.githubusercontent.com/Mqxx/GitHub-Markdown/main/blockquotes/badge/dark-theme/tip.svg">
> </picture><br>
>
> You can also generate an array of human-readable formulas for each constraint using `parseConstraints`:
>
> ```ts
> console.log((await circuit.parseConstraints()).join('\n'));
> ```

If you want more control over the output signals, you can use the `compute` function. It takes in an input, and an array of output signal names used in the `main` component so that they can be extracted from the witness.

```ts
Expand Down
10 changes: 10 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ function cli(args: string[]) {
console.log(`Number of Labels: ${info.labels}`);
});

///////////////////////////////////////////////////////////////////////////////
const constraints = new Command('constraints')
.description('print constraint formulas')
.argument('<circuit>', 'Circuit name')
.action(async circuit => {
const formulas = await circomkit.constraints(circuit);
console.log(formulas.join('\n'));
});

///////////////////////////////////////////////////////////////////////////////
const clear = new Command('clear')
.description('clear circuit build artifacts')
Expand Down Expand Up @@ -232,6 +241,7 @@ function cli(args: string[]) {
.addCommand(circuit)
.addCommand(instantiate)
.addCommand(info)
.addCommand(constraints)
.addCommand(clear)
.addCommand(contract)
.addCommand(vkey)
Expand Down
22 changes: 21 additions & 1 deletion src/core/index.ts
8000
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ import type {CircuitConfig, CircuitSignals, CircomTester} from '../types';
import {WitnessTester, ProofTester} from '../testers';
import {prettyStringify} from '../utils';
import {CircomkitConfig, DEFAULT, PRIMES, PROTOCOLS} from '../configs';
import {compileCircuit, instantiateCircuit, readR1CSInfo, getCalldata} from '../functions';
import {
compileCircuit,
instantiateCircuit,
readR1CSInfo,
getCalldata,
parseConstraints,
readSymbols,
stringifyBigIntsWithField,
} from '../functions';
import {CircomkitPath} from './pathing';

/**
Expand Down Expand Up @@ -132,6 +140,18 @@ export class Circomkit {
return await readR1CSInfo(this.path.ofCircuit(circuit, 'r1cs'));
}

/** Returns human-readable constraint formula array. */
async constraints(circuit: string) {
const r1csPath = this.path.ofCircuit(circuit, 'r1cs');
const symPath = this.path.ofCircuit(circuit, 'sym');
// @ts-ignore
const r1csFile = await import('r1csfile');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this dependency is loaded synchronously at the top of the file, when I use the witnessTester to parse the constraints, I get this error:


(node:1210393) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)

 Exception during run: /home/ben/circomkit/node_modules/web-worker/node.js:17
import URL from 'url';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (node:internal/modules/cjs/loader:1281:20)
    at Module._compile (node:internal/modules/cjs/loader:1321:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
    at Module.load (node:internal/modules/cjs/loader:1208:32)
    at Module._load (node:internal/modules/cjs/loader:1024:12)
    at cjsLoader (node:internal/modules/esm/translators:348:17)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:297:7)
    at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    at async formattedImport (/home/ben/ntru-circom/node_modules/mocha/lib/nodejs/esm-utils.js:9:14)
    at async exports.requireOrImport (/home/ben/ntru-circom/node_modules/mocha/lib/nodejs/esm-utils.js:42:28)
    at async exports.loadFilesAsync (/home/ben/ntru-circom/node_modules/mocha/lib/nodejs/esm-utils.js:100:20)
    at async singleRun (/home/ben/ntru-circom/node_modules/mocha/lib/cli/run-helpers.js:162:3)
    at async exports.handler (/home/ben/ntru-circom/node_modules/mocha/lib/cli/run.js:375:5)

With the dynamic import, I can both print constraints through the witness tester and the cli without error.

const r1cs = await r1csFile.readR1cs(r1csPath, true, true, true);
const sym = await readSymbols(symPath);
const constraints = stringifyBigIntsWithField(r1cs.curve.Fr, r1cs.constraints);
return parseConstraints(constraints, sym, r1cs.prime);
}

/** Downloads the phase-1 setup PTAU file for a circuit based on it's number of constraints.
*
* The downloaded PTAU files can be seen at [SnarkJS docs](https://github.com/iden3/snarkjs#7-prepare-phase-2).
Expand Down
117 changes: 117 additions & 0 deletions src/functions/r1cs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {ReadPosition, openSync, readSync} from 'fs';
// @ts-ignore
import * as fastFile from "fastfile";
import type {SymbolsType} from '../types/';
import {primeToName} from '../utils';

/**
Expand Down Expand Up @@ -100,6 +103,98 @@ export async function readR1CSInfo(r1csPath: string): Promise<R1CSInfoType> {
return r1csInfoType;
}

export async function readSymbols(symFileName: string): Promise<SymbolsType> {
const fd = await fastFile.readExisting(symFileName);
const buff = await fd.read(fd.totalSize);
await fd.close();
const symsStr = new TextDecoder("utf-8").decode(buff);
const lines = symsStr.split("\n");
const out = {};
for (let i=0; i<lines.length; i++) {
const arr = lines[i].split(",");
if (arr.length!=4) continue;
// @ts-ignore
out[arr[3]] = {
labelIdx: Number(arr[0]),
varIdx: Number(arr[1]),
componentIdx: Number(arr[2]),
};
}
return out;
}

// @ts-ignore
export function parseConstraints(constraints, symbols, fieldSize): Promise<string[]> {
// @ts-ignore
const varsById = Object.entries(symbols).reduce((out, cur) => {
// @ts-ignore
const id = cur[1].varIdx;
if(id !== -1) {
// @ts-ignore
out[id] = cur[0].slice(5); // Remove main.
}
return out;
}, {});

// @ts-ignore
const parsedConstraints = constraints.map(constraint => {
// Every constraint is 3 groups: <1> * <2> - <3> = 0
// @ts-ignore
const groups = constraint.map(item => {
// Each group can contain many signals (with coefficients) summed
const vars = Object.keys(item).reduce((out, cur) => {
// @ts-ignore
const coeffRaw = BigInt(item[cur]);
// Display the coefficient as a signed value, helps a lot with -1
let coeff = coeffRaw > fieldSize / BigInt(2) ? coeffRaw - fieldSize : coeffRaw;
// Reduce numbers that are factors of the field size for better readability
// @ts-ignore
const modP = BigInt(fieldSize) % BigInt(coeff);
// XXX: Why within 10000?
if(modP !== BigInt(0) && modP <= BigInt(10000)) {
// @ts-ignore
coeff = `(p-${fieldSize % coeff})/${fieldSize/coeff}`;
}
// @ts-ignore
const varName = varsById[cur];
out.push(
// @ts-ignore
coeff === BigInt(-1) && varName ? '-' + varName :
coeff === BigInt(1) && varName ? varName :
!varName ? `${coeff}` :
`(${coeff} * ${varName})`,
);
return out;
}, []);

// Combine all the signals into one statement
return vars.reduce((out, cur, index) =>
// @ts-ignore
out + (index > 0 ? cur.startsWith('-') ? ` - ${cur.slice(1)}` : ` + ${cur}` : cur),
'');
})
// @ts-ignore
.map(curVar => curVar.indexOf(' ') === -1 ? curVar : `(${curVar})`);

return (
groups[0] +
(groups[1] ? ' * ' + groups[1] : '') +
(groups[2] ?
groups[2].startsWith('-') ?
` + ${groups[2].slice(1)}`
: groups[0] || groups[1] ?
' - ' + groups[2]
: groups[2].startsWith('(') ?
groups[2].slice(1, -1)
: groups[2]
: '') +
' = 0'
);
});
// @ts-ignore
return parsedConstraints;
}

/** Reads a specified number of bytes from a file and converts them to a `BigInt`.
*
* @param offset The position in `buffer` to write the data to.
Expand All @@ -113,3 +208,25 @@ export function readBytesFromFile(fd: number, offset: number, length: number, po

return BigInt(`0x${buffer.reverse().toString('hex')}`);
}

// From Snarkjs
// @ts-ignore
export function stringifyBigIntsWithField(Fr, o) {
if (o instanceof Uint8Array) {
return Fr.toString(o);
} else if (Array.isArray(o)) {
return o.map(stringifyBigIntsWithField.bind(null, Fr));
} else if (typeof o == "object") {
const res = {};
const keys = Object.keys(o);
keys.forEach( (k) => {
// @ts-ignore
res[k] = stringifyBigIntsWithField(Fr, o[k]);
});
return res;
} else if ((typeof(o) == "bigint") || o.eq !== undefined) {
return o.toString(10);
} else {
return o;
}
}
14 changes: 13 additions & 1 deletion src/testers/witnessTester.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {AssertionError} from 'node:assert';
import type {CircomTester, WitnessType, CircuitSignals, SymbolsType, SignalValueType} from '../types/';
import {parseConstraints} from '../functions/';

// @todo detect optimized symbols https://github.com/erhant/circomkit/issues/80

Expand Down Expand Up @@ -138,6 +139,17 @@ export class WitnessTester<IN extends readonly string[] = [], OUT extends readon
return await this.readWitnessSignals(witness, signals);
}

/**
* Parse R1CS constraints into human-readable formulas.
*/
async parseConstraints(): Promise<string[]> {
await this.loadConstraints();
await this.loadSymbols();
// @ts-ignore
const fieldSize = this.circomTester.witnessCalculator.prime;
return parseConstraints(this.constraints, this.symbols, fieldSize);
}

/**
* Override witness value to try and fake a proof. If the circuit has soundness problems (i.e.
* some signals are not constrained correctly), then you may be able to create a fake witness by
Expand Down Expand Up @@ -223,7 +235,7 @@ export class WitnessTester<IN extends readonly string[] = [], OUT extends readon
const signalDotCount = dotCount(signal) + 1; // +1 for the dot in `main.`
const signalLength = signal.length + 5; // +5 for prefix `main.`
const symbolNames = Object.keys(this.symbols!).filter(
s => s.startsWith(`main.${signal}`) && signalDotCount === dotCount(s)
s => s.match(new RegExp(`^main\\.${signal}(\\[|$|\\.)`)) && signalDotCount === dotCount(s)
);

// get the symbol values from symbol names, ignoring `main.` prefix
Expand Down
14 changes: 14 additions & 0 deletions tests/common/circuits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type PreparedTestCircuit<S extends Record<string, CircuitSignals>> = {
signals: S;
/** Name for the input path to an existing input JSON file under `inputs` folder. */
inputName: string;
/** To compare against generated value. */
parsedConstraints: string[];
/** Circuit information. */
circuit: {
/** Circuit name. */
Expand Down Expand Up @@ -45,6 +47,17 @@ export function prepareMultiplier(N: number, order: bigint = primes['bn128']) {
// TOTAL: 3*N - 1
const size = 3 * N - 1;

let parsedConstraints;
if(N >= 3) {
parsedConstraints = [
'-in[0] * in[1] + inner[0] = 0',
...Array(N-3).fill(0).map((_, i) => `-inner[${i}] * in[${i+2}] + inner[${i+1}] = 0`),
`-inner[${N-3}] * in[${N-1}] + out = 0`,
...Array(N).fill(0).map((_, i) => `isZero[${i}].in * isZero[${i}].inv - 1 = 0`),
...Array(N).fill(0).map((_, i) => `-1 + in[${i}] - isZero[${i}].in = 0`),
];
}

const numbers: bigint[] = Array.from({length: N}, () => BigInt(2) + BigInt('0x' + randomBytes(8).toString('hex')));
const product: bigint = numbers.reduce((prev, acc) => acc * prev) % order;
const malicious: bigint[] = Array.from({length: N}, () => BigInt(1));
Expand All @@ -64,5 +77,6 @@ export function prepareMultiplier(N: number, order: bigint = primes['bn128']) {
signals,
circuit: {name, config, size, exact: true},
inputName: 'input.test',
parsedConstraints,
} as PreparedTestCircuit<typeof signals>;
}
5 changes: 5 additions & 0 deletions tests/witnessTester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('witness tester', () => {
const {
circuit: {name, config, size, exact},
signals,
parsedConstraints,
} = prepareMultiplier(4);

beforeAll(async () => {
Expand All @@ -31,6 +32,10 @@ describe('witness tester', () => {
// should also work for non-exact too, where we expect at least some amount
await circuit.expectConstraintCount(size!);
await circuit.expectConstraintCount(size! - 1);

const myParsedConstraints = await circuit.parseConstraints();
expect(myParsedConstraints).toStrictEqual(parsedConstraints);

});

it('should assert correctly', async () => {
Expand Down
0