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.

154 lines 6.76 kB
/** * This module contains a prover counter-part to the Minauth ERC721 time-lock plugin. * It interacts with an Ethereum smart contract pointed by the plugin (the verifier) * to obtain public inputs for the zkproof used as the Minauth authorization mean. */ import { Cache, Poseidon } from 'o1js'; import * as ZkProgram from './merkle-membership-program.js'; import * as z from 'zod'; import { Erc721TimeLock } from './erc721timelock.js'; import { commitmentFieldToHex, commitmentHexToField, mkUserSecret, userCommitmentHex } from './commitment-types.js'; import { MerkleMembershipProgram } from './merkle-membership-program.js'; /** * With this class you can build proofs and interact with `Erc721TimelockPlugin`. * The plugin monitors the state of an Ethereum contract. * 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 Erc721TimelockProver { /** * Build a proof for given inputs. * In order to obtain the secret hash use `buildSecretHash` from a secret user string. * NOTE that even though TreeWitness is passed as public input, it should not be known to the verifier. * TODO fix the above */ async prove(autoInput, userSecretInput) { this.logger.info(`Building proof started for merkle root: ${autoInput.merkleRoot}}`); const publicInput = new ZkProgram.PublicInput({ merkleRoot: autoInput.merkleRoot }); const secretInput = new ZkProgram.PrivateInput({ witness: autoInput.treeWitness, secret: mkUserSecret(userSecretInput).secretHash }); let proof = null; try { proof = await MerkleMembershipProgram.proveMembership(publicInput, secretInput); } catch (e) { this.logger.error('Error in proof generation:', e); this.logger.debug('Public input:', publicInput, 'Computed commitment:', Poseidon.hash([secretInput.secret]), 'Computed commitment hex:', commitmentFieldToHex({ commitment: Poseidon.hash([secretInput.secret]) })); throw e; } this.logger.info('Building proof finished.'); return proof.toJSON(); } async buildInputAndProve(userSecretInput) { let secretPreimage = null; try { secretPreimage = mkUserSecret(userSecretInput); } catch (e) { throw new Error('Could not encode secret preimage'); } const userCommitment = userCommitmentHex(secretPreimage); const autoInput = await this.fetchPublicInputs({ userCommitment }); return await this.prove(autoInput, userSecretInput); } async fetchEligibleCommitments() { this.logger.debug('Fetching eligible commitments started.'); const r = await this.ethContract.fetchEligibleCommitments(); this.logger.debug('Fetching eligible commitments finished.', r); return r; } /** * Fetch the data necessary to build the proof inputs. * In this case these are Merkle trees related to the roots * passed as arguments. */ async fetchPublicInputs(args) { const commitmentTree = await this.ethContract.buildCommitmentTree(); const commitmentField = commitmentHexToField(args.userCommitment); let witness = null; try { witness = commitmentTree.getWitness(commitmentField.commitment); } catch (e) { const msg = `Could not build the witness for the commitment ${args.userCommitment.commitmentHex}`; this.logger.error(msg); throw new Error(msg); } return { merkleRoot: commitmentTree.root, treeWitness: witness }; } /** * The plugin provides an auxiliary method to lock an NFT along with a commitment, * indirectly via the Ethereum contract. */ async lockNft(commitment, tokenId) { this.logger.debug(`Locking NFT ${tokenId} with commitment ${commitment} started.`); await this.ethContract.lockToken(tokenId, commitment); this.logger.debug('Locking NFT finished.'); } /** * The plugin provides an auxiliary method to unlock a locked NFT after * the lock-up period is over. */ async unlockNft(index) { this.logger.debug(`Unlocking NFT at index ${index} started.`); await this.ethContract.unlockToken(index); this.logger.debug('Unlocking NFT finished.'); } constructor(logger, ethContract) { this.logger = logger; this.ethContract = ethContract; /** This class uses the functionl style interface of the plugin. */ this.__interface_tag = 'ts'; } /** Compile the underlying zk circuit */ static async compile() { // disable cache because of bug in o1js 0.14.1: // you have a verification key acquired by using cached circuit AND // not build a proof locally, // but use a serialized one - it will hang during verification. return await MerkleMembershipProgram.compile({ cache: Cache.None }); } get ethereumProvider() { return this.ethContract.ethereumProvider.toString(); } get lockContractAddress() { return this.ethContract.lockContractAddress; } get erc721ContractAddress() { return this.ethContract.erc721ContractAddress; } /** Initialize the prover */ static async initialize(cfg, { compile = true } = {}) { const { logger, pluginRoutes } = cfg; logger.info('Erc721TimelockPlugin.initialize'); if (compile) { logger.info('compiling the circuit'); await Erc721TimelockProver.compile(); logger.info('compiled'); } const lockContractAddress = await pluginRoutes.get('/timelock-address', z.string()); const nftContractAddress = await pluginRoutes.get('/erc721-address', z.string()); const ethContract = await Erc721TimeLock.initialize({ lockContractAddress, nftContractAddress }, cfg.ethereumProvider, logger); return new Erc721TimelockProver(logger, ethContract); } } Erc721TimelockProver.__interface_tag = 'ts'; Erc721TimelockProver; export default Erc721TimelockProver; //# sourceMappingURL=prover.js.map