mina-attestations
Version:
Private Attestations on Mina
332 lines (293 loc) • 9.17 kB
text/typescript
import {
Bool,
Field,
type InferProvable,
Option,
Provable,
UInt32,
type InferValue,
Gadgets,
type From,
type ProvablePure,
type IsPure,
} from 'o1js';
import { assert, assertHasProperty, chunk, zip } from '../util.ts';
import { type ProvableHashableWide, ProvableType } from '../o1js-missing.ts';
import { assertLessThan16, lessThan16 } from './gadgets.ts';
import { TypeBuilder } from '../provable-type-builder.ts';
export { StaticArray };
type StaticArray<T = any, V = any> = StaticArrayBase<T, V>;
type StaticArrayClass<A, T, V> = typeof StaticArrayBase<T, V> & {
provable: ProvableHashableWide<StaticArrayBase<T, V>, V[], (T | From<A>)[]>;
/**
* Create a new StaticArray from an array of values.
*/
from(v: (T | From<A>)[] | StaticArrayBase<T, V>): StaticArrayBase<T, V>;
};
type StaticArrayClassPure<A, T, V> = typeof StaticArrayBase<T, V> &
Omit<StaticArrayClass<A, T, V>, 'provable'> & {
provable: ProvableHashableWide<
StaticArrayBase<T, V>,
V[],
(T | From<A>)[]
> &
Omit<ProvablePure<StaticArrayBase<T, V>, V[]>, 'fromValue'>;
};
/**
* Array with a fixed number of elements and several helper methods.
*
* ```ts
* const Bytes32 = StaticArray(UInt8, 32);
* ```
*
* The second parameter is the `length`. It can be any number from 0 to 2^16-1.
*/
function StaticArray<
A extends ProvableType,
T extends InferProvable<A> = InferProvable<A>,
V extends InferValue<A> = InferValue<A>
>(
type: A,
length: number
): IsPure<A, Field> extends true
? StaticArrayClassPure<A, T, V>
: StaticArrayClass<A, T, V>;
function StaticArray<
A extends ProvableType,
T extends InferProvable<A> = InferProvable<A>,
V extends InferValue<A> = InferValue<A>
>(type: A, length: number): StaticArrayClass<A, T, V> {
let innerType: Provable<T, V> = ProvableType.get(type);
// assert length bounds
assert(length >= 0, 'length must be >= 0');
assert(length < 2 ** 16, 'length must be < 2^16');
const provableArray = TypeBuilder.shape({
array: Provable.Array(innerType, length),
})
.forConstructor<StaticArrayBase<T, V>>(
({ array }) => new StaticArray_(array)
)
// convert to/from plain array
.mapValue<V[]>({
there: ({ array }) => array,
back: (array) => ({ array }),
distinguish: (s) => s instanceof StaticArrayBase,
})
.build();
class StaticArray_ extends StaticArrayBase<T, V> {
get innerType() {
return innerType;
}
static get length() {
return length;
}
static from(input: (T | V)[] | StaticArrayBase<T, V>) {
return provableArray.fromValue(input);
}
static provable = provableArray;
}
return StaticArray_;
}
StaticArray.from = function <A extends ProvableType>(
type: A,
array: From<A>[]
) {
return StaticArray(type, array.length).from(array);
};
class StaticArrayBase<T = any, V = any> {
/**
* The plain array
*/
array: T[];
// props to override
get innerType(): Provable<T, V> {
throw Error('Inner type must be defined in a subclass.');
}
static get length(): number {
throw Error('Length must be defined in a subclass.');
}
// derived prop
get length(): number {
return (this.constructor as typeof StaticArrayBase).length;
}
/**
* The `length` of the array. For compatibility with `DynamicArray`, we also provide it under `maxLength`.
*/
get maxLength() {
return this.length;
}
constructor(array: T[]) {
assert(array.length === this.length, 'input has to match length');
this.array = array;
}
*[Symbol.iterator]() {
for (let a of this.array) yield a;
}
/**
* Asserts that 0 <= i < this.length, using a cached check that's not duplicated when doing it on the same variable multiple times.
*
* Handles constants without creating constraints.
*
* Cost: 1.5
*/
assertIndexInRange(i: UInt32 | number) {
i = UInt32.from(i);
if (!this._indicesInRange.has(i.value)) {
assertLessThan16(i, this.length);
this._indicesInRange.add(i.value);
}
}
/**
* Gets value at index i, and proves that the index is in the array.
*
* Handles constant indices without creating constraints.
*
* Cost: TN + 1.5
*/
get(i: UInt32 | number): T {
i = UInt32.from(i);
this.assertIndexInRange(i);
return this.getOrUnconstrained(i.value);
}
/**
* 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.
*
* Cost: TN + 2.5
*/
getOption(i: UInt32 | number): Option<T> {
i = UInt32.from(i);
let type = this.innerType;
let isContained = lessThan16(i.value, this.length);
let value = this.getOrUnconstrained(i.value);
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.
*
* Cost: T*N where T = size of the type
*/
getOrUnconstrained(i: Field): T {
let NULL = ProvableType.synthesize(this.innerType);
if (i.isConstant()) return this.array[Number(i)] ?? NULL;
let type = this.innerType;
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));
for (let j = 0; j < type.sizeInFields(); j++) {
let column = fields.map((x) => x[j]!);
Gadgets.arrayGet(column, i).assertEquals(aiFields[j]!);
}
return ai;
}
/**
* Sets a value at index i and proves that the index is in the array.
*
* Cost: 1.5(T + 1)N + 1.5
*/
set(i: UInt32 | number, value: T): void {
i = UInt32.from(i);
this.assertIndexInRange(i);
this.setOrDoNothing(i.value, value);
}
/**
* Sets a value at index i, or does nothing if the index is not in the array
*
* Cost: 1.5(T + 1)N
*/
setOrDoNothing(i: Field, value: T): void {
if (i.isConstant()) {
let i0 = i.toBigInt();
if (i0 < this.length) this.array[Number(i0)] = value;
return;
}
zip(this.array, this._indexMask(i)).forEach(([t, equalsIJ], j) => {
this.array[j] = Provable.if(equalsIJ, this.innerType, value, t);
});
}
/**
* Map every element of the array to a new value.
*/
map<S>(type: ProvableType<S>, f: (t: T, i: number) => S): StaticArray<S> {
let NewArray = StaticArray(type, this.length);
let array = this.array.map(f);
let newArray = new NewArray(array);
// new array has same length, so it can use the same cached masks
newArray._indexMasks = this._indexMasks;
newArray._indicesInRange = this._indicesInRange;
return newArray;
}
/**
* Iterate over all elements of the array.
*/
forEach(f: (t: T, i: number) => void) {
this.array.forEach(f);
}
/**
* Reduce the array to a single value.
*/
reduce<S>(state: S, f: (state: S, t: T) => S): S {
this.forEach((t) => {
state = f(state, t);
});
return state;
}
/**
* Split into a static number of fixed-size chunks.
* Requires that the length is a multiple of the chunk size.
*/
chunk(chunkSize: number) {
let chunked = chunk(this.array, chunkSize);
let newLength = this.length / chunkSize;
const Chunk = StaticArray(this.innerType, chunkSize);
const Chunked = StaticArray(Chunk, newLength);
return new Chunked(chunked.map(Chunk.from));
}
/**
* Reverse the array.
*
* Returns a copy and does not modify the original array.
*/
toReversed() {
return new (this.constructor as typeof StaticArrayBase<T, V>)(
this.array.toReversed()
);
}
slice(start: number, end: number) {
assert(start >= 0, 'start must be >= 0');
assert(end <= this.length, 'end must be <= length');
const Array = StaticArray(this.innerType, end - start);
return new Array(this.array.slice(start, end));
}
// cached variables to not duplicate constraints if we do something like array.get(i), array.set(i, ..) on the same index
_indexMasks: Map<Field, Bool[]> = new Map();
_indicesInRange: Set<Field> = new Set();
/**
* Compute i.equals(j) for all indices j in the static-size array.
*
* Costs: 1.5N
*
* TODO: equals() could be optimized to just 1 double generic because j is constant, o1js doesn't do that
*/
_indexMask(i: Field) {
let mask = this._indexMasks.get(i);
mask ??= this.array.map((_, j) => i.equals(j));
this._indexMasks.set(i, mask);
return mask;
}
toValue() {
assertHasProperty(this.constructor, 'provable', 'Need subclass');
return (this.constructor.provable as Provable<this, V[]>).toValue(this);
}
}
/**
* Base class of all StaticArray subclasses
*/
StaticArray.Base = StaticArrayBase;