UNPKG

mina-attestations

Version:
441 lines (404 loc) 12.8 kB
/** * JSON serialization of provable types and values. */ import { NestedProvable } from './nested.ts'; import { array, ProvableType } from './o1js-missing.ts'; import { Field, Bool, UInt8, UInt32, UInt64, PublicKey, Signature, Undefined, Bytes, DynamicProof, VerificationKey, Struct, type JsonProof, Int64, } from 'o1js'; import { assert, assertHasMethod, defined, hasProperty, mapObject, } from './util.ts'; import { ProvableFactory, type SerializedFactory } from './provable-factory.ts'; import type { Json } from './types.ts'; export { type SerializedType, type SerializedValue, type SerializedNestedType, serializeProvableType, serializeProvableValue, serializeProvableField, serializeProvablePublicKey, serializeNestedProvableType, serializeNestedProvableValue, serializeSimplyNestedProvableValue, deserializeProvableType, deserializeProvableValue, deserializeNestedProvable, deserializeNestedProvableValue, replaceNull, replaceUndefined, }; // Supported o1js base types const supportedTypes = { Field, Bool, UInt8, UInt32, UInt64, Int64, PublicKey, Signature, Undefined, VerificationKey, }; type O1jsTypeName = keyof typeof supportedTypes; let mapProvableTypeToName = new Map<ProvableType<any>, O1jsTypeName>(); for (let [key, value] of Object.entries(supportedTypes)) { mapProvableTypeToName.set(value, key as O1jsTypeName); } type SerializedType = | { _type: O1jsTypeName } | { _type: 'Struct'; properties: SerializedNestedType } | { _type: 'Array'; inner: SerializedType; size: number } | { _type: 'Constant'; value: Json } | { _type: 'Bytes'; size: number } | { _type: 'Proof'; proof: Record<string, any> } | { _type: 'String' } | SerializedFactory; type SerializedNestedType = | SerializedType | { [key: string]: SerializedNestedType }; // SERIALIZE function serializeProvableType(type: ProvableType<any>): SerializedType { let serialized = ProvableFactory.tryToJSON(type); if (serialized !== undefined) return serialized; if ('serialize' in type && typeof type.serialize === 'function') { return type.serialize(); } if ((type as any).prototype instanceof Bytes.Base) { return { _type: 'Bytes', size: (type as typeof Bytes.Base).size }; } if ((type as any).prototype instanceof DynamicProof) { let { publicInputType, publicOutputType, maxProofsVerified, featureFlags } = type as typeof DynamicProof; let proof = { name: (type as typeof DynamicProof).name, publicInput: serializeProvableType(publicInputType), publicOutput: serializeProvableType(publicOutputType), maxProofsVerified, featureFlags: replaceUndefined(featureFlags), }; return { _type: 'Proof', proof }; } let _type = mapProvableTypeToName.get(type); if (_type === undefined && (type as any)._isStruct) { return serializeStructType(type as Struct<any>); } if (_type === undefined && (type as any)._isArray) { return { _type: 'Array', inner: serializeProvableType((type as any).innerType), size: (type as any).size, }; } assert(_type !== undefined, () => { console.log('serializeProvableType', type); return `serializeProvableType: Unsupported provable type: ${type}`; }); return { _type }; } type SerializedValue = SerializedType & { value: Json }; type SerializedValueAny = SerializedType & { value: any }; type SerializedNestedValue = | SerializedValue | string | number | boolean | { [key: string]: SerializedNestedValue }; function serializeProvableValue(value: any): SerializedValue { let typeClass = ProvableType.fromValue(value); let serializedType = serializeProvableType(typeClass); if (ProvableFactory.isSerialized(serializedType)) { let serialized = ProvableFactory.tryValueToJSON(value); return defined(serialized); } switch (serializedType._type) { case 'Bytes': return { ...serializedType, value: (value as Bytes).toHex() }; case 'Proof': let json = (value as DynamicProof<any, any>).toJSON(); return { ...serializedType, value: json }; case 'Array': { return { ...serializedType, value: value.map((x: any) => serializeProvableValue(x)), }; } case 'Struct': let result: Record<string, any> = {}; for (let key in serializedType.properties) { result[key] = serializeNestedProvableValue(value[key]); } return { ...serializedType, value: result }; case 'Undefined': return { ...serializedType, value: null }; case 'Constant': return serializedType; case 'String': return { ...serializedType, value }; case 'UInt8': return { ...serializedType, value: (value as UInt8).toString() }; case 'Int64': return { ...serializedType, value: (value as Int64).toString() }; case 'VerificationKey': let vk: VerificationKey = value; return { ...serializedType, value: { data: vk.data, hash: vk.hash.toString() }, }; default: assertHasMethod( value, 'toJSON', `Missing toJSON method for ${serializedType._type}` ); return { ...serializedType, value: value.toJSON() }; } } // some special cases function serializeProvableField(value: Field) { return { _type: 'Field', value: value.toJSON() } satisfies SerializedValue; } function serializeProvablePublicKey(value: PublicKey) { return { _type: 'PublicKey', value: value.toJSON(), } satisfies SerializedValue; } function serializeStructType(type: Struct<any>): SerializedType { let value = type.empty(); let properties: SerializedNestedType = {}; for (let key in value) { let type = NestedProvable.fromValue(value[key]); properties[key] = serializeNestedProvableType(type); } return { _type: 'Struct', properties }; } function serializeNestedProvableType( type: NestedProvable ): SerializedNestedType { if (ProvableType.isProvableType(type)) { return serializeProvableType(type); } if (typeof type === 'string' || (type as any) === String) return { _type: 'String' }; if (typeof type === 'object' && type !== null) { const serializedObject: Record<string, any> = {}; for (const key of Object.keys(type)) { serializedObject[key] = serializeNestedProvableType(type[key]!); } return serializedObject; } throw Error(`Unsupported type in NestedProvable: ${type}`); } function serializeNestedProvableValue(value: any): SerializedNestedValue { let type = NestedProvable.fromValue(value); return serializeNestedProvableTypeAndValue({ type, value }); } function serializeSimplyNestedProvableValue( value: Record<string, any> ): Record<string, SerializedValue> { return mapObject(value, (v) => serializeProvableValue(v)); } function serializeNestedProvableTypeAndValue(t: { type: NestedProvable; value: any; }): SerializedNestedValue { if (ProvableType.isProvableType(t.type)) { return serializeProvableValue(t.value); } if (typeof t.type === 'string' || (t.type as any) === String) return t.value as string; return Object.fromEntries( Object.keys(t.type).map((key) => { assert(key in t.value, `Missing value for key ${key}`); return [ key, serializeNestedProvableTypeAndValue({ type: (t.type as any)[key], value: t.value[key], }), ]; }) ); } // DESERIALIZE function deserializeProvableType(type: SerializedType): ProvableType<any> { if (ProvableFactory.isSerialized(type)) return ProvableFactory.fromJSON(type); if (type._type === 'Constant') { return ProvableType.constant((type as any).value); } if (type._type === 'Bytes') { return Bytes(type.size); } if (type._type === 'Proof') { let proof = type.proof; let Proof = class extends DynamicProof<any, any> { static publicInputType = ProvableType.get( deserializeProvableType(proof.publicInput) ); static publicOutputType = ProvableType.get( deserializeProvableType(proof.publicOutput) ); static maxProofsVerified = proof.maxProofsVerified; static featureFlags = replaceNull(proof.featureFlags) as any; }; Object.defineProperty(Proof, 'name', { value: proof.name }); return Proof; } if (type._type === 'Struct') { let properties = deserializeNestedProvable(type.properties); return Struct(properties); } if (type._type === 'Array') { let inner = deserializeProvableType(type.inner); return array(inner, type.size); } if (type._type === 'String') { return String as any; } let result = supportedTypes[type._type]; assert(result !== undefined, `Unsupported provable type: ${type._type}`); return result; } function deserializeProvableValue(json: SerializedValueAny): any { if (ProvableFactory.isSerialized(json)) return ProvableFactory.valueFromJSON(json); let { _type, value } = json; switch (json._type) { case 'Field': return Field.fromJSON(value); case 'Bool': return Bool.fromJSON(value); case 'UInt8': return UInt8.fromJSON({ value }); case 'UInt32': return UInt32.fromJSON(value); case 'UInt64': return UInt64.fromJSON(value); case 'Int64': if (hasProperty(value, 'magnitude')) return Int64.fromJSON(value); return Int64.from(value); case 'PublicKey': return PublicKey.fromJSON(value); case 'Signature': return Signature.fromJSON(value); case 'Undefined': return undefined; case 'VerificationKey': return VerificationKey.fromJSON(value); case 'Bytes': let BytesN = deserializeProvableType(json) as typeof Bytes.Base; return BytesN.fromHex(value); case 'Proof': return proofFromJSONSync(json); case 'Array': return (value as any[]).map((v) => deserializeProvableValue(v)); case 'Struct': let type = deserializeProvableType(json) as Struct<any>; let result: Record<string, any> = {}; for (let key in json.properties) { result[key] = deserializeNestedProvableValue(value[key]); } return new type(result); case 'Constant': return value; case 'String': return value; default: json satisfies never; throw Error(`Unsupported provable type: ${_type}`); } } // this only works if `await initializeBindings()` from o1js has been called before // but this implicit dependency seems better than making every `fromJSON` method async function proofFromJSONSync(json: { _type: 'Proof'; proof: Record<string, any>; value: JsonProof; }) { let Proof = deserializeProvableType(json) as typeof DynamicProof; let { maxProofsVerified, proof: proofString, publicInput, publicOutput, } = json.value; let proof = Proof._proofFromBase64(proofString, maxProofsVerified); let fields = publicInput.map(Field).concat(publicOutput.map(Field)); return Proof.provable.fromFields(fields, [ [], [], [proof, maxProofsVerified], ]); } function deserializeNestedProvable(type: SerializedNestedType): NestedProvable { if (typeof type === 'object' && type !== null) { if ('_type' in type) { // basic provable type return deserializeProvableType(type as SerializedType); } else { // nested object let result: NestedProvable = {}; for (const [key, value] of Object.entries(type)) { result[key] = deserializeNestedProvable(value); } return result; } } throw Error(`Invalid type in NestedProvable: ${type}`); } function deserializeNestedProvableValue(value: SerializedNestedValue) { if (typeof value === 'string') return value; if (typeof value === 'object' && value !== null) { if ('_type' in value) { // basic provable type return deserializeProvableValue(value as SerializedValue); } else { // nested object const result: Record<string, any> = {}; for (let [key, v] of Object.entries(value)) { result[key] = deserializeNestedProvableValue(v); } return result; } } throw Error(`Invalid nested provable value: ${value}`); } function replaceNull<Input extends Record<string, Json>>( obj: Input ): { [K in keyof Input]: Input[K] extends infer T | null ? T | undefined : Input[K]; } { return mapObject(obj, (value) => (value === null ? undefined : value) as any); } // `null` is preserved in JSON, but `undefined` is removed function replaceUndefined<Input extends Record<string, Json | undefined>>( obj: Input ): { [K in keyof Input]: Input[K] extends infer T | undefined ? T | null : Input[K]; } { return mapObject(obj, (value) => (value === undefined ? null : value) as any); }