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