mina-attestations
Version:
Private Attestations on Mina
288 lines (247 loc) • 8.19 kB
text/typescript
/**
* A dynamic record is a key-value list which can contain keys/values you are not aware of at compile time.
*/
import {
Field,
type From,
type InferProvable,
Option,
Poseidon,
Provable,
type ProvableHashable,
Struct,
Unconstrained,
} from 'o1js';
import {
array,
ProvableType,
type ProvableHashableType,
} from '../o1js-missing.ts';
import { TypeBuilder } from '../provable-type-builder.ts';
import {
assertExtendsShape,
assertHasProperty,
mapObject,
pad,
zipObjects,
} from '../util.ts';
import { NestedProvable } from '../nested.ts';
import { ProvableFactory } from '../provable-factory.ts';
import {
deserializeNestedProvable,
deserializeNestedProvableValue,
serializeNestedProvable,
serializeNestedProvableValue,
} from '../serialize-provable.ts';
import { hashString, packToField } from './dynamic-hash.ts';
import { BaseType } from './dynamic-base-types.ts';
import { z } from 'zod';
import { SerializedTypeSchema, SerializedValueSchema } from '../validation.ts';
export {
DynamicRecord,
GenericRecord,
type UnknownRecord,
type DynamicRecordClass,
extractProperty,
};
type DynamicRecord<TKnown = any> = DynamicRecordBase<TKnown>;
type DynamicRecordClass<AKnown extends Record<string, any>> = ReturnType<
typeof DynamicRecord<AKnown>
>;
function DynamicRecord<
AKnown extends Record<string, ProvableHashableType>,
TKnown extends { [K in keyof AKnown]: InferProvable<AKnown[K]> } = {
[K in keyof AKnown]: InferProvable<AKnown[K]>;
}
>(knownShape: AKnown, { maxEntries }: { maxEntries: number }) {
let shape = mapObject<
AKnown,
{ [K in keyof TKnown]: ProvableHashableType<TKnown[K]> }
>(knownShape, (type) => type);
const emptyTKnown: TKnown = mapObject(shape, (type) =>
ProvableType.get(type).empty()
);
return class DynamicRecord extends DynamicRecordBase<TKnown> {
// accepted type is From<> for the known subfields and unchanged for the unknown ones
static from<T extends From<AKnown> & UnknownRecord>(
value: T
): DynamicRecordBase<TKnown> {
return DynamicRecord.provable.fromValue(value);
}
static fromShape<A extends AKnown>(
type: A,
value: { [K in keyof A]: From<A[K]> }
): DynamicRecordBase<TKnown> {
let actual: { [K in keyof A]: InferProvable<A[K]> } = mapObject(
zipObjects(type, value),
([type, value]) => ProvableType.get(type).fromValue(value)
);
return DynamicRecord.provable.fromValue(actual);
}
static get shape(): {
[K in keyof TKnown]: ProvableHashableType<TKnown[K]>;
} {
return shape;
}
static provable = TypeBuilder.shape({
entries: array(Option(Struct({ key: Field, value: Field })), maxEntries),
actual: Unconstrained.withEmpty<UnknownRecord>(emptyTKnown),
})
.forClass<DynamicRecordBase<TKnown>>(DynamicRecord)
.mapValue<UnknownRecord>({
there({ actual }) {
return actual;
},
back(actual) {
// validate that `actual` (at least) contains all known keys
assertExtendsShape(actual, knownShape);
let entries = Object.entries<unknown>(actual).map(([key, value]) => {
let type =
key in knownShape
? NestedProvable.get(knownShape[key]!)
: undefined;
let actualValue =
type === undefined ? value : type.fromValue(value);
return {
key: hashString(key).toBigInt(),
value: packToField(actualValue, type).toBigInt(),
};
});
return { entries: pad(entries, maxEntries, undefined), actual };
},
distinguish(x) {
return x instanceof DynamicRecordBase;
},
})
.build();
get maxEntries() {
return maxEntries;
}
get knownShape() {
return shape;
}
};
}
const OptionField = Option(Field);
const OptionKeyValue = Option(Struct({ key: Field, value: Field }));
type GenericRecord = GenericRecordBase;
function GenericRecord({ maxEntries }: { maxEntries: number }) {
// TODO provable
return class GenericRecord extends GenericRecordBase {
get maxEntries() {
return maxEntries;
}
};
}
class GenericRecordBase {
entries: Option<{ key: Field; value: Field }>[];
actual: Unconstrained<UnknownRecord>;
constructor(value: DynamicRecordRaw) {
this.entries = value.entries;
this.actual = value.actual;
}
get maxEntries(): number {
throw Error('Need subclass');
}
get knownShape() {
return {};
}
static from(actual: UnknownRecord): GenericRecordBase {
let entries = Object.entries(actual).map(([key, value]) => {
return OptionKeyValue.from({
key: hashString(key),
value: packToField(value),
});
});
let maxEntries = this.prototype.maxEntries;
let padded = pad(entries, maxEntries, OptionKeyValue.none());
return new this({ entries: padded, actual: Unconstrained.from(actual) });
}
getAny<A extends ProvableHashableType>(valueType: A, key: string) {
// find valueHash for key
let keyHash = hashString(key);
let current = OptionField.none();
for (let { isSome, value: entry } of this.entries) {
let isCurrentKey = isSome.and(entry.key.equals(keyHash));
current.isSome = current.isSome.or(isCurrentKey);
current.value = Provable.if(isCurrentKey, entry.value, current.value);
}
let valueHash = current.assertSome(`Key not found: "${key}"`);
// witness actual value for key
let value = Provable.witness(
valueType,
() => this.actual.get()[key] as any
);
// assert that value matches hash, and return it
packToField(value, valueType).assertEquals(
valueHash,
`Bug: Invalid value for key "${key}"`
);
return value;
}
hash(): Field {
// hash one entry at a time, ignoring dummy entries
let state = Poseidon.initialState();
for (let { isSome, value: entry } of this.entries) {
let { key, value } = entry;
let newState = Poseidon.update(state, [key, value]);
state[0] = Provable.if(isSome, newState[0], state[0]);
state[1] = Provable.if(isSome, newState[1], state[1]);
state[2] = Provable.if(isSome, newState[2], state[2]);
}
return state[0];
}
}
BaseType.GenericRecord = GenericRecord;
GenericRecord.Base = GenericRecordBase;
class DynamicRecordBase<TKnown = any> extends GenericRecordBase {
get knownShape(): { [K in keyof TKnown]: ProvableHashableType<TKnown[K]> } {
throw Error('Need subclass');
}
get<K extends keyof TKnown & string>(key: K): TKnown[K] {
let valueType: ProvableHashable<TKnown[K]> = ProvableType.get(
this.knownShape[key]
);
return this.getAny(valueType, key);
}
}
BaseType.DynamicRecord = DynamicRecord;
DynamicRecord.Base = DynamicRecordBase;
type DynamicRecordRaw = {
entries: Option<{ key: Field; value: Field }>[];
actual: Unconstrained<UnknownRecord>;
};
type UnknownRecord = Record<string, unknown>;
// compatible key extraction
function extractProperty(data: unknown, key: string): unknown {
if (data instanceof DynamicRecord.Base) return data.get(key);
assertHasProperty(data, key, `Key not found: "${key}"`);
return data[key];
}
// serialize/deserialize
ProvableFactory.register('DynamicRecord', DynamicRecord, {
typeSchema: z.object({
maxEntries: z.number(),
knownShape: z.record(SerializedTypeSchema),
}),
valueSchema: z.record(SerializedValueSchema),
typeToJSON(constructor) {
return {
maxEntries: constructor.prototype.maxEntries,
knownShape: serializeNestedProvable(constructor.prototype.knownShape),
};
},
typeFromJSON(json) {
let { maxEntries, knownShape } = json;
let shape = deserializeNestedProvable(knownShape);
return DynamicRecord(shape as any, { maxEntries });
},
valueToJSON(type, value) {
let actual = type.provable.toValue(value);
return serializeNestedProvableValue(actual);
},
valueFromJSON(type, value) {
let actual = deserializeNestedProvableValue(value);
return type.provable.fromValue(actual);
},
});