Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merkle Memberships Program; Clean up #2

Merged
merged 21 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"start-api": "nodemon src/someServer/apiServer.ts",
"start-plugin-server": "nodemon src/library/tools/pluginServer.ts",
"start-plugin-server": "nodemon src/library/tools/pluginServer/index.ts",
"start-client": "nodemon src/headlessClient/client.ts",
"build": "tsc",
"serve-api": "node ./dist/someServer/apiServer.js",
Expand All @@ -28,6 +28,7 @@
"crypto": "^1.0.1",
"env-var": "^7.4.1",
"express": "^4.18.2",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.2",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
Expand Down
22 changes: 11 additions & 11 deletions src/library/plugin/pluginType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import z from 'zod';

// Interfaces used on the server side.

export interface IMinAuthPlugin<PublicInputsArgs, Output> {
export interface IMinAuthPlugin<PublicInputArgs, Output> {
// Verify a proof give the arguments for fetching public inputs, and return
// the output.
verifyAndGetOutput(
publicInputArgs: PublicInputsArgs,
publicInputArgs: PublicInputArgs,
serializedProof: JsonProof): Promise<Output>;

// The schema of the arguments for fetching public inputs.
readonly publicInputArgsSchema: z.ZodType<PublicInputsArgs>;
readonly publicInputArgsSchema: z.ZodType<PublicInputArgs>;

// TODO: enable plugins to invalidate a proof.
// FIXME(Connor): I still have some questions regarding the validation functionality.
// In particular, what if a plugin want to invalidate the proof once the public inputs change?
// We have to at least pass PublicInputsArgs.
// We have to at least pass PublicInputArgs.
//
// checkOutputValidity(output: Output): Promise<boolean>;

Expand All @@ -30,8 +30,8 @@ export interface IMinAuthPlugin<PublicInputsArgs, Output> {

// TODO: generic type inference?
export interface IMinAuthPluginFactory<
T extends IMinAuthPlugin<PublicInputsArgs, Output>,
Configuration, PublicInputsArgs, Output> {
T extends IMinAuthPlugin<PublicInputArgs, Output>,
Configuration, PublicInputArgs, Output> {

// Initialize the plugin given the configuration. The underlying zk program is
// typically compiled here.
Expand All @@ -42,20 +42,20 @@ export interface IMinAuthPluginFactory<

// Interfaces used on the client side.

export interface IMinAuthProver<PublicInputsArgs, PublicInput, PrivateInput> {
export interface IMinAuthProver<PublicInputArgs, PublicInput, PrivateInput> {
prove(publicInput: PublicInput, secretInput: PrivateInput): Promise<JsonProof>;

fetchPublicInputs(args: PublicInputsArgs): Promise<PublicInput>;
fetchPublicInputs(args: PublicInputArgs): Promise<PublicInput>;
}

export interface IMinAuthProverFactory<
T extends IMinAuthProver<
PublicInputsArgs,
PublicInputArgs,
PublicInput,
PrivateInput>,
Configuration,
PublicInputsArgs,
PublicInputArgs,
PublicInput,
PrivateInput> {
initialize(cfg: Configuration): Promise<T>;
}
}
28 changes: 22 additions & 6 deletions src/library/tools/pluginServer/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IMinAuthPlugin, IMinAuthPluginFactory } from "plugin/pluginType";
import { IMinAuthPlugin, IMinAuthPluginFactory } from '../../../library/plugin/pluginType';
import z from "zod";
import env from 'env-var';
import fs from 'fs';
import yaml from 'yaml';
import { SimplePreimagePlugin } from "plugins/simplePreimage/plugin";
import { SimplePasswordTreePlugin } from "plugins/passwordTree/plugin";
import { SimplePreimagePlugin } from "../../../plugins/simplePreimage/server";
import { MerkleMembershipsPlugin } from "../../../plugins/merkleMemberships/server";

// TODO: make use of heterogeneous lists
/**
Expand All @@ -15,13 +15,13 @@ export const untypedPlugins:
IMinAuthPluginFactory<IMinAuthPlugin<any, any>, any, any, any>>
= {
"SimplePreimagePlugin": SimplePreimagePlugin,
"SimplePasswordTreePlugin": SimplePasswordTreePlugin
"MerkleMembershipsPlugin": MerkleMembershipsPlugin
};

const serverConfigurationsSchema = z.object({
server: z.object({
address: z.string().ip().default("127.0.0.1"),
port: z.bigint().default(BigInt(3001)),
port: z.number().default(3001),
}),
plugins: z.object
(Object
Expand All @@ -34,6 +34,17 @@ const serverConfigurationsSchema = z.object({

export type ServerConfigurations = z.infer<typeof serverConfigurationsSchema>;

const defaultConfiguration: ServerConfigurations = {
server: {
address: "127.0.0.1",
port: 3001
},
plugins: {
SimplePreimagePlugin: { roles: {} },
MerkleMembershipsPlugin: { trees: [] }
}
}

/**
* Load configurations from disk. The configuration is encoded in yaml and
* should conform to `serverConfigurationsSchema`. The location of the file can
Expand All @@ -46,7 +57,12 @@ export function readConfigurations(): ServerConfigurations {
env.get('MINAUTH_CONFIG')
.default("config.yaml")
.asString();

if (!fs.existsSync(configFile)) {
console.warn("configuration file not exists, use the default configuration")
return defaultConfiguration;
}
const configFileContent = fs.readFileSync(configFile, 'utf8');
const untypedConfig: any = yaml.parse(configFileContent);
return serverConfigurationsSchema.parse(untypedConfig);
}
}
12 changes: 8 additions & 4 deletions src/library/tools/pluginServer/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
import { JsonProof, verify } from 'o1js';
import { IMinAuthPlugin } from 'plugin/pluginType';
import { IMinAuthPlugin } from '../../../library/plugin/pluginType';
chfanghr marked this conversation as resolved.
Show resolved Hide resolved
import { readConfigurations, untypedPlugins } from './config';

const configurations = readConfigurations();

console.log("configuration loaded", configurations)

/**
* Construct plugins which are enabled in the configuration.
* @returns A record of plugin instances.
*/
async function initializePlugins():
Promise<Record<string, IMinAuthPlugin<any, any>>> {
console.log('compiling plugins');
console.log('initializing plugins');
return Object
.entries(configurations.plugins)
.reduce(async (o, [name, cfg]) => {
console.debug(`initializing ${name}`, cfg);
const factory = untypedPlugins[name];
const plugin = await factory.initialize(cfg);
const typedCfg = factory.configurationSchema.parse(cfg)
const plugin = await factory.initialize(typedCfg);
return { ...o, [name]: plugin };
}, {});
}
Expand Down Expand Up @@ -82,4 +86,4 @@ initializePlugins()
.catch((error) => {
console.error('Error during server initialization:', error);
process.exit(1);
});
});
113 changes: 113 additions & 0 deletions src/plugins/merkleMemberships/client/index.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a nice functional implementation but got noisy. Typescript syntax is not very beautiful it appears. I'll propose some changes.

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Field, JsonProof, SelfProof } from "o1js";
import {
MerkleMembershipTreeWitness,
MerkleMembershipsOutput,
MerkleMembershipsPrivateInputs,
MerkleMembershipsProgram,
MerkleRoot
} from "../common/merkleMembershipsProgram";
import { IMinAuthProver, IMinAuthProverFactory } from '../../../library/plugin/pluginType';
import * as A from 'fp-ts/Array'
import * as O from 'fp-ts/Option'
import axios from "axios";

export type MembershipsProverConfiguration = {
baseUrl: string
}

export type MembershipsPublicInputArgs =
Array<{
treeIndex: bigint,
leafIndex: bigint
}>

// Prove that you belong to a set of user without revealing which user you are.
export class MembershipsProver implements
IMinAuthProver<
MembershipsPublicInputArgs, // TODO how to fetch
Array<[MerkleRoot, MerkleMembershipTreeWitness]>,
Array<Field>>
{
private readonly cfg: MembershipsProverConfiguration;

async prove(
publicInput: Array<[MerkleRoot, MerkleMembershipTreeWitness]>,
secretInput: Array<Field>)
: Promise<JsonProof> {
if (publicInput.length != secretInput.length)
throw "unmatched public/secret input list"

const proof: O.Option<SelfProof<MerkleRoot, MerkleMembershipsOutput>> =
await
A.reduce
(
Promise.resolve<O.Option<SelfProof<MerkleRoot, MerkleMembershipsOutput>>>(O.none),
(acc, [[root, witness], secret]: [[MerkleRoot, MerkleMembershipTreeWitness], Field]) => {
const privInput = new MerkleMembershipsPrivateInputs({ witness, secret })
return acc.then(
O.match(
() => MerkleMembershipsProgram.baseCase(root, privInput).then(O.some),
(prev) => MerkleMembershipsProgram.inductiveCase(root, prev, privInput).then(O.some)
)
)
}
)
(A.zip(publicInput, secretInput));

return O.match(
() => { throw "empty input list" }, // TODO: make it pure
(p: SelfProof<MerkleRoot, MerkleMembershipsOutput>) => p.toJSON()
)(proof);
}

async fetchPublicInputs(args: MembershipsPublicInputArgs): Promise<Array<[MerkleRoot, MerkleMembershipTreeWitness]>> {
const mkUrl = (treeIndex: bigint, leafIndex: bigint) =>
`${this.cfg.baseUrl}/getRootAndWitness/${treeIndex.toString()}/${leafIndex.toString()}`;
const getRootAndWitness =
async (treeIndex: bigint, leafIndex: bigint):
Promise<[MerkleRoot, MerkleMembershipTreeWitness]> => {
const url =
`${this.cfg.baseUrl}/getRootAndWitness/${treeIndex.toString()}/${leafIndex.toString()}`;
const resp = await axios.get(url);
if (resp.status == 200) {
const body: {
root: string,
witness: string
} = resp.data;
const root = Field.fromJSON(body.root);
const witness = MerkleMembershipTreeWitness.fromJSON(body.witness);
return [new MerkleRoot({ root }), witness];
} else {
const body: { error: string } = resp.data;
throw `error while getting root and witness: ${body.error}`;
}
}

return Promise.all(A.map(
(args: {
treeIndex: bigint,
leafIndex: bigint
}): Promise<[MerkleRoot, MerkleMembershipTreeWitness]> =>
getRootAndWitness(args.treeIndex, args.leafIndex)
)(args));
}

constructor(cfg: MembershipsProverConfiguration) {
this.cfg = cfg;
}

static async initialize(cfg: MembershipsProverConfiguration):
Promise<MembershipsProver> {
return new MembershipsProver(cfg);
}
}

MembershipsProver satisfies IMinAuthProverFactory<
MembershipsProver,
MembershipsProverConfiguration,
MembershipsPublicInputArgs,
Array<[MerkleRoot, MerkleMembershipTreeWitness]>,
Array<Field>
>

export default MembershipsProver;
71 changes: 71 additions & 0 deletions src/plugins/merkleMemberships/common/merkleMembershipsProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Experimental, Field, MerkleWitness, Poseidon, SelfProof, Struct } from "o1js";

// TODO how can this be made dynamic
export const MERKLE_MEMBERSHIP_TREE_HEIGHT = 10;

export class MerkleMembershipTreeWitness extends
chfanghr marked this conversation as resolved.
Show resolved Hide resolved
MerkleWitness(MERKLE_MEMBERSHIP_TREE_HEIGHT)
{ }

export class MerkleMembershipsPrivateInputs extends Struct({
witness: MerkleMembershipTreeWitness,
secret: Field
}) { }

export class MerkleRoot extends Struct({
root: Field
}) { }

export class MerkleMembershipsOutput extends Struct({
recursiveHash: 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 MerkleMembershipsProgram = Experimental.ZkProgram({
publicInput: MerkleRoot,
publicOutput: MerkleMembershipsOutput,

methods: {
baseCase: {
privateInputs: [MerkleMembershipsPrivateInputs],
method(publicInput: MerkleRoot,
privateInput: MerkleMembershipsPrivateInputs)
: MerkleMembershipsOutput {
privateInput.witness
.calculateRoot(Poseidon.hash([privateInput.secret]))
.assertEquals(publicInput.root);
return new MerkleMembershipsOutput({
recursiveHash: publicInput.root
});
}
},

inductiveCase: {
privateInputs: [SelfProof, MerkleMembershipsPrivateInputs],
method(
publicInput: MerkleRoot,
earlierProof: SelfProof<MerkleRoot, MerkleMembershipsOutput>,
privateInput: MerkleMembershipsPrivateInputs
): MerkleMembershipsOutput {
earlierProof.verify();
privateInput.witness
.calculateRoot(Poseidon.hash([privateInput.secret]))
.assertEquals(publicInput.root);
return new MerkleMembershipsOutput(
{
recursiveHash:
Poseidon.hash([
publicInput.root,
earlierProof.publicOutput.recursiveHash
])
});
}
}
}
});

export default MerkleMembershipsProgram;
Loading