UNPKG

minauth-erc721-timelock-plugin

Version:

This package contains an implementation of a MinAuth plugin that combines zero-knowledge Merkle "membership" proofs with the ability to register into to a Merkle tree with locking an Ethereum NFT.

195 lines 8.43 kB
/** * This module contains an example of a plugin for the minauth library * that interacts with an Ethereum smart contract indirectly via a * ethers.js client. See the plugin's class documentation for more details. */ import crypto from 'crypto'; import { Cache, Field, verify } from 'o1js'; import { outputInvalid, outputValid } from 'minauth/dist/plugin/plugintype.js'; import { MerkleMembershipProgram, MerkleMembershipProof } from './merkle-membership-program.js'; // import { Router, Request, Response } from 'express'; import { z } from 'zod'; import { wrapZodDec, combineEncDec, noOpEncoder } from 'minauth/dist/plugin/encodedecoder.js'; import { JsonProofSchema } from 'minauth/dist/common/proof.js'; import { Erc721TimeLock } from './erc721timelock.js'; import { ethers } from 'ethers'; import { Router } from 'express'; /** * The plugin configuration schema. * `timeLockContractAddress` - an address to the ethereum contract handling * time-locking NFTs and hashes. * `erc721ContractAddress` - an address to the ethereum contract for NFTs * that configured to be used with this plugin (in future might be extended to * support multiple such addresses) * `ethereumJsonRpcProvider` - a json rpc provider for the ethereum network */ export const ConfigurationSchema = z.object({ timeLockContractAddress: z.string(), erc721ContractAddress: z.string(), ethereumJsonRpcProvider: z.string() }); /** * The plugin's output schema. */ export const OutputSchema = z.object({ merkleRoot: z.string(), contractConfigurationHash: z.string() }); /** * The Ethereum contract implements an NFT timelock scheme. * One can lock an NFT for a given period of time along with a hash. * All the hashes behind the locked NFTs are stored in a merkle tree. * The plugin allows one to prove that they have the preimage of the hash * and thus have the right to get the authorization. * When use against suffiently large merkle tree provides a level * of privacy - the proof does not reveal which hash nor the merkle witness * for the hash. * Some care must be taken to avoid timing attacks. */ export class Erc721TimelockPlugin { /** * Verify a proof and return the role. */ async verifyAndGetOutput(input) { try { // fetch valid commitments from the eth contract const merkleTree = await this.ethContract.buildCommitmentTree(); this.logger.debug(`Fetched ${merkleTree.leafCount} commitments with merkle root ${merkleTree.root}.`); const proof = MerkleMembershipProof.fromJSON(input.proof); this.logger.debug(`Verifying proof for merkle root ${proof.publicInput.merkleRoot}...`); // the proof was generated for another merkle root if (!proof.publicInput.merkleRoot.equals(merkleTree.root).toBoolean()) { const msg = 'Invalid merkle root. ${proof.publicInput.merkleRoot} !== ${merkleTree.root}'; this.logger.info(msg); throw new Error(msg); } this.logger.info('ZK Proof verification...'); const valid = await verify(proof, this.verificationKey.data); if (!valid) { this.logger.info('Proof verification failed.'); throw new Error('Invalid proof!'); } // The proof is valid which means that the prover has the knowledge of the // preimage of one of the hashes stored in Eth contract. this.logger.info('Proof verification succeeded.'); // hash the configuration and pin it to the output return { merkleRoot: proof.publicInput.merkleRoot.toString(), contractConfigurationHash: this.configurationHash() }; } catch (error) { this.logger.error('Error verifying proof: ', error); throw error; } } // .post('/admin/mint', this.handleAdminMint); /** * Check if produced output is still valid. * It DOES NOT verify the proof. * It assumes that the output was produced by the plugin. * So use it only to check if produced output is still valid. * Not to re-verifiy the proof. * In case of this plugin it means that the merkle tree has not changed. */ async checkOutputValidity(output) { this.logger.debug('Checking validity of ', output); let outputRoot; try { outputRoot = Field.from(output.merkleRoot); } catch (error) { return outputInvalid('Invalid merkle root.'); } const tree = await this.ethContract.buildCommitmentTree(); if (!tree.root.equals(outputRoot).toBoolean()) { return outputInvalid('Merkle root has changed.'); } if (this.configurationHash() !== output.contractConfigurationHash) { return outputInvalid('Configuration has changed.'); } return Promise.resolve(outputValid); } /** * This ctor is meant to be called by the `initialize` function. */ constructor(ethContract, verificationKey, configuration, logger) { this.ethContract = ethContract; this.verificationKey = verificationKey; this.configuration = configuration; this.logger = logger; /** * This plugin uses an idiomatic Typescript interface */ this.__interface_tag = 'ts'; /** * A utility function that hashes (SHA256) the current configuration. */ this.configurationHash = () => { const contractConfigurationHash = crypto .createHash('sha256') .update(JSON.stringify(this.configuration)) .digest('hex'); return contractConfigurationHash; }; /** * Trivial - no public inputs. */ this.publicInputArgsSchema = z.any(); // private handleAdminMint = async (req: Request, res: Response) => { // const reqSchema = z.object({ // address: z.string().regex(/^0x[a-fA-F0-9]{40}$/) // }); // const r = reqSchema.safeParse(req.body); // if (!r.success) { // res.status(400).json({ // error: // "Invalid ethereum address. The body should be {'address':[recipient_eth_address]}" // }); // return; // } // const { address } = r.data; // try { // const tx = await this.ethContract.mint(address); // } catch (e) { // res.status(500).json({ error: 'Error minting token.' }); // return; // } // res.status(200).json({ success: true }); // }; /** * The plugin exposes eth contract addresses via http endpoints. * The prover should directly interact with the contract to build the proof. */ this.customRoutes = Router() .get('/timelock-address', async (_, res) => { res.json(this.ethContract.lockContractAddress); }) .get('/erc721-address', async (_, res) => { res.json(this.ethContract.erc721ContractAddress); }); } /** * Initialize the plugin with a configuration. */ static async initialize(configuration, logger) { const { verificationKey } = await MerkleMembershipProgram.compile({ cache: Cache.None }); const provider = new ethers.JsonRpcProvider(configuration.ethereumJsonRpcProvider); const erc721timelock = await Erc721TimeLock.initialize({ lockContractAddress: configuration.timeLockContractAddress, nftContractAddress: configuration.erc721ContractAddress }, provider, logger); return new Erc721TimelockPlugin(erc721timelock, verificationKey, configuration, logger); } } Erc721TimelockPlugin.__interface_tag = 'ts'; Erc721TimelockPlugin.configurationDec = wrapZodDec('ts', ConfigurationSchema); Erc721TimelockPlugin.inputDecoder = wrapZodDec('ts', z.object({ proof: JsonProofSchema })); /** output parsing and serialization */ Erc721TimelockPlugin.outputEncDec = combineEncDec(noOpEncoder('ts'), wrapZodDec('ts', OutputSchema)); // verify the factory interface implementation Erc721TimelockPlugin; export default Erc721TimelockPlugin; //# sourceMappingURL=plugin.js.map