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