UNPKG

mina-attestations

Version:
178 lines 7.07 kB
/** * A dynamic record is a key-value list which can contain keys/values you are not aware of at compile time. */ import { Field, Option, Poseidon, Provable, Struct, Unconstrained, } from 'o1js'; import { array, ProvableType, } from "../o1js-missing.js"; import { TypeBuilder } from "../provable-type-builder.js"; import { assertExtendsShape, assertHasProperty, mapObject, pad, zipObjects, } from "../util.js"; import { NestedProvable } from "../nested.js"; import { ProvableFactory } from "../provable-factory.js"; import { deserializeNestedProvable, deserializeNestedProvableValue, serializeNestedProvableType, serializeNestedProvableValue, } from "../serialize-provable.js"; import { hashString, packToField } from "./dynamic-hash.js"; import { BaseType } from "./dynamic-base-types.js"; import { z } from 'zod'; import { SerializedTypeSchema, SerializedValueSchema } from "../validation.js"; export { DynamicRecord, GenericRecord, extractProperty, }; function DynamicRecord(knownShape, { maxEntries }) { let shape = mapObject(knownShape, (type) => type); const emptyTKnown = mapObject(shape, (type) => ProvableType.get(type).empty()); return class DynamicRecord extends DynamicRecordBase { // accepted type is From<> for the known subfields and unchanged for the unknown ones static from(value) { return DynamicRecord.provable.fromValue(value); } static fromShape(type, value) { let actual = mapObject(zipObjects(type, value), ([type, value]) => ProvableType.get(type).fromValue(value)); return DynamicRecord.provable.fromValue(actual); } static get shape() { return shape; } static provable = TypeBuilder.shape({ entries: array(Option(Struct({ key: Field, value: Field })), maxEntries), actual: Unconstrained.withEmpty(emptyTKnown), }) .forClass(DynamicRecord) .mapValue({ there({ actual }) { return actual; }, back(actual) { // validate that `actual` (at least) contains all known keys assertExtendsShape(actual, knownShape); let entries = Object.entries(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 })); function GenericRecord({ maxEntries }) { // TODO provable return class GenericRecord extends GenericRecordBase { get maxEntries() { return maxEntries; } }; } class GenericRecordBase { entries; actual; constructor(value) { this.entries = value.entries; this.actual = value.actual; } get maxEntries() { throw Error('Need subclass'); } get knownShape() { return {}; } static from(actual) { 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(valueType, key) { // 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]); // assert that value matches hash, and return it packToField(value, valueType).assertEquals(valueHash, `Bug: Invalid value for key "${key}"`); return value; } hash() { // 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 extends GenericRecordBase { get knownShape() { throw Error('Need subclass'); } get(key) { let valueType = ProvableType.get(this.knownShape[key]); return this.getAny(valueType, key); } } BaseType.DynamicRecord = DynamicRecord; DynamicRecord.Base = DynamicRecordBase; // compatible key extraction function extractProperty(data, key) { 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: serializeNestedProvableType(constructor.prototype.knownShape), }; }, typeFromJSON(json) { let { maxEntries, knownShape } = json; let shape = deserializeNestedProvable(knownShape); return DynamicRecord(shape, { 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); }, }); //# sourceMappingURL=dynamic-record.js.map