UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

603 lines (513 loc) 23 kB
import { UInt8 } from '../int.js'; import { Field } from '../field.js'; import { ZkProgram } from '../../proof-system/zkprogram.js'; import { DynamicArray } from '../dynamic-array.js'; import { assert } from '../gadgets/common.js'; import { Provable } from '../provable.js'; // Out-of-circuit checks of dynamic arrays { function expectThrows(fn: () => void | Promise<void>, msg: string) { let threw = false; try { fn(); } catch { threw = true; } assert(threw, msg); } // Define classes of dynamic arrays for specific provable types class Bytestring extends DynamicArray(UInt8, { capacity: 8 }) {} // Test the constructor with different initializations let fromArray = new Bytestring([new UInt8(1), new UInt8(2), new UInt8(3)]); assert(fromArray.length.equals(new Field(3))); assert(fromArray.capacity === 8); let fromLength = new Bytestring(undefined, new Field(0)); assert(fromLength.length.equals(new Field(0))); assert(fromLength.capacity === 8); let fromArrayLength = new Bytestring( [new UInt8(1), new UInt8(2), new UInt8(3), new UInt8(0)], new Field(3) ); assert(fromArrayLength.length.equals(new Field(3))); assert(fromArrayLength.capacity === 8); assert(fromArrayLength.get(new Field(0)).value.equals(new Field(1))); assert(fromArrayLength.get(new Field(1)).value.equals(new Field(2))); assert(fromArrayLength.get(new Field(2)).value.equals(new Field(3))); fromArrayLength.getOption(new Field(3)).assertNone(); // Copy into new dynamic arrays let copySame = fromArray.copy(); assert(copySame.length.equals(fromArray.length)); assert(copySame.capacity === fromArray.capacity); assert(copySame.get(new Field(0)).value.equals(new Field(1))); assert(copySame.get(new Field(1)).value.equals(new Field(2))); assert(copySame.get(new Field(2)).value.equals(new Field(3))); for (let i = 3; i < 8; i++) { copySame.getOption(new Field(i)).assertNone(); } // Initialize an empty dynamic array let bytes = new Bytestring(); // Initialization should produce right capacity and length assert(bytes.capacity === 8); assert(bytes.length.equals(new Field(0))); // Pushing elements should increase length but keep capacity bytes.push(new UInt8(1)); assert(bytes.length.equals(new Field(1))); assert(bytes.capacity === 8); assert(bytes.get(new Field(0)).value.equals(new Field(1))); // Popping elements should decrease length but keep capacity bytes.pop(); assert(bytes.length.equals(new Field(0))); assert(bytes.capacity === 8); // Cannot push more elements than the capacity for (let i = 0; i < 8; i++) { bytes.push(new UInt8(i)); } expectThrows(() => { bytes.push(new UInt8(8)); }, 'Cannot push more elements than the capacity'); // Popping zero elements should not change the array bytes.pop(new Field(0)); // Popping multiple elements should decrease length by the specified amount assert(bytes.length.equals(new Field(8))); bytes.pop(new Field(8)); assert(bytes.length.equals(new Field(0))); // Cannot pop more elements than the current length expectThrows(() => { bytes.pop(); }, 'Cannot pop more elements than the length'); // Empty behaviour should be correct assert(bytes.isEmpty().toBoolean()); bytes.push(new UInt8(1)); assert(!bytes.isEmpty().toBoolean()); // Getters and setters for existing positions assert(bytes.get(new Field(0)).value.equals(new Field(1))); bytes.set(new Field(0), new UInt8(2)); assert(bytes.get(new Field(0)).value.equals(new Field(2))); assert(bytes.length.equals(new Field(1))); // getOption returns None for out-of-bounds and Some for in-bounds index bytes.getOption(new Field(0)).assertSome(); bytes.getOption(new Field(1)).assertNone(); // Error if getting out-of-bounds index expectThrows(() => { bytes.get(new Field(1)); }, 'Cannot get out-of-bounds index'); // Error if setting out-of-bounds index expectThrows(() => { bytes.set(new Field(1), new UInt8(3)); }, 'Cannot set out-of-bounds index'); // Growing capacity should work correctly let longerArray = bytes.growCapacityTo(10); assert(longerArray.capacity === 10); assert(longerArray.length.equals(new Field(1))); // Growing capacity by increment should work correctly let sameArray = longerArray.growCapacityBy(0); assert(sameArray.capacity === 10); assert(sameArray.length.equals(new Field(1))); let otherArray = bytes.growCapacityBy(2); assert(otherArray.capacity === 10); assert(otherArray.length.equals(new Field(1))); // Mapping over elements should work correctly bytes.push(new UInt8(1)); bytes.push(new UInt8(0)); let mapped = bytes.map(UInt8, (value) => value.add(UInt8.from(1))); assert(mapped.get(new Field(0)).value.equals(new Field(3))); assert(mapped.get(new Field(1)).value.equals(new Field(2))); assert(mapped.get(new Field(2)).value.equals(new Field(1))); // Reinstantiating the array for further tests bytes = new Bytestring(); for (let i = 0; i < 8; i++) { bytes.push(new UInt8(10 + i)); } // Shifting left 0 positions does not change the array bytes.shiftLeft(new Field(0)); assert(bytes.length.equals(new Field(8))); for (let i = 0; i < 8; i++) { assert(bytes.get(new Field(i)).value.equals(new Field(10 + i))); } // Checks shifting left elements bytes.shiftLeft(new Field(3)); assert(bytes.length.equals(new Field(5))); assert(bytes.get(new Field(0)).value.equals(new Field(13))); assert(bytes.get(new Field(1)).value.equals(new Field(14))); assert(bytes.get(new Field(2)).value.equals(new Field(15))); assert(bytes.get(new Field(3)).value.equals(new Field(16))); assert(bytes.get(new Field(4)).value.equals(new Field(17))); bytes.getOption(new Field(5)).assertNone(); bytes.getOption(new Field(6)).assertNone(); bytes.getOption(new Field(7)).assertNone(); // Shift left by the length bytes.shiftLeft(bytes.length); assert(bytes.length.equals(new Field(0))); // Cannot shift left more elements than the current length expectThrows(() => { bytes.shiftLeft(new Field(1)); }, 'Cannot shift left further than the length'); // Reinstantiating the array for further tests with space for shifting bytes = new Bytestring(); for (let i = 0; i < 5; i++) { bytes.push(new UInt8(10 + i)); } // Shifting right 0 positions does not change the array bytes.shiftRight(new Field(0)); assert(bytes.length.equals(new Field(5))); for (let i = 0; i < 5; i++) { assert(bytes.get(new Field(i)).value.equals(new Field(10 + i))); } // Checks shifting right elements bytes.shiftRight(new Field(2)); assert(bytes.length.equals(new Field(7))); let NULL = new Field(0); assert(bytes.get(new Field(0)).value.equals(NULL)); assert(bytes.get(new Field(1)).value.equals(NULL)); assert(bytes.get(new Field(2)).value.equals(new Field(10))); assert(bytes.get(new Field(3)).value.equals(new Field(11))); assert(bytes.get(new Field(4)).value.equals(new Field(12))); assert(bytes.get(new Field(5)).value.equals(new Field(13))); assert(bytes.get(new Field(6)).value.equals(new Field(14))); bytes.getOption(new Field(7)).assertNone(); // Cannot shift right more elements than the capacity expectThrows(() => { bytes.shiftRight(new Field(2)); }, 'Cannot shift right above capacity'); // Slicing [0, length) should return the same array bytes = new Bytestring(); for (let i = 0; i < 4; i++) { bytes.push(new UInt8(10 + i)); } let whole = bytes.slice(new Field(0), bytes.length); assert(bytes.length.equals(new Field(4))); assert(whole.length.equals(bytes.length)); for (let i = 0; i < 4; i++) { assert(whole.get(new Field(i)).value.equals(bytes.get(new Field(i)).value)); } // Slicing [0, 0) should return an empty array let empty = bytes.slice(new Field(0), new Field(0)); assert(bytes.length.equals(new Field(4))); assert(empty.length.equals(new Field(0))); // Slicing [0, 1) should return an array with the first element let first = bytes.slice(new Field(0), new Field(1)); assert(bytes.length.equals(new Field(4))); assert(first.length.equals(new Field(1))); assert(first.get(new Field(0)).value.equals(bytes.get(new Field(0)).value)); // Slicing intermediate positions should work correctly let intermediate = bytes.slice(new Field(1), new Field(3)); assert(intermediate.length.equals(new Field(2))); assert(intermediate.get(new Field(0)).value.equals(bytes.get(new Field(1)).value)); assert(intermediate.get(new Field(1)).value.equals(bytes.get(new Field(2)).value)); // Cannot slice out-of-bounds positions expectThrows(() => { bytes.slice(new Field(1), new Field(5)); }, 'Cannot slice out-of-bounds positions'); // Cannot slice with end position smaller than start position expectThrows(() => { bytes.slice(new Field(2), new Field(1)); }, 'Cannot slice with end position smaller than start position'); // Concatenate two empty arrays gives an empty array let emptyLeft = new Bytestring(); let emptyRight = new Bytestring(); let emptyConcat = emptyLeft.concat(emptyRight); assert(emptyConcat.length.equals(new Field(0))); assert(emptyConcat.capacity === 16); // Concatenate an empty array with a non-empty array gives the non-empty array let right = new Bytestring([new UInt8(10), new UInt8(20), new UInt8(30)]); let nonEmptyRight = emptyLeft.concat(right); assert(nonEmptyRight.length.equals(new Field(3))); assert(nonEmptyRight.capacity === 16); for (let i = 0; i < 3; i++) { assert(nonEmptyRight.get(new Field(i)).value.equals(right.get(new Field(i)).value)); } // Concatenate a non-empty array with an empty array gives the non-empty array let left = new Bytestring([ new UInt8(1), new UInt8(2), new UInt8(3), new UInt8(4), new UInt8(5), new UInt8(6), new UInt8(7), new UInt8(8), ]); let nonEmptyLeft = left.concat(emptyRight); assert(nonEmptyLeft.length.equals(new Field(8))); assert(nonEmptyLeft.capacity === 16); for (let i = 0; i < 8; i++) { assert(nonEmptyLeft.get(new Field(i)).value.equals(left.get(new Field(i)).value)); } // Concatenate two non-empty arrays gives the concatenation of both let both = left.concat(right); assert(both.length.equals(new Field(11))); assert(both.capacity === 16); for (let i = 0; i < 8; i++) { assert(both.get(new Field(i)).value.equals(left.get(new Field(i)).value)); } for (let i = 0; i < 3; i++) { assert(both.get(new Field(i + 8)).value.equals(right.get(new Field(i)).value)); } // Inserting elements at the beginning of the array bytes = new Bytestring([new UInt8(2), new UInt8(3), new UInt8(4), new UInt8(6), new UInt8(7)]); bytes.insert(new Field(0), new UInt8(1)); assert(bytes.length.equals(new Field(6))); assert(bytes.get(new Field(0)).value.equals(new Field(1))); assert(bytes.get(new Field(1)).value.equals(new Field(2))); assert(bytes.get(new Field(2)).value.equals(new Field(3))); assert(bytes.get(new Field(3)).value.equals(new Field(4))); assert(bytes.get(new Field(4)).value.equals(new Field(6))); assert(bytes.get(new Field(5)).value.equals(new Field(7))); // Inserting elements at the end of the array bytes.insert(bytes.length, new UInt8(8)); assert(bytes.length.equals(new Field(7))); assert(bytes.get(new Field(0)).value.equals(new Field(1))); assert(bytes.get(new Field(1)).value.equals(new Field(2))); assert(bytes.get(new Field(2)).value.equals(new Field(3))); assert(bytes.get(new Field(3)).value.equals(new Field(4))); assert(bytes.get(new Field(4)).value.equals(new Field(6))); assert(bytes.get(new Field(5)).value.equals(new Field(7))); assert(bytes.get(new Field(6)).value.equals(new Field(8))); // Inserting elements in the middle of the array bytes.insert(new Field(4), new UInt8(5)); assert(bytes.length.equals(new Field(8))); for (let i = 0; i < 8; i++) { assert(bytes.get(new Field(i)).value.equals(new Field(i + 1))); } // Cannot insert elements exceeding capacity expectThrows(() => { bytes.insert(new Field(1), new UInt8(0)); }, 'Cannot insert above capacity'); // Cannot insert elements out-of-bounds bytes = new Bytestring([new UInt8(1), new UInt8(2), new UInt8(3)]); expectThrows(() => { bytes.insert(new Field(4), new UInt8(5)); }, 'Cannot insert out-of-bounds'); // Checking inclusion of elements assert(bytes.includes(new UInt8(1)).toBoolean()); assert(bytes.includes(new UInt8(20)).not().toBoolean()); } // Using dynamic arrays as private input { class Bytestring extends DynamicArray(UInt8, { capacity: 8 }) {} let AsPrivateInput = ZkProgram({ name: 'dynamicarrays', methods: { pushAndPop: { privateInputs: [Bytestring, UInt8], async method(bytes: Bytestring, v: UInt8) { let last = bytes.length; bytes.push(v); assert(bytes.get(last).value.equals(v.value)); bytes.pop(last); assert(bytes.isEmpty().not()); bytes.pop(); assert(bytes.isEmpty()); }, }, }, }); await AsPrivateInput.compile(); let bytes = new Bytestring([new UInt8(1), new UInt8(2), new UInt8(3)]); await AsPrivateInput.pushAndPop(bytes, UInt8.from(4)); } // Provable behaviour { class Bytestring extends DynamicArray(UInt8, { capacity: 8 }) {} await Provable.runAndCheck(() => { let b = Provable.witness(Bytestring, () => new Bytestring()); b.push(UInt8.from(1)); }); } // In-circuit test for dynamic arrays { let List = ZkProgram({ name: 'dynamicarrays-circuit', methods: { incircuit: { privateInputs: [UInt8], async method(v0: UInt8) { // Define classes of dynamic arrays for specific provable types class Bytestring extends DynamicArray(UInt8, { capacity: 8 }) {} // Initialize an empty dynamic array let bytes = new Bytestring(); // Pushing and popping elements bytes.push(v0); assert(bytes.get(new Field(0)).value.equals(v0.value)); // Popping elements should decrease length but keep capacity bytes.pop(); // Cannot push more elements than the capacity for (let i = 0; i < 8; i++) { bytes.push(v0.add(new UInt8(i))); } // Popping zero elements should not change the array bytes.pop(new Field(0)); for (let i = 0; i < 8; i++) { assert(bytes.get(new Field(i)).value.equals(new Field(i).add(v0.value))); } // Popping multiple elements should decrease length by the specified amount bytes.pop(new Field(8)); // Empty behaviour should be correct assert(bytes.isEmpty()); bytes.push(v0); assert(bytes.isEmpty().not()); // Getters and setters for existing positions bytes.set(new Field(0), v0.mul(new UInt8(2))); assert(bytes.get(new Field(0)).value.equals(new Field(2).mul(v0.value))); // getOption returns None for out-of-bounds and Some for in-bounds index bytes.getOption(new Field(0)).assertSome(); bytes.getOption(new Field(1)).assertNone(); // Growing capacity should work correctly let longerArray = bytes.growCapacityTo(10); // Growing capacity by increment should work correctly let sameArray = longerArray.growCapacityBy(0); let otherArray = bytes.growCapacityBy(2); assert(longerArray.get(new Field(0)).value.equals(sameArray.get(new Field(0)).value)); assert(otherArray.get(new Field(0)).value.equals(bytes.get(new Field(0)).value)); // Mapping over elements should work correctly bytes.push(v0.add(new UInt8(1))); bytes.push(v0.mul(new UInt8(0))); let mapped = bytes.map(UInt8, (value) => value.add(UInt8.from(1))); assert( mapped.get(new Field(0)).value.equals(v0.value.mul(new Field(2)).add(new Field(1))) ); assert(mapped.get(new Field(1)).value.equals(v0.value.add(new Field(2)))); assert(mapped.get(new Field(2)).value.equals(new Field(1))); // Reinstantiating the array for further tests bytes = new Bytestring(); for (let i = 0; i < 8; i++) { bytes.push(v0.sub(new UInt8(i))); } // Shifting left 0 positions does not change the array bytes.shiftLeft(new Field(0)); for (let i = 0; i < 8; i++) { assert(bytes.get(new Field(i)).value.equals(v0.value.sub(new Field(i)))); } // Checks shifting left elements let shl = 3; bytes.shiftLeft(new Field(shl)); for (let i = 0; i < 8 - shl; i++) { assert(bytes.get(new Field(i)).value.equals(v0.value.sub(new Field(i + shl)))); } for (let i = 8 - shl; i < 8; i++) { bytes.getOption(new Field(i)).assertNone(); } // Reinstantiating the array for further tests with space for shifting bytes = new Bytestring(); for (let i = 0; i < 5; i++) { bytes.push(v0.add(new UInt8(10 + i))); } // Shifting right 0 positions does not change the array bytes.shiftRight(new Field(0)); for (let i = 0; i < 5; i++) { assert(bytes.get(new Field(i)).value.equals(v0.value.add(new Field(10 + i)))); } // Checks shifting right elements let shr = 2; bytes.shiftRight(new Field(shr)); let NULL = new Field(0); for (let i = 0; i < shr; i++) { assert(bytes.get(new Field(i)).value.equals(NULL)); } for (let i = shr; i < 7; i++) { assert(bytes.get(new Field(i)).value.equals(v0.value.add(new Field(10 + i - shr)))); } bytes.getOption(new Field(7)).assertNone(); // Slicing [0, length) should return the same array bytes = new Bytestring(); for (let i = 0; i < 4; i++) { bytes.push(v0.add(new UInt8(i))); } let whole = bytes.slice(new Field(0), bytes.length); for (let i = 0; i < 4; i++) { assert(whole.get(new Field(i)).value.equals(bytes.get(new Field(i)).value)); } // Slicing [0, 0) should return an empty array let empty = bytes.slice(new Field(0), new Field(0)); assert(empty.isEmpty()); // Slicing [0, 1) should return an array with the first element let first = bytes.slice(new Field(0), new Field(1)); assert(first.get(new Field(0)).value.equals(bytes.get(new Field(0)).value)); // Slicing intermediate positions should work correctly let intermediate = bytes.slice(new Field(1), new Field(3)); assert(intermediate.get(new Field(0)).value.equals(bytes.get(new Field(1)).value)); assert(intermediate.get(new Field(1)).value.equals(bytes.get(new Field(2)).value)); // Concatenate two empty arrays gives an empty array let emptyLeft = new Bytestring(); let emptyRight = new Bytestring(); let emptyConcat = emptyLeft.concat(emptyRight); assert(emptyConcat.isEmpty()); // Concatenate an empty array with a non-empty array gives the non-empty array let right = new Bytestring([new UInt8(10), new UInt8(20), new UInt8(30)]); let nonEmptyRight = emptyLeft.concat(right); for (let i = 0; i < 3; i++) { assert(nonEmptyRight.get(new Field(i)).value.equals(right.get(new Field(i)).value)); } // Concatenate a non-empty array with an empty array gives the non-empty array let left = new Bytestring([ new UInt8(1), new UInt8(2), new UInt8(3), new UInt8(4), new UInt8(5), new UInt8(6), new UInt8(7), new UInt8(8), ]); let nonEmptyLeft = left.concat(emptyRight); assert(nonEmptyLeft.capacity === 16); for (let i = 0; i < 8; i++) { assert(nonEmptyLeft.get(new Field(i)).value.equals(left.get(new Field(i)).value)); } // Concatenate two non-empty arrays gives the concatenation of both let both = left.concat(right); assert(both.capacity === 16); for (let i = 0; i < 8; i++) { assert(both.get(new Field(i)).value.equals(left.get(new Field(i)).value)); } for (let i = 0; i < 3; i++) { assert(both.get(new Field(i + 8)).value.equals(right.get(new Field(i)).value)); } // Inserting elements at the beginning of the array bytes = new Bytestring([ new UInt8(2), new UInt8(3), new UInt8(4), new UInt8(6), new UInt8(7), ]); bytes.insert(new Field(0), new UInt8(1)); assert(bytes.get(new Field(0)).value.equals(new Field(1))); assert(bytes.get(new Field(1)).value.equals(new Field(2))); assert(bytes.get(new Field(2)).value.equals(new Field(3))); assert(bytes.get(new Field(3)).value.equals(new Field(4))); assert(bytes.get(new Field(4)).value.equals(new Field(6))); assert(bytes.get(new Field(5)).value.equals(new Field(7))); // Inserting elements at the end of the array bytes.insert(bytes.length, new UInt8(8)); assert(bytes.get(new Field(0)).value.equals(new Field(1))); assert(bytes.get(new Field(1)).value.equals(new Field(2))); assert(bytes.get(new Field(2)).value.equals(new Field(3))); assert(bytes.get(new Field(3)).value.equals(new Field(4))); assert(bytes.get(new Field(4)).value.equals(new Field(6))); assert(bytes.get(new Field(5)).value.equals(new Field(7))); assert(bytes.get(new Field(6)).value.equals(new Field(8))); // Inserting elements in the middle of the array bytes.insert(new Field(4), new UInt8(5)); for (let i = 0; i < 8; i++) { assert(bytes.get(new Field(i)).value.equals(new Field(i + 1))); } // Checking inclusion of elements assert(bytes.includes(new UInt8(1))); assert(bytes.includes(new UInt8(20)).not()); // Reverse the array let reversed = bytes.reverse(); for (let i = 0; i < 8; i++) { assert(reversed.get(new Field(i)).value.equals(new Field(8 - i))); // the original array is not modified assert(bytes.get(new Field(i)).value.equals(new Field(i + 1))); } }, }, }, }); await List.compile(); let { proof } = await List.incircuit(new UInt8(100)); let isValid = await List.verify(proof); assert(isValid, 'Proof for dynamic arrays should be verified'); }