mina-attestations
Version:
Private Attestations on Mina
178 lines • 7.07 kB
JavaScript
/**
* 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