UNPKG

minauth-merkle-membership-plugin

Version:

This package contains an implementation of a simple MinAuth plugin that extends the concept of password authentication into authenticating within sets that provide a level of anonymity. The proofs are built and verified with MINA's `o1js` library & proof

146 lines 7.85 kB
import { Poseidon, ZkProgram, verify } from 'o1js'; import { Program } from './merklemembershipsprogram.js'; import z from 'zod'; import { outputInvalid, outputValid } from 'minauth/dist/plugin/plugintype.js'; import { MinaTreesProvider, minaTreesProviderConfigurationSchema } from './treestorage.js'; import { Router } from 'express'; import { pipe } from 'fp-ts/lib/function.js'; import * as TE from 'fp-ts/lib/TaskEither.js'; import * as IOE from 'fp-ts/lib/IOEither.js'; import { fromFailablePromise, guard, safeGetFieldParam } from 'minauth/dist/utils/fp/taskeither.js'; import * as NE from 'fp-ts/lib/NonEmptyArray.js'; import * as E from 'fp-ts/lib/Either.js'; import * as O from 'fp-ts/lib/Option.js'; import * as A from 'fp-ts/lib/Array.js'; import { wrapZodDec } from 'minauth/dist/plugin/encodedecoder.js'; import { wrapTrivialExpressHandler } from 'minauth/dist/plugin/express.js'; import { fieldEncDec } from 'minauth/dist/utils/fp/fieldEncDec.js'; import { JsonProofSchema } from 'minauth/dist/common/proof.js'; const merkleRootsEncDec = { __interface_tag: 'fp', decode: (i) => pipe(wrapZodDec('fp', z.array(z.unknown())).decode(i), E.chain(A.traverse(E.Applicative)(fieldEncDec.decode)), E.chain((arr) => E.fromOption(() => 'empty public input')(NE.fromArray(arr)))), encode: NE.map(fieldEncDec.encode) }; /** * Encode/decode `PublicInputArgs` to/from an array of strings. */ const inputEncDec = { __interface_tag: 'fp', decode: (i) => pipe(E.Do, E.bind('inp', () => wrapZodDec('fp', z.object({ merkleRoots: z.array(z.string()), proof: z.unknown() })).decode(i)), E.bind('merkleRoots', ({ inp }) => merkleRootsEncDec.decode(inp.merkleRoots)), E.bind('proof', ({ inp }) => wrapZodDec('fp', JsonProofSchema).decode(inp.proof)), E.map(({ merkleRoots, proof }) => { return { merkleRoots, proof }; })), encode: (i) => { return { merkleRoots: merkleRootsEncDec.encode(i.merkleRoots), proof: i.proof }; } }; /** * Encode/decode `Output` to/from an object. */ const outputEncDec = { __interface_tag: 'fp', decode: (i) => pipe(E.Do, E.bind('rawObj', () => wrapZodDec('fp', z.object({ merkleRoots: z.unknown(), recursiveHash: z.string() })).decode(i)), E.bind('merkleRoots', ({ rawObj }) => merkleRootsEncDec.decode(rawObj.merkleRoots)), E.bind('recursiveHash', ({ rawObj }) => fieldEncDec.decode(rawObj.recursiveHash)), E.map(({ merkleRoots, recursiveHash }) => { return { merkleRoots, recursiveHash }; })), encode: ({ merkleRoots, recursiveHash }) => { return { merkleRoots: merkleRootsEncDec.encode(merkleRoots), recursiveHash: fieldEncDec.encode(recursiveHash) }; } }; const treeMissingError = (root) => { return { __kind: 'tree_missing', root }; }; const otherError = (error) => { return { __kind: 'other', error }; }; const computeExpectedHashErrorToString = (e) => e.__kind == 'tree_missing' ? `tree with root ${e.root.toString()} not found` : e.error; /** There's a particular hash connected to a set of roots. * It is also computed by the zk program that generates the proof. * This function computes the expected hash. * To understand the hash construction consult `merkleMembershipsProgram.ts` */ const computeExpectedHash = (forest) => (roots) => pipe(NE.traverse(TE.ApplicativeSeq)((root) => pipe(forest.getTree(root), TE.mapLeft(otherError), TE.tap(TE.fromOption(() => treeMissingError(root))), TE.map(() => root)))(roots), TE.map((roots) => A.reduce(NE.head(roots), (acc, x) => Poseidon.hash([x, acc]))(NE.tail(roots)))); /** * The MerkleMemberships Minauth plugin. * The plugin keeps a configured set of Merkle trees. * Each tree represents a set of authorized members. * A user can prove that they are a member of a set by providing * a Merkle witness to a known secret within a tree. * The user identity is not revealed - only the set of proven * memberships */ export class MerkleMembershipsPlugin { /** Given public input description and a zk proof validate the proof * and produce the output */ verifyAndGetOutput(input) { const treeRoots = TE.fromOption(() => 'empty input list')(NE.fromArray(input.merkleRoots)); const deserializedProof = TE.fromIOEither(IOE.tryCatch(() => ZkProgram.Proof(Program).fromJSON(input.proof), (err) => String(err))); return pipe(TE.Do, TE.bind('treeRoots', () => treeRoots), TE.bind('deserializedProof', () => deserializedProof), TE.bind('proofIsValid', ({ deserializedProof }) => fromFailablePromise(() => verify(deserializedProof, this.verificationKey), 'Error during proof verification')), TE.tap(({ proofIsValid }) => guard(proofIsValid, 'The proof was verified and it is invalid.')), TE.bind('expectedHash', ({ treeRoots }) => pipe(computeExpectedHash(this.storageProvider)(treeRoots), TE.mapLeft(computeExpectedHashErrorToString), TE.tapIO((hash) => () => this.logger.debug(`expected hash`, hash.toString())))), TE.tap(({ expectedHash, deserializedProof }) => guard(expectedHash .equals(deserializedProof.publicOutput.recursiveHash) .toBoolean(), 'unexpected recursive hash')), TE.map(({ expectedHash }) => { return { merkleRoots: input.merkleRoots, recursiveHash: expectedHash }; })); } /** * The output of the plugin may become invalid if the underlying * Merkle trees got updated. This function checks if the output * is still valid. */ checkOutputValidity(o) { return pipe(computeExpectedHash(this.storageProvider)(o.merkleRoots), TE.map((expectedHash) => expectedHash.equals(o.recursiveHash).toBoolean() ? outputValid : outputInvalid('invalid revursive hash')), TE.orElse((err) => err.__kind == 'other' ? TE.left(err.error) : TE.right(outputInvalid(`tree missing: ${err.root.toString()}`)))); } constructor(verificationKey, storageProvider, logger) { this.__interface_tag = 'fp'; /** * A set of express.js routes for communicating with the prover. */ this.customRoutes = Router() .get( /** Return all the leaves for a given tree root */ '/getLeaves/:treeRoot/', wrapTrivialExpressHandler((req) => pipe(TE.Do, TE.bind('treeRoot', () => safeGetFieldParam('treeRoot', req.params)), TE.chain(({ treeRoot }) => pipe(this.storageProvider.getTree(treeRoot), TE.chain(TE.fromOption(() => `tree with root ${treeRoot.toString()} missing`)), TE.chain((tree) => tree.getLeaves()), TE.map(A.map(O.toUndefined))))))) /** Return all merkle tree roots supported the plugin */ .get('/getTreeRoots', wrapTrivialExpressHandler(() => this.storageProvider.getTreeRoots())); this.verificationKey = verificationKey; this.storageProvider = storageProvider; this.logger = logger; } /** * Initialize plugin with a typed configuration. */ static initialize(cfg, logger) { return pipe(TE.Do, TE.bind('compilationResult', () => fromFailablePromise(Program.compile, 'bug: unable to compile MerkleMembershipsProgram')), TE.bind('storage', () => MinaTreesProvider.initialize(cfg)), TE.map(({ compilationResult, storage }) => new MerkleMembershipsPlugin(compilationResult.verificationKey, storage, logger))); } } MerkleMembershipsPlugin.__interface_tag = 'fp'; MerkleMembershipsPlugin.configurationDec = wrapZodDec('fp', minaTreesProviderConfigurationSchema); MerkleMembershipsPlugin.inputDecoder = inputEncDec; MerkleMembershipsPlugin.outputEncDec = outputEncDec; MerkleMembershipsPlugin; export default MerkleMembershipsPlugin; //# sourceMappingURL=plugin.js.map