UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

568 lines 24.5 kB
var _DynamicArrayBase_instances, _DynamicArrayBase_indexMasks, _DynamicArrayBase_indicesInRange, _DynamicArrayBase_dummies, _DynamicArrayBase_indexMask, _DynamicArrayBase_dummyMask; import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; /* * Array type inspired by zksecurity's implementation at * https://github.com/zksecurity/mina-attestations and * gretke's at https://github.com/gretzke/zkApp-data-types */ import { Bool } from './bool.js'; import { Field } from './field.js'; import { Provable } from './provable.js'; import { provable as struct } from './types/provable-derivers.js'; import { Option } from './option.js'; import { ProvableType } from './types/provable-intf.js'; import { assert } from './gadgets/common.js'; import { zip, pad } from '../util/arrays.js'; import { arrayGet } from './gadgets/basic.js'; // external API export { DynamicArray }; /** * Dynamic-length array type that has a * - constant maximum capacity, but * - dynamic actual length * * ```ts * const Bytes = DynamicArray(UInt8, { capacity: 32 }); * ``` * * `capacity` can be any number from 0 to 2^16-1. * * **Details**: Internally, this is represented as a static-sized array, plus a * Field element that represents the length. * The _only_ requirement on these is that the length is less or equal capacity. * In particular, there are no provable guarantees maintained on the content of * the static-sized array beyond the actual length. Instead, our methods ensure * integrity of array operations _within_ the actual length. */ function DynamicArray(type, { capacity, }) { let innerType = ProvableType.get(type); // assert capacity bounds assert(capacity >= 0, 'DynamicArray(): capacity must be >= 0'); assert(capacity < 2 ** 16, 'DynamicArray(): capacity must be < 2^16'); class DynamicArray_ extends DynamicArrayBase { get innerType() { return innerType; } static get capacity() { return capacity; } static get provable() { return provableArray; } static from(input) { return provableArray.fromValue(input); } } const provableArray = provable(innerType, DynamicArray_); return DynamicArray_; } class DynamicArrayBase { // properties to override in subclass get innerType() { throw Error('Inner type must be defined in a subclass'); } static get capacity() { throw Error('Capacity must be defined in a subclass'); } // derived property get capacity() { return this.constructor.capacity; } get Constructor() { return this.constructor; } /** * Create a new {@link DynamicArrayBase} instance from an optional list of * {@link ProvableValue} elements, and optional length. * * - If no parameters are passed, it creates an empty array of length 0. * - If only `array` is passed, it creates a new array with the elements * of the array. * - If only `length` is passed, it creates a dummy array of the given length * filled with NULL values. * - If both `array` and `length` are passed, it creates a new array with the * elements of the array, and the length of the dynamic array is set to the * `length` passed, which should be less or equal than the length of the * `array` passed (this is to allow for arrays with dummy values at the end). * - In any case, if `length` is larger than the capacity, it throws. * * @example * ```ts * let arr = new DynamicArray([new Field(1), new Field(2), new Field(3)]); * let empty = new DynamicArray(); * ``` * * Note: this is different from `ProvableValue[]` because it is a provable type. */ constructor(array, length) { _DynamicArrayBase_instances.add(this); // cached variables to not duplicate constraints if we do something like // array.get(i), array.set(i, ..) on the same index _DynamicArrayBase_indexMasks.set(this, new Map()); _DynamicArrayBase_indicesInRange.set(this, new Set()); _DynamicArrayBase_dummies.set(this, void 0); const NULL = ProvableType.synthesize(this.innerType); const a = array ?? []; const l = length ?? new Field(a.length); assert(a.length <= this.capacity, 'DynamicArray(): array must not exceed capacity'); if (length?.isConstant()) { assert(l.toBigInt() <= BigInt(a.length), 'DynamicArray(): length must be at most as long as the array'); } this.array = pad(a, this.capacity, NULL); this.length = l; } /** * In-circuit assertion that the given index is within the bounds of the * dynamic array. * Asserts 0 <= i < this.length, using a cached check that's not * duplicated when doing it on the same variable multiple times, failing * with an error message otherwise. * * @param i - the index to check * @param message - optional error message to use in case the assertion fails */ assertIndexInRange(i, message) { let errorMessage = message ?? `assertIndexInRange(): index must be in range [0, length]`; if (!__classPrivateFieldGet(this, _DynamicArrayBase_indicesInRange, "f").has(i)) { if (i.isConstant() && this.length.isConstant()) { assert(i.toBigInt() < this.length.toBigInt(), errorMessage); } i.assertLessThan(this.length, errorMessage); __classPrivateFieldGet(this, _DynamicArrayBase_indicesInRange, "f").add(i); } } /** * Gets value at index i, and proves that the index is in the array. * It uses an internal cache to avoid duplication of constraints when the * same index is used multiple times. */ get(i) { this.assertIndexInRange(i); return this.getOrUnconstrained(i); } /** * Gets a value at index i, as an option that is None if the index is not in * the array. * * Note: The correct type for `i` is actually UInt16 which doesn't exist. The * method is not complete (but sound) for i >= 2^16. This means that if the * index is larger than 2^16, the constraints could be satisfiable but the * result is not correct (because the capacity can at most be 2^16). */ getOption(i) { let type = this.innerType; let isContained = i.lessThan(this.length); let value = this.getOrUnconstrained(i); const OptionT = Option(type); return OptionT.fromValue({ isSome: isContained, value }); } /** * Gets a value at index i, ASSUMING that the index is in the array. * * If the index is in fact not in the array, the return value is completely * unconstrained. * * **Warning**: Only use this if you already know/proved by other means that * the index is within bounds. */ getOrUnconstrained(i) { let type = this.innerType; let NULL = ProvableType.synthesize(type); let ai = Provable.witness(type, () => this.array[Number(i)] ?? NULL); let aiFields = type.toFields(ai); // assert a is correct on every field column with arrayGet() let fields = this.array.map((t) => type.toFields(t)); // this allows each array entry to be larger than a single field element for (let j = 0; j < type.sizeInFields(); j++) { let column = fields.map((x) => x[j]); arrayGet(column, i).assertEquals(aiFields[j]); } return ai; } /** * Sets a value at index i and proves that the index is in the array. */ set(i, value, message) { let errorMessage = message ?? `set(): index must be in range [0, length]`; this.assertIndexInRange(i, errorMessage); this.setOrDoNothing(i, value); } /** * Sets a value at index i, or does nothing if the index is not in the array */ setOrDoNothing(i, value) { zip(this.array, __classPrivateFieldGet(this, _DynamicArrayBase_instances, "m", _DynamicArrayBase_indexMask).call(this, i)).forEach(([t, equalsIJ], i) => { this.array[i] = Provable.if(equalsIJ, this.innerType, value, t); }); } /** * Map every element of the array to a new value. * * **Warning**: The callback will be passed unconstrained dummy values. */ map(type, f) { let Array = DynamicArray(type, { capacity: this.capacity }); let provable = ProvableType.get(type); let array = this.array.map((x) => provable.fromValue(f(x))); let newArray = new Array(array, this.length); // new array has same length/capacity, so it can use the same cached masks __classPrivateFieldSet(newArray, _DynamicArrayBase_indexMasks, __classPrivateFieldGet(this, _DynamicArrayBase_indexMasks, "f"), "f"); __classPrivateFieldSet(newArray, _DynamicArrayBase_indicesInRange, __classPrivateFieldGet(this, _DynamicArrayBase_indicesInRange, "f"), "f"); __classPrivateFieldSet(newArray, _DynamicArrayBase_dummies, __classPrivateFieldGet(this, _DynamicArrayBase_dummies, "f"), "f"); return newArray; } /** * Iterate over all elements of the array. * * The callback will be passed an element and a boolean `isDummy` indicating * whether the value is part of the actual array. Optionally, an index can be * passed as a third argument (used in `forEachReversed`) */ forEach(f) { zip(this.array, __classPrivateFieldGet(this, _DynamicArrayBase_instances, "m", _DynamicArrayBase_dummyMask).call(this)).forEach(([t, isDummy], i) => f(t, isDummy, i)); } /** * Iterate over all elements of the array, in reverse order. * * The callback will be passed an element and a boolean `isDummy` indicating whether the value is part of the actual array. * * Note: the indices are also passed in reverse order, i.e. we always have `t = this.array[i]`. */ forEachReverse(f) { zip(this.array, __classPrivateFieldGet(this, _DynamicArrayBase_instances, "m", _DynamicArrayBase_dummyMask).call(this)) .reverse() .forEach(([t, isDummy], i) => { f(t, isDummy, this.capacity - 1 - i); }); } /** * Return a version of the same array with a larger capacity. * * **Warning**: Does not modify the array, but returns a new one. * * **Note**: this doesn't cost constraints, but currently doesn't preserve any * cached constraints. * * @param capacity - the new capacity of the array */ growCapacityTo(capacity, message) { let errorMessage = message ?? `growCapacityTo: new capacity ${capacity} must be greater than current capacity ${this.capacity}`; assert(capacity >= this.capacity, errorMessage); let NewArray = DynamicArray(this.innerType, { capacity }); let NULL = ProvableType.synthesize(this.innerType); let array = pad(this.array, capacity, NULL); return new NewArray(array, this.length); } /** * Return a version of the same array with a larger capacity. * * **Warning**: Does not modify the array, but returns a new one. * * **Note**: this doesn't cost constraints, but currently doesn't preserve any * cached constraints. * * @param increment - the amount to increase the capacity by */ growCapacityBy(increment) { return this.growCapacityTo(this.capacity + increment); } /** * Increments the length of the current array by n elements, checking that the * new length is within the capacity, failing with the error message otherwise. * * @param n - the number of elements to increase the length by * @param message - optional error message to use in case the assertion fails */ increaseLengthBy(n, message) { let errorMessage = message ?? `increaseLengthBy: cannot increase length because provided n would exceed capacity ${this.capacity}.`; let newLength = this.length.add(n).seal(); newLength.assertLessThanOrEqual(new Field(this.capacity), errorMessage); this.length = newLength; } /** * Decrements the length of the current array by `n` elements, checking that * the `n` is less or equal than the current length, failing with the error * message otherwise. * * @param n - the number of elements to decrease the length by * @param message - optional error message to use in case the assertion fails */ decreaseLengthBy(n, message) { let errorMessage = message ?? `decreaseLengthBy: cannot decrease length because provided n is larger than current array length`; let oldLength = this.length; n.assertLessThanOrEqual(this.length, errorMessage); this.length = oldLength.sub(n).seal(); } /** * Sets the length of the current array to a new value, checking that the * new length is less or equal than the capacity. * * An optional error message can be provided to be used in case the inner * assertion fails. * * @param newLength - the new length to set the array to * @param message - optional error message * * **Warning**: This does not change (add nor remove) the values of the array. */ setLengthTo(n, message) { let errorMessage = message ?? `setLengthTo: cannot set length to n because it exceeds capacity ${this.capacity}`; n.assertLessThanOrEqual(new Field(this.capacity), errorMessage); this.length = n; } /** * Push a value, without changing the capacity. * * Proves that the new length is still within the capacity, fails otherwise. * * To grow the capacity along with the actual length, you can use: * * ```ts * array = array.growCapacityhBy(1); * array.push(value); * ``` * * @param value - the value to push into the array * @param message - optional error message to use in case the assertion fails */ push(value, message) { let errorMessage = message ?? `push(): cannot push value because it would exceed capacity ${this.capacity}.`; let oldLength = this.length; this.increaseLengthBy(new Field(1), errorMessage); this.setOrDoNothing(oldLength, value); } /** * Removes the last `n` elements from the dynamic array, decreasing the length * by n. If no amount is provided, only one element is popped. The popped * positions are set to NULL values. * * @param n - the number of elements to pop (one if not provided) * @param message - optional error message to use in case the assertion fails */ pop(n, message) { let errorMessage = message ?? `pop(): cannot pop n elements because the length is smaller`; let dec = n !== undefined ? n : new Field(1); this.decreaseLengthBy(dec, errorMessage); let NULL = ProvableType.synthesize(this.innerType); if (n !== undefined) { // set the last n elements to NULL for (let i = 0; i < this.capacity; i++) { this.array[i] = Provable.if(new Field(i).lessThanOrEqual(this.length), this.innerType, this.array[i], NULL); } } else { // set the last element to NULL this.setOrDoNothing(this.length, NULL); } } /** * In-circuit check whether the array is empty. * * @returns true or false depending on whether the dynamic array is empty */ isEmpty() { return this.length.equals(0); } /** * Shifts all elements of the array to the left by `n` positions, reducing * the length by `n`, which must be less than or equal to the current length * (failing with an error message otherwise). * * @param n - the number of positions to shift left * @param message - optional error message to use in case the assertion fails */ shiftLeft(n, message) { let errorMessage = message ?? `shiftLeft(): cannot shift left because provided n would exceed current length.`; let NULL = ProvableType.synthesize(this.innerType); for (let i = 0; i < this.capacity; i++) { let offset = new Field(i).add(n); this.array[i] = Provable.if(offset.lessThan(this.length), this.innerType, this.getOrUnconstrained(offset), NULL); } this.decreaseLengthBy(n, errorMessage); } /** * Shifts all elements of the array to the right by `n` positions, increasing * the length by `n`, which must result in less than or equal to the capacity * (failing with an error message otherwise). The new elements on the left are * set to NULL values. * * @param n - the number of positions to shift right * @param message - optional error message to use in case the assertion fails */ shiftRight(n, message) { let errorMessage = message ?? `shiftRight(): cannot shift right because provided n would exceed capacity ${this.capacity}`; this.increaseLengthBy(n, errorMessage); let NULL = ProvableType.synthesize(this.innerType); for (let i = this.capacity - 1; i >= 0; i--) { let offset = new Field(i).sub(n); this.array[i] = Provable.if(new Field(i).lessThan(n), this.innerType, NULL, this.getOrUnconstrained(offset)); } } /** * Copies the current dynamic array, returning a new instance with the same * values and length. * * @returns a new DynamicArray instance with the same values as the current. * */ copy() { let newArr = new this.constructor(); newArr.array = this.array.slice(); newArr.length = this.length; return newArr; } /** * Creates a new dynamic array with the values of the current array from * index `start` (included) to index `end` (excluded). If `start` is not * provided, it defaults to 0. If `end` is not provided, it defaults to the * length of the array. * * @param start - the starting index of the slice (inclusive) * @param end - the ending index of the slice (exclusive) * * @returns a new DynamicArray instance with the sliced values */ slice(start, end) { start ??= new Field(0); end ??= this.length; let sliced = this.copy(); sliced.shiftLeft(start, `slice(): provided start is greater than current length`); sliced.pop(this.length.sub(end), `slice(): provided end is greater than current length`); return sliced; } /** * Returns a new array with the elements reversed. */ reverse() { let Array = DynamicArray(this.innerType, { capacity: this.capacity }); // first, copy the inner array of length capacity and reverse it let array = this.array.slice().reverse(); // now, slice off the padding that is now at the beginning of the array let capacity = new Field(this.capacity); return new Array(array, capacity).slice(capacity.sub(this.length).seal()); } /** * Concatenates the current array with another dynamic array, returning a new * dynamic array with the values of both arrays. The capacity of the new array * is the sum of the capacities of the two arrays. * * @param other - the dynamic array to concatenate * * @returns a new DynamicArray instance with the concatenated values */ concat(other) { let res = this.growCapacityTo(this.capacity + other.capacity); let offset = new Field(0).sub(new Field(this.length)); for (let i = 0; i < res.capacity; i++) { res.array[i] = Provable.if(new Field(i).lessThan(this.length), this.innerType, this.getOrUnconstrained(new Field(i)), other.getOrUnconstrained(offset)); offset = offset.add(new Field(1)); } res.length = this.length.add(other.length); return res; } /** * Inserts a value at index i, shifting all elements after that position to * the right by one. The length of the array is increased by one, which must * result in less than or equal to the capacity. * * @param i - the index at which to insert the value * @param value - the value to insert * @param message - optional error message to use in case the assertion fails */ insert(index, value, message) { let errorMessage = message ?? `insert(): cannot insert value at index because it would exceed capacity ${this.capacity}.`; const right = this.slice(index, this.length); this.increaseLengthBy(new Field(1), errorMessage); this.set(index, value); for (let i = 0; i < this.capacity; i++) { let offset = new Field(i).sub(index).sub(new Field(1)); this.array[i] = Provable.if(new Field(i).lessThanOrEqual(index), this.innerType, this.getOrUnconstrained(new Field(i)), right.getOrUnconstrained(offset)); } } /** * Checks whether the dynamic array includes a value. * * @param value - the value to check for inclusion in the array * @returns */ includes(value) { let type = this.innerType; let isIncluded = this.array.map((t) => Provable.equal(type, t, value)); let isSome = isIncluded.reduce((acc, curr) => acc.or(curr), new Bool(false)); return isSome; } /** * Converts the current instance of the dynamic array to a plain array of values. * * @returns An array of values representing the elements in the dynamic array. */ toValue() { return this.constructor.provable.toValue(this); } } _DynamicArrayBase_indexMasks = new WeakMap(), _DynamicArrayBase_indicesInRange = new WeakMap(), _DynamicArrayBase_dummies = new WeakMap(), _DynamicArrayBase_instances = new WeakSet(), _DynamicArrayBase_indexMask = function _DynamicArrayBase_indexMask(i) { let mask = __classPrivateFieldGet(this, _DynamicArrayBase_indexMasks, "f").get(i); mask ??= this.array.map((_, j) => i.equals(j)); __classPrivateFieldGet(this, _DynamicArrayBase_indexMasks, "f").set(i, mask); return mask; }, _DynamicArrayBase_dummyMask = function _DynamicArrayBase_dummyMask() { if (__classPrivateFieldGet(this, _DynamicArrayBase_dummies, "f") !== undefined) return __classPrivateFieldGet(this, _DynamicArrayBase_dummies, "f"); let isLength = __classPrivateFieldGet(this, _DynamicArrayBase_instances, "m", _DynamicArrayBase_indexMask).call(this, this.length); let wasLength = new Bool(false); let mask = isLength.map((isLength) => { wasLength = wasLength.or(isLength); return wasLength; }); __classPrivateFieldSet(this, _DynamicArrayBase_dummies, mask, "f"); return mask; }; /** * Base class of all DynamicArray subclasses */ DynamicArray.Base = DynamicArrayBase; function provable(type, Class) { let capacity = Class.capacity; let NULL = ProvableType.synthesize(type); let PlainArray = struct({ array: Provable.Array(type, capacity), length: Field, }); return { ...PlainArray, // make fromFields return a class instance fromFields(fields, aux) { let raw = PlainArray.fromFields(fields, aux); return new Class(raw.array, raw.length); }, // convert to/from plain array that has the correct length toValue(value) { let length = Number(value.length); return value.array.map((t) => type.toValue(t)).slice(0, length); }, fromValue(value) { if (value instanceof DynamicArrayBase) return value; let array = value.map((t) => type.fromValue(t)); let padded = pad(array, capacity, NULL); return new Class(padded, new Field(value.length)); }, toCanonical(value) { return value; }, // check has to validate length in addition to the other checks check(value) { PlainArray.check(value); value.length.lessThanOrEqual(new Field(capacity)).assertTrue(); }, empty() { return new Class(); }, }; } //# sourceMappingURL=dynamic-array.js.map