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