commit 7bdc59a48352de12713966923f2743f9a89a20a4 Author: ryanycw Date: Fri Jun 2 12:40:50 2023 +0800 chore: init repo diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5391ac11 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules/ +keys/ +**/keys/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f965c56a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ + +.DS_Store +config.ts \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..17e6e596 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/artifacts +**/cache +**/typechain-types \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..da652cd0 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "singleQuote": true, + "semi": false +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..73022dca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:16-buster + +COPY . /src + +WORKDIR /src + +RUN yarn && rm -rf packages/frontend + +RUN sh scripts/loadKeys.sh + +RUN rm -r packages/relay/keys/buildOrdered* + +FROM node:16-buster + +COPY --from=0 /src /src +WORKDIR /src/packages/relay + +CMD ["npm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..c00e25cc --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# create-unirep-app + +This is a demo app of a [unirep](https://github.com/Unirep/Unirep) attester. In this demo app, users can request data from the example attester. After transition, user can prove how much data he has. + +> See: [Users and Attesters](https://developer.unirep.io/docs/protocol/users-and-attesters) + +## 1. Installation + +```shell +npx create-unirep-app +``` + +Then `cd` into the directory that was created. + +## 2 Start with each daemon + +### 2.1 Build the files + +```shell +yarn build +``` + +### 2.2 Start a node + +```shell +yarn contracts hardhat node +``` + +### 2.3 Deploy smart contracts + +in new terminal window, from root: + +```shell +yarn contracts deploy +``` + +### 2.4 Start a relayer (backend) + +```shell +yarn relay start +``` + +### 2.5 Start a frontend + +in new terminal window, from root: + +```shell +yarn frontend start +``` + +It will be running at: http://localhost:3000/ diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..66ffdac4 --- /dev/null +++ b/lerna.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useWorkspaces": true, + "version": "independent", + "npmClient": "yarn", + "packages": ["packages/*"] +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..56bc3f2d --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "social-tw-website", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "install": "lerna bootstrap", + "build": "lerna run build", + "circuits": "yarn workspace @unirep-app/circuits run", + "contracts": "yarn workspace @unirep-app/contracts run", + "frontend": "yarn workspace @unirep-app/frontend run", + "relay": "yarn workspace @unirep-app/relay run", + "start": "node scripts/start.mjs", + "linkUnirep": "sh ./scripts/linkUnirep.sh", + "copyUnirep": "sh ./scripts/copyUnirep.sh", + "lint": "prettier ." + }, + "devDependencies": { + "lerna": "^6.0.1", + "node-fetch": "^3.3.0" + }, + "dependencies": { + "prettier": "^2.8.4" + } +} \ No newline at end of file diff --git a/packages/circuits/.gitignore b/packages/circuits/.gitignore new file mode 100644 index 00000000..f879def4 --- /dev/null +++ b/packages/circuits/.gitignore @@ -0,0 +1,7 @@ +dist +*.ptau +zksnarkBuild/* +!*/dataProof.vkey.json +!*/dataProof.wasm +!*/dataProof.zkey +!*/dataProof_main.circom \ No newline at end of file diff --git a/packages/circuits/README.md b/packages/circuits/README.md new file mode 100644 index 00000000..f38e68c2 --- /dev/null +++ b/packages/circuits/README.md @@ -0,0 +1,11 @@ +# `circuits` + +> TODO: description + +## Usage + +``` +const circuits = require('circuits'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/circuits/circuits/dataProof.circom b/packages/circuits/circuits/dataProof.circom new file mode 100644 index 00000000..210be1fd --- /dev/null +++ b/packages/circuits/circuits/dataProof.circom @@ -0,0 +1,54 @@ +pragma circom 2.0.0; + +include "../../../node_modules/@unirep/circuits/circuits/proveReputation.circom"; +include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/poseidon.circom"; +include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/mux1.circom"; +include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/gates.circom"; +include "../../../node_modules/@unirep/circuits/circuits/circomlib/circuits/comparators.circom"; + + +template DataProof(STATE_TREE_DEPTH, FIELD_COUNT, SUM_FIELD_COUNT) { + // State tree leaf: Identity & user state root + signal input identity_secret; + // State tree + signal input state_tree_indexes[STATE_TREE_DEPTH]; + signal input state_tree_elements[STATE_TREE_DEPTH]; + signal input attester_id; + signal input data[FIELD_COUNT]; + signal output state_tree_root; + signal input epoch; + + // Prove values + signal input value[SUM_FIELD_COUNT]; + + /* 1. Check if user exists in the State Tree */ + + // Compute state tree root + component leaf_hasher = StateTreeLeaf(FIELD_COUNT); + leaf_hasher.identity_secret <== identity_secret; + leaf_hasher.attester_id <== attester_id; + leaf_hasher.epoch <== epoch; + for (var x = 0; x < FIELD_COUNT; x++) { + leaf_hasher.data[x] <== data[x]; + } + + component merkletree = MerkleTreeInclusionProof(STATE_TREE_DEPTH); + merkletree.leaf <== leaf_hasher.out; + for (var i = 0; i < STATE_TREE_DEPTH; i++) { + merkletree.path_index[i] <== state_tree_indexes[i]; + merkletree.path_elements[i] <== state_tree_elements[i]; + } + state_tree_root <== merkletree.root; + + /* End of check 1 */ + + /* 2. Check if user data more than given value */ + component get[SUM_FIELD_COUNT]; + for (var x = 0; x < SUM_FIELD_COUNT; x++) { + get[x] = GreaterEqThan(252); + get[x].in[0] <== data[x]; + get[x].in[1] <== value[x]; + get[x].out === 1; + } + /* End of check 2 */ +} diff --git a/packages/circuits/package.json b/packages/circuits/package.json new file mode 100644 index 00000000..239508e3 --- /dev/null +++ b/packages/circuits/package.json @@ -0,0 +1,36 @@ +{ + "name": "@unirep-app/circuits", + "version": "1.0.0", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", + "description": "ZK proofs used for the unirep attesters", + "author": "Unirep Team ", + "homepage": "https://github.com/Unirep/create-unirep-app#readme", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Unirep/create-unirep-app.git" + }, + "scripts": { + "build": "tsc && yarn copyCircom", + "buildsnark": "ts-node ./scripts/buildSnarks.ts && tsc && yarn copyCircom", + "test": "mocha -r ts-node/register test/*.test.ts --exit", + "copyCircom": "sh scripts/copyCircom.sh" + }, + "bugs": { + "url": "https://github.com/Unirep/create-unirep-app/issues" + }, + "devDependencies": { + "@semaphore-protocol/identity": "^3.6.0", + "@types/mocha": "^10.0.1", + "chai": "^4.3.7", + "mocha": "^10.0.0", + "snarkjs": "^0.4.7", + "ts-node": "^10.9.1", + "typescript": "^4.8.2" + }, + "dependencies": { + "@unirep/circuits": "2.0.0-beta-3", + "@unirep/core": "2.0.0-beta-3" + } +} diff --git a/packages/circuits/provers/defaultProver.ts b/packages/circuits/provers/defaultProver.ts new file mode 100644 index 00000000..ec5d06c2 --- /dev/null +++ b/packages/circuits/provers/defaultProver.ts @@ -0,0 +1,61 @@ +import path from 'path' +import { Circuit } from '@unirep/circuits' +import * as snarkjs from 'snarkjs' +import { SnarkProof, SnarkPublicSignals } from '@unirep/utils' + +const buildPath = '../zksnarkBuild' + +/** + * The default prover that uses the circuits in default built folder `zksnarkBuild/` + */ +export const defaultProver = { + /** + * Generate proof and public signals with `snarkjs.groth16.fullProve` + * @param circuitName Name of the circuit, which can be chosen from `Circuit` + * @param inputs The user inputs of the circuit + * @returns snark proof and public signals + */ + genProofAndPublicSignals: async ( + circuitName: string | Circuit, + inputs: any + ): Promise => { + const circuitWasmPath = path.join( + __dirname, + buildPath, + `${circuitName}.wasm` + ) + const zkeyPath = path.join(__dirname, buildPath, `${circuitName}.zkey`) + const { proof, publicSignals } = await snarkjs.groth16.fullProve( + inputs, + circuitWasmPath, + zkeyPath + ) + + return { proof, publicSignals } + }, + + /** + * Verify the snark proof and public signals with `snarkjs.groth16.verify` + * @param circuitName Name of the circuit, which can be chosen from `Circuit` + * @param publicSignals The snark public signals that is generated from `genProofAndPublicSignals` + * @param proof The snark proof that is generated from `genProofAndPublicSignals` + * @returns True if the proof is valid, false otherwise + */ + verifyProof: async ( + circuitName: string | Circuit, + publicSignals: SnarkPublicSignals, + proof: SnarkProof + ): Promise => { + const vkey = require(path.join(buildPath, `${circuitName}.vkey.json`)) + return snarkjs.groth16.verify(vkey, publicSignals, proof) + }, + + /** + * Get vkey from default built folder `zksnarkBuild/` + * @param name Name of the circuit, which can be chosen from `Circuit` + * @returns vkey of the circuit + */ + getVKey: async (name: string | Circuit) => { + return require(path.join(buildPath, `${name}.vkey.json`)) + }, +} diff --git a/packages/circuits/scripts/buildSnarks.ts b/packages/circuits/scripts/buildSnarks.ts new file mode 100644 index 00000000..359e0937 --- /dev/null +++ b/packages/circuits/scripts/buildSnarks.ts @@ -0,0 +1,95 @@ +import { stringifyBigInts } from '@unirep/utils' +import fs from 'fs' +import path from 'path' +import * as snarkjs from 'snarkjs' +import child_process from 'child_process' +import { circuitContents, ptauName } from './circuits' +import { downloadPtau } from './downloadPtau' + +main().catch((err) => { + console.log(`Uncaught error: ${err}`) + process.exit(0) +}) + +async function main() { + await downloadPtau() + + const outDir = path.join(__dirname, '../zksnarkBuild') + await fs.promises.mkdir(outDir, { recursive: true }) + + // pass a space separated list of circuit names to this executable + const [, , ...circuits] = process.argv + if (circuits.length === 0) { + // if no arguments build all + circuits.push(...Object.keys(circuitContents)) + } + + for (const name of circuits) { + if (!circuitContents[name]) + throw new Error(`Unknown circuit name: "${name}"`) + + await fs.promises.writeFile( + path.join(outDir, `${name}_main.circom`), + circuitContents[name] + ) + + const inputFile = path.join(outDir, `${name}_main.circom`) + const circuitOut = path.join(outDir, `${name}_main.r1cs`) + const wasmOut = path.join(outDir, `${name}_main_js/${name}_main.wasm`) + const wasmOutDir = path.join(outDir, `${name}_main_js`) + const wasmOutFinal = path.join(outDir, `${name}.wasm`) + const ptau = path.join(outDir, ptauName) + const zkey = path.join(outDir, `${name}.zkey`) + const vkOut = path.join(outDir, `${name}.vkey.json`) + + // Check if the circuitOut file exists + const circuitOutFileExists = await fs.promises + .stat(circuitOut) + .catch(() => false) + if (circuitOutFileExists) { + console.log( + circuitOut.split('/').pop(), + 'exists. Skipping compilation.' + ) + } else { + console.log(`Compiling ${inputFile.split('/').pop()}...`) + // Compile the .circom file + await new Promise((rs, rj) => + child_process.exec( + `$HOME/.cargo/bin/circom --r1cs --wasm -o ${outDir} ${inputFile}`, + (err, stdout, stderr) => { + if (err) rj(err) + else rs('') + } + ) + ) + console.log( + 'Generated', + circuitOut.split('/').pop(), + 'and', + wasmOut.split('/').pop() + ) + } + + const zkeyOutFileExists = await fs.promises + .stat(zkey) + .catch(() => false) + if (zkeyOutFileExists) { + console.log(zkey.split('/').pop(), 'exists. Skipping compilation.') + } else { + console.log('Exporting verification key...') + await snarkjs.zKey.newZKey(circuitOut, ptau, zkey) + const vkeyJson = await snarkjs.zKey.exportVerificationKey(zkey) + const S = JSON.stringify(stringifyBigInts(vkeyJson), null, 1) + await fs.promises.writeFile(vkOut, S) + console.log( + `Generated ${zkey.split('/').pop()} and ${vkOut + .split('/') + .pop()}` + ) + await fs.promises.rename(wasmOut, wasmOutFinal) + await fs.promises.rm(wasmOutDir, { recursive: true, force: true }) + process.exit(0) + } + } +} diff --git a/packages/circuits/scripts/circuits.ts b/packages/circuits/scripts/circuits.ts new file mode 100644 index 00000000..546a3406 --- /dev/null +++ b/packages/circuits/scripts/circuits.ts @@ -0,0 +1,8 @@ +import { CircuitConfig } from '@unirep/circuits' +const { STATE_TREE_DEPTH, FIELD_COUNT, SUM_FIELD_COUNT } = CircuitConfig.default + +export const ptauName = 'powersOfTau28_hez_final_18.ptau' + +export const circuitContents = { + dataProof: `pragma circom 2.0.0; include "../circuits/dataProof.circom"; \n\ncomponent main { public [ value ] } = DataProof(${STATE_TREE_DEPTH}, ${FIELD_COUNT}, ${SUM_FIELD_COUNT});`, +} diff --git a/packages/circuits/scripts/copyCircom.sh b/packages/circuits/scripts/copyCircom.sh new file mode 100644 index 00000000..79c2a876 --- /dev/null +++ b/packages/circuits/scripts/copyCircom.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +rm -rf ./dist/zksnarkBuild + +cp -r ../../node_modules/@unirep/circuits/zksnarkBuild/. ./zksnarkBuild +cp -rf ./zksnarkBuild ./dist/zksnarkBuild \ No newline at end of file diff --git a/packages/circuits/scripts/downloadPtau.ts b/packages/circuits/scripts/downloadPtau.ts new file mode 100644 index 00000000..7d400c61 --- /dev/null +++ b/packages/circuits/scripts/downloadPtau.ts @@ -0,0 +1,66 @@ +import path from 'path' +import https from 'https' +import readline from 'readline' +import fs from 'fs' + +import { ptauName } from './circuits' + +export async function downloadPtau() { + const outDir = path.join(__dirname, '../zksnarkBuild') + await fs.promises.mkdir(outDir, { recursive: true }) + const ptau = path.join(outDir, ptauName) + + const ptauExists = await fs.promises.stat(ptau).catch(() => false) + if (!ptauExists) { + // download to a temporary file and then move it into place + const tmp = path.join(outDir, 'ptau.download.tmp') + await fs.promises.unlink(tmp).catch(() => {}) + await new Promise((rs, rj) => { + const logPercent = (p) => { + readline.clearLine(process.stdout, 0) + readline.cursorTo(process.stdout, 0) + process.stdout.write( + `Downloading ptau file, please wait... ${p}%` + ) + } + const file = fs.createWriteStream(tmp, { flags: 'w' }) + logPercent(0) + https.get( + `https://hermez.s3-eu-west-1.amazonaws.com/${ptauName}`, + (res) => { + const { statusCode } = res + const contentLength = res.headers['content-length'] + if (statusCode !== 200) { + return rj( + `Received non-200 status code from ptau url: ${statusCode}` + ) + } + let totalReceived = 0 + const logTimer = setInterval(() => { + logPercent( + Math.floor( + (100 * totalReceived) / Number(contentLength) + ) + ) + }, 1000) + res.on('data', (chunk) => { + file.write(chunk) + totalReceived += chunk.length + }) + res.on('error', (err) => { + clearInterval(logTimer) + rj(err) + }) + res.on('end', () => { + file.end() + clearInterval(logTimer) + logPercent(100) + console.log() + rs('') + }) + } + ) + }) + await fs.promises.rename(tmp, ptau) + } +} diff --git a/packages/circuits/src/DataProof.ts b/packages/circuits/src/DataProof.ts new file mode 100644 index 00000000..226ce0da --- /dev/null +++ b/packages/circuits/src/DataProof.ts @@ -0,0 +1,34 @@ +import { SnarkProof } from '@unirep/utils' +import { BigNumberish } from '@ethersproject/bignumber' +import { BaseProof, Prover } from '@unirep/circuits' + +/** + * The data proof structure that helps to query the public signals + */ +export class DataProof extends BaseProof { + readonly idx = { + stateTreeRoot: 0, + value: [1, 5], + } + public stateTreeRoot: BigNumberish + public value: BigNumberish[] + + /** + * @param _publicSignals The public signals of the data proof that can be verified by the prover + * @param _proof The proof that can be verified by the prover + * @param prover The prover that can verify the public signals and the proof + */ + constructor( + _publicSignals: BigNumberish[], + _proof: SnarkProof, + prover?: Prover + ) { + super(_publicSignals, _proof, prover) + this.stateTreeRoot = _publicSignals[this.idx.stateTreeRoot] + this.value = [] + for (let i = this.idx.value[0]; i < this.idx.value[1]; i++) { + this.value.push(_publicSignals[i]) + } + ;(this as any).circuit = 'dataProof' + } +} diff --git a/packages/circuits/src/index.ts b/packages/circuits/src/index.ts new file mode 100644 index 00000000..b029ce06 --- /dev/null +++ b/packages/circuits/src/index.ts @@ -0,0 +1 @@ +export * from './DataProof' diff --git a/packages/circuits/test/proveData.test.ts b/packages/circuits/test/proveData.test.ts new file mode 100644 index 00000000..4d5506c7 --- /dev/null +++ b/packages/circuits/test/proveData.test.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai' +import * as utils from '@unirep/utils' +import { Identity } from '@semaphore-protocol/identity' +import { Circuit, CircuitConfig } from '@unirep/circuits' +import { defaultProver } from '../provers/defaultProver' + +const { FIELD_COUNT, SUM_FIELD_COUNT, STATE_TREE_DEPTH } = CircuitConfig.default + +const circuit = 'dataProof' + +const genCircuitInput = (config: { + id: Identity + epoch: number + attesterId: number | bigint + sumField?: (bigint | number)[] + replField?: (bigint | number)[] + proveValues?: (bigint | number)[] +}) => { + const { id, epoch, attesterId, sumField, replField, proveValues } = + Object.assign( + { + minRep: 0, + maxRep: 0, + graffitiPreImage: 0, + sumField: [], + replField: [], + proveValues: [], + }, + config + ) + + const startBalance = [ + ...sumField, + ...Array(SUM_FIELD_COUNT - sumField.length).fill(0), + ...replField, + ...Array(FIELD_COUNT - SUM_FIELD_COUNT - replField.length).fill(0), + ] + // Global state tree + const stateTree = new utils.IncrementalMerkleTree(STATE_TREE_DEPTH) + const hashedLeaf = utils.genStateTreeLeaf( + id.secret, + BigInt(attesterId), + epoch, + startBalance as any + ) + stateTree.insert(hashedLeaf) + const stateTreeProof = stateTree.createProof(0) // if there is only one GST leaf, the index is 0 + + const value = [ + ...proveValues, + Array(SUM_FIELD_COUNT - proveValues.length).fill(0), + ] + + const circuitInputs = { + identity_secret: id.secret, + state_tree_indexes: stateTreeProof.pathIndices, + state_tree_elements: stateTreeProof.siblings, + data: startBalance, + epoch: epoch, + attester_id: attesterId, + value: value, + } + return utils.stringifyBigInts(circuitInputs) +} + +const genProofAndVerify = async ( + circuit: Circuit | string, + circuitInputs: any +) => { + const startTime = new Date().getTime() + const { proof, publicSignals } = + await defaultProver.genProofAndPublicSignals(circuit, circuitInputs) + const endTime = new Date().getTime() + console.log( + `Gen Proof time: ${endTime - startTime} ms (${Math.floor( + (endTime - startTime) / 1000 + )} s)` + ) + const isValid = await defaultProver.verifyProof( + circuit, + publicSignals, + proof + ) + return { isValid, proof, publicSignals } +} + +describe('Prove data in Unirep App', function () { + this.timeout(300000) + + it('should generate a data proof', async () => { + const id = new Identity() + const epoch = 20 + const attesterId = BigInt(219090124810) + const circuitInputs = genCircuitInput({ + id, + epoch, + attesterId, + }) + const { isValid } = await genProofAndVerify(circuit, circuitInputs) + expect(isValid).to.be.true + }) + + it('should generate a data proof with values', async () => { + const id = new Identity() + const epoch = 20 + const attesterId = BigInt(219090124810) + const sumField = Array(SUM_FIELD_COUNT).fill(5) + const proveValues = Array(SUM_FIELD_COUNT).fill(4) + const circuitInputs = genCircuitInput({ + id, + epoch, + attesterId, + sumField, + proveValues, + }) + const { isValid } = await genProofAndVerify(circuit, circuitInputs) + expect(isValid).to.be.true + }) + + it('should not generate a data proof with invalid values', async () => { + const id = new Identity() + const epoch = 20 + const attesterId = BigInt(219090124810) + const sumField = Array(SUM_FIELD_COUNT).fill(5) + const proveValues = Array(SUM_FIELD_COUNT).fill(6) + const circuitInputs = genCircuitInput({ + id, + epoch, + attesterId, + sumField, + proveValues, + }) + await new Promise((rs, rj) => { + genProofAndVerify(circuit, circuitInputs) + .then(() => rj()) + .catch(() => rs()) + }) + }) +}) diff --git a/packages/circuits/tsconfig.json b/packages/circuits/tsconfig.json new file mode 100644 index 00000000..ec067319 --- /dev/null +++ b/packages/circuits/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "exclude": ["node_modules/**"], + "include": ["./src", "./config", "./provers"] +} diff --git a/packages/circuits/zksnarkBuild/dataProof.vkey.json b/packages/circuits/zksnarkBuild/dataProof.vkey.json new file mode 100644 index 00000000..6d54888d --- /dev/null +++ b/packages/circuits/zksnarkBuild/dataProof.vkey.json @@ -0,0 +1,105 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 5, + "vk_alpha_1": [ + "20491192805390485299153009773594534940189261866228447918068658471970481763042", + "9383485363053290200918347156157836566562967994039712273449902621266178545958", + "1" + ], + "vk_beta_2": [ + [ + "6375614351688725206403948262868962793625744043794305715222011528459656738731", + "4252822878758300859123897981450591353533073413197771768651442665752259397132" + ], + [ + "10505242626370262277552901082094356697409835680220590971873171140371331206856", + "21847035105528745403288232691147584728191162732299865338377159692350059136679" + ], + ["1", "0"] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + ["1", "0"] + ], + "vk_delta_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + ["1", "0"] + ], + "vk_alphabeta_12": [ + [ + [ + "2029413683389138792403550203267699914886160938906632433982220835551125967885", + "21072700047562757817161031222997517981543347628379360635925549008442030252106" + ], + [ + "5940354580057074848093997050200682056184807770593307860589430076672439820312", + "12156638873931618554171829126792193045421052652279363021382169897324752428276" + ], + [ + "7898200236362823042373859371574133993780991612861777490112507062703164551277", + "7074218545237549455313236346927434013100842096812539264420499035217050630853" + ] + ], + [ + [ + "7077479683546002997211712695946002074877511277312570035766170199895071832130", + "10093483419865920389913245021038182291233451549023025229112148274109565435465" + ], + [ + "4595479056700221319381530156280926371456704509942304414423590385166031118820", + "19831328484489333784475432780421641293929726139240675179672856274388269393268" + ], + [ + "11934129596455521040620786944827826205713621633706285934057045369193958244500", + "8037395052364110730298837004334506829870972346962140206007064471173334027475" + ] + ] + ], + "IC": [ + [ + "14726110358384591134086760454227211266393232809490471160396967997329870336324", + "6301497693133236182284138855830975407731307849664315530158455800051197105107", + "1" + ], + [ + "13329857957128329839690880117138325412550294258410191683945258391022632331307", + "15701325130861937333320206471915054307038089909991540806157830619886259981632", + "1" + ], + [ + "17478358363831002902765013020426580443221917792536599839946876386745318685717", + "10830371275639987859500296749997362406047033916109142385832377177340910597784", + "1" + ], + [ + "15664464782499262950870691161009364854866354078489291401447836163624272644191", + "19332680862751361917789648482767065641444158897516551791437007516298388954870", + "1" + ], + [ + "15865221843292426519624482668434972003968238162007413637120806559812532466797", + "7153207475716291327489834715960116524511661268987194669718830099813831489228", + "1" + ], + [ + "16545623641835559434451166127442830578864858667488981821708517210008152079051", + "2131437898838391121269897774987270826175119692047631536172420890124968652800", + "1" + ] + ] +} diff --git a/packages/circuits/zksnarkBuild/dataProof.wasm b/packages/circuits/zksnarkBuild/dataProof.wasm new file mode 100644 index 00000000..24329755 Binary files /dev/null and b/packages/circuits/zksnarkBuild/dataProof.wasm differ diff --git a/packages/circuits/zksnarkBuild/dataProof.zkey b/packages/circuits/zksnarkBuild/dataProof.zkey new file mode 100644 index 00000000..6e38a4f3 Binary files /dev/null and b/packages/circuits/zksnarkBuild/dataProof.zkey differ diff --git a/packages/circuits/zksnarkBuild/dataProof_main.circom b/packages/circuits/zksnarkBuild/dataProof_main.circom new file mode 100644 index 00000000..93e66f46 --- /dev/null +++ b/packages/circuits/zksnarkBuild/dataProof_main.circom @@ -0,0 +1,3 @@ +pragma circom 2.0.0; include "../circuits/dataProof.circom"; + +component main { public [ value ] } = DataProof(17, 6, 4); \ No newline at end of file diff --git a/packages/contracts/.gitignore b/packages/contracts/.gitignore new file mode 100644 index 00000000..8e7c70d7 --- /dev/null +++ b/packages/contracts/.gitignore @@ -0,0 +1,25 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +#Hardhat files +cache +artifacts + + +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +#Hardhat files +cache +artifacts + +build +*Verifier.sol \ No newline at end of file diff --git a/packages/contracts/README.md b/packages/contracts/README.md new file mode 100644 index 00000000..87b70033 --- /dev/null +++ b/packages/contracts/README.md @@ -0,0 +1,29 @@ +# Sample Unirep Project + +This project demonstrates a basic Unirep application. It comes with a sample contract, a test for that contract, and a script that deploys that contract. + +Try running some of the following tasks: + +## Compile + +```shell +yarn build +``` + +## Test + +```shell +yarn test +``` + +## Deploy to local hardhat network + +```shell +yarn hardhat node +``` + +and + +```shell +yarn deploy +``` diff --git a/packages/contracts/abi/UnirepApp.json b/packages/contracts/abi/UnirepApp.json new file mode 100644 index 00000000..2852f8de --- /dev/null +++ b/packages/contracts/abi/UnirepApp.json @@ -0,0 +1,116 @@ +[ + { + "inputs": [ + { + "internalType": "contract Unirep", + "name": "_unirep", + "type": "address" + }, + { + "internalType": "contract IVerifier", + "name": "_dataVerifier", + "type": "address" + }, + { + "internalType": "uint48", + "name": "_epochLength", + "type": "uint48" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "epochKey", + "type": "uint256" + }, + { + "internalType": "uint48", + "name": "targetEpoch", + "type": "uint48" + }, + { + "internalType": "uint256", + "name": "fieldIndex", + "type": "uint256" + }, + { "internalType": "uint256", "name": "val", "type": "uint256" } + ], + "name": "submitAttestation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "epochKey", + "type": "uint256" + }, + { + "internalType": "uint48", + "name": "targetEpoch", + "type": "uint48" + }, + { + "internalType": "uint256[]", + "name": "fieldIndices", + "type": "uint256[]" + }, + { "internalType": "uint256[]", "name": "vals", "type": "uint256[]" } + ], + "name": "submitManyAttestations", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unirep", + "outputs": [ + { "internalType": "contract Unirep", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "publicSignals", + "type": "uint256[]" + }, + { + "internalType": "uint256[8]", + "name": "proof", + "type": "uint256[8]" + } + ], + "name": "userSignUp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[5]", + "name": "publicSignals", + "type": "uint256[5]" + }, + { + "internalType": "uint256[8]", + "name": "proof", + "type": "uint256[8]" + } + ], + "name": "verifyDataProof", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/contracts/contracts/UnirepApp.sol b/packages/contracts/contracts/UnirepApp.sol new file mode 100644 index 00000000..440dd24f --- /dev/null +++ b/packages/contracts/contracts/UnirepApp.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; +import { Unirep } from "@unirep/contracts/Unirep.sol"; + +// Uncomment this line to use console.log +// import "hardhat/console.sol"; + +interface IVerifier { + function verifyProof( + uint256[5] calldata publicSignals, + uint256[8] calldata proof + ) external view returns (bool); +} + + +contract UnirepApp { + Unirep public unirep; + IVerifier internal dataVerifier; + + constructor(Unirep _unirep, IVerifier _dataVerifier, uint48 _epochLength) { + // set unirep address + unirep = _unirep; + + // set verifier address + dataVerifier = _dataVerifier; + + // sign up as an attester + unirep.attesterSignUp(_epochLength); + } + + // sign up users in this app + function userSignUp( + uint256[] memory publicSignals, + uint256[8] memory proof + ) public { + unirep.userSignUp(publicSignals, proof); + } + + function submitManyAttestations( + uint256 epochKey, + uint48 targetEpoch, + uint[] calldata fieldIndices, + uint[] calldata vals + ) public { + require(fieldIndices.length == vals.length, 'arrmismatch'); + for (uint8 x = 0; x < fieldIndices.length; x++) { + unirep.attest(epochKey, targetEpoch, fieldIndices[x], vals[x]); + } + } + + function submitAttestation( + uint256 epochKey, + uint48 targetEpoch, + uint256 fieldIndex, + uint256 val + ) public { + unirep.attest( + epochKey, + targetEpoch, + fieldIndex, + val + ); + } + + function verifyDataProof( + uint256[5] calldata publicSignals, + uint256[8] calldata proof + ) public view returns(bool) { + return dataVerifier.verifyProof( + publicSignals, + proof + ); + } +} diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts new file mode 100644 index 00000000..6c20a8f7 --- /dev/null +++ b/packages/contracts/hardhat.config.ts @@ -0,0 +1,34 @@ +import '@typechain/hardhat' +import '@nomiclabs/hardhat-ethers' + +export default { + defaultNetwork: 'local', + networks: { + hardhat: { + blockGasLimit: 12000000, + }, + local: { + url: 'http://127.0.0.1:8545', + blockGasLimit: 12000000, + accounts: [ + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + ], + }, + arb: { + url: 'https://arbitrum.goerli.unirep.io', + accounts: [ + '0x0f70e777f814334daa4456ac32b9a1fdca75ae07f70c2e6cef92679bad06c88b', + ], + }, + }, + solidity: { + compilers: [ + { + version: '0.8.17', + settings: { + optimizer: { enabled: true, runs: 200 }, + }, + }, + ], + }, +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 00000000..8d81d224 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,39 @@ +{ + "name": "@unirep-app/contracts", + "version": "1.0.0", + "description": "Smart contracts of the Unirep Application", + "keywords": [], + "author": "Unirep Team ", + "license": "ISC", + "main": "build/src/index.js", + "repository": "git+https://github.com/Unirep/create-unirep-app.git", + "scripts": { + "build": "yarn buildVerifier && hardhat compile && yarn abi", + "buildVerifier": "ts-node ./scripts/genVerifier.ts ", + "abi": "ts-node scripts/abi", + "hardhat": "hardhat", + "deploy": "hardhat run scripts/deploy.ts", + "test": "hardhat test --network hardhat" + }, + "bugs": { + "url": "https://github.com/Unirep/create-unirep-app/issue" + }, + "homepage": "https://github.com/Unirep/create-unirep-app#readme", + "devDependencies": { + "@semaphore-protocol/identity": "^3.6.0", + "@nomiclabs/hardhat-ethers": "^2.2.0", + "@openzeppelin/contracts": "^4.7.3", + "@typechain/ethers-v5": "^10.2.0", + "@typechain/hardhat": "^6.1.5", + "@types/node": "^18.15.11", + "@unirep-app/circuits": "1.0.0", + "@unirep/contracts": "2.0.0-beta-3", + "hardhat": "^2.12.0", + "ts-node": "^10.9.1", + "typechain": "^8.1.1", + "typescript": "^5.0.3" + }, + "dependencies": { + "poseidon-solidity": "^0.0.3" + } +} diff --git a/packages/contracts/scripts/abi.ts b/packages/contracts/scripts/abi.ts new file mode 100644 index 00000000..1ad37c03 --- /dev/null +++ b/packages/contracts/scripts/abi.ts @@ -0,0 +1,8 @@ +import * as fs from 'fs' +import * as path from 'path' +import UNIREP_APP_ABI from '../artifacts/contracts/UnirepApp.sol/UnirepApp.json' + +fs.writeFileSync( + path.join(__dirname, '../abi/UnirepApp.json'), + JSON.stringify(UNIREP_APP_ABI.abi) +) diff --git a/packages/contracts/scripts/deploy.ts b/packages/contracts/scripts/deploy.ts new file mode 100644 index 00000000..97e6dd9f --- /dev/null +++ b/packages/contracts/scripts/deploy.ts @@ -0,0 +1,48 @@ +import { ethers } from 'hardhat' +import * as fs from 'fs' +import * as path from 'path' +import { deployUnirep } from '@unirep/contracts/deploy/index.js' +import * as hardhat from 'hardhat' + +const epochLength = 300 + +deployApp().catch((err) => { + console.log(`Uncaught error: ${err}`) + process.exit(1) +}) + +export async function deployApp() { + const [signer] = await ethers.getSigners() + const unirep = await deployUnirep(signer) + + const verifierF = await ethers.getContractFactory('DataProofVerifier') + const verifier = await verifierF.deploy() + await verifier.deployed() + const App = await ethers.getContractFactory('UnirepApp') + const app = await App.deploy(unirep.address, verifier.address, epochLength) + + await app.deployed() + + console.log( + `Unirep app with epoch length ${epochLength} is deployed to ${app.address}` + ) + + const config = `export default { + UNIREP_ADDRESS: '${unirep.address}', + APP_ADDRESS: '${app.address}', + ETH_PROVIDER_URL: '${hardhat.network.config.url ?? ''}', + ${ + Array.isArray(hardhat.network.config.accounts) + ? `PRIVATE_KEY: '${hardhat.network.config.accounts[0]}',` + : `/** + This contract was deployed using a mnemonic. The PRIVATE_KEY variable needs to be set manually + **/` + } + } + ` + + const configPath = path.join(__dirname, '../../../config.ts') + await fs.promises.writeFile(configPath, config) + + console.log(`Config written to ${configPath}`) +} diff --git a/packages/contracts/scripts/genVerifier.ts b/packages/contracts/scripts/genVerifier.ts new file mode 100644 index 00000000..ecfcce5d --- /dev/null +++ b/packages/contracts/scripts/genVerifier.ts @@ -0,0 +1,105 @@ +import path from 'path' +import fs from 'fs' + +import { config } from 'hardhat' + +const verifiersPath = path.join(config.paths.sources) + +const zkFilesPath = path.join('../../circuits/zksnarkBuild') +const Circuit = { + dataProof: 'dataProof', +} + +const main = async (): Promise => { + // create verifier folder + try { + fs.mkdirSync(verifiersPath, { recursive: true }) + } catch (e) { + console.log('Cannot create folder ', e) + } + + for (const circuit of Object.keys(Circuit)) { + const verifierName = createVerifierName(circuit) + const solOut = path.join(verifiersPath, `${verifierName}.sol`) + const vKey = require(path.join(zkFilesPath, `${circuit}.vkey.json`)) + + console.log(`Exporting ${circuit} verification contract...`) + const verifier = genVerifier(verifierName, vKey) + + fs.writeFileSync(solOut, verifier) + fs.copyFileSync(solOut, path.join(verifiersPath, `${verifierName}.sol`)) + } + return 0 +} +export const createVerifierName = (circuit: string) => { + return `${circuit.charAt(0).toUpperCase() + circuit.slice(1)}Verifier` +} + +export const genVerifier = (contractName: string, vk: any): string => { + const templatePath = path.resolve( + __dirname, + './template/groth16Verifier.txt' + ) + + let template = fs.readFileSync(templatePath, 'utf8') + + template = template.replace('<%contract_name%>', contractName) + + const vkalpha1 = + `uint256(${vk.vk_alpha_1[0].toString()}),` + + `uint256(${vk.vk_alpha_1[1].toString()})` + template = template.replace('<%vk_alpha1%>', vkalpha1) + + const vkbeta2 = + `[uint256(${vk.vk_beta_2[0][1].toString()}),` + + `uint256(${vk.vk_beta_2[0][0].toString()})], ` + + `[uint256(${vk.vk_beta_2[1][1].toString()}),` + + `uint256(${vk.vk_beta_2[1][0].toString()})]` + template = template.replace('<%vk_beta2%>', vkbeta2) + + const vkgamma2 = + `[uint256(${vk.vk_gamma_2[0][1].toString()}),` + + `uint256(${vk.vk_gamma_2[0][0].toString()})], ` + + `[uint256(${vk.vk_gamma_2[1][1].toString()}),` + + `uint256(${vk.vk_gamma_2[1][0].toString()})]` + template = template.replace('<%vk_gamma2%>', vkgamma2) + + const vkdelta2 = + `[uint256(${vk.vk_delta_2[0][1].toString()}),` + + `uint256(${vk.vk_delta_2[0][0].toString()})], ` + + `[uint256(${vk.vk_delta_2[1][1].toString()}),` + + `uint256(${vk.vk_delta_2[1][0].toString()})]` + template = template.replace('<%vk_delta2%>', vkdelta2) + + template = template.replaceAll( + '<%vk_input_length%>', + (vk.IC.length - 1).toString() + ) + template = template.replace('<%vk_ic_length%>', vk.IC.length.toString()) + let vi = '' + for (let i = 0; i < vk.IC.length; i++) { + if (vi.length !== 0) { + vi = vi + ' ' + } + vi = + vi + + `vk.IC[${i}] = Pairing.G1Point(uint256(${vk.IC[ + i + ][0].toString()}),` + + `uint256(${vk.IC[i][1].toString()}));\n` + } + template = template.replace('<%vk_ic_pts%>', vi) + + return template +} + +void (async () => { + let exitCode + try { + exitCode = await main() + } catch (err) { + console.error(err) + exitCode = 1 + } + process.exit(exitCode) +})() diff --git a/packages/contracts/scripts/template/groth16Verifier.txt b/packages/contracts/scripts/template/groth16Verifier.txt new file mode 100644 index 00000000..97d7272a --- /dev/null +++ b/packages/contracts/scripts/template/groth16Verifier.txt @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library Pairing { + uint256 constant PRIME_Q = + 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + struct G1Point { + uint256 X; + uint256 Y; + } + + // Encoding of field elements is: X[0] * z + X[1] + struct G2Point { + uint256[2] X; + uint256[2] Y; + } + + /* + * @return The negation of p, i.e. p.plus(p.negate()) should be zero. + */ + function negate(G1Point memory p) internal pure returns (G1Point memory) { + // The prime q in the base field F_q for G1 + if (p.X == 0 && p.Y == 0) { + return G1Point(0, 0); + } else { + return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q)); + } + } + + /* + * @return The sum of two points of G1 + */ + function plus(G1Point memory p1, G1Point memory p2) + internal + view + returns (G1Point memory r) + { + uint256[4] memory input; + input[0] = p1.X; + input[1] = p1.Y; + input[2] = p2.X; + input[3] = p2.Y; + bool success; + + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + + require(success, 'pairing-add-failed'); + } + + /* + * @return The product of a point on G1 and a scalar, i.e. + * p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all + * points p. + */ + function scalar_mul(G1Point memory p, uint256 s) + internal + view + returns (G1Point memory r) + { + uint256[3] memory input; + input[0] = p.X; + input[1] = p.Y; + input[2] = s; + bool success; + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + require(success, 'pairing-mul-failed'); + } + + /* @return The result of computing the pairing check + * e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 + * For example, + * pairing([P1(), P1().negate()], [P2(), P2()]) should return true. + */ + function pairing( + G1Point memory a1, + G2Point memory a2, + G1Point memory b1, + G2Point memory b2, + G1Point memory c1, + G2Point memory c2, + G1Point memory d1, + G2Point memory d2 + ) internal view returns (bool) { + G1Point[4] memory p1 = [a1, b1, c1, d1]; + G2Point[4] memory p2 = [a2, b2, c2, d2]; + + uint256 inputSize = 24; + uint256[] memory input = new uint256[](inputSize); + + for (uint256 i = 0; i < 4; i++) { + uint256 j = i * 6; + input[j + 0] = p1[i].X; + input[j + 1] = p1[i].Y; + input[j + 2] = p2[i].X[0]; + input[j + 3] = p2[i].X[1]; + input[j + 4] = p2[i].Y[0]; + input[j + 5] = p2[i].Y[1]; + } + + uint256[1] memory out; + bool success; + + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall( + sub(gas(), 2000), + 8, + add(input, 0x20), + mul(inputSize, 0x20), + out, + 0x20 + ) + // Use "invalid" to make gas estimation work + switch success + case 0 { + invalid() + } + } + + require(success, 'pairing-opcode-failed'); + + return out[0] != 0; + } +} + +contract <%contract_name%> { + + using Pairing for *; + + uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + struct VerifyingKey { + Pairing.G1Point alpha1; + Pairing.G2Point beta2; + Pairing.G2Point gamma2; + Pairing.G2Point delta2; + Pairing.G1Point[<%vk_ic_length%>] IC; + } + + struct Proof { + Pairing.G1Point A; + Pairing.G2Point B; + Pairing.G1Point C; + } + + function verifyingKey() internal pure returns (VerifyingKey memory vk) { + vk.alpha1 = Pairing.G1Point(<%vk_alpha1%>); + vk.beta2 = Pairing.G2Point(<%vk_beta2%>); + vk.gamma2 = Pairing.G2Point(<%vk_gamma2%>); + vk.delta2 = Pairing.G2Point(<%vk_delta2%>); + <%vk_ic_pts%> + } + + /* + * @returns Whether the proof is valid given the hardcoded verifying key + * above and the public inputs + */ + function verifyProof( + uint256[<%vk_input_length%>] calldata input, + uint256[8] calldata _proof + ) public view returns (bool) { + + Proof memory proof; + proof.A = Pairing.G1Point(_proof[0], _proof[1]); + proof.B = Pairing.G2Point([_proof[2], _proof[3]], [_proof[4], _proof[5]]); + proof.C = Pairing.G1Point(_proof[6], _proof[7]); + + VerifyingKey memory vk = verifyingKey(); + + // Compute the linear combination vk_x + Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); + + // Make sure that proof.A, B, and C are each less than the prime q + require(proof.A.X < PRIME_Q, "verifier-aX-gte-prime-q"); + require(proof.A.Y < PRIME_Q, "verifier-aY-gte-prime-q"); + + require(proof.B.X[0] < PRIME_Q, "verifier-bX0-gte-prime-q"); + require(proof.B.Y[0] < PRIME_Q, "verifier-bY0-gte-prime-q"); + + require(proof.B.X[1] < PRIME_Q, "verifier-bX1-gte-prime-q"); + require(proof.B.Y[1] < PRIME_Q, "verifier-bY1-gte-prime-q"); + + require(proof.C.X < PRIME_Q, "verifier-cX-gte-prime-q"); + require(proof.C.Y < PRIME_Q, "verifier-cY-gte-prime-q"); + + // Make sure that every input is less than the snark scalar field + //for (uint256 i = 0; i < input.length; i++) { + for (uint256 i = 0; i < <%vk_input_length%>; i++) { + require(input[i] < SNARK_SCALAR_FIELD,"verifier-gte-snark-scalar-field"); + vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); + } + + vk_x = Pairing.plus(vk_x, vk.IC[0]); + + return Pairing.pairing( + Pairing.negate(proof.A), + proof.B, + vk.alpha1, + vk.beta2, + vk_x, + vk.gamma2, + proof.C, + vk.delta2 + ); + } +} diff --git a/packages/contracts/test/UnirepApp.test.ts b/packages/contracts/test/UnirepApp.test.ts new file mode 100644 index 00000000..3bee0758 --- /dev/null +++ b/packages/contracts/test/UnirepApp.test.ts @@ -0,0 +1,127 @@ +//@ts-ignore +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { deployUnirep } from '@unirep/contracts/deploy' +import { stringifyBigInts } from '@unirep/utils' +import { schema, UserState } from '@unirep/core' +import { SQLiteConnector } from 'anondb/node' +import { DataProof } from '@unirep-app/circuits' +import defaultConfig from '@unirep/circuits/config' +import { Identity } from '@semaphore-protocol/identity' +const { SUM_FIELD_COUNT } = defaultConfig +import { defaultProver as prover } from '@unirep-app/circuits/provers/defaultProver' + +async function genUserState(id, app) { + // generate a user state + const db = await SQLiteConnector.create(schema, ':memory:') + const unirepAddress = await app.unirep() + const attesterId = BigInt(app.address) + const userState = new UserState( + { + db, + prover, + unirepAddress, + provider: ethers.provider, + attesterId, + }, + id + ) + await userState.sync.start() + await userState.waitForSync() + return userState +} + +describe('Unirep App', function () { + let unirep + let app + + // epoch length + const epochLength = 300 + // generate random user id + const id = new Identity() + + it('deployment', async function () { + const [deployer] = await ethers.getSigners() + unirep = await deployUnirep(deployer) + const verifierF = await ethers.getContractFactory('DataProofVerifier') + const verifier = await verifierF.deploy() + await verifier.deployed() + const App = await ethers.getContractFactory('UnirepApp') + app = await App.deploy(unirep.address, verifier.address, epochLength) + await app.deployed() + }) + + it('user sign up', async () => { + const userState = await genUserState(id, app) + + // generate + const { publicSignals, proof } = await userState.genUserSignUpProof() + await app.userSignUp(publicSignals, proof).then((t) => t.wait()) + userState.sync.stop() + }) + + it('submit attestations', async () => { + const userState = await genUserState(id, app) + + const nonce = 0 + const { publicSignals, proof, epochKey, epoch } = + await userState.genEpochKeyProof({ nonce }) + await unirep + .verifyEpochKeyProof(publicSignals, proof) + .then((t) => t.wait()) + + const field = 0 + const val = 10 + await app + .submitAttestation(epochKey, epoch, field, val) + .then((t) => t.wait()) + userState.sync.stop() + }) + + it('user state transition', async () => { + await ethers.provider.send('evm_increaseTime', [epochLength]) + await ethers.provider.send('evm_mine', []) + + const newEpoch = await unirep.attesterCurrentEpoch(app.address) + const userState = await genUserState(id, app) + const { publicSignals, proof } = + await userState.genUserStateTransitionProof({ + toEpoch: newEpoch, + }) + await unirep + .userStateTransition(publicSignals, proof) + .then((t) => t.wait()) + userState.sync.stop() + }) + + it('data proof', async () => { + const userState = await genUserState(id, app) + const epoch = await userState.sync.loadCurrentEpoch() + const stateTree = await userState.sync.genStateTree(epoch) + const index = await userState.latestStateTreeLeafIndex(epoch) + const stateTreeProof = stateTree.createProof(index) + const attesterId = app.address + const data = await userState.getProvableData() + const value = Array(SUM_FIELD_COUNT).fill(0) + const circuitInputs = stringifyBigInts({ + identity_secret: id.secret, + state_tree_indexes: stateTreeProof.pathIndices, + state_tree_elements: stateTreeProof.siblings, + data: data, + epoch: epoch, + attester_id: attesterId, + value: value, + }) + const p = await prover.genProofAndPublicSignals( + 'dataProof', + circuitInputs + ) + const dataProof = new DataProof(p.publicSignals, p.proof, prover) + const isValid = await app.verifyDataProof( + dataProof.publicSignals, + dataProof.proof + ) + expect(isValid).to.be.true + userState.sync.stop() + }) +}) diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 00000000..3349289d --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./build", + "lib": ["ES2021.String"] + }, + "include": ["./src", "./deploy", "./typechain"], + "files": ["./hardhat.config.ts"] +} diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore new file mode 100644 index 00000000..c795b054 --- /dev/null +++ b/packages/frontend/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/packages/frontend/README.md b/packages/frontend/README.md new file mode 100644 index 00000000..4c2ef16e --- /dev/null +++ b/packages/frontend/README.md @@ -0,0 +1,11 @@ +# `frontend` + +> TODO: description + +## Usage + +``` +const frontend = require('frontend'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/frontend/externals/buffer.js b/packages/frontend/externals/buffer.js new file mode 100644 index 00000000..521aefb7 --- /dev/null +++ b/packages/frontend/externals/buffer.js @@ -0,0 +1 @@ +module.exports = require('buffer/').Buffer diff --git a/packages/frontend/package.json b/packages/frontend/package.json new file mode 100644 index 00000000..88cc3383 --- /dev/null +++ b/packages/frontend/package.json @@ -0,0 +1,54 @@ +{ + "name": "@unirep-app/frontend", + "version": "1.0.0", + "description": "> TODO: description", + "author": "Unirep team ", + "homepage": "https://github.com/Unirep/create-unirep-app#readme", + "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/Unirep/create-unirep-app.git" + }, + "scripts": { + "start": "webpack-dev-server", + "build": "webpack" + }, + "dependencies": { + "@cloudflare/kv-asset-handler": "^0.2.0", + "@unirep/core": "2.0.0-beta-3", + "@unirep-app/circuits": "1.0.0", + "ethers": "^5.7.2", + "file-loader": "^6.2.0", + "mobx-react-lite": "^3.4.0" + }, + "devDependencies": { + "@semaphore-protocol/identity": "^3.6.0", + "@babel/core": "^7.19.6", + "@babel/preset-react": "^7.18.6", + "@types/react": "^18.0.32", + "@types/react-dom": "^18.0.11", + "@types/react-router-dom": "^5.3.3", + "anondb": "^0.0.16", + "assert": "^2.0.0", + "babel-loader": "^8.2.5", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", + "css-loader": "^6.7.1", + "events": "^3.3.0", + "html-webpack-plugin": "^5.5.0", + "mini-css-extract-plugin": "^2.6.1", + "mobx": "^6.6.2", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.2", + "snarkjs": "^0.5.0", + "stream-browserify": "^3.0.0", + "ts-loader": "^9.4.2", + "typescript": "^5.0.3", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0", + "webpack-dev-server": "^4.11.1" + } +} diff --git a/packages/frontend/public/arrow.svg b/packages/frontend/public/arrow.svg new file mode 100644 index 00000000..b323d5a1 --- /dev/null +++ b/packages/frontend/public/arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/frontend/public/hummingbird.svg b/packages/frontend/public/hummingbird.svg new file mode 100644 index 00000000..cda1b821 --- /dev/null +++ b/packages/frontend/public/hummingbird.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/public/index.html b/packages/frontend/public/index.html new file mode 100644 index 00000000..3bd1bae1 --- /dev/null +++ b/packages/frontend/public/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + unirep app + + + + +
+ + diff --git a/packages/frontend/public/info_icon.svg b/packages/frontend/public/info_icon.svg new file mode 100644 index 00000000..bba21230 --- /dev/null +++ b/packages/frontend/public/info_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/frontend/public/logo.svg b/packages/frontend/public/logo.svg new file mode 100644 index 00000000..6f8dab9a --- /dev/null +++ b/packages/frontend/public/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/components/Button.tsx b/packages/frontend/src/components/Button.tsx new file mode 100644 index 00000000..26089826 --- /dev/null +++ b/packages/frontend/src/components/Button.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import './button.css' + +type Props = { + style?: { [key: string]: string } + loadingText?: string + onClick?: () => void + children: any +} + +export default ({ style, loadingText, onClick, children }: Props) => { + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState('') + const handleClick = async () => { + if (loading) return + if (typeof onClick !== 'function') return + try { + setLoading(true) + await onClick() + } catch (err: any) { + console.log(err) + setError(err.toString()) + setTimeout(() => setError(''), 2000) + } finally { + setLoading(false) + } + } + return ( +
+
+ {!loading && !error ? children : null} + {loading ? loadingText ?? 'Loading...' : null} + {error ? error : null} +
+
+ ) +} diff --git a/packages/frontend/src/components/Tooltip.tsx b/packages/frontend/src/components/Tooltip.tsx new file mode 100644 index 00000000..c711a70b --- /dev/null +++ b/packages/frontend/src/components/Tooltip.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from 'react' +import measureText from '../utils/measure-text' +import './tooltip.css' +import UIContext from '../contexts/interface' +import { observer } from 'mobx-react-lite' + +type Props = { + text: string + maxWidth?: number +} + +export default observer(({ text, maxWidth, ...props }: Props) => { + const ui = React.useContext(UIContext) + const containerEl: React.RefObject = React.createRef() + const [timer, setTimer] = useState(null) + const [showingPopup, setShowingPopup] = useState(false) + const [leftOffset, setLeftOffset] = useState(0) + const [textWidth, setTextWidth] = useState(0) + useEffect(() => { + const _textWidth = measureText(text, { + fontSize: '12px', + fontWeight: 'normal', + }) + const _maxWidth = maxWidth ?? 200 + const calcWidth = Math.min(_maxWidth, _textWidth) + setTextWidth(calcWidth) + const { x } = containerEl.current.getBoundingClientRect() + const screenMaxWidth = window.innerWidth - x + const minWidth = _maxWidth + 20 + setLeftOffset(screenMaxWidth > minWidth ? 0 : minWidth - screenMaxWidth) + }) + + const onMouseEnter = () => { + if (!ui.isMobile) { + setShowingPopup(true) + } + } + + const onMouseLeave = () => { + if (!ui.isMobile) { + setShowingPopup(false) + } + } + + return ( +
{ + if (!ui.isMobile) return + if (timer) clearTimeout(timer) + if (showingPopup) { + setShowingPopup(false) + return + } + setShowingPopup(true) + const _timer: ReturnType = setTimeout(() => { + setShowingPopup(false) + setTimer(null) + }, 3000) + setTimer(_timer) + }} + className="tooltip-outer" + ref={containerEl} + {...props} + > +
+ info icon +
+ {showingPopup && ( +
+
+ {text} +
+
+ )} +
+ ) +}) diff --git a/packages/frontend/src/components/button.css b/packages/frontend/src/components/button.css new file mode 100644 index 00000000..da6f80c5 --- /dev/null +++ b/packages/frontend/src/components/button.css @@ -0,0 +1,19 @@ +.button-outer { + display: flex; + justify-content: left; + align-items: center; + user-select: none; +} + +.button-inner { + border-radius: 32px; + cursor: pointer; + font-weight: 600; + user-select: none; + text-align: center; + font-size: 20px; + background-color: #a0d8db; + padding: 16px 48px; + -webkit-user-select: none; + -ms-user-select: none; +} diff --git a/packages/frontend/src/components/tooltip.css b/packages/frontend/src/components/tooltip.css new file mode 100644 index 00000000..94a20823 --- /dev/null +++ b/packages/frontend/src/components/tooltip.css @@ -0,0 +1,19 @@ +.tooltip-outer { + position: relative; +} + +.tooltip-popup { + padding: 12px; + background: black; + z-index: 100000; + position: absolute; + border-radius: 8px; + display: flex; +} + +.tooltip-inner { + font-size: 12px; + font-weight: normal; + line-height: 20px; + color: #a0d8db; +} diff --git a/packages/frontend/src/contexts/User.ts b/packages/frontend/src/contexts/User.ts new file mode 100644 index 00000000..73033b8c --- /dev/null +++ b/packages/frontend/src/contexts/User.ts @@ -0,0 +1,196 @@ +import { createContext } from 'react' +import { makeAutoObservable } from 'mobx' +import { stringifyBigInts } from '@unirep/utils' +import { Identity } from '@semaphore-protocol/identity' +import { UserState } from '@unirep/core' +import { DataProof } from '@unirep-app/circuits' +import { SERVER } from '../config' +import prover from './prover' +import { ethers } from 'ethers' + +class User { + currentEpoch: number = 0 + latestTransitionedEpoch: number = 0 + hasSignedUp: boolean = false + data: bigint[] = [] + provableData: bigint[] = [] + userState?: UserState + provider: any + + constructor() { + makeAutoObservable(this) + this.load() + } + + async load() { + const id: string = localStorage.getItem('id') ?? '' + const identity = new Identity(id) + if (!id) { + localStorage.setItem('id', identity.toString()) + } + + const { UNIREP_ADDRESS, APP_ADDRESS, ETH_PROVIDER_URL } = await fetch( + `${SERVER}/api/config` + ).then((r) => r.json()) + + const provider = ETH_PROVIDER_URL.startsWith('http') + ? new ethers.providers.JsonRpcProvider(ETH_PROVIDER_URL) + : new ethers.providers.WebSocketProvider(ETH_PROVIDER_URL) + this.provider = provider + + const userState = new UserState( + { + provider, + prover, + unirepAddress: UNIREP_ADDRESS, + attesterId: BigInt(APP_ADDRESS), + _id: identity, + }, + identity + ) + await userState.sync.start() + this.userState = userState + await userState.waitForSync() + this.hasSignedUp = await userState.hasSignedUp() + await this.loadData() + this.latestTransitionedEpoch = + await this.userState.latestTransitionedEpoch() + } + + get fieldCount() { + return this.userState?.sync.settings.fieldCount + } + + get sumFieldCount() { + return this.userState?.sync.settings.sumFieldCount + } + + epochKey(nonce: number) { + if (!this.userState) return '0x' + const epoch = this.userState.sync.calcCurrentEpoch() + const key = this.userState.getEpochKeys(epoch, nonce) + return `0x${key.toString(16)}` + } + + async loadData() { + if (!this.userState) throw new Error('user state not initialized') + + this.data = await this.userState.getData() + this.provableData = await this.userState.getProvableData() + } + + async signup() { + if (!this.userState) throw new Error('user state not initialized') + + const signupProof = await this.userState.genUserSignUpProof() + const data = await fetch(`${SERVER}/api/signup`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + publicSignals: signupProof.publicSignals, + proof: signupProof.proof, + }), + }).then((r) => r.json()) + await this.provider.waitForTransaction(data.hash) + await this.userState.waitForSync() + this.hasSignedUp = await this.userState.hasSignedUp() + this.latestTransitionedEpoch = this.userState.sync.calcCurrentEpoch() + } + + async requestData( + reqData: { [key: number]: string | number }, + epkNonce: number + ) { + if (!this.userState) throw new Error('user state not initialized') + + for (const key of Object.keys(reqData)) { + if (reqData[+key] === '') { + delete reqData[+key] + continue + } + } + if (Object.keys(reqData).length === 0) { + throw new Error('No data in the attestation') + } + const epochKeyProof = await this.userState.genEpochKeyProof({ + nonce: epkNonce, + }) + const data = await fetch(`${SERVER}/api/request`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify( + stringifyBigInts({ + reqData, + publicSignals: epochKeyProof.publicSignals, + proof: epochKeyProof.proof, + }) + ), + }).then((r) => r.json()) + await this.provider.waitForTransaction(data.hash) + await this.userState.waitForSync() + await this.loadData() + } + + async stateTransition() { + if (!this.userState) throw new Error('user state not initialized') + + await this.userState.waitForSync() + const signupProof = await this.userState.genUserStateTransitionProof() + const data = await fetch(`${SERVER}/api/transition`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + publicSignals: signupProof.publicSignals, + proof: signupProof.proof, + }), + }).then((r) => r.json()) + await this.provider.waitForTransaction(data.hash) + await this.userState.waitForSync() + await this.loadData() + this.latestTransitionedEpoch = + await this.userState.latestTransitionedEpoch() + } + + async proveData(data: { [key: number]: string | number }) { + if (!this.userState) throw new Error('user state not initialized') + const epoch = await this.userState.sync.loadCurrentEpoch() + const stateTree = await this.userState.sync.genStateTree(epoch) + const index = await this.userState.latestStateTreeLeafIndex(epoch) + const stateTreeProof = stateTree.createProof(index) + const provableData = await this.userState.getProvableData() + const sumFieldCount = this.userState.sync.settings.sumFieldCount + const values = Array(sumFieldCount).fill(0) + for (let [key, value] of Object.entries(data)) { + values[Number(key)] = value + } + const attesterId = this.userState.sync.attesterId + const circuitInputs = stringifyBigInts({ + identity_secret: this.userState.id.secret, + state_tree_indexes: stateTreeProof.pathIndices, + state_tree_elements: stateTreeProof.siblings, + data: provableData, + epoch: epoch, + attester_id: attesterId, + value: values, + }) + const { publicSignals, proof } = await prover.genProofAndPublicSignals( + 'dataProof', + circuitInputs + ) + const dataProof = new DataProof(publicSignals, proof, prover) + const valid = await dataProof.verify() + return stringifyBigInts({ + publicSignals: dataProof.publicSignals, + proof: dataProof.proof, + valid, + }) + } +} + +export default createContext(new User()) diff --git a/packages/frontend/src/contexts/interface.ts b/packages/frontend/src/contexts/interface.ts new file mode 100644 index 00000000..8b421e21 --- /dev/null +++ b/packages/frontend/src/contexts/interface.ts @@ -0,0 +1,55 @@ +import { createContext } from 'react' +import { makeAutoObservable } from 'mobx' + +const MAX_MOBILE_WIDTH = 780 + +export class Interface { + // dark/light mode + // interface viewport size + darkmode = false + modeCssClass = '' + screenWidth = -1 + screenHeight = -1 + isMobile = false + + constructor() { + makeAutoObservable(this) + if (typeof window !== 'undefined') { + this.load() + } + } + + // must be called in browser, not in SSR + load() { + this.updateWindowSize() + window.addEventListener('resize', this.updateWindowSize.bind(this)) + this.setDarkmode(!!localStorage.getItem('darkmode')) + document.cookie = `darkmode=${this.darkmode.toString()}` + } + + updateWindowSize() { + // possibly throttle on heavy sites + this.screenWidth = window.innerWidth + this.screenHeight = window.innerHeight + this.isMobile = this.screenWidth <= MAX_MOBILE_WIDTH + } + + setDarkmode(enabled: boolean) { + this.darkmode = enabled + if (enabled) { + if (typeof window !== 'undefined') { + localStorage.setItem('darkmode', 'true') + document.cookie = `darkmode=${this.darkmode.toString()}` + } + this.modeCssClass = 'dark' + } else { + if (typeof window !== 'undefined') { + localStorage.removeItem('darkmode') + document.cookie = `darkmode=${this.darkmode.toString()}` + } + this.modeCssClass = '' + } + } +} + +export default createContext(new Interface()) diff --git a/packages/frontend/src/contexts/prover.ts b/packages/frontend/src/contexts/prover.ts new file mode 100644 index 00000000..33cbed3b --- /dev/null +++ b/packages/frontend/src/contexts/prover.ts @@ -0,0 +1,44 @@ +import * as snarkjs from 'snarkjs' +import { SnarkPublicSignals, SnarkProof } from '@unirep/utils' +import { Circuit } from '@unirep/circuits' +import { KEY_SERVER } from '../config' + +export default { + verifyProof: async ( + circuitName: string | Circuit, + publicSignals: SnarkPublicSignals, + proof: SnarkProof + ) => { + const url = new URL(`/build/${circuitName}.vkey.json`, KEY_SERVER) + const vkey = await fetch(url.toString()).then((r) => r.json()) + return snarkjs.groth16.verify(vkey, publicSignals, proof) + }, + genProofAndPublicSignals: async ( + circuitName: string | Circuit, + inputs: any + ) => { + const wasmUrl = new URL(`${circuitName}.wasm`, KEY_SERVER) + + const wasm = await fetch(wasmUrl.toString()).then((r) => + r.arrayBuffer() + ) + const zkeyUrl = new URL(`${circuitName}.zkey`, KEY_SERVER) + const zkey = await fetch(zkeyUrl.toString()).then((r) => + r.arrayBuffer() + ) + const { proof, publicSignals } = await snarkjs.groth16.fullProve( + inputs, + new Uint8Array(wasm), + new Uint8Array(zkey) + ) + return { proof, publicSignals } + }, + /** + * Get vkey from default built folder `zksnarkBuild/` + * @param name Name of the circuit, which can be chosen from `Circuit` + * @returns vkey of the circuit + */ + getVKey: async (name: string | Circuit) => { + // return require(path.join(buildPath, `${name}.vkey.json`)) + }, +} diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css new file mode 100644 index 00000000..9bf0b788 --- /dev/null +++ b/packages/frontend/src/index.css @@ -0,0 +1,5 @@ +body { + /* font-family: sans-serif, helvetica; */ + font-family: 'Azeret Mono'; + font-weight: 200; +} diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx new file mode 100644 index 00000000..979d747f --- /dev/null +++ b/packages/frontend/src/index.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import Header from './pages/Header' +import Start from './pages/Start' +import Dashboard from './pages/Dashboard' +import './index.css' + +export default function App() { + return ( + + + }> + } /> + } /> + + + + ) +} + +const rootElement = document.getElementById('root') +if (rootElement) { + const root = createRoot(rootElement) + root.render() +} diff --git a/packages/frontend/src/pages/Dashboard.tsx b/packages/frontend/src/pages/Dashboard.tsx new file mode 100644 index 00000000..798388ed --- /dev/null +++ b/packages/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,323 @@ +import React from 'react' +import { observer } from 'mobx-react-lite' +import './dashboard.css' +import Button from '../components/Button' +import Tooltip from '../components/Tooltip' + +import User from '../contexts/User' + +type ReqInfo = { + nonce: number +} + +type ProofInfo = { + publicSignals: string[] + proof: string[] + valid: boolean +} + +export default observer(() => { + const userContext = React.useContext(User) + const [remainingTime, setRemainingTime] = React.useState(0) + const [reqData, setReqData] = React.useState<{ + [key: number]: number | string + }>({}) + const [reqInfo, setReqInfo] = React.useState({ nonce: 0 }) + const [proveData, setProveData] = React.useState<{ + [key: number]: number | string + }>({}) + const [repProof, setRepProof] = React.useState({ + publicSignals: [], + proof: [], + valid: false, + }) + + const updateTimer = () => { + if (!userContext.userState) { + setRemainingTime('Loading...') + return + } + const time = userContext.userState.sync.calcEpochRemainingTime() + setRemainingTime(time) + } + + const fieldType = (i: number) => { + if (i < userContext.sumFieldCount) { + return 'sum' + } else return 'replace' + } + + React.useEffect(() => { + setInterval(() => { + updateTimer() + }, 1000) + }, []) + + if (!userContext.userState) { + return
Loading...
+ } + + return ( +
+

Dashboard

+
+
+
+

Epoch

+ +
+
+
Current epoch #
+
+ {userContext.userState?.sync.calcCurrentEpoch()} +
+
+
+
Remaining time
+
{remainingTime}
+
+
+
Latest transition epoch
+
+ {userContext.latestTransitionedEpoch} +
+
+ +
+ +
+

Latest Data

+ +
+ {userContext.data.map((data, i) => { + if (i < userContext.sumFieldCount) { + return ( +
+
Data {i}
+
+ {(data || 0).toString()} +
+
+ ) + } else { + return ( +
+
Data {i}
+
+ {( + data % BigInt(2 ** 206) || 0 + ).toString()} +
+
+ ) + } + })} + +
+ +
+

Provable Data

+ +
+ {userContext.provableData.map((data, i) => { + if (i < userContext.sumFieldCount) { + return ( +
+
Data {i}
+
+ {(data || 0).toString()} +
+
+ ) + } else { + return ( +
+
Data {i}
+
+ {( + data % BigInt(2 ** 206) || 0 + ).toString()} +
+
+ ) + } + })} +
+ +
+
+
+

Change Data

+ +
+
+ {Array( + userContext.userState.sync.settings.fieldCount + ) + .fill(0) + .map((_, i) => { + return ( +
+

+ Data {i} ({fieldType(i)}) +

+ { + if ( + !/^\d*$/.test( + event.target.value + ) + ) + return + setReqData(() => ({ + ...reqData, + [i]: event.target.value, + })) + }} + /> +
+ ) + })} +
+
+

+ Epoch key nonce +

+ +
+ +

+ Requesting data with epoch key: +

+

+ {userContext.epochKey(reqInfo.nonce ?? 0)} +

+ + +
+ +
+
+

User State Transition

+ +
+ +
+ +
+
+

Prove Data

+ +
+ {Array( + userContext.userState.sync.settings.sumFieldCount + ) + .fill(0) + .map((_, i) => { + return ( +
+

+ Data {i} ({fieldType(i)}) +

+ { + if ( + !/^\d*$/.test( + event.target.value + ) + ) + return + setProveData(() => ({ + ...proveData, + [i]: event.target.value, + })) + }} + /> +
+ ) + })} +
+ +
+ {repProof.proof.length ? ( + <> +
+ Is proof valid?{' '} + + {' '} + {repProof.proof.length === 0 + ? '' + : repProof.valid.toString()} + +
+