UNPKG

mina-attestations

Version:
556 lines 27.9 kB
import { test } from 'node:test'; import assert from 'node:assert'; import { Field, Bool, UInt8, UInt32, UInt64, PublicKey, Signature, PrivateKey, Undefined, Struct, FeatureFlags, Bytes, } from 'o1js'; import { Spec, Constant, Claim, isCredentialSpec, } from "../src/program-spec.js"; import { Operation } from "../src/operation.js"; import { serializeNode, serializeInput, serializeSpec, deserializeSpec, deserializeInput, deserializeNode, } from "../src/serialize-spec.js"; import { Credential } from "../src/credential-index.js"; import { withOwner } from "../src/credential.js"; import { HttpsRequest, PresentationRequest, ZkAppRequest, } from "../src/presentation.js"; import { zkAppIdentity } from "./test-utils.js"; import { computeHttpsContext, computeZkAppContext, hashContext, } from "../src/context.js"; import { deserializeProvableValue, deserializeProvableType, serializeProvableValue, } from "../src/serialize-provable.js"; import { PresentationRequestSchema } from "../src/validation.js"; import { mapObject } from "../src//util.js"; test('Deserialize Spec', async (t) => { await t.test('deserializeProvable', async (t) => { await t.test('Field', () => { const deserialized = deserializeProvableValue({ _type: 'Field', value: '42', }); assert(deserialized instanceof Field, 'Should be instance of Field'); assert.strictEqual(deserialized.toString(), '42', 'Should have correct value'); }); await t.test('Bool', () => { const deserializedTrue = deserializeProvableValue({ _type: 'Bool', value: true, }); assert(deserializedTrue instanceof Bool, 'Should be instance of Bool'); assert.strictEqual(deserializedTrue.toBoolean(), true, 'Should be true'); const deserializedFalse = deserializeProvableValue({ _type: 'Bool', value: false, }); assert(deserializedFalse instanceof Bool, 'Should be instance of Bool'); assert.strictEqual(deserializedFalse.toBoolean(), false, 'Should be false'); }); await t.test('UInt8', () => { const deserialized = deserializeProvableValue({ _type: 'UInt8', value: '255', }); assert(deserialized instanceof UInt8, 'Should be instance of UInt8'); assert.strictEqual(deserialized.toString(), '255', 'Should have correct value'); }); await t.test('UInt32', () => { const deserialized = deserializeProvableValue({ _type: 'UInt32', value: '4294967295', }); assert(deserialized instanceof UInt32, 'Should be instance of UInt32'); assert.strictEqual(deserialized.toString(), '4294967295', 'Should have correct value'); }); await t.test('UInt64', () => { const deserialized = deserializeProvableValue({ _type: 'UInt64', value: '18446744073709551615', }); assert(deserialized instanceof UInt64, 'Should be instance of UInt64'); assert.strictEqual(deserialized.toString(), '18446744073709551615', 'Should have correct value'); }); await t.test('PublicKey', () => { const publicKeyBase58 = 'B62qiy32p8kAKnny8ZFwoMhYpBppM1DWVCqAPBYNcXnsAHhnfAAuXgg'; const deserialized = deserializeProvableValue({ _type: 'PublicKey', value: publicKeyBase58, }); assert(deserialized instanceof PublicKey, 'Should be instance of PublicKey'); assert.strictEqual(deserialized.toBase58(), publicKeyBase58, 'Should have correct value'); }); await t.test('Signature', () => { // Create a valid signature const privateKey = PrivateKey.random(); const publicKey = privateKey.toPublicKey(); const message = [Field(1), Field(2), Field(3)]; const signature = Signature.create(privateKey, message); // Serialize the signature using serializeProvable const serializedSignature = serializeProvableValue(signature); // Deserialize the signature const deserialized = deserializeProvableValue(serializedSignature); assert(deserialized instanceof Signature, 'Should be instance of Signature'); assert(deserialized.verify(publicKey, message).toBoolean(), 'Deserialized signature should be valid'); // Additional check to ensure serialized and deserialized signatures match const reserializedSignature = serializeProvableValue(deserialized); assert.deepStrictEqual(serializedSignature, reserializedSignature, 'Serialized and reserialized signatures should match'); }); await t.test('Invalid type', () => { assert.throws(() => deserializeProvableValue({ _type: 'InvalidType', value: '42', }), { message: 'Unsupported provable type: InvalidType' }, 'Should throw for invalid type'); }); }); }); test('deserializeProvableType', async (t) => { await t.test('should deserialize Field type', () => { const result = deserializeProvableType({ _type: 'Field' }); assert.strictEqual(result, Field); }); await t.test('should deserialize Bool type', () => { const result = deserializeProvableType({ _type: 'Bool' }); assert.strictEqual(result, Bool); }); await t.test('should deserialize UInt8 type', () => { const result = deserializeProvableType({ _type: 'UInt8' }); assert.strictEqual(result, UInt8); }); await t.test('should deserialize UInt32 type', () => { const result = deserializeProvableType({ _type: 'UInt32' }); assert.strictEqual(result, UInt32); }); await t.test('should deserialize UInt64 type', () => { const result = deserializeProvableType({ _type: 'UInt64' }); assert.strictEqual(result, UInt64); }); await t.test('should deserialize PublicKey type', () => { const result = deserializeProvableType({ _type: 'PublicKey' }); assert.strictEqual(result, PublicKey); }); await t.test('should deserialize Signature type', () => { const result = deserializeProvableType({ _type: 'Signature' }); assert.strictEqual(result, Signature); }); }); test('deserializeInput', async (t) => { await t.test('should deserialize constant input', () => { const input = Constant(Field, Field(42)); const serialized = serializeInput(input); const deserialized = deserializeInput(serialized); assert.deepStrictEqual(deserialized, input); }); await t.test('should deserialize public input', () => { const input = Claim(Field); const serialized = serializeInput(input); const deserialized = deserializeInput(serialized); assert.deepStrictEqual(deserialized, input); }); await t.test('should deserialize private input', () => { const input = Credential.Unsigned(Signature); const serialized = serializeInput(input); const deserialized = deserializeInput(serialized); assert.deepStrictEqual(deserialized, input); }); await t.test('should deserialize credential input', () => { const InputData = { age: Field, isAdmin: Bool }; const input = Credential.Native(InputData); const serialized = serializeInput(input); const deserialized = deserializeInput(serialized); const reserialized = serializeInput(deserialized); assert.deepStrictEqual(serialized, reserialized); assert.deepStrictEqual(deserialized, input); }); await t.test('should deserialize nested input', () => { const input = Credential.Unsigned({ personal: { age: Field, id: UInt64, }, score: UInt32, }); const serialized = serializeInput(input); const deserialized = deserializeInput(serialized); assert.deepStrictEqual(deserialized, input); }); await t.test('should throw error for unsupported input type', async (t) => { try { deserializeInput({ type: 'invalid' }); assert.fail('Expected an error to be thrown'); } catch (error) { assert(error instanceof Error); assert.strictEqual(error.message, 'Invalid input type: invalid'); } }); }); test('deserializeInputs', async (t) => { await t.test('should deserialize inputs with various type', () => { const inputs = { field: Credential.Unsigned(Field), bool: Claim(Bool), uint: Constant(UInt64, UInt64.from(42)), nested: Credential.Unsigned({ inner: Field, deep: { value: Bool, }, }), }; const serialized = mapObject(inputs, serializeInput); const deserialized = mapObject(serialized, deserializeInput); assert.deepStrictEqual(deserialized, inputs); }); await t.test('should deserialize credential input', () => { const InputData = { age: Field, isAdmin: Bool }; const inputs = { credential: Credential.Native(InputData), }; const serialized = mapObject(inputs, serializeInput); const deserialized = mapObject(serialized, deserializeInput); const reserialized = mapObject(deserialized, serializeInput); assert.deepStrictEqual(serialized, reserialized); }); await t.test('should handle mixed input types', () => { const inputs = { privateField: Credential.Unsigned(Field), publicBool: Claim(Bool), constantUint: Constant(UInt32, UInt32.from(42)), credential: Credential.Native({ score: UInt64 }), }; const serialized = mapObject(inputs, serializeInput); const deserialized = mapObject(serialized, deserializeInput); const reserialized = mapObject(deserialized, serializeInput); assert.deepStrictEqual(serialized, reserialized); }); }); test('deserializeNode', async (t) => { await t.test('should deserialize constant node', () => { const node = { type: 'constant', data: Field(123) }; const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize root node', () => { let input = { age: Credential.Unsigned(Field), isAdmin: Claim(Bool), }; const node = { type: 'root', input }; const serialized = serializeNode(node); const deserialized = deserializeNode(input, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize property node', () => { let input = { age: Credential.Unsigned(Field), isAdmin: Claim(Bool), }; const node = { type: 'property', key: 'age', inner: { type: 'root', input }, }; const serialized = serializeNode(node); const deserialized = deserializeNode(input, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize equals node', () => { const node = Operation.equals({ type: 'constant', data: Field(10) }, { type: 'constant', data: Field(10) }); const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize lessThan node', () => { const node = Operation.lessThan({ type: 'constant', data: UInt32.from(5) }, { type: 'constant', data: UInt32.from(10) }); const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize lessThanEq node', () => { const node = Operation.lessThanEq({ type: 'constant', data: UInt64.from(15) }, { type: 'constant', data: UInt64.from(15) }); const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize and node', () => { const node = Operation.and({ type: 'constant', data: Bool(true) }, { type: 'constant', data: Bool(false) }); const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should deserialize nested nodes', () => { const node = Operation.and(Operation.lessThan({ type: 'constant', data: Field(5) }, { type: 'constant', data: Field(10) }), Operation.equals({ type: 'constant', data: Bool(true) }, { type: 'constant', data: Bool(true) })); const serialized = serializeNode(node); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, node); }); await t.test('should throw error for invalid node type', async () => { const invalidNode = { type: 'invalid' }; try { deserializeNode({}, invalidNode); assert.fail('Expected an error to be thrown'); } catch (error) { assert(error instanceof Error, 'Error should be an instance of Error'); assert.strictEqual(error.message, 'Invalid node type: invalid', 'Error message should match expected message'); } }); await t.test('should deserialize record node', () => { const originalNode = Operation.record({ field1: { type: 'constant', data: Field(123) }, field2: { type: 'constant', data: Bool(true) }, }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize add node', () => { const originalNode = Operation.add({ type: 'constant', data: Field(5) }, { type: 'constant', data: Field(10) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize sub node', () => { const originalNode = Operation.sub({ type: 'constant', data: Field(15) }, { type: 'constant', data: Field(7) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize mul node', () => { const originalNode = Operation.mul({ type: 'constant', data: UInt32.from(3) }, { type: 'constant', data: UInt32.from(4) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize div node', () => { const originalNode = Operation.div({ type: 'constant', data: Field(20) }, { type: 'constant', data: Field(5) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize not node', () => { const originalNode = Operation.not({ type: 'constant', data: Bool(true), }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize hash node', () => { const originalNode = Operation.hash({ type: 'constant', data: Field(123), }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize or node', () => { const originalNode = Operation.or({ type: 'constant', data: Bool(true) }, { type: 'constant', data: Bool(false) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize ifThenElse node', () => { const originalNode = Operation.ifThenElse({ type: 'constant', data: Bool(true) }, { type: 'constant', data: Field(1) }, { type: 'constant', data: Field(0) }); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); await t.test('should deserialize complex nested node', () => { const originalNode = Operation.and(Operation.lessThan(Operation.add({ type: 'constant', data: Field(5) }, { type: 'constant', data: Field(10) }), { type: 'constant', data: Field(20) }), Operation.or({ type: 'constant', data: Bool(true) }, Operation.not({ type: 'constant', data: Bool(false) }))); const serialized = serializeNode(originalNode); const deserialized = deserializeNode({}, serialized); assert.deepStrictEqual(deserialized, originalNode); }); }); test('deserializeSpec', async (t) => { await t.test('should correctly deserialize a simple Spec with Bytes', async () => { const originalSpec = Spec({ age: Credential.Unsigned(Field), isAdmin: Claim(Bool), maxAge: Constant(Field, Field(100)), name: Claim(Bytes(32)), constantName: Constant(Bytes(32), Bytes(32).fromString('hello')), }, ({ age, isAdmin, maxAge }) => ({ assert: Operation.and(Operation.lessThan(age, maxAge), isAdmin), outputClaim: age, })); const serialized = serializeSpec(originalSpec); const deserialized = deserializeSpec(serialized); assert.deepStrictEqual(deserialized.inputs.age, originalSpec.inputs.age); assert.deepStrictEqual(deserialized.inputs.isAdmin, originalSpec.inputs.isAdmin); assert.deepEqual((deserialized.inputs.name?.data).size, originalSpec.inputs.name.data.size); assert.deepStrictEqual(deserialized.inputs.maxAge, originalSpec.inputs.maxAge); assert.deepStrictEqual(serialized, await serializeSpec(deserialized)); }); // it is not possible to directly compare credentials because of the verify function await t.test('should correctly deserialize a Spec with credential', async () => { const originalSpec = Spec({ signedData: Credential.Native({ field: Field }), zeroField: Constant(Field, Field(0)), }, ({ signedData, zeroField }) => ({ assert: Operation.equals(Operation.property(signedData, 'field'), zeroField), outputClaim: signedData, })); const serialized = serializeSpec(originalSpec); const deserialized = deserializeSpec(serialized); const reserialized = serializeSpec(deserialized); assert.deepStrictEqual(serialized, reserialized); assert.deepStrictEqual(deserialized.inputs.zeroField, originalSpec.inputs.zeroField); assert(isCredentialSpec(deserialized.inputs.signedData)); assert.deepStrictEqual(deserialized.inputs.signedData.witness, originalSpec.inputs.signedData.witness); assert.deepStrictEqual(deserialized.inputs.signedData.data, originalSpec.inputs.signedData.data); }); await t.test('should correctly deserialize a Spec with nested operations', async () => { const originalSpec = Spec({ field1: Credential.Unsigned(Field), field2: Credential.Unsigned(Field), threshold: Claim(UInt64), }, ({ field1, field2, threshold }) => ({ assert: Operation.and(Operation.lessThan(field1, field2), Operation.lessThanEq(field2, threshold)), outputClaim: Operation.equals(field1, field2), })); const serialized = serializeSpec(originalSpec); const deserialized = deserializeSpec(serialized); const reserialized = serializeSpec(deserialized); assert.deepStrictEqual(serialized, reserialized); assert.deepStrictEqual(deserialized, originalSpec); }); await t.test('should correctly deserialize a spec with owner and issuer', async (t) => { const InputData = { age: Field }; const SignedData = Credential.Native(InputData); const originalSpec = Spec({ signedData: SignedData, targetAge: Claim(Field), }, ({ signedData, targetAge }) => ({ assert: Operation.equals(Operation.property(signedData, 'age'), targetAge), outputClaim: Operation.record({ owner: Operation.owner, issuer: Operation.issuer(signedData), age: Operation.property(signedData, 'age'), }), })); const originalSerialized = serializeSpec(originalSpec); const deserialized = deserializeSpec(originalSerialized); const reSerialized = serializeSpec(deserialized); assert.deepStrictEqual(originalSpec, deserialized); assert.deepStrictEqual(originalSerialized, reSerialized); }); await t.test('should correctly deserialize a Spec with imported credential', async () => { const ProofSpec = { type: 'imported', publicInputType: Undefined, publicOutputType: Struct(withOwner(Field)), maxProofsVerified: 0, featureFlags: FeatureFlags.allMaybe, }; const originalSpec = Spec({ provedData: Credential.Imported.create({ data: Field, witness: ProofSpec, }), zeroField: Constant(Field, Field(0)), }, ({ provedData, zeroField }) => ({ assert: Operation.equals(provedData, zeroField), outputClaim: provedData, })); const serialized = serializeSpec(originalSpec); const deserialized = deserializeSpec(serialized); const reserialized = serializeSpec(deserialized); assert.deepStrictEqual(serialized, reserialized); assert.deepStrictEqual(deserialized.inputs.zeroField, originalSpec.inputs.zeroField); assert(isCredentialSpec(deserialized.inputs.provedData)); assert.deepStrictEqual(deserialized.inputs.provedData.data, originalSpec.inputs.provedData.data); let deserializedWitnessSpec = originalSpec.inputs.provedData.witness; assert.deepStrictEqual(deserializedWitnessSpec?.featureFlags, ProofSpec.featureFlags); assert.deepStrictEqual(deserializedWitnessSpec.maxProofsVerified, ProofSpec.maxProofsVerified); }); }); test('deserializePresentationRequest with context', async (t) => { const Bytes32 = Bytes(32); const InputData = { age: Field, name: Bytes32 }; const spec = Spec({ signedData: Credential.Native(InputData), targetAge: Claim(Field), targetName: Constant(Bytes32, Bytes32.fromString('Alice')), }, ({ signedData, targetAge, targetName }) => ({ assert: Operation.and(Operation.equals(Operation.property(signedData, 'age'), targetAge), Operation.equals(Operation.property(signedData, 'name'), targetName)), outputClaim: Operation.property(signedData, 'age'), })); await t.test('should deserialize zk-app context correctly', () => { const originalRequest = ZkAppRequest({ spec, claims: { targetAge: Field(18) }, inputContext: { type: 'zk-app', action: 'myMethod', serverNonce: Field(789), verifierIdentity: zkAppIdentity, }, }); const serialized = PresentationRequest.toJSON(originalRequest); const parsed = JSON.parse(serialized); const result = PresentationRequestSchema.safeParse(parsed); assert(result.success, 'ZkApp presentation request should be valid: ' + (result.success ? '' : JSON.stringify(result.error.issues, null, 2))); const deserialized = PresentationRequest.fromJSON('zk-app', serialized); assert.strictEqual(deserialized.type, 'zk-app'); assert.strictEqual(deserialized.claims.targetAge.toString(), '18'); const reserialized = PresentationRequest.toJSON(deserialized); assert.deepStrictEqual(reserialized, serialized); const context = deserialized.inputContext; assert(context, 'Context should exist'); assert.deepStrictEqual(context.action, 'myMethod'); assert.deepStrictEqual(context.serverNonce, Field(789)); let derivedContext = { clientNonce: Field(999), vkHash: Field(123), claims: Field(456), }; const originalContext = hashContext(computeZkAppContext({ ...originalRequest.inputContext, ...derivedContext, })); const deserializedContext = hashContext(computeZkAppContext({ ...deserialized.inputContext, ...derivedContext, })); assert.deepStrictEqual(deserializedContext, originalContext); }); await t.test('should deserialize https context correctly', async () => { const serverUrl = 'test.com'; const originalRequest = HttpsRequest({ spec, claims: { targetAge: Field(18) }, inputContext: { type: 'https', action: 'POST /api/verify', serverNonce: Field(789), }, }); const serialized = PresentationRequest.toJSON(originalRequest); const parsed = JSON.parse(serialized); const result = PresentationRequestSchema.safeParse(parsed); assert(result.success, 'HTTPS presentation request should be valid: ' + (result.success ? '' : JSON.stringify(result.error.issues, null, 2))); const deserialized = PresentationRequest.fromJSON('https', serialized); assert.strictEqual(deserialized.type, 'https'); assert.strictEqual(deserialized.claims.targetAge.toString(), '18'); const reserialized = PresentationRequest.toJSON(deserialized); assert.deepStrictEqual(reserialized, serialized); const context = deserialized.inputContext; assert(context, 'Context should exist'); assert.strictEqual(context.action, 'POST /api/verify'); assert.deepStrictEqual(context.serverNonce, Field(789)); let derivedContext = { clientNonce: Field(999), vkHash: Field(123), claims: Field(456), }; const originalContext = hashContext(computeHttpsContext({ ...originalRequest.inputContext, verifierIdentity: serverUrl, ...derivedContext, })); const deserializedContext = hashContext(computeHttpsContext({ ...originalRequest.inputContext, verifierIdentity: serverUrl, ...derivedContext, })); assert.deepStrictEqual(deserializedContext, originalContext); }); }); //# sourceMappingURL=deserialize.test.js.map