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
JavaScript
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