diff --git a/src/library/plugin/pluginType.ts b/src/library/plugin/pluginType.ts index e5e8c55..54044d1 100644 --- a/src/library/plugin/pluginType.ts +++ b/src/library/plugin/pluginType.ts @@ -58,4 +58,4 @@ export interface IMinAuthProverFactory< PublicInput, PrivateInput> { initialize(cfg: Configuration): Promise; -} \ No newline at end of file +} diff --git a/src/library/tools/pluginServer/config.ts b/src/library/tools/pluginServer/config.ts index 7c5ad2b..4b76d72 100644 --- a/src/library/tools/pluginServer/config.ts +++ b/src/library/tools/pluginServer/config.ts @@ -4,7 +4,7 @@ import env from 'env-var'; import fs from 'fs'; import yaml from 'yaml'; import { SimplePreimagePlugin } from "./plugins/simplePreimage/server"; -import { SimplePasswordTreePlugin } from "./plugins/passwordTree/server"; +import { MemberSetPlugin } from "./plugins/passwordTree/server"; // TODO: make use of heterogeneous lists /** @@ -15,7 +15,7 @@ export const untypedPlugins: IMinAuthPluginFactory, any, any, any>> = { "SimplePreimagePlugin": SimplePreimagePlugin, - "SimplePasswordTreePlugin": SimplePasswordTreePlugin + "MemberSetPlugin": MemberSetPlugin }; const serverConfigurationsSchema = z.object({ diff --git a/src/library/tools/pluginServer/index.ts b/src/library/tools/pluginServer/index.ts index afffd65..5cc4fa4 100644 --- a/src/library/tools/pluginServer/index.ts +++ b/src/library/tools/pluginServer/index.ts @@ -27,7 +27,7 @@ initializePlugins() // The type of `POST /verifyProof` requests' body. interface VerifyProofData { plugin: string; - publicInputArgs: any; + publicInputsArgs: any; proof: JsonProof; } @@ -45,7 +45,7 @@ initializePlugins() // Step 2: use the plugin to extract the output. The plugin is also responsible // for checking the legitimacy of the public inputs. const typedPublicInputArgs - = pluginInstance.publicInputArgsSchema.parse(data.publicInputArgs); + = pluginInstance.publicInputsArgsSchema.parse(data.publicInputsArgs); const output = await pluginInstance.verifyAndGetOutput(typedPublicInputArgs, data.proof); return output; diff --git a/src/plugins/passwordTree/client/index.ts b/src/plugins/passwordTree/client/index.ts index de8dc4a..d477b20 100644 --- a/src/plugins/passwordTree/client/index.ts +++ b/src/plugins/passwordTree/client/index.ts @@ -1,58 +1,61 @@ +// TODO requires changes import { Field, JsonProof } from "o1js"; -import ProvePasswordInTreeProgram, { PasswordTreePublicInput, PasswordTreeWitness } from "../common/passwordTreeProgram"; +import ProvePasswordInTreeProgram, { PasswordInTreeWitness } from "../common/passwordTreeProgram"; import { IMinAuthProver, IMinAuthProverFactory } from '../../../library/plugin/pluginType'; import axios from "axios"; -export type SimplePasswordTreeProverConfiguration = { - apiServer: URL, +export type MemberSetProverConfiguration = { + apiServer: URL, } -export class SimplePasswordTreeProver implements - IMinAuthProver + +// Prove that you belong to a set of user without revealing which user you are. +export class MemberSetProver implements + IMinAuthProver { - private readonly cfg: SimplePasswordTreeProverConfiguration; - - async prove(publicInput: PasswordTreePublicInput, secretInput: Field) - : Promise { - const proof = await ProvePasswordInTreeProgram.baseCase( - publicInput, Field.from(secretInput)); - return proof.toJSON(); - } - - async fetchPublicInputs(uid: bigint): Promise { - const mkUrl = (endpoint: string) => `${this.cfg.apiServer}/${endpoint}`; - const getWitness = async (): Promise => { - const resp = await axios.get(mkUrl(`/witness/${uid.toString()}`)); - if (resp.status != 200) { - throw `unable to fetch witness for ${uid.toString()}, error: ${(resp.data as { error: string }).error}`; - } - return PasswordTreeWitness.fromJSON(resp.data); - }; - const getRoot = async (): Promise => { - const resp = await axios.get(mkUrl('/root')); - return Field.fromJSON(resp.data); + private readonly cfg: MemberSetProverConfiguration; + + async prove(publicInput: PasswordInTreeWitness, secretInput: Field) + : Promise { + const proof = await ProvePasswordInTreeProgram.baseCase( + publicInput, Field.from(secretInput)); + return proof.toJSON(); } - const witness = await getWitness(); - const root = await getRoot(); - return new PasswordTreePublicInput({ witness, root }); - } + async fetchPublicInputs(uid: bigint): Promise { + const mkUrl = (endpoint: string) => `${this.cfg.apiServer}/${endpoint}`; + const getWitness = async (): Promise => { + const resp = await axios.get(mkUrl(`/witness/${uid.toString()}`)); + if (resp.status != 200) { + throw `unable to fetch witness for ${uid.toString()}, error: ${(resp.data as { error: string }).error}`; + } + return PasswordInTreeWitness.fromJSON(resp.data); + }; + const getRoot = async (): Promise => { + const resp = await axios.get(mkUrl('/root')); + return Field.fromJSON(resp.data); + } + const witness = await getWitness(); + const root = await getRoot(); + + return new PasswordInTreeWitness({ witness, root }); + } - constructor(cfg: SimplePasswordTreeProverConfiguration) { - this.cfg = cfg; - } + constructor(cfg: MemberSetProverConfiguration) { + this.cfg = cfg; + } - static async initialize(cfg: SimplePasswordTreeProverConfiguration): - Promise { - return new SimplePasswordTreeProver(cfg); - } + static async initialize(cfg: MemberSetProverConfiguration): + Promise { + return new MemberSetProver(cfg); + } } -SimplePasswordTreeProver satisfies IMinAuthProverFactory< - SimplePasswordTreeProver, - SimplePasswordTreeProverConfiguration, - bigint, - PasswordTreePublicInput, - Field -> +MemberSetProver satisfies IMinAuthProverFactory< + MemberSetProver, + MemberSetProverConfiguration, + bigint, + PasswordInTreeWitness, + Field + > diff --git a/src/plugins/passwordTree/common/passwordTreeProgram.ts b/src/plugins/passwordTree/common/passwordTreeProgram.ts index d06e4c6..8128994 100644 --- a/src/plugins/passwordTree/common/passwordTreeProgram.ts +++ b/src/plugins/passwordTree/common/passwordTreeProgram.ts @@ -1,29 +1,57 @@ -import { Experimental, Field, MerkleWitness, Poseidon, Struct } from "o1js"; +import { Experimental, Field, MerkleWitness, Poseidon, SelfProof, Struct } from "o1js"; +// TODO how can this be made dynamic export const PASSWORD_TREE_HEIGHT = 10; -export class PasswordTreeWitness extends MerkleWitness(PASSWORD_TREE_HEIGHT) { } +export class PasswordTreeWitness extends MerkleWitness(PASSWORD_TREE_HEIGHT) {} -export class PasswordTreePublicInput extends Struct({ - witness: PasswordTreeWitness, - root: Field -}) { }; +export class PasswordInTreeWitness extends Struct({ + witness: PasswordTreeWitness, + preImage: Field +}) {}; +export class MerkleRoot extends Struct({ + root: Field +}) {}; + + +export class ProvePasswordInTreeOutput extends Struct({ + recursiveMekleRootHash: Field, +}) {}; + +// Prove knowledge of a preimage of a hash in a merkle tree. +// The proof does not reveal the preimage nor the hash. +// The output contains a recursive hash of all the roots for which the preimage is known. +// output = hash(lastRoot + hash(secondLastRoot, ... hash(xLastRoot, lastRoot) ...) +// Therefore the order of the proofs matters. export const ProvePasswordInTreeProgram = Experimental.ZkProgram({ - publicInput: PasswordTreePublicInput, - publicOutput: Field, - - methods: { - baseCase: { - privateInputs: [Field], - method(publicInput: PasswordTreePublicInput, privateInput: Field): Field { - publicInput.witness - .calculateRoot(Poseidon.hash([privateInput])) - .assertEquals(publicInput.root); - return publicInput.witness.calculateIndex(); - } + publicInput: MerkleRoot, + publicOutput: ProvePasswordInTreeOutput, + + methods: { + baseCase: { + privateInputs: [PasswordInTreeWitness], + method(publicInput: MerkleRoot, privateInput: PasswordInTreeWitness): ProvePasswordInTreeOutput { + privateInput.witness + .calculateRoot(Poseidon.hash([publicInput.root])) + .assertEquals(publicInput.root); + return new ProvePasswordInTreeOutput( + { recursiveMekleRootHash: publicInput.root }); + } + }, + + inductiveCase: { + privateInputs: [SelfProof, PasswordInTreeWitness], + method(publicInput: MerkleRoot, earlierProof: SelfProof, privateInput: PasswordInTreeWitness): ProvePasswordInTreeOutput { + earlierProof.verify(); + privateInput.witness + .calculateRoot(Poseidon.hash([publicInput.root])) + .assertEquals(publicInput.root); + return new ProvePasswordInTreeOutput( + { recursiveMekleRootHash: Poseidon.hash([publicInput.root, earlierProof.publicOutput.recursiveMekleRootHash]) }); + } + } } - } }); -export default ProvePasswordInTreeProgram; \ No newline at end of file +export default ProvePasswordInTreeProgram; diff --git a/src/plugins/passwordTree/server/index.ts b/src/plugins/passwordTree/server/index.ts index e8415c4..715d2d6 100644 --- a/src/plugins/passwordTree/server/index.ts +++ b/src/plugins/passwordTree/server/index.ts @@ -184,30 +184,39 @@ class MinaBlockchainStorage } } -export class SimplePasswordTreePlugin implements IMinAuthPlugin{ +const PoseidonHashSchema = z.bigint(); + +const publicInputsArgsSchema = z.array(PoseidonHashSchema); + +export class MemberSetPlugin implements IMinAuthPlugin, string>{ readonly verificationKey: string; private readonly storage: TreeStorage customRoutes: Record = { - "/witness/:uid": async (req, resp) => { - if (req.method != 'GET') { - resp.status(400); - return; - } - - const uid = BigInt(req.params['uid']); - const witness = await this.storage.getWitness(uid); - - if (!witness) { - resp - .status(400) - .json({ error: "requested user doesn't exist" }); - return; - } - - resp.status(200).json(witness); - }, - "/root": async (req, resp) => { + // NOTE: witnesses are not public inputs now + // "/witness/:uid": async (req, resp) => { + // if (req.method != 'GET') { + // resp.status(400); + // return; + // } + + // const uid = BigInt(req.params['uid']); + // const witness = await this.storage.getWitness(uid); + + // if (!witness) { + // resp + // .status(400) + // .json({ error: "requested user doesn't exist" }); + // return; + // } + + // resp.status(200).json(witness); + // }, + + // TODO: + // input: array of merkle roots (eg. [root1, root2, root3]) + // output: object of the form { root1: tree1, root2: tree2, root3: tree3 } + "/roots": async (req, resp) => { if (req.method != 'GET') { resp.status(400); return; @@ -228,10 +237,12 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin{ } }; - publicInputArgsSchema: z.ZodType = z.bigint(); + publicInputsArgsSchema = publicInputsArgsSchema; - async verifyAndGetOutput(uid: bigint, jsonProof: JsonProof): + async verifyAndGetOutput(uid: z.infer, jsonProof: JsonProof): Promise { + + // build an array of merkle trees const proof = PasswordInTreeProofClass.fromJSON(jsonProof); const expectedWitness = await this.storage.getWitness(uid); const expectedRoot = await this.storage.getRoot(); @@ -253,7 +264,7 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin{ storageFile: string, contractPrivateKey: string, feePayerPrivateKey: string - }): Promise { + }): Promise { const { verificationKey } = await ProvePasswordInTreeProgram.compile(); const storage = await MinaBlockchainStorage .initialize( @@ -261,7 +272,7 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin{ PrivateKey.fromBase58(configuration.contractPrivateKey), PrivateKey.fromBase58(configuration.feePayerPrivateKey) ) - return new SimplePasswordTreePlugin(verificationKey, storage); + return new MemberSetPlugin(verificationKey, storage); } static readonly configurationSchema: @@ -277,7 +288,7 @@ export class SimplePasswordTreePlugin implements IMinAuthPlugin{ }) } -SimplePasswordTreePlugin satisfies +MemberSetPlugin satisfies IMinAuthPluginFactory< IMinAuthPlugin, { diff --git a/src/plugins/simplePreimage/server/index.ts b/src/plugins/simplePreimage/server/index.ts index 71f90ba..5bedd93 100644 --- a/src/plugins/simplePreimage/server/index.ts +++ b/src/plugins/simplePreimage/server/index.ts @@ -23,7 +23,7 @@ export class SimplePreimagePlugin implements IMinAuthPlugin{ return role; }; - publicInputArgsSchema: z.ZodType = z.any(); + publicInputsArgsSchema: z.ZodType = z.any(); customRoutes: Record = { "/roles": (_, res) => {