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

227 lines 10.5 kB
import * as A from 'fp-ts/lib/Array.js'; import * as E from 'fp-ts/lib/Either.js'; import * as O from 'fp-ts/lib/Option.js'; import { toArray } from 'fp-ts/lib/ReadonlyArray.js'; import * as R from 'fp-ts/lib/Record.js'; import * as TE from 'fp-ts/lib/TaskEither.js'; import { pipe } from 'fp-ts/lib/function.js'; import * as Str from 'fp-ts/lib/string.js'; import fs from 'fs/promises'; import { dropResult, findM, fromFailablePromise, liftZodParseResult } from 'minauth/dist/utils/fp/taskeither.js'; import { AccountUpdate, Field, MerkleTree, Mina, PrivateKey, Cache } from 'o1js'; import z from 'zod'; import * as ZkProgram from './merklemembershipsprogram.js'; import { TreeRootStorageContract } from './treerootstoragecontract.js'; /** * An implementation of the tree storage using in-memory data structures. */ export class InMemoryStorage { constructor() { /** Set of indexes of occupied leaves */ this.occupied = new Set(); /** The underlying Merkle tree */ this.merkleTree = new MerkleTree(ZkProgram.TREE_HEIGHT); } /** The Merkle tree root */ getRoot() { return TE.of(this.merkleTree.getRoot()); } /** Get a witness for given leaf index */ getWitness(leafIndex) { return TE.of(this.occupied.has(leafIndex) ? O.none : O.some(new ZkProgram.TreeWitness(this.merkleTree.getWitness(leafIndex)))); } /** Check if there's a leaf under the given index */ hasLeaf(leafIndex) { return TE.of(this.occupied.has(leafIndex)); } /** Set a Field value under the given index */ setLeaf(leafIndex, leaf) { return TE.fromIO(() => { this.occupied.add(leafIndex); this.merkleTree.setLeaf(leafIndex, leaf); }); } /** Get the set of leaves as an array of optional Fields */ getLeaves() { return () => { const leaves = new Array(Number(this.merkleTree.leafCount)); for (let i = 0; i < this.merkleTree.leafCount; i++) leaves[i] = this.occupied.has(BigInt(i)) ? O.some(this.merkleTree.getNode(0, BigInt(i))) : O.none; return Promise.resolve(E.right(leaves)); }; } } /** * An implementation of the tree storage using a file system handle. */ export class PersistentInMemoryStorage extends InMemoryStorage { /** * Write current state of the storage to the file. */ persist() { const storageObj = Array.from(this.occupied.values()).reduce((acc, idx) => { acc[Number(idx)] = this.merkleTree.getNode(0, idx).toJSON(); return acc; }, {}); return dropResult(fromFailablePromise(() => this.file.write(JSON.stringify(storageObj), 0, 'utf-8'), '')); } constructor(file, occupied, merkleTree) { super(); this.file = file; this.occupied = occupied; this.merkleTree = merkleTree; } /** * Initialize the storage from a file. * If the file is empty, initialize the storage with the given leaves. */ static initialize(path, initialLeaves) { const { O_CREAT, O_RDWR } = fs.constants; return pipe(TE.Do, TE.bind('handle', () => fromFailablePromise(() => fs.open(path, O_CREAT | O_RDWR), `unable to open file ${path} that stores the tree`)), TE.bind('content', ({ handle }) => fromFailablePromise(() => handle.readFile('utf-8'), `unable to read the content of the tree file`)), TE.bind('storageObject', ({ content }) => Str.isEmpty(content) ? TE.right(initialLeaves ?? {}) : liftZodParseResult(z.record(z.string(), z.string()).safeParse(JSON.parse(content)))), TE.map(({ handle, storageObject }) => { const { occupied, merkleTree } = R.reduceWithIndex(Str.Ord)({ occupied: new Set(), merkleTree: new MerkleTree(ZkProgram.TREE_HEIGHT) }, (rawIdx, { occupied, merkleTree }, rawLeaf) => { const idx = BigInt(rawIdx); occupied.add(BigInt(rawIdx)); merkleTree.setLeaf(idx, Field.fromJSON(rawLeaf)); return { occupied, merkleTree }; })(storageObject); return new PersistentInMemoryStorage(handle, occupied, merkleTree); }), // call persist in case the file is newly created TE.tap((s) => s.persist())); } } /** * A tree storage implementation with additional method `updateTreeRootOnChainIfNecessary` * that updates the root stored on chain if the off-chain root differs from the on-chain one. */ export class GenericMinaBlockchainTreeStorage { constructor(storage, contract, mkTx) { this.underlyingStorage = storage; this.contract = contract; this.mkTx = mkTx; } fetchOnChainRoot() { return fromFailablePromise(this.contract.treeRoot.fetch, 'unable to fetch root stored on chain, did you deploy the contract?'); } /** * Fetch the root stored on chain, compare to the off-chain counterpart * and update the on-chain root if necessary. */ updateTreeRootOnChainIfNecessary() { return pipe(TE.Do, TE.bind('onChainRoot', () => this.fetchOnChainRoot()), TE.bind('offChainRoot', () => this.underlyingStorage.getRoot()), TE.chain(({ onChainRoot, offChainRoot }) => { return onChainRoot.equals(offChainRoot).toBoolean() ? TE.of(undefined) : this.mkTx(() => this.contract.treeRoot.set(offChainRoot)); })); } /** * Get the off-chain Merkle tree root. */ getRoot() { return this.underlyingStorage.getRoot(); } /** Get a witness for given leaf index */ getWitness(leafIdx) { return this.underlyingStorage.getWitness(leafIdx); } /** Check if there's a leaf under the given index */ hasLeaf(leafIdx) { return this.underlyingStorage.hasLeaf(leafIdx); } /** Set a Field value under the given index * * NOTE. This function does not update the on-chain root. */ setLeaf(leafIndex, leaf) { return TE.chain(() => this.updateTreeRootOnChainIfNecessary())(this.underlyingStorage.setLeaf(leafIndex, leaf)); } /** Get the set of leaves as an array of optional Fields */ getLeaves() { return this.underlyingStorage.getLeaves(); } } /** * Initialize a blockchain tree storage. * The funciton will: * - compile and deploy the tree root storage contract if necessary * - initialize the mina storage using the given storage * - update the on-chain root to the one available through proviced storage */ function initializeGenericMinaBlockchainTreeStorage(storage, contractPrivateKey, feePayerPrivateKey) { const contractPublicKey = contractPrivateKey.toPublicKey(); const contractInstance = new TreeRootStorageContract(contractPublicKey); const feePayerPublicKey = feePayerPrivateKey.toPublicKey(); const mkTx = (txFn) => fromFailablePromise(async () => { const txn = await Mina.transaction(feePayerPublicKey, txFn); await txn.prove(); await txn.sign([feePayerPrivateKey, contractPrivateKey]).send(); }, 'unable to make transaction'); const blockchainStorage = new GenericMinaBlockchainTreeStorage(storage, contractInstance, mkTx); const compileContract = fromFailablePromise(() => TreeRootStorageContract.compile({ cache: Cache.None }), 'cannot compile tree root storage contract, this is a bug'); const deployContractIfNecessary = pipe(TE.Do, TE.bind('treeRoot', () => storage.getRoot()), TE.bind('shouldDeployContract', () => TE.of(Mina.hasAccount(contractPublicKey))), TE.chain(({ shouldDeployContract, treeRoot }) => shouldDeployContract ? mkTx(() => { AccountUpdate.fundNewAccount(feePayerPublicKey); contractInstance.treeRoot.set(treeRoot); contractInstance.deploy(); }) : blockchainStorage.updateTreeRootOnChainIfNecessary())); return pipe(TE.Do, TE.chain(() => compileContract), TE.chain(() => deployContractIfNecessary), TE.chain(() => TE.of(blockchainStorage))); } /** * A Merkle tree storage that keeps the tree root on Mina blockchain * guarded by a contract controlled by a private key. * The tree itself is stored in the off-chain file storage. */ export class MinaBlockchainTreeStorage extends GenericMinaBlockchainTreeStorage { static initialize(path, contractPrivateKey, feePayerPrivateKey, initialLeaves) { return pipe(TE.Do, TE.bind('storage', () => PersistentInMemoryStorage.initialize(path, initialLeaves)), TE.chain(({ storage }) => initializeGenericMinaBlockchainTreeStorage(storage, contractPrivateKey, feePayerPrivateKey))); } } /** * Schema for the configuration of a Mina trees provider. * If for given tree `feePayerPrivateKey` and `contractPrivateKey` * are not simultanously present the tree root will NOT be commited to * the MINA blockchain. */ export const minaTreesProviderConfigurationSchema = z.object({ feePayerPrivateKey: z.string().optional(), trees: z.array(z.object({ contractPrivateKey: z.string().optional(), offchainStoragePath: z.string(), initialLeaves: z.record(z.string()).optional() })) }); /** * Implements a trees provider that uses Mina blockchain to store the roots of the trees. * The trees are stored in the off-chain storage. */ export class MinaTreesProvider { getTree(treeRoot) { return findM((t) => pipe(t.getRoot(), TE.map((root) => root.equals(treeRoot).toBoolean())))(this.treeStorages); } getTreeRoots() { return A.traverse(TE.ApplicativePar)((t) => t.getRoot())(this.treeStorages); } constructor(treeStorages) { this.treeStorages = treeStorages; } static initialize(cfg) { const feePayerPrivateKey = cfg.feePayerPrivateKey ? PrivateKey.fromBase58(cfg.feePayerPrivateKey) : undefined; const trees = TE.traverseArray((tCfg) => feePayerPrivateKey && tCfg.contractPrivateKey ? MinaBlockchainTreeStorage.initialize(tCfg.offchainStoragePath, PrivateKey.fromBase58(tCfg.contractPrivateKey), feePayerPrivateKey, tCfg.initialLeaves) : PersistentInMemoryStorage.initialize(tCfg.offchainStoragePath, tCfg.initialLeaves))(cfg.trees); return TE.map((ts) => new MinaTreesProvider(toArray(ts)))(trees); } } //# sourceMappingURL=treestorage.js.map