Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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
81 changes: 80 additions & 1 deletion src/testers/witnessTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,85 @@ 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 varsById = Object.entries(this.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 fieldSize = this.circomTester.witnessCalculator.prime;

// @ts-ignore
const parsedConstraints = this.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 = 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 % coeff);
// XXX: Why within 10000?
if(modP !== BigInt(0) && modP <= BigInt(10000)) {
coeff = `(ρ-${fieldSize % coeff})/${fieldSize/coeff}`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changes some hard-to-read numbers into more-readable:

(-1 + (-667976161860329444038281425331337740739391003430665110586493047686029312 * y) + (-84832972556261839392861741017079893073902657435694469044484617056125722624 * x) + (85500948718122168836900022442411230814642048439125134155071110103811751936 * ltQ.n2b.out[0]) + (171001897436244337673800044884822461629284096878250268310142220207623503872 * ltQ.n2b.out[1]) + (342003794872488675347600089769644923258568193756500536620284440415247007744 * ltQ.n2b.out[2]) + (684007589744977350695200179539289846517136387513001073240568880830494015488 * ltQ.n2b.out[3]) + (1368015179489954701390400359078579693034272775026002146481137761660988030976 * ltQ.n2b.out[4]) + (2736030358979909402780800718157159386068545550052004292962275523321976061952 * ltQ.n2b.out[5]) + (5472060717959818805561601436314318772137091100104008585924551046643952123904 * ltQ.n2b.out[6]) + (10944121435919637611123202872628637544274182200208017171849102093287904247808 * ltQ.n2b.out[7])) * ((-667976161860329444038281425331337740739391003430665110586493047686029312 * y) + (-84832972556261839392861741017079893073902657435694469044484617056125722624 * x) + (85500948718122168836900022442411230814642048439125134155071110103811751936 * ltQ.n2b.out[0]) + (171001897436244337673800044884822461629284096878250268310142220207623503872 * ltQ.n2b.out[1]) + (342003794872488675347600089769644923258568193756500536620284440415247007744 * ltQ.n2b.out[2]) + (684007589744977350695200179539289846517136387513001073240568880830494015488 * ltQ.n2b.out[3]) + (1368015179489954701390400359078579693034272775026002146481137761660988030976 * ltQ.n2b.out[4]) + (2736030358979909402780800718157159386068545550052004292962275523321976061952 * ltQ.n2b.out[5]) + (5472060717959818805561601436314318772137091100104008585924551046643952123904 * ltQ.n2b.out[6]) + (10944121435919637611123202872628637544274182200208017171849102093287904247808 * ltQ.n2b.out[7])) = 0

becomes:

(-1 + ((ρ-1)/-32768 * y) + (-84832972556261839392861741017079893073902657435694469044484617056125722624 * x) + ((ρ-1)/256 * ltQ.n2b.out[0]) + ((ρ-1)/128 * ltQ.n2b.out[1]) + ((ρ-1)/64 * ltQ.n2b.out[2]) + ((ρ-1)/32 * ltQ.n2b.out[3]) + ((ρ-1)/16 * ltQ.n2b.out[4]) + ((ρ-1)/8 * ltQ.n2b.out[5]) + ((ρ-1)/4 * ltQ.n2b.out[6]) + ((ρ-1)/2 * ltQ.n2b.out[7])) * (((ρ-1)/-32768 * y) + (-84832972556261839392861741017079893073902657435694469044484617056125722624 * x) + ((ρ-1)/256 * ltQ.n2b.out[0]) + ((ρ-1)/128 * ltQ.n2b.out[1]) + ((ρ-1)/64 * ltQ.n2b.out[2]) + ((ρ-1)/32 * ltQ.n2b.out[3]) + ((ρ-1)/16 * ltQ.n2b.out[4]) + ((ρ-1)/8 * ltQ.n2b.out[5]) + ((ρ-1)/4 * ltQ.n2b.out[6]) + ((ρ-1)/2 * ltQ.n2b.out[7])) = 0

}
// @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;
}

/**
* 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 +302,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