From 1c86b851cb3b1953c7cdca3f0dc5f1b8496c7d7d Mon Sep 17 00:00:00 2001 From: Malatrax Date: Thu, 1 Aug 2024 12:38:32 +0200 Subject: [PATCH 01/11] feat: add cairo1-run compilation target --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 570ba90f..3bf8e35e 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,18 @@ run-all diff-test bench clean-diff-test clean-bench clean-tmp # Clone & build the other VMs - Assume that related lang are installed CAIRO_VM_RS_CLI:=cairo-vm/target/release/cairo-vm-cli +CAIRO_VM_RS_CAIRO_CLI:=cairo-vm/target/release/cairo1-run CAIRO_VM_ZIG_CLI:=ziggy-starkdust/zig-out/bin/ziggy-starkdust $(CAIRO_VM_RS_CLI): @git submodule update --init cairo-vm; \ cd cairo-vm; cargo build --release --bin cairo-vm-cli +$(CAIRO_VM_RS_CAIRO_CLI): + @git submodule update --init cairo-vm; \ + cd cairo-vm/cairo1-run; make deps; \ + cd ..; cargo build --release --bin cairo1-run + $(CAIRO_VM_ZIG_CLI): @git submodule update --init ziggy-starkdust \ cd ziggy-starkdust; zig build -Doptimize=ReleaseFast @@ -17,7 +23,7 @@ $(CAIRO_VM_ZIG_CLI): build: @bun install; bun link -build-cairo-vm-rs-cli: | $(CAIRO_VM_RS_CLI) +build-cairo-vm-rs-cli: | $(CAIRO_VM_RS_CLI) $(CAIRO_VM_RS_CAIRO_CLI) build-cairo-vm-zig-cli: | $(CAIRO_VM_ZIG_CLI) From 4ede2a7e046b46f5f77ad388cd341c8f53aaae4c Mon Sep 17 00:00:00 2001 From: Malatrax Date: Thu, 1 Aug 2024 12:39:55 +0200 Subject: [PATCH 02/11] feat: add proof mode stack initialization --- src/cli.ts | 1 + src/errors/cairoRunner.ts | 12 +++- src/runners/cairoRunner.test.ts | 22 +++++- src/runners/cairoRunner.ts | 124 +++++++++++++++++++++++++------- src/scripts/run.ts | 16 +++-- 5 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 5a52a3d3..30dae09b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -30,6 +30,7 @@ program }) ) .option('-s, --silent', 'silent all logs') + .option('--proof-mode', 'run in proof mode') .addOption( new Option('-l, --layout ', 'Layout to be used').default('plain') ) diff --git a/src/errors/cairoRunner.ts b/src/errors/cairoRunner.ts index ab883655..25ab1fb6 100644 --- a/src/errors/cairoRunner.ts +++ b/src/errors/cairoRunner.ts @@ -17,7 +17,7 @@ export class CairoZeroHintsNotSupported extends CairoRunnerError { /** The given entrypoint is not in the program, it cannot be executed. */ export class UndefinedEntrypoint extends CairoRunnerError { constructor(name: string) { - super(`The function to be executed doesn't exist: ${name}`); + super(`The entrypoint to be executed doesn't exist: ${name}`); } } @@ -35,3 +35,13 @@ Layout builtins: ${layoutBuiltins.join(', ')}` ); } } + +/** + * The label `__main__.__end__` must be in the compilation artifacts + * to run a Cairo Zero program in proof mode. + */ +export class MissingEndLabel extends CairoRunnerError { + constructor() { + super(`Label __end__ not found in program.`); + } +} diff --git a/src/runners/cairoRunner.test.ts b/src/runners/cairoRunner.test.ts index ce66e07f..7c10dad2 100644 --- a/src/runners/cairoRunner.test.ts +++ b/src/runners/cairoRunner.test.ts @@ -14,7 +14,7 @@ import { parseProgram } from 'vm/program'; import { outputHandler } from 'builtins/output'; import { bitwiseHandler } from 'builtins/bitwise'; import { layouts } from './layout'; -import { CairoRunner, RunOptions } from './cairoRunner'; +import { CairoRunner, RunnerMode, RunOptions } from './cairoRunner'; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cairo-vm-ts-')); @@ -468,7 +468,15 @@ describe('cairoRunner', () => { ); expect( - () => new CairoRunner(dummyProgram, [], layout, 0, builtins) + () => + new CairoRunner( + dummyProgram, + [], + layout, + RunnerMode.ExecutionMode, + 0, + builtins + ) ).not.toThrow(); } ); @@ -489,7 +497,15 @@ describe('cairoRunner', () => { ); expect( - () => new CairoRunner(dummyProgram, [], layout, 0, builtins) + () => + new CairoRunner( + dummyProgram, + [], + layout, + RunnerMode.ExecutionMode, + 0, + builtins + ) ).toThrow( new InvalidBuiltins(builtins, layouts[layout].builtins, layout) ); diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index 12b552d2..88ed796e 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -5,6 +5,7 @@ import { EmptyRelocatedMemory, UndefinedEntrypoint, InvalidBuiltins, + MissingEndLabel, } from 'errors/cairoRunner'; import { Felt } from 'primitives/felt'; @@ -26,13 +27,20 @@ export type RunOptions = { offset: number; }; +export enum RunnerMode { + ExecutionMode = 'Execution Mode', + ProofModeCairoZero = 'Proof Mode - Cairo Zero', + ProofModeCairo = 'Proof Mode - Cairo', +} + export class CairoRunner { program: Program; + layout: Layout; + mode: RunnerMode; hints: Hints; vm: VirtualMachine; programBase: Relocatable; executionBase: Relocatable; - layout: Layout; builtins: string[]; finalPc: Relocatable; @@ -45,11 +53,13 @@ export class CairoRunner { program: Program, bytecode: Felt[], layoutName: string = 'plain', + mode: RunnerMode = RunnerMode.ExecutionMode, initialPc: number = 0, builtins: string[] = [], hints: Hints = new Map() ) { this.program = program; + this.mode = mode; this.hints = hints; this.vm = new VirtualMachine(); this.programBase = this.vm.memory.addSegment(); @@ -64,29 +74,69 @@ export class CairoRunner { if (!isSubsequence(builtins, allowedBuiltins)) throw new InvalidBuiltins(builtins, this.layout.builtins, layoutName); - this.builtins = builtins; - const builtin_stack = builtins.map((builtin) => { - const handler = getBuiltin(builtin); - if (builtin === 'segment_arena') { - const initialValues = [ - this.vm.memory.addSegment(handler), - new Felt(0n), - new Felt(0n), - ]; - const base = this.vm.memory.addSegment(handler); - initialValues.map((value, offset) => - this.vm.memory.assertEq(base.add(offset), value) - ); - return base.add(initialValues.length); - } - return this.vm.memory.addSegment(handler); - }); - const returnFp = this.vm.memory.addSegment(); - this.finalPc = this.vm.memory.addSegment(); - const stack = [...builtin_stack, returnFp, this.finalPc]; + const isProofMode = mode !== RunnerMode.ExecutionMode; + this.builtins = isProofMode + ? [...new Set(this.layout.builtins.concat(builtins))] + : builtins; + const builtin_stack = this.builtins + .map((builtin) => { + const handler = getBuiltin(builtin); + let base: Relocatable; + if (builtin === 'segment_arena') { + const initialValues = [ + this.vm.memory.addSegment(handler), + new Felt(0n), + new Felt(0n), + ]; + base = this.vm.memory.addSegment(handler); + initialValues.map((value, offset) => + this.vm.memory.assertEq(base.add(offset), value) + ); + base = base.add(initialValues.length); + } else { + base = this.vm.memory.addSegment(handler); + } + if (isProofMode) { + return builtins.includes(builtin) ? base : 0; + } + return base; + }) + .filter((value) => value !== 0); + + let stack: SegmentValue[] = []; + let apOffset: number = 0; + switch (mode) { + case RunnerMode.ExecutionMode: + const returnFp = this.vm.memory.addSegment(); + this.finalPc = this.vm.memory.addSegment(); + stack = [...builtin_stack, returnFp, this.finalPc]; + apOffset = stack.length; + break; + + case RunnerMode.ProofModeCairoZero: + const finalPc = (program as CairoZeroProgram).identifiers.get( + '__main__.__end__' + )?.pc; + if (!finalPc) throw new MissingEndLabel(); + this.finalPc = this.programBase.add(finalPc); + + stack = [this.executionBase.add(2), new Felt(0n), ...builtin_stack]; + apOffset = 2; + break; + case RunnerMode.ProofModeCairo: + const retFp = this.vm.memory.addSegment(); + this.finalPc = this.vm.memory.addSegment(); + const jmpRelInstruction = new Felt(BigInt('0x10780017FFF7FFF')); + this.vm.memory.assertEq(this.finalPc, jmpRelInstruction); + this.vm.memory.assertEq(this.finalPc.add(1), new Felt(0n)); + + stack = [...builtin_stack, retFp, this.finalPc]; + apOffset = stack.length; + break; + } this.vm.pc = this.programBase.add(initialPc); - this.vm.ap = this.executionBase.add(stack.length); + this.vm.ap = this.executionBase.add(apOffset); this.vm.fp = this.vm.ap; this.vm.memory.setValues(this.programBase, bytecode); @@ -97,32 +147,51 @@ export class CairoRunner { static fromCairoZeroProgram( program: CairoZeroProgram, layoutName: string = 'plain', + proofMode: boolean = false, fnName: string = 'main' ): CairoRunner { - const id = program.identifiers.get('__main__.'.concat(fnName)); - if (!id) throw new UndefinedEntrypoint(fnName); + const identifier = proofMode ? '__start__' : fnName; + const id = program.identifiers.get('__main__.'.concat(identifier)); + if (!id) throw new UndefinedEntrypoint(identifier); const offset = id.pc; const builtins = program.builtins; + const mode = proofMode + ? RunnerMode.ProofModeCairoZero + : RunnerMode.ExecutionMode; if (program.hints.length) throw new CairoZeroHintsNotSupported(); - return new CairoRunner(program, program.data, layoutName, offset, builtins); + return new CairoRunner( + program, + program.data, + layoutName, + mode, + offset, + builtins + ); } /** Instantiate a CairoRunner from parsed Cairo compilation artifacts. */ static fromCairoProgram( program: CairoProgram, layoutName: string = 'plain', + proofMode: boolean = false, fnName: string = 'main' ): CairoRunner { const fn = program.entry_points_by_function[fnName]; if (!fn) throw new UndefinedEntrypoint(fnName); + const mode = proofMode + ? RunnerMode.ProofModeCairo + : RunnerMode.ExecutionMode; + + const offset = proofMode ? 0 : fn.offset; return new CairoRunner( program, program.bytecode, layoutName, - fn.offset, + mode, + offset, fn.builtins, program.hints ); @@ -132,18 +201,21 @@ export class CairoRunner { static fromProgram( program: Program, layout: string = 'plain', + proofMode: boolean = false, fnName: string = 'main' ) { if (program.compiler_version.split('.')[0] == '0') { return CairoRunner.fromCairoZeroProgram( program as CairoZeroProgram, layout, + proofMode, fnName ); } return CairoRunner.fromCairoProgram( program as CairoProgram, layout, + proofMode, fnName ); } diff --git a/src/scripts/run.ts b/src/scripts/run.ts index 3a910bbc..891cbf7d 100644 --- a/src/scripts/run.ts +++ b/src/scripts/run.ts @@ -17,6 +17,7 @@ export const run = ( try { const { silent, + proofMode, layout, fn, relocate, @@ -31,6 +32,10 @@ export const run = ( if (silent) consola.level = LogLevels.silent; + consola.info( + `Cairo VM TS ${version} - ${proofMode ? 'Proof' : 'Execution'} Mode` + ); + if (!ALL_LAYOUTS.includes(layout)) { consola.error( `Layout "${layout}" is not a valid layout. @@ -40,23 +45,22 @@ Use one from {${ALL_LAYOUTS.join(', ')}}` } if ( - (!relocate && !!offset) || + (!relocate && proofMode) || + (!relocate && offset !== undefined) || (!relocate && exportMemory) || (!relocate && printRelocatedMemory) ) { consola.error( - "option '--no-relocate' cannot be used with options '--offset ', '--export-memory ' or '--print-relocated-memory'" + "option '--no-relocate' cannot be used with options '--proof-mode', '--offset ', '--export-memory ' or '--print-relocated-memory'" ); process.exit(1); } - consola.info(`Cairo VM TS ${version} - Execution Mode`); - const file = fs.readFileSync(String(path), 'utf-8'); const program = parseProgram(file); - runner = CairoRunner.fromProgram(program, layout, fn); - const config: RunOptions = { relocate: relocate, offset: offset }; + runner = CairoRunner.fromProgram(program, layout, proofMode, fn); + const config: RunOptions = { relocate, offset }; runner.run(config); consola.success('Execution finished!'); From 094535516d59f9fa5e762136c161611bf9f5d699 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 10:48:12 +0200 Subject: [PATCH 03/11] feat: add proof mode next power of two loop --- src/primitives/utils.test.ts | 23 +++++++++++++++++++++++ src/primitives/utils.ts | 3 +++ src/runners/cairoRunner.ts | 33 ++++++++++++++++++++++++++++++++- src/vm/virtualMachine.ts | 5 +++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/primitives/utils.test.ts create mode 100644 src/primitives/utils.ts diff --git a/src/primitives/utils.test.ts b/src/primitives/utils.test.ts new file mode 100644 index 00000000..4beb29c7 --- /dev/null +++ b/src/primitives/utils.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'bun:test'; +import { nextPowerOfTwo } from './utils'; + +describe('utils', () => { + describe('nextPowerOfTwo', () => { + test.each([ + [0, 1], + [1, 1], + [3, 4], + [31, 32], + [33, 64], + [57, 64], + [28465, 32768], + [(1 << 62) - 1, 1 << 62], + [-1, 1], + ])( + 'should correctly compute the next power of two of a given number', + (n: number, expected: number) => { + expect(nextPowerOfTwo(n)).toEqual(expected); + } + ); + }); +}); diff --git a/src/primitives/utils.ts b/src/primitives/utils.ts new file mode 100644 index 00000000..58231b8f --- /dev/null +++ b/src/primitives/utils.ts @@ -0,0 +1,3 @@ +export function nextPowerOfTwo(n: number): number { + return 1 << Math.ceil(Math.log2(n)); +} diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index 88ed796e..12c1edf8 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -228,7 +228,38 @@ export class CairoRunner { this.vm.step(this.hints.get(this.vm.pc.offset)); } const { relocate, offset } = config; - if (relocate) this.vm.relocate(offset); + const isProofMode = this.mode !== RunnerMode.ExecutionMode; + + if (isProofMode) { + this.runFor(this.vm.nextPowerOfTwoStep()); + while (!this.checkCellUsage()) { + this.runFor(1); + this.runFor(this.vm.nextPowerOfTwoStep()); + } + } + + if (isProofMode || relocate) this.vm.relocate(offset); + } + + /** + * Execute a fixed number of steps. + * + * @param steps - The number of steps to execute. + */ + runFor(steps: number) { + for (let i = 0; i < steps; i++) { + this.vm.step(this.hints.get(this.vm.pc.offset)); + } + } + + /** + * @returns {boolean} Whether there are enough allocated cells for + * generating a proof of this program execution for the chosen layout. + * @throws {} - If the number of allocated cells of the layout is insufficient. + * + */ + checkCellUsage(): boolean { + return true; } /** diff --git a/src/vm/virtualMachine.ts b/src/vm/virtualMachine.ts index 0216925c..df24e003 100644 --- a/src/vm/virtualMachine.ts +++ b/src/vm/virtualMachine.ts @@ -45,6 +45,7 @@ import { Register, ResLogic, } from './instruction'; +import { nextPowerOfTwo } from 'primitives/utils'; export type TraceEntry = { pc: Relocatable; @@ -476,6 +477,10 @@ export class VirtualMachine { .join('\n'); } + nextPowerOfTwoStep(): number { + return nextPowerOfTwo(Number(this.currentStep)); + } + /** * Return the memory address defined by `cell` * From 8dfe185c6e6f6de6710fc69ca021c5b5b575a9d7 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 12:22:04 +0200 Subject: [PATCH 04/11] feat: add name field to layout --- src/runners/layout.test.ts | 12 +++--------- src/runners/layout.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/runners/layout.test.ts b/src/runners/layout.test.ts index 7f9f4d2f..6e5c4403 100644 --- a/src/runners/layout.test.ts +++ b/src/runners/layout.test.ts @@ -8,16 +8,8 @@ import { describe('layouts', () => { describe('Layout', () => { - test('plain layout should the right values', () => { - const plain = layouts['plain']; - expect(plain.builtins).toEqual([]); - expect(plain.rcUnits).toEqual(16); - expect(plain.publicMemoryFraction).toEqual(4); - expect(plain.dilutedPool).toBeUndefined(); - expect(plain.ratios).toBeUndefined(); - }); - test.each([ + ['plain', [], 16, 4, {}], [ 'small', ['output', 'pedersen', 'range_check', 'ecdsa'], @@ -36,6 +28,7 @@ describe('layouts', () => { 'should have the correct values with undefined DilutedPool', (layoutName, builtins, rcUnits, publicMemoryFraction, ratios) => { const layout = layouts[layoutName]; + expect(layout.name).toEqual(layoutName); expect(layout.builtins).toEqual(builtins); expect(layout.rcUnits).toEqual(rcUnits); expect(layout.publicMemoryFraction).toEqual(publicMemoryFraction); @@ -209,6 +202,7 @@ describe('layouts', () => { ratios: { [key: string]: number } ) => { const layout = layouts[layoutName]; + expect(layout.name).toEqual(layoutName); expect(layout.builtins).toEqual(builtins); expect(layout.rcUnits).toEqual(rcUnits); expect(layout.publicMemoryFraction).toEqual(publicMemoryFraction); diff --git a/src/runners/layout.ts b/src/runners/layout.ts index d92c2485..020b2409 100644 --- a/src/runners/layout.ts +++ b/src/runners/layout.ts @@ -24,11 +24,12 @@ export type DilutedPool = { * An empty ratio represents a dynamic ratio. */ export type Layout = { + name: string; builtins: string[]; rcUnits: number; publicMemoryFraction: number; dilutedPool?: DilutedPool; - ratios?: { [key: string]: number }; + ratios: { [key: string]: number }; }; export const DEFAULT_DILUTED_POOL: DilutedPool = { @@ -53,11 +54,14 @@ export const DEFAULT_DILUTED_POOL: DilutedPool = { */ export const layouts: { [key: string]: Layout } = { plain: { + name: 'plain', builtins: [], rcUnits: 16, publicMemoryFraction: 4, + ratios: {}, }, small: { + name: 'small', builtins: ['output', 'pedersen', 'range_check', 'ecdsa'], rcUnits: 16, publicMemoryFraction: 4, @@ -68,6 +72,7 @@ export const layouts: { [key: string]: Layout } = { }, }, dex: { + name: 'dex', builtins: ['output', 'pedersen', 'range_check', 'ecdsa'], rcUnits: 4, publicMemoryFraction: 4, @@ -78,6 +83,7 @@ export const layouts: { [key: string]: Layout } = { }, }, recursive: { + name: 'recursive', builtins: ['output', 'pedersen', 'range_check', 'bitwise'], rcUnits: 4, publicMemoryFraction: 8, @@ -89,6 +95,7 @@ export const layouts: { [key: string]: Layout } = { }, }, starknet: { + name: 'starknet', builtins: [ 'output', 'pedersen', @@ -115,6 +122,7 @@ export const layouts: { [key: string]: Layout } = { }, }, starknet_with_keccak: { + name: 'starknet_with_keccak', builtins: [ 'output', 'pedersen', @@ -139,6 +147,7 @@ export const layouts: { [key: string]: Layout } = { }, }, recursive_large_output: { + name: 'recursive_large_output', builtins: ['output', 'pedersen', 'range_check', 'bitwise', 'poseidon'], rcUnits: 4, publicMemoryFraction: 8, @@ -151,6 +160,7 @@ export const layouts: { [key: string]: Layout } = { }, }, recursive_with_poseidon: { + name: 'recursive_with_poseidon', builtins: ['output', 'pedersen', 'range_check', 'bitwise', 'poseidon'], rcUnits: 4, publicMemoryFraction: 8, @@ -167,6 +177,7 @@ export const layouts: { [key: string]: Layout } = { }, }, all_cairo: { + name: 'all_cairo', builtins: [ 'output', 'pedersen', @@ -193,6 +204,7 @@ export const layouts: { [key: string]: Layout } = { }, }, all_solidity: { + name: 'all_solidity', builtins: [ 'output', 'pedersen', @@ -213,6 +225,7 @@ export const layouts: { [key: string]: Layout } = { }, }, dynamic: { + name: 'dynamic', builtins: [ 'output', 'pedersen', From 7786d2b9a0b3160ba60ee0cdd2e7bd3836a2c559 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 13:02:10 +0200 Subject: [PATCH 05/11] refactor: export cells per builtin instance constants --- src/builtins/bitwise.ts | 13 ++++++++----- src/builtins/builtin.ts | 41 +++++++++++++++++++++++++++------------- src/builtins/ecdsa.ts | 5 +++-- src/builtins/ecop.ts | 17 ++++++++++------- src/builtins/keccak.ts | 17 ++++++++++------- src/builtins/pedersen.ts | 13 ++++++++----- src/builtins/poseidon.ts | 15 +++++++++------ 7 files changed, 76 insertions(+), 45 deletions(-) diff --git a/src/builtins/bitwise.ts b/src/builtins/bitwise.ts index 2da55fb8..99f3bd84 100644 --- a/src/builtins/bitwise.ts +++ b/src/builtins/bitwise.ts @@ -5,18 +5,21 @@ import { Felt } from 'primitives/felt'; import { isFelt } from 'primitives/segmentValue'; import { BuiltinHandler } from './builtin'; +/** Total number of cells per bitwise operation */ +export const CELLS_PER_BITWISE = 5; + +/** Number of input cells for a bitwise operation */ +export const INPUT_CELLS_PER_BITWISE = 2; + export const bitwiseHandler: BuiltinHandler = { get(target, prop) { if (isNaN(Number(prop))) { return Reflect.get(target, prop); } - const cellsPerBitwise = 5; - const inputCellsPerBitwise = 2; - const offset = Number(prop); - const bitwiseIndex = offset % cellsPerBitwise; - if (bitwiseIndex < inputCellsPerBitwise || target[offset]) { + const bitwiseIndex = offset % CELLS_PER_BITWISE; + if (bitwiseIndex < INPUT_CELLS_PER_BITWISE || target[offset]) { return target[offset]; } diff --git a/src/builtins/builtin.ts b/src/builtins/builtin.ts index 36bf8dd7..ab679a3a 100644 --- a/src/builtins/builtin.ts +++ b/src/builtins/builtin.ts @@ -1,12 +1,13 @@ import { SegmentValue } from 'primitives/segmentValue'; -import { bitwiseHandler } from './bitwise'; -import { ecOpHandler } from './ecop'; -import { ecdsaHandler } from './ecdsa'; -import { pedersenHandler } from './pedersen'; -import { poseidonHandler } from './poseidon'; -import { keccakHandler } from './keccak'; + import { outputHandler } from './output'; +import { CELLS_PER_PEDERSEN, pedersenHandler } from './pedersen'; import { rangeCheckHandler } from './rangeCheck'; +import { CELLS_PER_ECDSA, ecdsaHandler } from './ecdsa'; +import { bitwiseHandler, CELLS_PER_BITWISE } from './bitwise'; +import { CELLS_PER_EC_OP, ecOpHandler } from './ecop'; +import { CELLS_PER_KECCAK, keccakHandler } from './keccak'; +import { CELLS_PER_POSEIDON, poseidonHandler } from './poseidon'; import { segmentArenaHandler } from './segmentArena'; /** Proxy handler to abstract validation & deduction rules off the VM */ @@ -14,30 +15,44 @@ export type BuiltinHandler = ProxyHandler>; /** * Object to map builtin names to their ProxyHandler: - * - Bitwise: Builtin for bitwise operations + * - Output: Output builtin + * - Pedersen: Builtin for pedersen hash family * - RangeCheck: Builtin for inequality operations * - ECDSA: Builtin for Elliptic Curve Digital Signature Algorithm + * - Bitwise: Builtin for bitwise operations * - EcOp: Builtin for Elliptic curve Operations * - Keccak: Builtin for keccak hash family - * - Pedersen: Builtin for pedersen hash family * - Poseidon: Builtin for poseidon hash family * - Segment Arena: Builtin to manage the dictionaries - * - Output: Output builtin */ const BUILTIN_HANDLER: { [key: string]: BuiltinHandler; } = { output: outputHandler, + pedersen: pedersenHandler, + range_check: rangeCheckHandler(128n), + ecdsa: ecdsaHandler, bitwise: bitwiseHandler, ec_op: ecOpHandler, - ecdsa: ecdsaHandler, - pedersen: pedersenHandler, - poseidon: poseidonHandler, keccak: keccakHandler, - range_check: rangeCheckHandler(128n), + poseidon: poseidonHandler, range_check96: rangeCheckHandler(96n), segment_arena: segmentArenaHandler, }; /** Getter of the object `BUILTIN_HANDLER` */ export const getBuiltin = (name: string) => BUILTIN_HANDLER[name]; + +export const CELLS_PER_INSTANCE: { + [key: string]: number; +} = { + output: 0, + pedersen: CELLS_PER_PEDERSEN, + range_check: 1, + ecdsa: CELLS_PER_ECDSA, + bitwise: CELLS_PER_BITWISE, + ec_op: CELLS_PER_EC_OP, + keccak: CELLS_PER_KECCAK, + poseidon: CELLS_PER_POSEIDON, + range_check96: 1, +}; diff --git a/src/builtins/ecdsa.ts b/src/builtins/ecdsa.ts index 33979130..6ca5231a 100644 --- a/src/builtins/ecdsa.ts +++ b/src/builtins/ecdsa.ts @@ -16,6 +16,8 @@ type EcdsaSignatureDict = { [key: number]: EcdsaSignature }; export type EcdsaSegment = SegmentValue[] & { signatures: EcdsaSignatureDict }; type EcdsaProxyHandler = ProxyHandler; +export const CELLS_PER_ECDSA = 2; + const signatureHandler: ProxyHandler = { set(target, prop, newValue): boolean { if (isNaN(Number(prop))) throw new ExpectedOffset(); @@ -50,9 +52,8 @@ export const ecdsaHandler: EcdsaProxyHandler = { if (!target.signatures) throw new UndefinedSignatureDict(); if (!isFelt(newValue)) throw new ExpectedFelt(newValue); - const cellsPerEcdsa = 2; const offset = Number(prop); - const ecdsaIndex = offset % cellsPerEcdsa; + const ecdsaIndex = offset % CELLS_PER_ECDSA; const pubKeyXOffset = ecdsaIndex ? offset - 1 : offset; const msgOffset = ecdsaIndex ? offset : offset + 1; diff --git a/src/builtins/ecop.ts b/src/builtins/ecop.ts index 6557ab23..85b0ec00 100644 --- a/src/builtins/ecop.ts +++ b/src/builtins/ecop.ts @@ -7,6 +7,12 @@ import { LadderFailed } from 'errors/builtins'; const _1n = BigInt(1); +/** Total number of cells per ec_op operation */ +export const CELLS_PER_EC_OP = 7; + +/** Number of input cells for a ec_op operation */ +export const INPUT_CELLS_PER_EC_OP = 5; + /** * EcOp Builtin - Computes R = P + mQ * P and Q are points on the STARK curve @@ -18,17 +24,14 @@ export const ecOpHandler: BuiltinHandler = { return Reflect.get(target, prop); } - const cellsPerEcOp = 7; - const inputCellsPerEcOp = 5; - const offset = Number(prop); - const ecOpIndex = offset % cellsPerEcOp; - if (ecOpIndex < inputCellsPerEcOp || target[offset]) { + const ecOpIndex = offset % CELLS_PER_EC_OP; + if (ecOpIndex < INPUT_CELLS_PER_EC_OP || target[offset]) { return target[offset]; } const inputOffset = offset - ecOpIndex; - const outputOffset = inputOffset + inputCellsPerEcOp; + const outputOffset = inputOffset + INPUT_CELLS_PER_EC_OP; const inputs = target.slice(inputOffset, outputOffset).map((value) => { if (!isFelt(value)) throw new ExpectedFelt(value); @@ -41,7 +44,7 @@ export const ecOpHandler: BuiltinHandler = { const r = p.multiplyAndAddUnsafe(q, _1n, inputs[4]); if (r === undefined) throw new LadderFailed(); - switch (ecOpIndex - inputCellsPerEcOp) { + switch (ecOpIndex - INPUT_CELLS_PER_EC_OP) { case 0: return (target[outputOffset] = new Felt(r.x)); default: diff --git a/src/builtins/keccak.ts b/src/builtins/keccak.ts index 18bd8219..280b14fd 100644 --- a/src/builtins/keccak.ts +++ b/src/builtins/keccak.ts @@ -11,6 +11,12 @@ import { BuiltinHandler } from './builtin'; const KECCAK_BYTES = 25; const KECCAK_BITS = 200n; +/** Total number of cells per keccak operation */ +export const CELLS_PER_KECCAK = 16; + +/** Number of input cells for a keccak operation */ +export const INPUT_CELLS_PER_KECCAK = 8; + /** * Compute the new state of the keccak-f1600 block permutation on 24 rounds * @@ -24,17 +30,14 @@ export const keccakHandler: BuiltinHandler = { return Reflect.get(target, prop); } - const cellsPerKeccak = 16; - const inputCellsPerKeccak = 8; - const offset = Number(prop); - const keccakIndex = offset % cellsPerKeccak; - if (keccakIndex < inputCellsPerKeccak || target[offset]) { + const keccakIndex = offset % CELLS_PER_KECCAK; + if (keccakIndex < INPUT_CELLS_PER_KECCAK || target[offset]) { return target[offset]; } const inputOffset = offset - keccakIndex; - const outputOffset = inputOffset + inputCellsPerKeccak; + const outputOffset = inputOffset + INPUT_CELLS_PER_KECCAK; const input = concatBytes( ...target.slice(inputOffset, outputOffset).map((value) => { @@ -53,7 +56,7 @@ export const keccakHandler: BuiltinHandler = { ).map(bytesToNumberLE); return (target[offset] = new Felt( - outputs[keccakIndex - inputCellsPerKeccak] + outputs[keccakIndex - INPUT_CELLS_PER_KECCAK] )); }, }; diff --git a/src/builtins/pedersen.ts b/src/builtins/pedersen.ts index 3722c0d3..f3f2989f 100644 --- a/src/builtins/pedersen.ts +++ b/src/builtins/pedersen.ts @@ -5,6 +5,12 @@ import { isFelt } from 'primitives/segmentValue'; import { pedersen } from '@scure/starknet'; import { Felt } from 'primitives/felt'; +/** Total number of cells per pedersen operation */ +export const CELLS_PER_PEDERSEN = 3; + +/** Number of input cells for a pedersen operation */ +export const INPUT_CELLS_PER_PEDERSEN = 2; + /** Pedersen Builtin - Computes Pedersen(x, y) */ export const pedersenHandler: BuiltinHandler = { get(target, prop) { @@ -12,12 +18,9 @@ export const pedersenHandler: BuiltinHandler = { return Reflect.get(target, prop); } - const cellsPerPedersen = 3; - const inputCellsPerPedersen = 2; - const offset = Number(prop); - const pedersenIndex = offset % cellsPerPedersen; - if (pedersenIndex < inputCellsPerPedersen || target[offset]) { + const pedersenIndex = offset % CELLS_PER_PEDERSEN; + if (pedersenIndex < INPUT_CELLS_PER_PEDERSEN || target[offset]) { return target[offset]; } diff --git a/src/builtins/poseidon.ts b/src/builtins/poseidon.ts index 34c2e798..0ba3becc 100644 --- a/src/builtins/poseidon.ts +++ b/src/builtins/poseidon.ts @@ -5,6 +5,12 @@ import { isFelt } from 'primitives/segmentValue'; import { poseidonSmall } from '@scure/starknet'; import { Felt } from 'primitives/felt'; +/** Total number of cells per poseidon operation */ +export const CELLS_PER_POSEIDON = 6; + +/** Number of input cells for a poseidon operation */ +export const INPUT_CELLS_PER_POSEIDON = 3; + const poseidon = (x: bigint, y: bigint, z: bigint) => poseidonSmall([x, y, z]); /** Poseidon Builtin - Computes state of Poseidon(x, y) @@ -17,12 +23,9 @@ export const poseidonHandler: BuiltinHandler = { return Reflect.get(target, prop); } - const cellsPerPoseidon = 6; - const inputCellsPerPoseidon = 3; - const offset = Number(prop); - const poseidonIndex = offset % cellsPerPoseidon; - if (poseidonIndex < inputCellsPerPoseidon || target[offset]) { + const poseidonIndex = offset % CELLS_PER_POSEIDON; + if (poseidonIndex < INPUT_CELLS_PER_POSEIDON || target[offset]) { return target[offset]; } @@ -47,7 +50,7 @@ export const poseidonHandler: BuiltinHandler = { const state = poseidon(x, y, z); return (target[offset] = new Felt( - state[poseidonIndex - inputCellsPerPoseidon] + state[poseidonIndex - INPUT_CELLS_PER_POSEIDON] )); }, }; From 3dc80657d6bd52cd1e77c0ab92c7654c838d7534 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 13:02:51 +0200 Subject: [PATCH 06/11] refactor: make currentStep attribute public --- src/vm/virtualMachine.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/vm/virtualMachine.ts b/src/vm/virtualMachine.ts index df24e003..e591131e 100644 --- a/src/vm/virtualMachine.ts +++ b/src/vm/virtualMachine.ts @@ -45,7 +45,6 @@ import { Register, ResLogic, } from './instruction'; -import { nextPowerOfTwo } from 'primitives/utils'; export type TraceEntry = { pc: Relocatable; @@ -65,7 +64,7 @@ export type RelocatedMemory = { }; export class VirtualMachine { - private currentStep: bigint; + currentStep: number; memory: Memory; pc: Relocatable; ap: Relocatable; @@ -80,7 +79,7 @@ export class VirtualMachine { private handlers: HintHandler = handlers; constructor() { - this.currentStep = 0n; + this.currentStep = 0; this.memory = new Memory(); this.trace = []; this.relocatedMemory = []; @@ -142,7 +141,7 @@ export class VirtualMachine { this.updateRegisters(instruction, res, dst); - this.currentStep += 1n; + this.currentStep += 1; } /** @@ -477,10 +476,6 @@ export class VirtualMachine { .join('\n'); } - nextPowerOfTwoStep(): number { - return nextPowerOfTwo(Number(this.currentStep)); - } - /** * Return the memory address defined by `cell` * From e65c92faf719315db0a0da2ac2087c5554984389 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 13:03:32 +0200 Subject: [PATCH 07/11] feat: add builtin cell usage check --- src/errors/cairoRunner.ts | 18 +++++++++++- src/runners/cairoRunner.ts | 56 +++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/errors/cairoRunner.ts b/src/errors/cairoRunner.ts index 25ab1fb6..dbf2bd2e 100644 --- a/src/errors/cairoRunner.ts +++ b/src/errors/cairoRunner.ts @@ -42,6 +42,22 @@ Layout builtins: ${layoutBuiltins.join(', ')}` */ export class MissingEndLabel extends CairoRunnerError { constructor() { - super(`Label __end__ not found in program.`); + super('Label __end__ not found in program.'); + } +} + +/** The requested builtin segment is undefined. */ +export class UndefinedBuiltinSegment extends CairoRunnerError { + constructor(builtin: string) { + super(`The requested builtin segment '${builtin}' is undefined.`); + } +} + +/** The program consumes too many steps for the chosen layout. */ +export class InsufficientAllocatedCells extends CairoRunnerError { + constructor(layout: string, used: number, capacity: number) { + super( + `The chosen layout ${layout} only has a capacity of ${capacity} cells, but the program used ${used} cells.` + ); } } diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index 12c1edf8..5c23f87f 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -6,6 +6,8 @@ import { UndefinedEntrypoint, InvalidBuiltins, MissingEndLabel, + UndefinedBuiltinSegment, + InsufficientAllocatedCells, } from 'errors/cairoRunner'; import { Felt } from 'primitives/felt'; @@ -13,9 +15,10 @@ import { SegmentValue } from 'primitives/segmentValue'; import { Relocatable } from 'primitives/relocatable'; import { CairoProgram, CairoZeroProgram, Program } from 'vm/program'; import { VirtualMachine } from 'vm/virtualMachine'; -import { getBuiltin } from 'builtins/builtin'; +import { CELLS_PER_INSTANCE, getBuiltin } from 'builtins/builtin'; import { Hint, Hints } from 'hints/hintSchema'; import { isSubsequence, Layout, layouts } from './layout'; +import { nextPowerOfTwo } from 'primitives/utils'; /** * Configuration of the run @@ -231,13 +234,14 @@ export class CairoRunner { const isProofMode = this.mode !== RunnerMode.ExecutionMode; if (isProofMode) { - this.runFor(this.vm.nextPowerOfTwoStep()); + this.runFor(nextPowerOfTwo(this.vm.currentStep)); while (!this.checkCellUsage()) { this.runFor(1); - this.runFor(this.vm.nextPowerOfTwoStep()); + this.runFor(nextPowerOfTwo(this.vm.currentStep) - this.vm.currentStep); } } + console.log('steps: ', this.vm.currentStep); if (isProofMode || relocate) this.vm.relocate(offset); } @@ -259,7 +263,51 @@ export class CairoRunner { * */ checkCellUsage(): boolean { - return true; + const builtinChecks = this.builtins + .filter( + (builtin) => + !['output', 'segment_arena', 'gas_builtin', 'system'].includes( + builtin + ) + ) + .map((builtin) => { + // const instancesPerComponent = builtin === 'keccak' ? 16 : 1; + const segment = this.getBuiltinSegment(builtin); + if (!segment) throw new UndefinedBuiltinSegment(builtin); + + const ratio = this.layout.ratios[builtin]; + const cellsPerInstance = CELLS_PER_INSTANCE[builtin]; + // TODO: set size to segment.length - SEGMENT_ARENA_INITIAL_SIZE (3) + // when implementing this builtin + const size = segment.length; + let capacity: number = 0; + + if (this.layout.name === 'dynamic') { + const instances = Math.ceil(size / cellsPerInstance); + const instancesPerComponent = builtin === 'keccak' ? 16 : 1; + const components = nextPowerOfTwo(instances / instancesPerComponent); + capacity = cellsPerInstance * instancesPerComponent * components; + } else { + const minStep = ratio * cellsPerInstance; + if (this.vm.currentStep < minStep) { + console.log( + `PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps` + ); + return false; + } + capacity = (this.vm.currentStep / ratio) * cellsPerInstance; + } + + if (size > capacity) { + throw new InsufficientAllocatedCells( + this.layout.name, + size, + capacity + ); + } + return true; + }); + return builtinChecks.reduce((prev, curr) => prev && curr); } /** From 39c3b1a407b225012acb4de27dd79a2f1d2191ec Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 16:39:18 +0200 Subject: [PATCH 08/11] refactor: export range_check inner shift and number of parts --- src/builtins/builtin.ts | 6 +++--- src/builtins/rangeCheck.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/builtins/builtin.ts b/src/builtins/builtin.ts index ab679a3a..75cd90f0 100644 --- a/src/builtins/builtin.ts +++ b/src/builtins/builtin.ts @@ -2,7 +2,7 @@ import { SegmentValue } from 'primitives/segmentValue'; import { outputHandler } from './output'; import { CELLS_PER_PEDERSEN, pedersenHandler } from './pedersen'; -import { rangeCheckHandler } from './rangeCheck'; +import { rangeCheckHandler, RC_BITS, RC_BITS_96 } from './rangeCheck'; import { CELLS_PER_ECDSA, ecdsaHandler } from './ecdsa'; import { bitwiseHandler, CELLS_PER_BITWISE } from './bitwise'; import { CELLS_PER_EC_OP, ecOpHandler } from './ecop'; @@ -30,13 +30,13 @@ const BUILTIN_HANDLER: { } = { output: outputHandler, pedersen: pedersenHandler, - range_check: rangeCheckHandler(128n), + range_check: rangeCheckHandler(RC_BITS), ecdsa: ecdsaHandler, bitwise: bitwiseHandler, ec_op: ecOpHandler, keccak: keccakHandler, poseidon: poseidonHandler, - range_check96: rangeCheckHandler(96n), + range_check96: rangeCheckHandler(RC_BITS_96), segment_arena: segmentArenaHandler, }; diff --git a/src/builtins/rangeCheck.ts b/src/builtins/rangeCheck.ts index b9e41a37..a9b5ee82 100644 --- a/src/builtins/rangeCheck.ts +++ b/src/builtins/rangeCheck.ts @@ -3,6 +3,18 @@ import { BuiltinHandler } from './builtin'; import { isFelt } from 'primitives/segmentValue'; import { ExpectedFelt } from 'errors/primitives'; +export const INNER_RC_BOUND_SHIFT = 16; +export const INNER_RC_BOUND_MASK = 0xffffn; + +export const RC_N_PARTS = 8; +export const RC_N_PARTS_96 = 6; + +/** Bound exponent of the range_check builtin, `128`. */ +export const RC_BITS = BigInt(INNER_RC_BOUND_SHIFT * RC_N_PARTS); + +/** Bound exponent of the range_check96 builtin, `96`. */ +export const RC_BITS_96 = BigInt(INNER_RC_BOUND_SHIFT * RC_N_PARTS_96); + export const rangeCheckHandler = (boundExponent: bigint): BuiltinHandler => { return { set(target, prop, newValue): boolean { From ede2a0891dfffc12e8737964ab5756858ede88c1 Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 18:04:49 +0200 Subject: [PATCH 09/11] feat: add proof mode cell checks --- src/builtins/keccak.ts | 17 +++ src/runners/cairoRunner.ts | 214 ++++++++++++++++++++++++++++++------- src/runners/layout.ts | 2 + src/vm/virtualMachine.ts | 14 +++ 4 files changed, 208 insertions(+), 39 deletions(-) diff --git a/src/builtins/keccak.ts b/src/builtins/keccak.ts index 280b14fd..6b2b6d33 100644 --- a/src/builtins/keccak.ts +++ b/src/builtins/keccak.ts @@ -17,6 +17,23 @@ export const CELLS_PER_KECCAK = 16; /** Number of input cells for a keccak operation */ export const INPUT_CELLS_PER_KECCAK = 8; +/** + * The diluted cells are: + * - state - 25 rounds times 1600 elements. + * - parity - 24 rounds times 1600/5 elements times 3 auxiliaries. + * - after_theta_rho_pi - 24 rounds times 1600 elements. + * - theta_aux - 24 rounds times 1600 elements. + * - chi_iota_aux - 24 rounds times 1600 elements times 2 auxiliaries. + * + * In total 25 * 1600 + 24 * 320 * 3 + 24 * 1600 + 24 * 1600 + 24 * 1600 * 2 = 216640. + * + * But we actually allocate 4 virtual columns, of dimensions 64 * 1024, in which we embed the + * real cells, and we don't free the unused ones. + * + * So the real number is 4 * 64 * 1024 = 262144. + */ +export const KECCAK_DILUTED_CELLS = 262144; + /** * Compute the new state of the keccak-f1600 block permutation on 24 rounds * diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index 5c23f87f..f6209e08 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -11,14 +11,27 @@ import { } from 'errors/cairoRunner'; import { Felt } from 'primitives/felt'; -import { SegmentValue } from 'primitives/segmentValue'; +import { isFelt, SegmentValue } from 'primitives/segmentValue'; import { Relocatable } from 'primitives/relocatable'; import { CairoProgram, CairoZeroProgram, Program } from 'vm/program'; -import { VirtualMachine } from 'vm/virtualMachine'; +import { RcLimits, VirtualMachine } from 'vm/virtualMachine'; import { CELLS_PER_INSTANCE, getBuiltin } from 'builtins/builtin'; import { Hint, Hints } from 'hints/hintSchema'; -import { isSubsequence, Layout, layouts } from './layout'; +import { + isSubsequence, + Layout, + layouts, + MEMORY_UNITS_PER_STEP, +} from './layout'; import { nextPowerOfTwo } from 'primitives/utils'; +import { ExpectedFelt } from 'errors/primitives'; +import { + INNER_RC_BOUND_MASK, + INNER_RC_BOUND_SHIFT, + RC_N_PARTS, + RC_N_PARTS_96, +} from 'builtins/rangeCheck'; +import { KECCAK_DILUTED_CELLS } from 'builtins/keccak'; /** * Configuration of the run @@ -36,6 +49,8 @@ export enum RunnerMode { ProofModeCairo = 'Proof Mode - Cairo', } +const MISSING_STEPS_CAPACITY = -1; + export class CairoRunner { program: Program; layout: Layout; @@ -259,45 +274,15 @@ export class CairoRunner { /** * @returns {boolean} Whether there are enough allocated cells for * generating a proof of this program execution for the chosen layout. - * @throws {} - If the number of allocated cells of the layout is insufficient. + * @throws {InsufficientAllocatedCells} - If the number of allocated cells of the layout is insufficient. * */ checkCellUsage(): boolean { const builtinChecks = this.builtins - .filter( - (builtin) => - !['output', 'segment_arena', 'gas_builtin', 'system'].includes( - builtin - ) - ) + .filter((builtin) => !['gas_builtin', 'system'].includes(builtin)) .map((builtin) => { - // const instancesPerComponent = builtin === 'keccak' ? 16 : 1; - const segment = this.getBuiltinSegment(builtin); - if (!segment) throw new UndefinedBuiltinSegment(builtin); - - const ratio = this.layout.ratios[builtin]; - const cellsPerInstance = CELLS_PER_INSTANCE[builtin]; - // TODO: set size to segment.length - SEGMENT_ARENA_INITIAL_SIZE (3) - // when implementing this builtin - const size = segment.length; - let capacity: number = 0; - - if (this.layout.name === 'dynamic') { - const instances = Math.ceil(size / cellsPerInstance); - const instancesPerComponent = builtin === 'keccak' ? 16 : 1; - const components = nextPowerOfTwo(instances / instancesPerComponent); - capacity = cellsPerInstance * instancesPerComponent * components; - } else { - const minStep = ratio * cellsPerInstance; - if (this.vm.currentStep < minStep) { - console.log( - `PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps` - ); - return false; - } - capacity = (this.vm.currentStep / ratio) * cellsPerInstance; - } - + const { size, capacity } = this.getSizeAndCapacity(builtin); + if (capacity === MISSING_STEPS_CAPACITY) return false; if (size > capacity) { throw new InsufficientAllocatedCells( this.layout.name, @@ -306,8 +291,159 @@ export class CairoRunner { ); } return true; - }); - return builtinChecks.reduce((prev, curr) => prev && curr); + }) + .reduce((prev, curr) => prev && curr); + + const initialBounds: RcLimits = { rcMin: 0, rcMax: 1 }; + const { rcMin, rcMax }: RcLimits = this.builtins + .filter((builtin) => ['range_check', 'range_check96'].includes(builtin)) + .map((builtin) => { + const segment = this.getBuiltinSegment(builtin); + if (!segment) throw new UndefinedBuiltinSegment(builtin); + if (!segment.length) return { rcMin: 0, rcMax: 0 }; + return segment.reduce((_, value) => { + if (!isFelt(value)) throw new ExpectedFelt(value); + const nParts = builtin === 'range_check' ? RC_N_PARTS : RC_N_PARTS_96; + return value + .to64BitsWords() + .flatMap((limb) => + [3, 2, 1, 0].map( + (i) => + (limb >> BigInt(i * INNER_RC_BOUND_SHIFT)) & + INNER_RC_BOUND_MASK + ) + ) + .slice(nParts) + .reduce((bounds, curr) => { + const x = Number(curr); + return { + rcMin: Math.min(bounds.rcMin, x), + rcMax: Math.max(bounds.rcMax, x), + }; + }, initialBounds); + }, initialBounds); + }) + .concat([this.vm.rcLimits]) + .reduce((acc, curr) => ({ + rcMin: Math.min(acc.rcMin, curr.rcMin), + rcMax: Math.max(curr.rcMin, curr.rcMax), + })); + + const usedRcUnits = this.builtins + .filter((builtin) => ['range_check', 'range_check96'].includes(builtin)) + .map((builtin) => { + const segment = this.getBuiltinSegment(builtin); + if (!segment) throw new UndefinedBuiltinSegment(builtin); + return builtin === 'range_check' + ? segment.length * RC_N_PARTS + : segment.length * RC_N_PARTS_96; + }) + .reduce((acc, curr) => acc + curr); + + const unusedRcUnits = + (this.layout.rcUnits - 3) * this.vm.currentStep - usedRcUnits; + const rcUnitsCheck = unusedRcUnits >= rcMax - rcMin; + + const builtinsCapacity = this.builtins + .map((builtin) => this.getSizeAndCapacity(builtin).capacity) + .reduce((acc, curr) => acc + curr); + const totalMemoryCapacity = this.vm.currentStep * MEMORY_UNITS_PER_STEP; + if (totalMemoryCapacity % this.layout.publicMemoryFraction) + throw new Error( + 'Total memory capacity is not a multiple of public memory fraction.' + ); + const publicMemoryCapacity = + totalMemoryCapacity / this.layout.publicMemoryFraction; + + const instructionCapacity = this.vm.currentStep * 4; + const unusedMemoryCapacity = + totalMemoryCapacity - + (publicMemoryCapacity + instructionCapacity + builtinsCapacity); + const memoryHoles = this.vm.memory.segments.reduce( + (acc, currSegment) => + acc + currSegment.reduce((acc, _) => acc--, currSegment.length), + 0 + ); + const memoryCheck = unusedMemoryCapacity >= memoryHoles; + + let dilutedCheck: boolean = true; + const dilutedPool = this.layout.dilutedPool; + if (dilutedPool) { + const dilutedUsedCapacity = this.builtins + .filter((builtin) => ['bitwise', 'keccak'].includes(builtin)) + .map((builtin) => { + const multiplier = + this.layout.name === 'dynamic' + ? this.vm.currentStep + : this.vm.currentStep / this.layout.ratios[builtin]; + if (builtin === 'bitwise') { + const totalNBits = 251; + const { nBits, spacing } = dilutedPool; + const step = nBits * spacing; + const partition: number[] = []; + for (let i = 0; i < totalNBits; i += step) { + for (let j = 0; j < spacing; j++) { + if (i + j < totalNBits) { + partition.push(i + j); + } + } + } + const trimmedNumber = partition.filter( + (value) => value + spacing * (nBits - 1) + 1 > totalNBits + ).length; + + return (4 * partition.length + trimmedNumber) * multiplier; + } + return (KECCAK_DILUTED_CELLS / dilutedPool.nBits) * multiplier; + }) + .reduce((acc, curr) => acc + curr); + + const dilutedUnits = this.vm.currentStep * dilutedPool.unitsPerStep; + const unusedDilutedCapacity = dilutedUnits - dilutedUsedCapacity; + dilutedCheck = unusedDilutedCapacity >= 1 << dilutedPool.nBits; + } + + return builtinChecks && rcUnitsCheck && memoryCheck && dilutedCheck; + } + + /** @returns The size of a builtin and its capacity for the chosen layout. */ + private getSizeAndCapacity(builtin: string): { + size: number; + capacity: number; + } { + const segment = this.getBuiltinSegment(builtin); + if (!segment) throw new UndefinedBuiltinSegment(builtin); + const size = + builtin === 'segment_arena' ? segment.length - 3 : segment.length; + + if (builtin === 'output' || builtin === 'segment_arena') { + return { size, capacity: size }; + } + + const ratio = this.layout.ratios[builtin]; + const cellsPerInstance = CELLS_PER_INSTANCE[builtin]; + let capacity: number = 0; + + switch (this.layout.name) { + case 'dynamic': + const instances = Math.ceil(size / cellsPerInstance); + const instancesPerComponent = builtin === 'keccak' ? 16 : 1; + const components = nextPowerOfTwo(instances / instancesPerComponent); + capacity = cellsPerInstance * instancesPerComponent * components; + break; + default: + const minStep = ratio * cellsPerInstance; + if (this.vm.currentStep < minStep) { + // console.log( + // `PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps` + // ); + return { size, capacity: MISSING_STEPS_CAPACITY }; + } + capacity = (this.vm.currentStep / ratio) * cellsPerInstance; + break; + } + + return { size, capacity }; } /** diff --git a/src/runners/layout.ts b/src/runners/layout.ts index 020b2409..73dafe4e 100644 --- a/src/runners/layout.ts +++ b/src/runners/layout.ts @@ -38,6 +38,8 @@ export const DEFAULT_DILUTED_POOL: DilutedPool = { nBits: 16, }; +export const MEMORY_UNITS_PER_STEP = 8; + /** * Dictionary containing all the available layouts: * - plain diff --git a/src/vm/virtualMachine.ts b/src/vm/virtualMachine.ts index e591131e..746a5beb 100644 --- a/src/vm/virtualMachine.ts +++ b/src/vm/virtualMachine.ts @@ -63,6 +63,12 @@ export type RelocatedMemory = { value: Felt; }; +/** The bounds of the range_check builtins during a run. */ +export type RcLimits = { + rcMin: number; + rcMax: number; +}; + export class VirtualMachine { currentStep: number; memory: Memory; @@ -73,6 +79,7 @@ export class VirtualMachine { squashedDictManager: SquashedDictManager; scopeManager: ScopeManager; trace: TraceEntry[]; + rcLimits: RcLimits; relocatedMemory: RelocatedMemory[]; relocatedTrace: RelocatedTraceEntry[]; @@ -82,6 +89,7 @@ export class VirtualMachine { this.currentStep = 0; this.memory = new Memory(); this.trace = []; + this.rcLimits = { rcMin: 0, rcMax: 0 }; this.relocatedMemory = []; this.relocatedTrace = []; @@ -139,6 +147,12 @@ export class VirtualMachine { this.trace.push({ pc: this.pc, ap: this.ap, fp: this.fp }); + const { dstOffset, op0Offset, op1Offset } = instruction; + this.rcLimits = { + rcMin: Math.min(dstOffset, op0Offset, op1Offset), + rcMax: Math.max(dstOffset, op0Offset, op1Offset), + }; + this.updateRegisters(instruction, res, dst); this.currentStep += 1; From 8a6a1e6a873ecce8f6e8b827da5703a9f39beada Mon Sep 17 00:00:00 2001 From: Malatrax Date: Fri, 2 Aug 2024 18:14:39 +0200 Subject: [PATCH 10/11] chore: cleanup --- src/runners/cairoRunner.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/runners/cairoRunner.ts b/src/runners/cairoRunner.ts index f6209e08..b841cbf8 100644 --- a/src/runners/cairoRunner.ts +++ b/src/runners/cairoRunner.ts @@ -256,7 +256,6 @@ export class CairoRunner { } } - console.log('steps: ', this.vm.currentStep); if (isProofMode || relocate) this.vm.relocate(offset); } @@ -434,9 +433,6 @@ export class CairoRunner { default: const minStep = ratio * cellsPerInstance; if (this.vm.currentStep < minStep) { - // console.log( - // `PROOF MODE (${builtin}): minimum steps (${minStep}) not reached yet: ${this.vm.currentStep} steps` - // ); return { size, capacity: MISSING_STEPS_CAPACITY }; } capacity = (this.vm.currentStep / ratio) * cellsPerInstance; From 13a5ab87bea70cf5f26f97377bd80df3f0a5d11c Mon Sep 17 00:00:00 2001 From: Malatrax Date: Mon, 7 Oct 2024 13:53:07 +0200 Subject: [PATCH 11/11] chore: use proper error on relocatable check --- src/vm/virtualMachine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vm/virtualMachine.ts b/src/vm/virtualMachine.ts index 746a5beb..9c890b51 100644 --- a/src/vm/virtualMachine.ts +++ b/src/vm/virtualMachine.ts @@ -197,7 +197,7 @@ export class VirtualMachine { switch (op1Register) { case Op1Src.Op0: if (!op0) throw new UndefinedOp0(); - if (!isRelocatable(op0)) throw new ExpectedFelt(op0); + if (!isRelocatable(op0)) throw new ExpectedRelocatable(op0); op1Addr = new Relocatable(op0.segmentId, op0.offset + op1Offset); break; default: