Skip to content

Commit ede2a08

Browse files
committed
feat: add proof mode cell checks
1 parent 39c3b1a commit ede2a08

File tree

4 files changed

+208
-39
lines changed

4 files changed

+208
-39
lines changed

src/builtins/keccak.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ export const CELLS_PER_KECCAK = 16;
1717
/** Number of input cells for a keccak operation */
1818
export const INPUT_CELLS_PER_KECCAK = 8;
1919

20+
/**
21+
* The diluted cells are:
22+
* - state - 25 rounds times 1600 elements.
23+
* - parity - 24 rounds times 1600/5 elements times 3 auxiliaries.
24+
* - after_theta_rho_pi - 24 rounds times 1600 elements.
25+
* - theta_aux - 24 rounds times 1600 elements.
26+
* - chi_iota_aux - 24 rounds times 1600 elements times 2 auxiliaries.
27+
*
28+
* In total 25 * 1600 + 24 * 320 * 3 + 24 * 1600 + 24 * 1600 + 24 * 1600 * 2 = 216640.
29+
*
30+
* But we actually allocate 4 virtual columns, of dimensions 64 * 1024, in which we embed the
31+
* real cells, and we don't free the unused ones.
32+
*
33+
* So the real number is 4 * 64 * 1024 = 262144.
34+
*/
35+
export const KECCAK_DILUTED_CELLS = 262144;
36+
2037
/**
2138
* Compute the new state of the keccak-f1600 block permutation on 24 rounds
2239
*

src/runners/cairoRunner.ts

Lines changed: 175 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,27 @@ import {
1111
} from 'errors/cairoRunner';
1212

1313
import { Felt } from 'primitives/felt';
14-
import { SegmentValue } from 'primitives/segmentValue';
14+
import { isFelt, SegmentValue } from 'primitives/segmentValue';
1515
import { Relocatable } from 'primitives/relocatable';
1616
import { CairoProgram, CairoZeroProgram, Program } from 'vm/program';
17-
import { VirtualMachine } from 'vm/virtualMachine';
17+
import { RcLimits, VirtualMachine } from 'vm/virtualMachine';
1818
import { CELLS_PER_INSTANCE, getBuiltin } from 'builtins/builtin';
1919
import { Hint, Hints } from 'hints/hintSchema';
20-
import { isSubsequence, Layout, layouts } from './layout';
20+
import {
21+
isSubsequence,
22+
Layout,
23+
layouts,
24+
MEMORY_UNITS_PER_STEP,
25+
} from './layout';
2126
import { nextPowerOfTwo } from 'primitives/utils';
27+
import { ExpectedFelt } from 'errors/primitives';
28+
import {
29+
INNER_RC_BOUND_MASK,
30+
INNER_RC_BOUND_SHIFT,
31+
RC_N_PARTS,
32+
RC_N_PARTS_96,
33+
} from 'builtins/rangeCheck';
34+
import { KECCAK_DILUTED_CELLS } from 'builtins/keccak';
2235

2336
/**
2437
* Configuration of the run
@@ -36,6 +49,8 @@ export enum RunnerMode {
3649
ProofModeCairo = 'Proof Mode - Cairo',
3750
}
3851

52+
const MISSING_STEPS_CAPACITY = -1;
53+
3954
export class CairoRunner {
4055
program: Program;
4156
layout: Layout;
@@ -259,45 +274,15 @@ export class CairoRunner {
259274
/**
260275
* @returns {boolean} Whether there are enough allocated cells for
261276
* generating a proof of this program execution for the chosen layout.
262-
* @throws {} - If the number of allocated cells of the layout is insufficient.
277+
* @throws {InsufficientAllocatedCells} - If the number of allocated cells of the layout is insufficient.
263278
*
264279
*/
265280
checkCellUsage(): boolean {
266281
const builtinChecks = this.builtins
267-
.filter(
268-
(builtin) =>
269-
!['output', 'segment_arena', 'gas_builtin', 'system'].includes(
270-
builtin
271-
)
272-
)
282+
.filter((builtin) => !['gas_builtin', 'system'].includes(builtin))
273283
.map((builtin) => {
274-
// const instancesPerComponent = builtin === 'keccak' ? 16 : 1;
275-
const segment = this.getBuiltinSegment(builtin);
276-
if (!segment) throw new UndefinedBuiltinSegment(builtin);
277-
278-
const ratio = this.layout.ratios[builtin];
279-
const cellsPerInstance = CELLS_PER_INSTANCE[builtin];
280-
// TODO: set size to segment.length - SEGMENT_ARENA_INITIAL_SIZE (3)
281-
// when implementing this builtin
282-
const size = segment.length;
283-
let capacity: number = 0;
284-
285-
if (this.layout.name === 'dynamic') {
286-
const instances = Math.ceil(size / cellsPerInstance);
287-
const instancesPerComponent = builtin === 'keccak' ? 16 : 1;
288-
const components = nextPowerOfTwo(instances / instancesPerComponent);
289-
capacity = cellsPerInstance * instancesPerComponent * components;
290-
} else {
291-
const minStep = ratio * cellsPerInstance;
292-
if (this.vm.currentStep < minStep) {
293-
console.log(
294-
`PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps`
295-
);
296-
return false;
297-
}
298-
capacity = (this.vm.currentStep / ratio) * cellsPerInstance;
299-
}
300-
284+
const { size, capacity } = this.getSizeAndCapacity(builtin);
285+
if (capacity === MISSING_STEPS_CAPACITY) return false;
301286
if (size > capacity) {
302287
throw new InsufficientAllocatedCells(
303288
this.layout.name,
@@ -306,8 +291,159 @@ export class CairoRunner {
306291
);
307292
}
308293
return true;
309-
});
310-
return builtinChecks.reduce((prev, curr) => prev && curr);
294+
})
295+
.reduce((prev, curr) => prev && curr);
296+
297+
const initialBounds: RcLimits = { rcMin: 0, rcMax: 1 };
298+
const { rcMin, rcMax }: RcLimits = this.builtins
299+
.filter((builtin) => ['range_check', 'range_check96'].includes(builtin))
300+
.map((builtin) => {
301+
const segment = this.getBuiltinSegment(builtin);
302+
if (!segment) throw new UndefinedBuiltinSegment(builtin);
303+
if (!segment.length) return { rcMin: 0, rcMax: 0 };
304+
return segment.reduce((_, value) => {
305+
if (!isFelt(value)) throw new ExpectedFelt(value);
306+
const nParts = builtin === 'range_check' ? RC_N_PARTS : RC_N_PARTS_96;
307+
return value
308+
.to64BitsWords()
309+
.flatMap((limb) =>
310+
[3, 2, 1, 0].map(
311+
(i) =>
312+
(limb >> BigInt(i * INNER_RC_BOUND_SHIFT)) &
313+
INNER_RC_BOUND_MASK
314+
)
315+
)
316+
.slice(nParts)
317+
.reduce((bounds, curr) => {
318+
const x = Number(curr);
319+
return {
320+
rcMin: Math.min(bounds.rcMin, x),
321+
rcMax: Math.max(bounds.rcMax, x),
322+
};
323+
}, initialBounds);
324+
}, initialBounds);
325+
})
326+
.concat([this.vm.rcLimits])
327+
.reduce((acc, curr) => ({
328+
rcMin: Math.min(acc.rcMin, curr.rcMin),
329+
rcMax: Math.max(curr.rcMin, curr.rcMax),
330+
}));
331+
332+
const usedRcUnits = this.builtins
333+
.filter((builtin) => ['range_check', 'range_check96'].includes(builtin))
334+
.map((builtin) => {
335+
const segment = this.getBuiltinSegment(builtin);
336+
if (!segment) throw new UndefinedBuiltinSegment(builtin);
337+
return builtin === 'range_check'
338+
? segment.length * RC_N_PARTS
339+
: segment.length * RC_N_PARTS_96;
340+
})
341+
.reduce((acc, curr) => acc + curr);
342+
343+
const unusedRcUnits =
344+
(this.layout.rcUnits - 3) * this.vm.currentStep - usedRcUnits;
345+
const rcUnitsCheck = unusedRcUnits >= rcMax - rcMin;
346+
347+
const builtinsCapacity = this.builtins
348+
.map((builtin) => this.getSizeAndCapacity(builtin).capacity)
349+
.reduce((acc, curr) => acc + curr);
350+
const totalMemoryCapacity = this.vm.currentStep * MEMORY_UNITS_PER_STEP;
351+
if (totalMemoryCapacity % this.layout.publicMemoryFraction)
352+
throw new Error(
353+
'Total memory capacity is not a multiple of public memory fraction.'
354+
);
355+
const publicMemoryCapacity =
356+
totalMemoryCapacity / this.layout.publicMemoryFraction;
357+
358+
const instructionCapacity = this.vm.currentStep * 4;
359+
const unusedMemoryCapacity =
360+
totalMemoryCapacity -
361+
(publicMemoryCapacity + instructionCapacity + builtinsCapacity);
362+
const memoryHoles = this.vm.memory.segments.reduce(
363+
(acc, currSegment) =>
364+
acc + currSegment.reduce((acc, _) => acc--, currSegment.length),
365+
0
366+
);
367+
const memoryCheck = unusedMemoryCapacity >= memoryHoles;
368+
369+
let dilutedCheck: boolean = true;
370+
const dilutedPool = this.layout.dilutedPool;
371+
if (dilutedPool) {
372+
const dilutedUsedCapacity = this.builtins
373+
.filter((builtin) => ['bitwise', 'keccak'].includes(builtin))
374+
.map((builtin) => {
375+
const multiplier =
376+
this.layout.name === 'dynamic'
377+
? this.vm.currentStep
378+
: this.vm.currentStep / this.layout.ratios[builtin];
379+
if (builtin === 'bitwise') {
380+
const totalNBits = 251;
381+
const { nBits, spacing } = dilutedPool;
382+
const step = nBits * spacing;
383+
const partition: number[] = [];
384+
for (let i = 0; i < totalNBits; i += step) {
385+
for (let j = 0; j < spacing; j++) {
386+
if (i + j < totalNBits) {
387+
partition.push(i + j);
388+
}
389+
}
390+
}
391+
const trimmedNumber = partition.filter(
392+
(value) => value + spacing * (nBits - 1) + 1 > totalNBits
393+
).length;
394+
395+
return (4 * partition.length + trimmedNumber) * multiplier;
396+
}
397+
return (KECCAK_DILUTED_CELLS / dilutedPool.nBits) * multiplier;
398+
})
399+
.reduce((acc, curr) => acc + curr);
400+
401+
const dilutedUnits = this.vm.currentStep * dilutedPool.unitsPerStep;
402+
const unusedDilutedCapacity = dilutedUnits - dilutedUsedCapacity;
403+
dilutedCheck = unusedDilutedCapacity >= 1 << dilutedPool.nBits;
404+
}
405+
406+
return builtinChecks && rcUnitsCheck && memoryCheck && dilutedCheck;
407+
}
408+
409+
/** @returns The size of a builtin and its capacity for the chosen layout. */
410+
private getSizeAndCapacity(builtin: string): {
411+
size: number;
412+
capacity: number;
413+
} {
414+
const segment = this.getBuiltinSegment(builtin);
415+
if (!segment) throw new UndefinedBuiltinSegment(builtin);
416+
const size =
417+
builtin === 'segment_arena' ? segment.length - 3 : segment.length;
418+
419+
if (builtin === 'output' || builtin === 'segment_arena') {
420+
return { size, capacity: size };
421+
}
422+
423+
const ratio = this.layout.ratios[builtin];
424+
const cellsPerInstance = CELLS_PER_INSTANCE[builtin];
425+
let capacity: number = 0;
426+
427+
switch (this.layout.name) {
428+
case 'dynamic':
429+
const instances = Math.ceil(size / cellsPerInstance);
430+
const instancesPerComponent = builtin === 'keccak' ? 16 : 1;
431+
const components = nextPowerOfTwo(instances / instancesPerComponent);
432+
capacity = cellsPerInstance * instancesPerComponent * components;
433+
break;
434+
default:
435+
const minStep = ratio * cellsPerInstance;
436+
if (this.vm.currentStep < minStep) {
437+
// console.log(
438+
// `PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps`
439+
// );
440+
return { size, capacity: MISSING_STEPS_CAPACITY };
441+
}
442+
capacity = (this.vm.currentStep / ratio) * cellsPerInstance;
443+
break;
444+
}
445+
446+
return { size, capacity };
311447
}
312448

313449
/**

src/runners/layout.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export const DEFAULT_DILUTED_POOL: DilutedPool = {
3838
nBits: 16,
3939
};
4040

41+
export const MEMORY_UNITS_PER_STEP = 8;
42+
4143
/**
4244
* Dictionary containing all the available layouts:
4345
* - plain

src/vm/virtualMachine.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export type RelocatedMemory = {
6363
value: Felt;
6464
};
6565

66+
/** The bounds of the range_check builtins during a run. */
67+
export type RcLimits = {
68+
rcMin: number;
69+
rcMax: number;
70+
};
71+
6672
export class VirtualMachine {
6773
currentStep: number;
6874
memory: Memory;
@@ -73,6 +79,7 @@ export class VirtualMachine {
7379
squashedDictManager: SquashedDictManager;
7480
scopeManager: ScopeManager;
7581
trace: TraceEntry[];
82+
rcLimits: RcLimits;
7683
relocatedMemory: RelocatedMemory[];
7784
relocatedTrace: RelocatedTraceEntry[];
7885

@@ -82,6 +89,7 @@ export class VirtualMachine {
8289
this.currentStep = 0;
8390
this.memory = new Memory();
8491
this.trace = [];
92+
this.rcLimits = { rcMin: 0, rcMax: 0 };
8593
this.relocatedMemory = [];
8694
this.relocatedTrace = [];
8795

@@ -139,6 +147,12 @@ export class VirtualMachine {
139147

140148
this.trace.push({ pc: this.pc, ap: this.ap, fp: this.fp });
141149

150+
const { dstOffset, op0Offset, op1Offset } = instruction;
151+
this.rcLimits = {
152+
rcMin: Math.min(dstOffset, op0Offset, op1Offset),
153+
rcMax: Math.max(dstOffset, op0Offset, op1Offset),
154+
};
155+
142156
this.updateRegisters(instruction, res, dst);
143157

144158
this.currentStep += 1;

0 commit comments

Comments
 (0)