├── .dockerignore ├── .gitignore ├── .gitmodules ├── .nvmrc ├── Dockerfile ├── LICENSE ├── README.md ├── docker └── docker-compose-goerli.yaml ├── lerna.json ├── package.json ├── packages ├── backend │ ├── Dockerfile │ ├── db │ │ ├── Dockerfile │ │ └── init.sql │ ├── index.js │ ├── knexfile.js │ ├── migrations │ │ ├── 20200318014631_create_posts.js │ │ └── 20200318014754_create_semaphore_logs.js │ ├── package.json │ ├── src │ │ ├── app.js │ │ ├── authentication.js │ │ ├── configs.js │ │ ├── groups.js │ │ ├── posts.js │ │ ├── schema.js │ │ └── validation.js │ └── test │ │ └── backend.test.js ├── cli │ ├── config.js │ ├── constants.js │ ├── identities.js │ ├── index.js │ ├── package.json │ ├── posts.js │ ├── provider.js │ ├── sendTx.html │ ├── setServer.js │ └── setup.js ├── common │ ├── abis │ │ ├── ProofOfBurn.json │ │ └── Semaphore.json │ ├── assets │ │ └── verification_key.json │ ├── index.js │ ├── package.json │ └── src │ │ └── externalNullifier.js ├── config │ ├── export-config.js │ ├── goerli.yaml │ ├── index.js │ ├── local-dev.yaml.example │ ├── package.json │ └── test.yaml ├── contracts │ ├── .gitattributes │ ├── .gitignore │ ├── constants.js │ ├── contracts │ │ ├── ProofOfBurn.sol │ │ └── semaphore │ │ │ ├── IncrementalMerkleTree.sol │ │ │ ├── MiMC.sol │ │ │ ├── Ownable.sol │ │ │ ├── Semaphore.sol │ │ │ ├── SnarkConstants.sol │ │ │ └── verifier.sol │ ├── package.json │ ├── scripts │ │ ├── copyABIs.js │ │ ├── deploy.js │ │ └── downloadSnarks.sh │ ├── src │ │ ├── compile.js │ │ └── deploy.js │ └── test │ │ └── proofOfBurn.test.js └── frontend │ ├── .gitignore │ ├── Dockerfile │ ├── externals │ └── worker_threads.js │ ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico │ ├── img │ └── og_hojicha.png │ ├── index.html │ ├── nginx.conf │ ├── package.json │ ├── server.js │ ├── site.webmanifest │ └── src │ ├── components │ ├── contracts.jsx │ ├── download_snarks.jsx │ └── github.html │ ├── configs.js │ ├── custom.scss │ ├── hooks.js │ ├── index.jsx │ ├── pages │ ├── about.jsx │ ├── group.jsx │ ├── identity.jsx │ └── posts.jsx │ ├── storage.js │ ├── utils │ └── fetch.js │ └── web3 │ ├── error.js │ ├── index.js │ ├── registration.js │ └── semaphore.js └── scripts ├── buildImagesGoerli.sh └── runAll.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | **/docker-compose.yml 2 | .dockerignore 3 | **/Dockerfile 4 | **/node_modules 5 | **/build 6 | **/*.log 7 | **/*.md 8 | **/.git 9 | **/.gitignore 10 | **/.gitmodules 11 | **/.gitattributes 12 | **/dist 13 | **/build 14 | **/.cache 15 | **/cache 16 | **/*.sqlite 17 | **/*.env 18 | **/nginx.conf 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *package-lock.json 2 | node_modules/ 3 | *.env 4 | mydb.sqlite 5 | packages/config/*.yaml 6 | packages/frontend/src/exported-config.json 7 | *.log 8 | goerli.json 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/.gitmodules -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1-stretch AS hojicha-build 2 | 3 | WORKDIR /hojicha 4 | 5 | ARG NODE_CONFIG_ENV 6 | ENV NODE_CONFIG_ENV=$NODE_CONFIG_ENV 7 | 8 | COPY package.json lerna.json /hojicha/ 9 | 10 | RUN npm install --quiet && \ 11 | npm cache clean --force 12 | 13 | WORKDIR /hojicha/packages 14 | 15 | RUN mkdir common && \ 16 | mkdir config && \ 17 | mkdir backend && \ 18 | mkdir frontend 19 | 20 | COPY packages/common/package.json common/ 21 | COPY packages/config/package.json config/ 22 | COPY packages/backend/package.json backend/ 23 | COPY packages/frontend/package.json frontend/ 24 | 25 | WORKDIR /hojicha 26 | 27 | # First bootstrap we cache the external packages 28 | RUN npx lerna bootstrap --force-local 29 | 30 | WORKDIR /hojicha/packages 31 | 32 | COPY packages/common common 33 | COPY packages/config config 34 | COPY packages/backend backend 35 | COPY packages/frontend frontend 36 | 37 | WORKDIR /hojicha/ 38 | 39 | # Second bootstrap we install the local dependencies 40 | RUN npx lerna bootstrap --force-local 41 | 42 | ENV NODE_ENV=production 43 | 44 | RUN cd packages/frontend && \ 45 | npm run build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Barry WhiteHat and Chih-Cheng Liang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Semaphore Authentication 2 | 3 | ![](packages/frontend/img/og_hojicha.png) 4 | 5 | Anonymous web login built on [Semaphore](https://github.com/kobigurk/semaphore#semaphore). 6 | 7 | See [spec](https://hackmd.io/HefATMWnRN6qrngW7iZ1Eg) here 8 | 9 | See [hojicha.art](https://hojicha.art) as an example of how it works. 10 | 11 | ## Getting Started 12 | 13 | ``` 14 | npm run bootstrap 15 | scripts/buildImagesGoerli.sh 16 | docker-compose -f docker/docker-compose-goerli.yaml up 17 | ``` 18 | -------------------------------------------------------------------------------- /docker/docker-compose-goerli.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | hojicha-db: 4 | container_name: hojicha-db 5 | build: 6 | context: ../packages/backend/db 7 | ports: 8 | - '5002:5432' 9 | expose: 10 | - '5432' 11 | networks: 12 | - 'hojicha-net' 13 | volumes: 14 | - db-data:/var/lib/postgresql/data 15 | 16 | hojicha-backend: 17 | container_name: hojicha-backend 18 | depends_on: 19 | - 'hojicha-db' 20 | build: 21 | context: ../packages/backend 22 | ports: 23 | - '3000:5566' 24 | expose: 25 | - '3000' 26 | networks: 27 | - 'hojicha-net' 28 | environment: 29 | - NODE_ENV=production 30 | - NODE_CONFIG_ENV=goerli 31 | 32 | hojicha-frontend: 33 | container_name: hojicha-frontend 34 | depends_on: 35 | - 'hojicha-backend' 36 | build: 37 | context: ../packages/frontend 38 | ports: 39 | - '80:8001' 40 | networks: 41 | - 'hojicha-net' 42 | 43 | networks: 44 | hojicha-net: 45 | 46 | volumes: 47 | db-data: 48 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/**"], 3 | "version": "independent" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "bootstrap": "lerna bootstrap --force-local" 6 | }, 7 | "devDependencies": { 8 | "lerna": "^3.20.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1-stretch AS hojicha-backend 2 | 3 | COPY --from=hojicha-build /hojicha/packages/backend /backend 4 | COPY --from=hojicha-build /hojicha/packages/config /config 5 | COPY --from=hojicha-build /hojicha/packages/common /common 6 | 7 | WORKDIR /backend 8 | 9 | CMD ["sh", "-c", "sleep 2 && npm run migrate && node index.js"] 10 | -------------------------------------------------------------------------------- /packages/backend/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:12-alpine 2 | 3 | ENV POSTGRES_HOST_AUTH_METHOD=trust 4 | 5 | COPY ./init.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /packages/backend/db/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE hojicha; 2 | GRANT ALL PRIVILEGES ON DATABASE hojicha TO postgres; 3 | -------------------------------------------------------------------------------- /packages/backend/index.js: -------------------------------------------------------------------------------- 1 | const { createApp, bindDb } = require('./src/app') 2 | 3 | const main = async () => { 4 | await bindDb() 5 | 6 | const app = createApp() 7 | app.listen(5566, () => { 8 | console.log('Backend listening in 5566') 9 | }) 10 | } 11 | 12 | if (require.main === module) { 13 | main() 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/knexfile.js: -------------------------------------------------------------------------------- 1 | const config = require('@hojicha/config') 2 | 3 | module.exports = config.backend.db 4 | -------------------------------------------------------------------------------- /packages/backend/migrations/20200318014631_create_posts.js: -------------------------------------------------------------------------------- 1 | exports.up = function (knex) { 2 | return knex.schema.createTable('posts', table => { 3 | table.increments('id').primary() 4 | table.string('postBody', 280) // Twitter magic number 5 | table 6 | .integer('semaphoreLogId') 7 | .unsigned() 8 | .notNullable() 9 | table 10 | .dateTime('createdAt') 11 | .notNullable() 12 | .defaultTo(knex.fn.now()) 13 | }) 14 | } 15 | 16 | exports.down = function (knex) { 17 | return knex.schema.dropTable('posts') 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend/migrations/20200318014754_create_semaphore_logs.js: -------------------------------------------------------------------------------- 1 | exports.up = function (knex) { 2 | return knex.schema.createTable('semaphore_logs', table => { 3 | table.increments('id').primary() 4 | table.string('root') 5 | table.string('nullifierHash') 6 | table.string('signalHash') 7 | table.string('externalNullifierStr') 8 | table.string('proof', 1000) // string length of proof is around 700 9 | 10 | table 11 | .dateTime('createdAt') 12 | .notNullable() 13 | .defaultTo(knex.fn.now()) 14 | }) 15 | } 16 | 17 | exports.down = function (knex) { 18 | return knex.schema.dropTable('semaphore_logs') 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/backend", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "test": "ava --timeout=1m", 9 | "migrate": "npx knex migrate:latest" 10 | }, 11 | "dependencies": { 12 | "@hojicha/common": "^0.0.3", 13 | "@hojicha/config": "^0.0.2", 14 | "ethers": "^4.0.45", 15 | "express": "^4.17.1", 16 | "helmet": "^3.21.3", 17 | "knex": "^0.21.1", 18 | "libsemaphore": "^1.0.15", 19 | "objection": "^2.1.3", 20 | "pg": "^8.0.3" 21 | }, 22 | "devDependencies": { 23 | "@hojicha/contracts": "^0.0.3", 24 | "ava": "^3.4.0", 25 | "cors": "^2.8.5", 26 | "nodemon": "^2.0.2", 27 | "supertest": "^4.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/backend/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { posts } = require('./posts') 3 | const { groups } = require('./groups') 4 | const Knex = require('knex') 5 | const configs = require('./configs') 6 | const { Model } = require('objection') 7 | const helmet = require('helmet') 8 | 9 | const bindDb = async () => { 10 | const knex = Knex(configs.db) 11 | Model.knex(knex) 12 | 13 | // Wait for the DB to be active 14 | await knex.raw('select 1+1 as result').catch(err => { 15 | console.error(err) 16 | process.exit(1) 17 | }) 18 | 19 | return knex 20 | } 21 | 22 | const naiveErrorHandler = (err, req, res, next) => { 23 | console.error(err.stack) 24 | res.status(400).json({ 25 | error: err.toString() 26 | }) 27 | } 28 | 29 | const createApp = () => { 30 | const app = express() 31 | app.use(helmet()) 32 | app.use(express.json()) 33 | app.use(express.urlencoded({ extended: true })) 34 | 35 | // Routes 36 | app.use('/posts', posts) 37 | app.use('/info', groups) 38 | 39 | // Error handling 40 | app.use(naiveErrorHandler) 41 | 42 | return app 43 | } 44 | 45 | module.exports = { createApp, bindDb } 46 | -------------------------------------------------------------------------------- /packages/backend/src/authentication.js: -------------------------------------------------------------------------------- 1 | const { 2 | validateExternalNullifierMatch, 3 | validateSignalHash, 4 | validateNullifierNotSeen, 5 | validateInRootHistory, 6 | validateProof 7 | } = require('./validation') 8 | 9 | const { EpochbasedExternalNullifier } = require('@hojicha/common') 10 | 11 | const { unstringifyBigInts } = require('libsemaphore') 12 | 13 | const { SemaphoreLog } = require('./schema') 14 | 15 | const newPostExternalNullifierGen = new EpochbasedExternalNullifier( 16 | '/posts/new', 17 | 300 * 1000 // rate limit to 30 seconds 18 | ) 19 | 20 | // Do semaphore authentication as a middleware 21 | const requireSemaphoreAuth = async (req, res, next) => { 22 | const rawProof = req.body.proof 23 | const parsedProof = unstringifyBigInts(JSON.parse(rawProof)) 24 | const parsedPublicSignals = unstringifyBigInts( 25 | JSON.parse(req.body.publicSignals) 26 | ) 27 | const [ 28 | root, 29 | nullifierHash, 30 | signalHash, 31 | externalNullifier 32 | ] = parsedPublicSignals 33 | const content = req.body.postBody 34 | 35 | const expectedExternalNullifierStr = newPostExternalNullifierGen.getString() 36 | 37 | try { 38 | validateExternalNullifierMatch( 39 | externalNullifier, 40 | expectedExternalNullifierStr 41 | ) 42 | validateSignalHash(content, signalHash) 43 | await validateInRootHistory(root) 44 | await validateNullifierNotSeen(nullifierHash) 45 | await validateProof(parsedProof, parsedPublicSignals) 46 | } catch (err) { 47 | next(err) 48 | } 49 | 50 | const data = { 51 | root: root.toString(), 52 | nullifierHash: nullifierHash.toString(), 53 | externalNullifierStr: expectedExternalNullifierStr, 54 | signalHash: signalHash.toString(), 55 | proof: rawProof 56 | } 57 | 58 | try { 59 | const semaphoreLog = await SemaphoreLog.query().insert(data) 60 | req.semaphoreLogId = semaphoreLog.id 61 | } catch (err) { 62 | next(err) 63 | } 64 | 65 | next() 66 | } 67 | 68 | module.exports = { requireSemaphoreAuth } 69 | -------------------------------------------------------------------------------- /packages/backend/src/configs.js: -------------------------------------------------------------------------------- 1 | const config = require('@hojicha/config') 2 | 3 | module.exports = { 4 | SERVER_NAME: config.backend.serverName || 'MyAwesomeForum', 5 | NETWORK: config.chain.network || 'localhost', 6 | PROOF_OF_BURN_ADDRESS: config.chain.contracts.proofOfBurn, 7 | SEMAPHORE_ADDRESS: config.chain.contracts.semaphore, 8 | db: config.backend.db, 9 | providerUrl: config.chain.url 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/src/groups.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express') 2 | const configs = require('./configs') 3 | 4 | const groups = Router() 5 | 6 | groups.get('/', (req, res) => { 7 | res.json({ 8 | serverName: configs.SERVER_NAME, 9 | network: configs.NETWORK, 10 | registrationStyle: 'ProofOfBurn', 11 | registrationAddress: configs.PROOF_OF_BURN_ADDRESS, 12 | semaphoreAddress: configs.SEMAPHORE_ADDRESS 13 | }) 14 | }) 15 | 16 | module.exports = { groups } 17 | -------------------------------------------------------------------------------- /packages/backend/src/posts.js: -------------------------------------------------------------------------------- 1 | const posts = require('express').Router() 2 | const { requireSemaphoreAuth } = require('./authentication') 3 | const { Post } = require('./schema') 4 | 5 | const PAGESIZE = 20 6 | 7 | posts.get('/', async (req, res) => { 8 | // { 9 | // results: [{Post}] 10 | // total: int 11 | // } 12 | const result = await Post.query() 13 | .orderBy('id', 'desc') 14 | .page(0, PAGESIZE) 15 | res.json(result) 16 | }) 17 | 18 | posts.get('/page/:pageNum', async (req, res) => { 19 | const pageNum = parseInt(req.params.pageNum) 20 | const result = await Post.query() 21 | .leftJoinRelated('authData') 22 | .select('authData.*') 23 | .select('posts.*') 24 | .orderBy('posts.id', 'desc') 25 | .page(pageNum, PAGESIZE) 26 | result.next = (pageNum + 1) * PAGESIZE < result.total ? pageNum + 1 : null 27 | res.json(result) 28 | }) 29 | 30 | posts.get('/:postId', async (req, res) => { 31 | const postId = req.params.postId 32 | const post = await Post.query().findOne({ id: postId }) 33 | res.json(post) 34 | }) 35 | 36 | posts.post('/new', requireSemaphoreAuth, async (req, res, next) => { 37 | const postBody = req.body.postBody 38 | const semaphoreLogId = req.semaphoreLogId 39 | const post = await Post.query() 40 | .insert({ postBody, semaphoreLogId }) 41 | .catch(next) 42 | res.json({ 43 | message: `Your article is published! Article id: ${post.id}`, 44 | postId: post.id 45 | }) 46 | }) 47 | 48 | module.exports = { posts } 49 | -------------------------------------------------------------------------------- /packages/backend/src/schema.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('objection') 2 | 3 | class Post extends Model { 4 | static get tableName () { 5 | return 'posts' 6 | } 7 | static get relationMappings () { 8 | return { 9 | authData: { 10 | relation: Model.HasOneRelation, 11 | modelClass: SemaphoreLog, 12 | join: { 13 | from: 'posts.semaphoreLogId', 14 | to: 'semaphore_logs.id' 15 | } 16 | } 17 | } 18 | } 19 | } 20 | class SemaphoreLog extends Model { 21 | static get tableName () { 22 | return 'semaphore_logs' 23 | } 24 | } 25 | 26 | module.exports = { Post, SemaphoreLog } 27 | -------------------------------------------------------------------------------- /packages/backend/src/validation.js: -------------------------------------------------------------------------------- 1 | const snarkjs = require('snarkjs') 2 | 3 | const { verificationKey, SemaphoreABI } = require('@hojicha/common') 4 | 5 | const ethers = require('ethers') 6 | 7 | const { 8 | genExternalNullifier, 9 | genSignalHash, 10 | stringifyBigInts, 11 | unstringifyBigInts, 12 | verifyProof 13 | } = require('libsemaphore') 14 | 15 | const { SemaphoreLog } = require('./schema') 16 | const configs = require('./configs') 17 | 18 | function validateExternalNullifierMatch (actual, externalNullifierStr) { 19 | const expected = snarkjs.bigInt(genExternalNullifier(externalNullifierStr)) 20 | if (expected !== actual) { 21 | throw Error( 22 | `Illegal externalNullifier: expect "${expected}" (${externalNullifierStr}), got "${actual}"` 23 | ) 24 | } 25 | } 26 | 27 | function validateSignalHash (content, actual) { 28 | const signalStr = ethers.utils.hashMessage(content) 29 | const expected = genSignalHash(ethers.utils.toUtf8Bytes(signalStr)) 30 | if (actual !== expected) { 31 | throw Error(`Expected signalHash ${expected}, got ${actual}`) 32 | } 33 | } 34 | async function validateNullifierNotSeen (nullifierHash) { 35 | const results = await SemaphoreLog.query() 36 | .select('nullifierHash', 'externalNullifierStr', 'id') 37 | .where('nullifierHash', nullifierHash.toString()) 38 | .catch(console.error) 39 | 40 | if (results.length > 0) { 41 | const log = results[0] 42 | throw new Error( 43 | `Posting too soon. nullifierHash (${nullifierHash}) has been seen before for the same external nullifier "${log.externalNullifierStr}"` 44 | ) 45 | } 46 | } 47 | async function validateInRootHistory (root) { 48 | const provider = new ethers.providers.JsonRpcProvider(configs.providerUrl) 49 | const semaphore = new ethers.Contract( 50 | configs.SEMAPHORE_ADDRESS, 51 | SemaphoreABI, 52 | provider 53 | ) 54 | 55 | const isInRootHistory = await semaphore.rootHistory(stringifyBigInts(root)) 56 | if (!isInRootHistory) throw Error(`Root (${root.toString()}) not in history`) 57 | } 58 | 59 | async function validateProof (proof, publicSignals) { 60 | const verifyingKey = unstringifyBigInts(verificationKey) 61 | const isValid = verifyProof(verifyingKey, proof, publicSignals) 62 | if (!isValid) throw Error('Invalid Proof') 63 | } 64 | 65 | module.exports = { 66 | validateExternalNullifierMatch, 67 | validateSignalHash, 68 | validateNullifierNotSeen, 69 | validateInRootHistory, 70 | validateProof 71 | } 72 | -------------------------------------------------------------------------------- /packages/backend/test/backend.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const { createApp, bindDb } = require('../src/app') 3 | const { deployContracts } = require('@hojicha/contracts/src/deploy') 4 | const { 5 | REGISTRATION_FEE, 6 | SEMAPHORE_TREE_DEPTH, 7 | CIRCUIT_CACHE_PATH, 8 | PROVING_KEY_CACHE_PATH 9 | } = require('@hojicha/contracts/constants') 10 | const { 11 | EpochbasedExternalNullifier, 12 | verificationKey 13 | } = require('@hojicha/common') 14 | const fs = require('fs') 15 | const libsemaphore = require('libsemaphore') 16 | const ethers = require('ethers') 17 | const test = require('ava') 18 | 19 | const configs = require('../src/configs') 20 | 21 | test('should post a new post', async t => { 22 | const app = createApp() 23 | await bindDb() 24 | 25 | const registrationFee = REGISTRATION_FEE 26 | const contracts = await deployContracts({ registrationFee }) 27 | 28 | // Override the address 29 | configs.SEMAPHORE_ADDRESS = contracts.Semaphore.address 30 | 31 | const identity = libsemaphore.genIdentity() 32 | const identityCommitment = libsemaphore.genIdentityCommitment(identity) 33 | await contracts.ProofOfBurn.register(identityCommitment.toString(), { 34 | value: ethers.utils.parseEther(registrationFee.toString()) 35 | }) 36 | 37 | console.log('Registered identity') 38 | 39 | const cirDef = require(CIRCUIT_CACHE_PATH) 40 | 41 | const circuit = libsemaphore.genCircuit(cirDef) 42 | const leaves = await contracts.ProofOfBurn.getIdentityCommitments() 43 | 44 | t.is(leaves[0].toString(), identityCommitment.toString()) 45 | 46 | const postBody = 'foooooo' 47 | const signalStr = ethers.utils.hashMessage(postBody) 48 | 49 | const newPostExternalNullifierGen = new EpochbasedExternalNullifier( 50 | '/posts/new', 51 | 300 * 1000 // rate limit to 30 seconds 52 | ) 53 | 54 | const externalNullifierStr = newPostExternalNullifierGen.getString() 55 | console.log(externalNullifierStr) 56 | const externalNullifier = libsemaphore.genExternalNullifier( 57 | externalNullifierStr 58 | ) 59 | 60 | console.log('genWitness') 61 | 62 | const { witness } = await libsemaphore.genWitness( 63 | signalStr, 64 | circuit, 65 | identity, 66 | leaves, 67 | SEMAPHORE_TREE_DEPTH, 68 | externalNullifier 69 | ) 70 | t.true(circuit.checkWitness(witness)) 71 | 72 | const provingKey = fs.readFileSync(PROVING_KEY_CACHE_PATH) 73 | 74 | console.log('genProof and genPublicSignals') 75 | const proof = await libsemaphore.genProof(witness, provingKey) 76 | const publicSignals = libsemaphore.genPublicSignals(witness, circuit) 77 | 78 | const verifyingKey = libsemaphore.unstringifyBigInts(verificationKey) 79 | 80 | t.true(libsemaphore.verifyProof(verifyingKey, proof, publicSignals)) 81 | console.log('Requesting') 82 | 83 | const stringfiedProof = JSON.stringify(libsemaphore.stringifyBigInts(proof)) 84 | const stringfiedPublicSignals = JSON.stringify( 85 | libsemaphore.stringifyBigInts(publicSignals) 86 | ) 87 | 88 | const root = libsemaphore.stringifyBigInts(publicSignals[0]) 89 | t.true(await contracts.Semaphore.rootHistory(root)) 90 | 91 | let postId 92 | await request(app) 93 | .post('/posts/new') 94 | .send({ 95 | postBody, 96 | proof: stringfiedProof, 97 | publicSignals: stringfiedPublicSignals 98 | }) 99 | .set('Accept', 'application/json') 100 | .expect(200) 101 | .then(res => { 102 | t.true(res.body.message.includes('Your article is published!')) 103 | postId = res.body.postId 104 | }) 105 | 106 | await request(app) 107 | .get(`/posts/${postId}`) 108 | .expect(200) 109 | .then(res => t.is(res.body.postBody, postBody)) 110 | }) 111 | -------------------------------------------------------------------------------- /packages/cli/config.js: -------------------------------------------------------------------------------- 1 | const Configstore = require('configstore') 2 | const packageJson = require('./package.json') 3 | 4 | class Config { 5 | constructor (key) { 6 | this.configstore = new Configstore(packageJson.name) 7 | this.key = key 8 | } 9 | 10 | get () { 11 | return this.configstore.get(this.key) 12 | } 13 | set (value) { 14 | return this.configstore.set(this.key, value) 15 | } 16 | } 17 | 18 | const defaultIdentityName = new Config('defaultIdentityName') 19 | const hostInfo = new Config('hostInfo') 20 | 21 | module.exports = { defaultIdentityName, hostInfo } 22 | -------------------------------------------------------------------------------- /packages/cli/constants.js: -------------------------------------------------------------------------------- 1 | const envPaths = require('env-paths') 2 | const path = require('path') 3 | 4 | const packageJson = require('./package.json') 5 | 6 | const paths = envPaths(packageJson.name) 7 | 8 | module.exports = { 9 | ROOT_DIR: paths.data, 10 | IDENTITIES_DIR: path.join(paths.data, 'identities/'), 11 | CIRCUIT_PATH: path.join(paths.data, 'circuit.json'), 12 | PROVING_KEY_PATH: path.join(paths.data, 'proving_key.bin') 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/identities.js: -------------------------------------------------------------------------------- 1 | const { 2 | genIdentity, 3 | serialiseIdentity, 4 | unSerialiseIdentity, 5 | genIdentityCommitment 6 | } = require('libsemaphore') 7 | const { IDENTITIES_DIR } = require('./constants') 8 | const { hostInfo, defaultIdentityName } = require('./config') 9 | const fs = require('fs') 10 | const path = require('path') 11 | const prompts = require('prompts') 12 | const { 13 | proofOfBurnContract 14 | } = require('semaphore-auth-contracts/src/contracts') 15 | const { REGISTRATION_FEE } = require('semaphore-auth-contracts/constants') 16 | const ethers = require('ethers') 17 | 18 | const registerByGanache = async identityCommitment => { 19 | const provider = new ethers.providers.JsonRpcProvider() 20 | const signer = provider.getSigner() 21 | const proofOfBurn = proofOfBurnContract( 22 | signer, 23 | hostInfo.get().registrationAddress 24 | ) 25 | const tx = await proofOfBurn.register(identityCommitment.toString(), { 26 | value: ethers.utils.parseEther(REGISTRATION_FEE.toString()) 27 | }) 28 | const receipt = await tx.wait() 29 | console.info( 30 | `Sent identityCommitment to contract: ${proofOfBurn.address} tx: ${receipt.transactionHash}` 31 | ) 32 | } 33 | 34 | const registerByMetamask = async identityCommitment => { 35 | const network = hostInfo.get().network 36 | const semaphoreABI = require('semaphore-auth-contracts/abis/ProofOfBurn.json') 37 | require('http') 38 | .createServer(function (request, response) { 39 | const content = fs 40 | .readFileSync(path.join(__dirname, './sendTx.html')) 41 | .toString() 42 | .replace('{{{NETWORK}}}', network) 43 | .replace('{{{ADDRESS}}}', hostInfo.get().registrationAddress) 44 | .replace('{{{COMMITMENT}}}', identityCommitment) 45 | .replace('{{{ABI}}}', JSON.stringify(semaphoreABI)) 46 | response.writeHead(200, { 'Content-Type': 'text/html' }) 47 | response.end(content, 'utf-8') 48 | }) 49 | .listen(8080) 50 | console.log('Server running at http://127.0.0.1:8080/') 51 | } 52 | 53 | const printTx = async identityCommitment => { 54 | console.info( 55 | `Please sent a transaction to ${hostInfo.get().network}: ${ 56 | hostInfo.get().registrationAddress 57 | }` 58 | ) 59 | console.info(`with identityCommitment as ${identityCommitment.toString()}`) 60 | console.info(`and with value ${REGISTRATION_FEE.toString()} ether`) 61 | } 62 | 63 | const createIdentity = () => { 64 | const id = genIdentity() 65 | console.info('Generated Identity', id) 66 | 67 | const filename = id.keypair.pubKey.toString().slice(0, 20) 68 | const filePath = path.join(IDENTITIES_DIR, filename) 69 | fs.writeFileSync(filePath, serialiseIdentity(id)) 70 | console.info('Saved identity to ', filePath) 71 | return { id, filename } 72 | } 73 | 74 | const createIdentityHandler = () => { 75 | createIdentity() 76 | } 77 | 78 | const listIdentityHandler = () => { 79 | const ids = fs.readdirSync(IDENTITIES_DIR) 80 | for (id of ids) { 81 | const serialized = fs.readFileSync(path.join(IDENTITIES_DIR, id)) 82 | console.info(unSerialiseIdentity(serialized)) 83 | } 84 | } 85 | 86 | const setIdentityHandler = async () => { 87 | const ids = fs.readdirSync(IDENTITIES_DIR) 88 | const choices = ids.map(filename => { 89 | return { 90 | title: filename, 91 | value: filename 92 | } 93 | }) 94 | const response = await prompts({ 95 | type: 'select', 96 | name: 'value', 97 | message: 'Select an identity as a default', 98 | choices 99 | }) 100 | selectedId = response.value 101 | defaultIdentityName.set(selectedId) 102 | console.info(`Set default id to ${selectedId}`) 103 | } 104 | 105 | const registerIdentityHandler = async argv => { 106 | const idName = defaultIdentityName.get() 107 | idToRegister = unSerialiseIdentity( 108 | fs.readFileSync(path.join(IDENTITIES_DIR, idName)) 109 | ) 110 | 111 | const identityCommitment = genIdentityCommitment(idToRegister) 112 | console.info('Generated identityCommitment', identityCommitment) 113 | 114 | if (argv.print) { 115 | printTx(identityCommitment) 116 | return 117 | } 118 | 119 | if (argv.local) { 120 | await registerByGanache(identityCommitment) 121 | return 122 | } 123 | 124 | await registerByMetamask(identityCommitment) 125 | } 126 | 127 | module.exports = { 128 | createIdentity, 129 | createIdentityHandler, 130 | listIdentityHandler, 131 | setIdentityHandler, 132 | registerIdentityHandler 133 | } 134 | -------------------------------------------------------------------------------- /packages/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | 4 | const { setupHandler } = require('./setup') 5 | const { setServerHandler } = require('./setServer') 6 | const { 7 | createIdentityHandler, 8 | listIdentityHandler, 9 | setIdentityHandler, 10 | registerIdentityHandler 11 | } = require('./identities') 12 | const { newPostHandler, viewPostHandler } = require('./posts') 13 | const { ROOT_DIR, IDENTITIES_DIR } = require('./constants') 14 | 15 | const initDirs = () => { 16 | for (dirs of [ROOT_DIR, IDENTITIES_DIR]) { 17 | if (!fs.existsSync(IDENTITIES_DIR)) fs.mkdirSync(IDENTITIES_DIR) 18 | } 19 | } 20 | 21 | initDirs() 22 | 23 | require('yargs') 24 | .usage('A forum using zeroknowledge authentication') 25 | .command('setup', 'Download verification keys etc ...', {}, setupHandler) 26 | .command( 27 | 'setServer ', 28 | 'Set the default server to interact', 29 | yargs => { 30 | yargs.positional('hostUrl', { 31 | describe: 'The server url to connect' 32 | }) 33 | }, 34 | setServerHandler 35 | ) 36 | .command('identity', 'Manage your identities', yargs => { 37 | yargs 38 | .command('create', 'Create a new identity', createIdentityHandler) 39 | .command('list', 'List existing identities', listIdentityHandler) 40 | .command('set', 'Set default identity', setIdentityHandler) 41 | .command( 42 | 'register', 43 | 'Register the default identity', 44 | yargs => { 45 | yargs 46 | .option('print', { 47 | type: 'boolean', 48 | describe: 'just print out the transaction' 49 | }) 50 | .option('local', { 51 | type: 'boolean', 52 | describe: 'Using ganache provider' 53 | }) 54 | }, 55 | registerIdentityHandler 56 | ) 57 | .demandCommand() 58 | }) 59 | .command('view', 'View latest posts', {}, viewPostHandler) 60 | .command( 61 | 'post [article]', 62 | 'Post a new article', 63 | yargs => { 64 | yargs.positional('article', { 65 | describe: 'The relative path of a markdown file to post' 66 | }) 67 | }, 68 | newPostHandler 69 | ) 70 | .demandCommand().argv 71 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/cli", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "hojicha": "./index.js" 11 | }, 12 | "dependencies": { 13 | "configstore": "^5.0.1", 14 | "dotenv": "^8.2.0", 15 | "env-paths": "^2.2.0", 16 | "libsemaphore": "^1.0.15", 17 | "node-fetch": "^2.6.0", 18 | "ora": "^4.0.3", 19 | "prompts": "^2.3.1", 20 | "yargs": "^15.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/posts.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const ethers = require('ethers') 5 | const prompts = require('prompts') 6 | 7 | const { 8 | proofOfBurnContract 9 | } = require('semaphore-auth-contracts/src/contracts') 10 | const { SEMAPHORE_TREE_DEPTH } = require('semaphore-auth-contracts/constants') 11 | const { 12 | EpochbasedExternalNullifier 13 | } = require('semaphore-auth-contracts/lib/externalNullifier') 14 | const { 15 | CIRCUIT_PATH, 16 | PROVING_KEY_PATH, 17 | IDENTITIES_DIR 18 | } = require('./constants') 19 | const { 20 | genCircuit, 21 | genExternalNullifier, 22 | genWitness, 23 | genProof, 24 | genPublicSignals, 25 | stringifyBigInts, 26 | unSerialiseIdentity, 27 | genIdentityCommitment 28 | } = require('libsemaphore') 29 | const ora = require('ora') 30 | const { defaultIdentityName, hostInfo } = require('./config') 31 | 32 | const fetch = require('node-fetch') 33 | 34 | const { getProvider } = require('./provider') 35 | 36 | const genAuth = async (externalNullifierStr, signalStr) => { 37 | const spinner = ora({ 38 | discardStdin: false, 39 | text: 'Generating Authentication Data' 40 | }).start() 41 | 42 | spinner.text = 'Loading circuit' 43 | const cirDef = require(CIRCUIT_PATH) 44 | 45 | spinner.text = 'Loading provingKey' 46 | const provingKey = fs.readFileSync(PROVING_KEY_PATH) 47 | spinner.text = 'Get provider' 48 | const provider = getProvider() 49 | 50 | const identityName = defaultIdentityName.get() 51 | spinner.info(`Using defaultIdentityName: ${identityName}`) 52 | const identity = unSerialiseIdentity( 53 | fs.readFileSync(path.join(IDENTITIES_DIR, identityName)) 54 | ) 55 | spinner.start() 56 | spinner.text = 'Formatting intputs' 57 | const circuit = genCircuit(cirDef) 58 | const proofOfBurnInstance = proofOfBurnContract( 59 | provider, 60 | hostInfo.get().registrationAddress 61 | ) 62 | const leaves = await proofOfBurnInstance.getIdentityCommitments() 63 | 64 | if ( 65 | !leaves 66 | .map(leaf => leaf.toString()) 67 | .includes(genIdentityCommitment(identity).toString()) 68 | ) { 69 | spinner.fail( 70 | `Could not find commitment of identity ${identityName} in leaves. Please register identity first` 71 | ) 72 | } 73 | 74 | const externalNullifier = genExternalNullifier(externalNullifierStr) 75 | 76 | spinner.text = 'Generating witness' 77 | const { witness } = await genWitness( 78 | signalStr, 79 | circuit, 80 | identity, 81 | leaves, 82 | SEMAPHORE_TREE_DEPTH, 83 | externalNullifier 84 | ) 85 | spinner.text = 'Generating proof' 86 | const proof = await genProof(witness, provingKey) 87 | 88 | spinner.text = 'Generating publicSignals' 89 | const publicSignals = genPublicSignals(witness, circuit) 90 | output = { 91 | proof: JSON.stringify(stringifyBigInts(proof)), 92 | publicSignals: JSON.stringify(stringifyBigInts(publicSignals)) 93 | } 94 | spinner.succeed('Done generating authentication data') 95 | return output 96 | } 97 | 98 | const viewPostHandler = async () => { 99 | const posts = await fetch(new URL('./posts', hostInfo.get().hostUrl)) 100 | .then(res => res.json()) 101 | .then(result => result.posts) 102 | 103 | console.clear() 104 | const response = await prompts({ 105 | type: 'select', 106 | name: 'value', 107 | message: 'Read the latest posts', 108 | choices: posts.map(post => { 109 | return { 110 | title: `${post.id} ${post.postBody.slice(0, 20)}`, 111 | value: post 112 | } 113 | }) 114 | }) 115 | console.info(`Article #${response.value.id}`) 116 | console.info(response.value.postBody) 117 | } 118 | 119 | const newPostExternalNullifierGen = new EpochbasedExternalNullifier( 120 | hostInfo.get().serverName, 121 | '/posts/new', 122 | 300 * 1000 // rate limit to 30 seconds 123 | ) 124 | 125 | const newPostHandler = async argv => { 126 | console.clear() 127 | const articlePath = argv.article 128 | const article = fs 129 | .readFileSync(path.join(process.cwd(), articlePath)) 130 | .toString() 131 | 132 | const signalStr = ethers.utils.hashMessage(article) 133 | 134 | const externalNullifierStr = newPostExternalNullifierGen.toString() 135 | console.info('Using externalNullifierStr as:', externalNullifierStr) 136 | const { proof, publicSignals } = await genAuth( 137 | externalNullifierStr, 138 | signalStr 139 | ) 140 | 141 | await fetch(new URL('./posts/new', hostInfo.get().hostUrl), { 142 | method: 'POST', 143 | headers: { 144 | Accept: 'application/json', 145 | 'Content-Type': 'application/json' 146 | }, 147 | body: JSON.stringify({ postBody: article, proof, publicSignals }) 148 | }) 149 | .then(res => res.text()) 150 | .then(result => console.info(result)) 151 | .catch(error => console.error(error)) 152 | } 153 | 154 | module.exports = { newPostHandler, viewPostHandler } 155 | -------------------------------------------------------------------------------- /packages/cli/provider.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers') 2 | const { hostInfo } = require('./config') 3 | 4 | const getProvider = () => { 5 | const network = hostInfo.get().network 6 | if (network === 'localhost') { 7 | return new ethers.providers.JsonRpcProvider() 8 | } else { 9 | return new ethers.getDefaultProvider(network) 10 | } 11 | } 12 | 13 | module.exports = { getProvider } 14 | -------------------------------------------------------------------------------- /packages/cli/sendTx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 |

Send Tx here

12 |

Proof of Burn Address

13 |

{{{NETWORK}}}

14 |

{{{ADDRESS}}}

15 |

Identity Commitment

16 |

{{{COMMITMENT}}}

17 | 18 |

Registration Fee

19 |

20 |

21 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /packages/cli/setServer.js: -------------------------------------------------------------------------------- 1 | const { hostInfo } = require('./config') 2 | const fetch = require('node-fetch') 3 | 4 | const setServerHandler = async argv => { 5 | //TODO: Better validation 6 | if (!argv.hostUrl.includes('http')) throw new Error('please add http') 7 | const hostUrl = new URL(argv.hostUrl) 8 | 9 | const registrationInfo = await fetch(new URL('./info', hostUrl)).then( 10 | response => response.json() 11 | ) 12 | 13 | registrationInfo.hostUrl = hostUrl.toString() 14 | console.info('Set the default server and registration info to') 15 | console.log(registrationInfo) 16 | hostInfo.set(registrationInfo) 17 | } 18 | module.exports = { setServerHandler } 19 | -------------------------------------------------------------------------------- /packages/cli/setup.js: -------------------------------------------------------------------------------- 1 | const { CIRCUIT_PATH, PROVING_KEY_PATH } = require('./constants') 2 | const { 3 | CIRCUIT_URL, 4 | PROVING_KEY_URL 5 | } = require('semaphore-auth-contracts/constants') 6 | const fs = require('fs') 7 | 8 | const ora = require('ora') 9 | 10 | const fetch = require('node-fetch') 11 | 12 | const { defaultIdentityName } = require('./config') 13 | const { createIdentity } = require('./identities') 14 | 15 | const download = async (fromURL, toPath, spinner, name) => { 16 | return fetch(fromURL).then(res => { 17 | const dest = fs.createWriteStream(toPath) 18 | const contentLength = res.headers.get('Content-Length') 19 | let progress = 0 20 | res.body.pipe(dest) 21 | res.body.on('data', chunk => { 22 | progress += chunk.length 23 | spinner.text = `Downloading ${name} ... ${parseInt( 24 | (progress / contentLength) * 100 25 | )}%` 26 | }) 27 | return new Promise(function (resolve, reject) { 28 | res.body.on('end', resolve) 29 | res.body.on('error', reject) 30 | }) 31 | }) 32 | } 33 | 34 | const checkMaybeDownload = async (name, fromURL, toPath) => { 35 | if (fs.existsSync(toPath)) { 36 | console.info(`${name} exists, no need to download`) 37 | } else { 38 | const spinner = ora(`Downloading ${name}`).start() 39 | 40 | await download(fromURL, toPath, spinner, name) 41 | 42 | spinner.succeed(`Downloaded ${name} to ${toPath}`) 43 | } 44 | } 45 | 46 | const checkMaybeGenIdentity = () => { 47 | if (defaultIdentityName.get() === undefined) { 48 | console.info( 49 | "Oh, you don't have a default identity, let me get one for you" 50 | ) 51 | const { filename } = createIdentity() 52 | defaultIdentityName.set(filename) 53 | } 54 | } 55 | 56 | const setupHandler = async argv => { 57 | console.log('Setting up') 58 | await checkMaybeDownload('Circuit', CIRCUIT_URL, CIRCUIT_PATH) 59 | await checkMaybeDownload('Proving key', PROVING_KEY_URL, PROVING_KEY_PATH) 60 | checkMaybeGenIdentity() 61 | } 62 | 63 | module.exports = { setupHandler } 64 | -------------------------------------------------------------------------------- /packages/common/abis/ProofOfBurn.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_semaphore", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "uint256", 11 | "name": "_registration_fee", 12 | "type": "uint256" 13 | } 14 | ], 15 | "payable": false, 16 | "stateMutability": "nonpayable", 17 | "type": "constructor" 18 | }, 19 | { 20 | "anonymous": false, 21 | "inputs": [ 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "_identityCommitment", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "Registered", 30 | "type": "event" 31 | }, 32 | { 33 | "constant": true, 34 | "inputs": [], 35 | "name": "getIdentityCommitments", 36 | "outputs": [ 37 | { 38 | "internalType": "uint256[]", 39 | "name": "", 40 | "type": "uint256[]" 41 | } 42 | ], 43 | "payable": false, 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": true, 49 | "inputs": [ 50 | { 51 | "internalType": "uint256", 52 | "name": "", 53 | "type": "uint256" 54 | } 55 | ], 56 | "name": "identityCommitments", 57 | "outputs": [ 58 | { 59 | "internalType": "uint256", 60 | "name": "", 61 | "type": "uint256" 62 | } 63 | ], 64 | "payable": false, 65 | "stateMutability": "view", 66 | "type": "function" 67 | }, 68 | { 69 | "constant": false, 70 | "inputs": [ 71 | { 72 | "internalType": "uint256", 73 | "name": "_identityCommitment", 74 | "type": "uint256" 75 | } 76 | ], 77 | "name": "register", 78 | "outputs": [], 79 | "payable": true, 80 | "stateMutability": "payable", 81 | "type": "function" 82 | }, 83 | { 84 | "constant": true, 85 | "inputs": [], 86 | "name": "registration_fee", 87 | "outputs": [ 88 | { 89 | "internalType": "uint256", 90 | "name": "", 91 | "type": "uint256" 92 | } 93 | ], 94 | "payable": false, 95 | "stateMutability": "view", 96 | "type": "function" 97 | }, 98 | { 99 | "constant": true, 100 | "inputs": [], 101 | "name": "semaphore", 102 | "outputs": [ 103 | { 104 | "internalType": "contract Semaphore", 105 | "name": "", 106 | "type": "address" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | } 113 | ] -------------------------------------------------------------------------------- /packages/common/abis/Semaphore.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint8", 6 | "name": "_treeLevels", 7 | "type": "uint8" 8 | }, 9 | { 10 | "internalType": "uint232", 11 | "name": "_firstExternalNullifier", 12 | "type": "uint232" 13 | } 14 | ], 15 | "payable": false, 16 | "stateMutability": "nonpayable", 17 | "type": "constructor" 18 | }, 19 | { 20 | "anonymous": false, 21 | "inputs": [ 22 | { 23 | "indexed": true, 24 | "internalType": "uint232", 25 | "name": "externalNullifier", 26 | "type": "uint232" 27 | } 28 | ], 29 | "name": "ExternalNullifierAdd", 30 | "type": "event" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { 36 | "indexed": true, 37 | "internalType": "uint232", 38 | "name": "externalNullifier", 39 | "type": "uint232" 40 | }, 41 | { 42 | "indexed": true, 43 | "internalType": "bool", 44 | "name": "active", 45 | "type": "bool" 46 | } 47 | ], 48 | "name": "ExternalNullifierChangeStatus", 49 | "type": "event" 50 | }, 51 | { 52 | "anonymous": false, 53 | "inputs": [ 54 | { 55 | "indexed": true, 56 | "internalType": "uint256", 57 | "name": "leaf", 58 | "type": "uint256" 59 | }, 60 | { 61 | "indexed": true, 62 | "internalType": "uint256", 63 | "name": "leafIndex", 64 | "type": "uint256" 65 | } 66 | ], 67 | "name": "LeafInsertion", 68 | "type": "event" 69 | }, 70 | { 71 | "anonymous": false, 72 | "inputs": [ 73 | { 74 | "indexed": true, 75 | "internalType": "address", 76 | "name": "previousOwner", 77 | "type": "address" 78 | }, 79 | { 80 | "indexed": true, 81 | "internalType": "address", 82 | "name": "newOwner", 83 | "type": "address" 84 | } 85 | ], 86 | "name": "OwnershipTransferred", 87 | "type": "event" 88 | }, 89 | { 90 | "anonymous": false, 91 | "inputs": [ 92 | { 93 | "indexed": true, 94 | "internalType": "bool", 95 | "name": "newPermission", 96 | "type": "bool" 97 | } 98 | ], 99 | "name": "PermissionSet", 100 | "type": "event" 101 | }, 102 | { 103 | "constant": true, 104 | "inputs": [], 105 | "name": "NOTHING_UP_MY_SLEEVE_ZERO", 106 | "outputs": [ 107 | { 108 | "internalType": "uint256", 109 | "name": "", 110 | "type": "uint256" 111 | } 112 | ], 113 | "payable": false, 114 | "stateMutability": "view", 115 | "type": "function" 116 | }, 117 | { 118 | "constant": false, 119 | "inputs": [ 120 | { 121 | "internalType": "uint232", 122 | "name": "_externalNullifier", 123 | "type": "uint232" 124 | } 125 | ], 126 | "name": "addExternalNullifier", 127 | "outputs": [], 128 | "payable": false, 129 | "stateMutability": "nonpayable", 130 | "type": "function" 131 | }, 132 | { 133 | "constant": false, 134 | "inputs": [ 135 | { 136 | "internalType": "bytes", 137 | "name": "_signal", 138 | "type": "bytes" 139 | }, 140 | { 141 | "internalType": "uint256[8]", 142 | "name": "_proof", 143 | "type": "uint256[8]" 144 | }, 145 | { 146 | "internalType": "uint256", 147 | "name": "_root", 148 | "type": "uint256" 149 | }, 150 | { 151 | "internalType": "uint256", 152 | "name": "_nullifiersHash", 153 | "type": "uint256" 154 | }, 155 | { 156 | "internalType": "uint232", 157 | "name": "_externalNullifier", 158 | "type": "uint232" 159 | } 160 | ], 161 | "name": "broadcastSignal", 162 | "outputs": [], 163 | "payable": false, 164 | "stateMutability": "nonpayable", 165 | "type": "function" 166 | }, 167 | { 168 | "constant": false, 169 | "inputs": [ 170 | { 171 | "internalType": "uint232", 172 | "name": "_externalNullifier", 173 | "type": "uint232" 174 | } 175 | ], 176 | "name": "deactivateExternalNullifier", 177 | "outputs": [], 178 | "payable": false, 179 | "stateMutability": "nonpayable", 180 | "type": "function" 181 | }, 182 | { 183 | "constant": true, 184 | "inputs": [ 185 | { 186 | "internalType": "uint232", 187 | "name": "", 188 | "type": "uint232" 189 | } 190 | ], 191 | "name": "externalNullifierLinkedList", 192 | "outputs": [ 193 | { 194 | "internalType": "uint232", 195 | "name": "next", 196 | "type": "uint232" 197 | }, 198 | { 199 | "internalType": "bool", 200 | "name": "exists", 201 | "type": "bool" 202 | }, 203 | { 204 | "internalType": "bool", 205 | "name": "isActive", 206 | "type": "bool" 207 | } 208 | ], 209 | "payable": false, 210 | "stateMutability": "view", 211 | "type": "function" 212 | }, 213 | { 214 | "constant": true, 215 | "inputs": [], 216 | "name": "firstExternalNullifier", 217 | "outputs": [ 218 | { 219 | "internalType": "uint232", 220 | "name": "", 221 | "type": "uint232" 222 | } 223 | ], 224 | "payable": false, 225 | "stateMutability": "view", 226 | "type": "function" 227 | }, 228 | { 229 | "constant": true, 230 | "inputs": [ 231 | { 232 | "internalType": "uint232", 233 | "name": "_externalNullifier", 234 | "type": "uint232" 235 | } 236 | ], 237 | "name": "getNextExternalNullifier", 238 | "outputs": [ 239 | { 240 | "internalType": "uint232", 241 | "name": "", 242 | "type": "uint232" 243 | } 244 | ], 245 | "payable": false, 246 | "stateMutability": "view", 247 | "type": "function" 248 | }, 249 | { 250 | "constant": true, 251 | "inputs": [], 252 | "name": "getNumIdentityCommitments", 253 | "outputs": [ 254 | { 255 | "internalType": "uint256", 256 | "name": "", 257 | "type": "uint256" 258 | } 259 | ], 260 | "payable": false, 261 | "stateMutability": "view", 262 | "type": "function" 263 | }, 264 | { 265 | "constant": false, 266 | "inputs": [ 267 | { 268 | "internalType": "uint256", 269 | "name": "_identityCommitment", 270 | "type": "uint256" 271 | } 272 | ], 273 | "name": "insertIdentity", 274 | "outputs": [ 275 | { 276 | "internalType": "uint256", 277 | "name": "", 278 | "type": "uint256" 279 | } 280 | ], 281 | "payable": false, 282 | "stateMutability": "nonpayable", 283 | "type": "function" 284 | }, 285 | { 286 | "constant": true, 287 | "inputs": [], 288 | "name": "isBroadcastPermissioned", 289 | "outputs": [ 290 | { 291 | "internalType": "bool", 292 | "name": "", 293 | "type": "bool" 294 | } 295 | ], 296 | "payable": false, 297 | "stateMutability": "view", 298 | "type": "function" 299 | }, 300 | { 301 | "constant": true, 302 | "inputs": [ 303 | { 304 | "internalType": "uint232", 305 | "name": "_externalNullifier", 306 | "type": "uint232" 307 | } 308 | ], 309 | "name": "isExternalNullifierActive", 310 | "outputs": [ 311 | { 312 | "internalType": "bool", 313 | "name": "", 314 | "type": "bool" 315 | } 316 | ], 317 | "payable": false, 318 | "stateMutability": "view", 319 | "type": "function" 320 | }, 321 | { 322 | "constant": true, 323 | "inputs": [], 324 | "name": "isOwner", 325 | "outputs": [ 326 | { 327 | "internalType": "bool", 328 | "name": "", 329 | "type": "bool" 330 | } 331 | ], 332 | "payable": false, 333 | "stateMutability": "view", 334 | "type": "function" 335 | }, 336 | { 337 | "constant": true, 338 | "inputs": [], 339 | "name": "lastExternalNullifier", 340 | "outputs": [ 341 | { 342 | "internalType": "uint232", 343 | "name": "", 344 | "type": "uint232" 345 | } 346 | ], 347 | "payable": false, 348 | "stateMutability": "view", 349 | "type": "function" 350 | }, 351 | { 352 | "constant": true, 353 | "inputs": [ 354 | { 355 | "internalType": "uint256", 356 | "name": "", 357 | "type": "uint256" 358 | } 359 | ], 360 | "name": "nullifierHashHistory", 361 | "outputs": [ 362 | { 363 | "internalType": "bool", 364 | "name": "", 365 | "type": "bool" 366 | } 367 | ], 368 | "payable": false, 369 | "stateMutability": "view", 370 | "type": "function" 371 | }, 372 | { 373 | "constant": true, 374 | "inputs": [], 375 | "name": "numExternalNullifiers", 376 | "outputs": [ 377 | { 378 | "internalType": "uint256", 379 | "name": "", 380 | "type": "uint256" 381 | } 382 | ], 383 | "payable": false, 384 | "stateMutability": "view", 385 | "type": "function" 386 | }, 387 | { 388 | "constant": true, 389 | "inputs": [], 390 | "name": "owner", 391 | "outputs": [ 392 | { 393 | "internalType": "address", 394 | "name": "", 395 | "type": "address" 396 | } 397 | ], 398 | "payable": false, 399 | "stateMutability": "view", 400 | "type": "function" 401 | }, 402 | { 403 | "constant": true, 404 | "inputs": [ 405 | { 406 | "internalType": "uint256[2]", 407 | "name": "_a", 408 | "type": "uint256[2]" 409 | }, 410 | { 411 | "internalType": "uint256[2][2]", 412 | "name": "_b", 413 | "type": "uint256[2][2]" 414 | }, 415 | { 416 | "internalType": "uint256[2]", 417 | "name": "_c", 418 | "type": "uint256[2]" 419 | } 420 | ], 421 | "name": "packProof", 422 | "outputs": [ 423 | { 424 | "internalType": "uint256[8]", 425 | "name": "", 426 | "type": "uint256[8]" 427 | } 428 | ], 429 | "payable": false, 430 | "stateMutability": "pure", 431 | "type": "function" 432 | }, 433 | { 434 | "constant": true, 435 | "inputs": [ 436 | { 437 | "internalType": "bytes", 438 | "name": "_signal", 439 | "type": "bytes" 440 | }, 441 | { 442 | "internalType": "uint256[8]", 443 | "name": "_proof", 444 | "type": "uint256[8]" 445 | }, 446 | { 447 | "internalType": "uint256", 448 | "name": "_root", 449 | "type": "uint256" 450 | }, 451 | { 452 | "internalType": "uint256", 453 | "name": "_nullifiersHash", 454 | "type": "uint256" 455 | }, 456 | { 457 | "internalType": "uint256", 458 | "name": "_signalHash", 459 | "type": "uint256" 460 | }, 461 | { 462 | "internalType": "uint232", 463 | "name": "_externalNullifier", 464 | "type": "uint232" 465 | } 466 | ], 467 | "name": "preBroadcastCheck", 468 | "outputs": [ 469 | { 470 | "internalType": "bool", 471 | "name": "", 472 | "type": "bool" 473 | } 474 | ], 475 | "payable": false, 476 | "stateMutability": "view", 477 | "type": "function" 478 | }, 479 | { 480 | "constant": false, 481 | "inputs": [ 482 | { 483 | "internalType": "uint232", 484 | "name": "_externalNullifier", 485 | "type": "uint232" 486 | } 487 | ], 488 | "name": "reactivateExternalNullifier", 489 | "outputs": [], 490 | "payable": false, 491 | "stateMutability": "nonpayable", 492 | "type": "function" 493 | }, 494 | { 495 | "constant": false, 496 | "inputs": [], 497 | "name": "renounceOwnership", 498 | "outputs": [], 499 | "payable": false, 500 | "stateMutability": "nonpayable", 501 | "type": "function" 502 | }, 503 | { 504 | "constant": true, 505 | "inputs": [], 506 | "name": "root", 507 | "outputs": [ 508 | { 509 | "internalType": "uint256", 510 | "name": "", 511 | "type": "uint256" 512 | } 513 | ], 514 | "payable": false, 515 | "stateMutability": "view", 516 | "type": "function" 517 | }, 518 | { 519 | "constant": true, 520 | "inputs": [ 521 | { 522 | "internalType": "uint256", 523 | "name": "", 524 | "type": "uint256" 525 | } 526 | ], 527 | "name": "rootHistory", 528 | "outputs": [ 529 | { 530 | "internalType": "bool", 531 | "name": "", 532 | "type": "bool" 533 | } 534 | ], 535 | "payable": false, 536 | "stateMutability": "view", 537 | "type": "function" 538 | }, 539 | { 540 | "constant": false, 541 | "inputs": [ 542 | { 543 | "internalType": "bool", 544 | "name": "_newPermission", 545 | "type": "bool" 546 | } 547 | ], 548 | "name": "setPermissioning", 549 | "outputs": [], 550 | "payable": false, 551 | "stateMutability": "nonpayable", 552 | "type": "function" 553 | }, 554 | { 555 | "constant": false, 556 | "inputs": [ 557 | { 558 | "internalType": "address", 559 | "name": "newOwner", 560 | "type": "address" 561 | } 562 | ], 563 | "name": "transferOwnership", 564 | "outputs": [], 565 | "payable": false, 566 | "stateMutability": "nonpayable", 567 | "type": "function" 568 | }, 569 | { 570 | "constant": true, 571 | "inputs": [ 572 | { 573 | "internalType": "uint256[8]", 574 | "name": "_proof", 575 | "type": "uint256[8]" 576 | } 577 | ], 578 | "name": "unpackProof", 579 | "outputs": [ 580 | { 581 | "internalType": "uint256[2]", 582 | "name": "", 583 | "type": "uint256[2]" 584 | }, 585 | { 586 | "internalType": "uint256[2][2]", 587 | "name": "", 588 | "type": "uint256[2][2]" 589 | }, 590 | { 591 | "internalType": "uint256[2]", 592 | "name": "", 593 | "type": "uint256[2]" 594 | } 595 | ], 596 | "payable": false, 597 | "stateMutability": "pure", 598 | "type": "function" 599 | }, 600 | { 601 | "constant": true, 602 | "inputs": [ 603 | { 604 | "internalType": "uint256[2]", 605 | "name": "a", 606 | "type": "uint256[2]" 607 | }, 608 | { 609 | "internalType": "uint256[2][2]", 610 | "name": "b", 611 | "type": "uint256[2][2]" 612 | }, 613 | { 614 | "internalType": "uint256[2]", 615 | "name": "c", 616 | "type": "uint256[2]" 617 | }, 618 | { 619 | "internalType": "uint256[4]", 620 | "name": "input", 621 | "type": "uint256[4]" 622 | } 623 | ], 624 | "name": "verifyProof", 625 | "outputs": [ 626 | { 627 | "internalType": "bool", 628 | "name": "r", 629 | "type": "bool" 630 | } 631 | ], 632 | "payable": false, 633 | "stateMutability": "view", 634 | "type": "function" 635 | } 636 | ] -------------------------------------------------------------------------------- /packages/common/assets/verification_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "groth", 3 | "nPublic": 4, 4 | "IC": [ 5 | [ 6 | "15132217740731663181077706894312753465210500809816534743630331656993829080728", 7 | "110196461348215931979632312103651461241391911014889357659299988542624772231", 8 | "1" 9 | ], 10 | [ 11 | "10128725078198782996699361178651605009720713215878609701039758932704577595075", 12 | "6404467707897071196443816328081887791672216217394045289711692279719912978002", 13 | "1" 14 | ], 15 | [ 16 | "10522674726928807143308489533204590506138344033395366996236503798983177262637", 17 | "4729387380831061502587298076566203099165040519549972799644442785786542168149", 18 | "1" 19 | ], 20 | [ 21 | "17764548214378097040054627843654571919160111811296549132642271073668911279441", 22 | "18047600752595374913303954050610046390396466710244400595444056973195374265781", 23 | "1" 24 | ], 25 | [ 26 | "7483741608009854810379599415076882430339556147495509532415486196559753628073", 27 | "15133497018284592127224880003457371607602553122563204208188040273053737050972", 28 | "1" 29 | ] 30 | ], 31 | "vk_alfa_1": [ 32 | "19412864166662840704199897530865176132379551878490098268546775855212807511623", 33 | "1659653118858063231936623514142744009632862245288902525823221677276210935161", 34 | "1" 35 | ], 36 | "vk_beta_2": [ 37 | [ 38 | "10167148937945084930725321596284210462590725859059519349238552658864205847292", 39 | "21642731927166283717597040076624334182856179384944638206172042877912623746964" 40 | ], 41 | [ 42 | "21818148805990469610055209831928752212083869699635755409679256612234566494214", 43 | "1224040963938177736215304045934094846196618140310043882848453181887293487598" 44 | ], 45 | [ 46 | "1", 47 | "0" 48 | ] 49 | ], 50 | "vk_gamma_2": [ 51 | [ 52 | "21338791141815158629032141160990160038215366570564838558882493662897305673845", 53 | "7084583264208126476050882701870582484117653569260992588151213710616665494315" 54 | ], 55 | [ 56 | "21694931277671378411802527161940275869759588185961914055258162012593400433477", 57 | "20409386432685531109985133572396335050408469502427908880430269312598850603009" 58 | ], 59 | [ 60 | "1", 61 | "0" 62 | ] 63 | ], 64 | "vk_delta_2": [ 65 | [ 66 | "10800304862034523735970868610105048512813625407055208260881866244104890739413", 67 | "15974226699330331350608610622797702188540365463638301508490182464179306746479" 68 | ], 69 | [ 70 | "20585865237865669840907249885451244426128970908885747090346049467936130099745", 71 | "3115757193545898321493679843214264410358333980282409841160781532582592563749" 72 | ], 73 | [ 74 | "1", 75 | "0" 76 | ] 77 | ], 78 | "vk_alfabeta_12": [ 79 | [ 80 | [ 81 | "1361573844147737749131565440067733247609864507421329053161693152672989950157", 82 | "8539212788038501693076827717337846589705920228330012089826046829756535574136" 83 | ], 84 | [ 85 | "10560960297078733991225877550434370712491501011250168745295036607023825386700", 86 | "19277957631632237569937848859475027938510889542920433239785268961737424015653" 87 | ], 88 | [ 89 | "8220061396350308332298933259651398557760894947457192645672177680949856464681", 90 | "4493013245151619010285850543308216736214590722643988052170732346007116316625" 91 | ] 92 | ], 93 | [ 94 | [ 95 | "7618928254123256026666147502024744385425906313190344473967645796774435866383", 96 | "19546005947403380629362797441625416112813435674848560098968657192780148094040" 97 | ], 98 | [ 99 | "13346613401912119585814374939890764399459400412022353959046434825715946421580", 100 | "1410409815388031481427723257072901950327339743543105546734776808277245463696" 101 | ], 102 | [ 103 | "14489111466978845296964223146808154108009686052579498835768101311992964662236", 104 | "1166016637763461841723020754110264827508996931956743463921425946504502764252" 105 | ] 106 | ] 107 | ] 108 | } -------------------------------------------------------------------------------- /packages/common/index.js: -------------------------------------------------------------------------------- 1 | const { EpochbasedExternalNullifier } = require('./src/externalNullifier') 2 | 3 | const verificationKey = require('./assets/verification_key.json') 4 | const SemaphoreABI = require('./abis/Semaphore.json') 5 | const ProofOfBurnABI = require('./abis/ProofOfBurn.json') 6 | 7 | const SEMAPHORE_TREE_DEPTH = 20 8 | 9 | module.exports = { 10 | EpochbasedExternalNullifier, 11 | verificationKey, 12 | SemaphoreABI, 13 | ProofOfBurnABI, 14 | SEMAPHORE_TREE_DEPTH 15 | } 16 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/common", 3 | "version": "0.0.3", 4 | "description": "Common utils for frontend and backend", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/src/externalNullifier.js: -------------------------------------------------------------------------------- 1 | class EpochbasedExternalNullifier { 2 | constructor (prefix, epochLength) { 3 | this.prefix = prefix 4 | this.epochLength = epochLength // In milliseconds 5 | } 6 | 7 | epochStart () { 8 | const now = new Date().valueOf() 9 | return now - (now % this.epochLength) 10 | } 11 | 12 | timeBeforeNextEpoch () { 13 | const now = new Date().valueOf() 14 | return this.epochLength - (now % this.epochLength) 15 | } 16 | 17 | getString () { 18 | return `${this.prefix}:${this.epochStart()}` 19 | } 20 | } 21 | 22 | module.exports = { 23 | EpochbasedExternalNullifier 24 | } 25 | -------------------------------------------------------------------------------- /packages/config/export-config.js: -------------------------------------------------------------------------------- 1 | const config = require('./index') 2 | 3 | console.log(JSON.stringify(config)) 4 | -------------------------------------------------------------------------------- /packages/config/goerli.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 'goerli' 3 | 4 | frontend: 5 | supportedNetworkName: 'goerli' 6 | supportedNetwork: 5 7 | 8 | backend: 9 | serverName: 'hojicha' 10 | network: 'goerli' 11 | db: 12 | client: 'pg' 13 | connection: 14 | host: 'hojicha-db' 15 | port: 5432 16 | user: 'postgres' 17 | database: 'hojicha' 18 | 19 | chain: 20 | url: 'https://goerli.infura.io/v3/be5a8f1b309e4831bbc4205d70d0e285' 21 | contracts: 22 | proofOfBurn: '0xa679454e43c84Cb449aa3470F58D2Ab333D0F09a' 23 | semaphore: '0x04a5a68a801A3a9577Cac8712D1D96e76C4E4A20' 24 | 25 | snarks: 26 | circuitUrl: 'https://dl.dropboxusercontent.com/s/3gzxjibqgb6ke13/circuit.json?dl=1' 27 | provingKeyUrl: 'https://dl.dropboxusercontent.com/s/qjlu6v125g7jkcq/proving_key.bin?dl=1' 28 | -------------------------------------------------------------------------------- /packages/config/index.js: -------------------------------------------------------------------------------- 1 | if (!process.env.hasOwnProperty('NODE_CONFIG_DIR')) { 2 | process.env.NODE_CONFIG_DIR = __dirname 3 | } 4 | 5 | if (!process.env.hasOwnProperty('NODE_ENV')) { 6 | process.env.NODE_ENV = 'local-dev' 7 | } 8 | 9 | const config = require('config') 10 | 11 | module.exports = config 12 | -------------------------------------------------------------------------------- /packages/config/local-dev.yaml.example: -------------------------------------------------------------------------------- 1 | --- 2 | env: 'local' 3 | 4 | frontend: 5 | supportedNetworkName: 'local' 6 | supportedNetworks: 1337 # Don't change 1337 for local environment. Not configurable in ganache-cli 7 | 8 | backend: 9 | serverName: 'hojicha' 10 | network: 'localhost' 11 | db: 12 | client: 'sqlite3' 13 | connection: 14 | filename: 'mydb.sqlite' 15 | 16 | chain: 17 | url: 'http://localhost:8545' 18 | contracts: 19 | proofOfBurn: '0x345cA3e014Aaf5dcA488057592ee47305D9B3e10' 20 | semaphore: '0xF12b5dd4EAD5F743C6BaA640B0216200e89B60Da' 21 | 22 | snarks: 23 | circuitUrl: 'http://localhost:3000/circuit' 24 | provingKeyUrl: 'http://localhost:3000/provingKey' 25 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/config", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "config": "^3.3.0", 11 | "js-yaml": "^3.13.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/config/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 'test' 3 | 4 | frontend: 5 | supportedNetworkName: 'local' 6 | supportedNetwork: 4 7 | 8 | backend: 9 | serverName: 'hojicha' 10 | network: 'localhost' 11 | db: 12 | client: 'sqlite3' 13 | connection: 14 | filename: 'mydb.sqlite' 15 | 16 | chain: 17 | contracts: 18 | # leave addresses blank, we deploy fresh contracts in tests 19 | proofOfBurn: 20 | semaphore: 21 | -------------------------------------------------------------------------------- /packages/contracts/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /packages/contracts/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | node_modules/ 4 | mydb.sqlite -------------------------------------------------------------------------------- /packages/contracts/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | REGISTRATION_FEE: 0.009, // in ether 5 | MIMC_SEED: 'mimcsponge', 6 | 7 | // The circuits, proving key, verification key, and semaphore tree depth are interdependent 8 | SEMAPHORE_TREE_DEPTH: 20, // This number depends on the circuit we use. 9 | CIRCUIT_URL: 10 | 'https://dl.dropboxusercontent.com/s/3gzxjibqgb6ke13/circuit.json?dl=1', 11 | CIRCUIT_CACHE_PATH: path.join(__dirname, './cache/circuit.json'), 12 | PROVING_KEY_URL: 13 | 'https://dl.dropboxusercontent.com/s/qjlu6v125g7jkcq/proving_key.bin?dl=1', 14 | PROVING_KEY_CACHE_PATH: path.join(__dirname, './cache/proving_key.bin'), 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/contracts/ProofOfBurn.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.15; 2 | 3 | import {Semaphore} from "./semaphore/Semaphore.sol"; 4 | 5 | contract ProofOfBurn { 6 | Semaphore public semaphore; 7 | uint256 public registration_fee; 8 | uint256[] public identityCommitments; 9 | 10 | event Registered(uint256 _identityCommitment); 11 | 12 | constructor(address _semaphore, uint256 _registration_fee) public { 13 | semaphore = Semaphore(_semaphore); 14 | registration_fee = _registration_fee; 15 | } 16 | 17 | function register(uint256 _identityCommitment) public payable { 18 | require( 19 | msg.value == registration_fee, 20 | "Not sending the right amount of ether" 21 | ); 22 | 23 | semaphore.insertIdentity(_identityCommitment); 24 | identityCommitments.push(_identityCommitment); 25 | 26 | emit Registered(_identityCommitment); 27 | } 28 | 29 | function getIdentityCommitments() public view returns (uint256[] memory) { 30 | return identityCommitments; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/IncrementalMerkleTree.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Semaphore - Zero-knowledge signaling on Ethereum 3 | * Copyright (C) 2020 Barry WhiteHat , Kobi 4 | * Gurkan and Koh Wei Jie (contact@kohweijie.com) 5 | * 6 | * This file is part of Semaphore. 7 | * 8 | * Semaphore is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * Semaphore is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with Semaphore. If not, see . 20 | */ 21 | 22 | pragma solidity ^0.5.0; 23 | 24 | import { SnarkConstants } from "./SnarkConstants.sol"; 25 | import { MiMC } from "./MiMC.sol"; 26 | 27 | contract IncrementalMerkleTree is SnarkConstants { 28 | // The maximum tree depth 29 | uint8 internal constant MAX_DEPTH = 32; 30 | 31 | // The tree depth 32 | uint8 internal treeLevels; 33 | 34 | // The number of inserted leaves 35 | uint256 internal nextLeafIndex = 0; 36 | 37 | // The Merkle root 38 | uint256 public root; 39 | 40 | // The Merkle path to the leftmost leaf upon initialisation. It *should 41 | // not* be modified after it has been set by the `initMerkleTree` function. 42 | // Caching these values is essential to efficient appends. 43 | uint256[MAX_DEPTH] internal zeros; 44 | 45 | // Allows you to compute the path to the element (but it's not the path to 46 | // the elements). Caching these values is essential to efficient appends. 47 | uint256[MAX_DEPTH] internal filledSubtrees; 48 | 49 | // Whether the contract has already seen a particular Merkle tree root 50 | mapping (uint256 => bool) public rootHistory; 51 | 52 | event LeafInsertion(uint256 indexed leaf, uint256 indexed leafIndex); 53 | 54 | /* 55 | * Stores the Merkle root and intermediate values (the Merkle path to the 56 | * the first leaf) assuming that all leaves are set to _zeroValue. 57 | * @param _treeLevels The number of levels of the tree 58 | * @param _zeroValue The value to set for every leaf. Ideally, this should 59 | * be a nothing-up-my-sleeve value, so that nobody can 60 | * say that the deployer knows the preimage of an empty 61 | * leaf. 62 | */ 63 | constructor(uint8 _treeLevels, uint256 _zeroValue) internal { 64 | // Limit the Merkle tree to MAX_DEPTH levels 65 | require( 66 | _treeLevels > 0 && _treeLevels <= MAX_DEPTH, 67 | "IncrementalMerkleTree: _treeLevels must be between 0 and 33" 68 | ); 69 | 70 | /* 71 | To initialise the Merkle tree, we need to calculate the Merkle root 72 | assuming that each leaf is the zero value. 73 | 74 | H(H(a,b), H(c,d)) 75 | / \ 76 | H(a,b) H(c,d) 77 | / \ / \ 78 | a b c d 79 | 80 | `zeros` and `filledSubtrees` will come in handy later when we do 81 | inserts or updates. e.g when we insert a value in index 1, we will 82 | need to look up values from those arrays to recalculate the Merkle 83 | root. 84 | */ 85 | treeLevels = _treeLevels; 86 | 87 | zeros[0] = _zeroValue; 88 | 89 | uint256 currentZero = _zeroValue; 90 | for (uint8 i = 1; i < _treeLevels; i++) { 91 | uint256 hashed = hashLeftRight(currentZero, currentZero); 92 | zeros[i] = hashed; 93 | filledSubtrees[i] = hashed; 94 | currentZero = hashed; 95 | } 96 | 97 | root = hashLeftRight(currentZero, currentZero); 98 | } 99 | 100 | /* 101 | * Inserts a leaf into the Merkle tree and updates the root and filled 102 | * subtrees. 103 | * @param _leaf The value to insert. It must be less than the snark scalar 104 | * field or this function will throw. 105 | * @return The leaf index. 106 | */ 107 | function insertLeaf(uint256 _leaf) internal returns (uint256) { 108 | require( 109 | _leaf < SNARK_SCALAR_FIELD, 110 | "IncrementalMerkleTree: insertLeaf argument must be < SNARK_SCALAR_FIELD" 111 | ); 112 | 113 | uint256 currentIndex = nextLeafIndex; 114 | 115 | uint256 depth = uint256(treeLevels); 116 | require(currentIndex < uint256(2) ** depth, "IncrementalMerkleTree: tree is full"); 117 | 118 | uint256 currentLevelHash = _leaf; 119 | uint256 left; 120 | uint256 right; 121 | 122 | for (uint8 i = 0; i < treeLevels; i++) { 123 | // if current_index is 5, for instance, over the iterations it will 124 | // look like this: 5, 2, 1, 0, 0, 0 ... 125 | 126 | if (currentIndex % 2 == 0) { 127 | // For later values of `i`, use the previous hash as `left`, and 128 | // the (hashed) zero value for `right` 129 | left = currentLevelHash; 130 | right = zeros[i]; 131 | 132 | filledSubtrees[i] = currentLevelHash; 133 | } else { 134 | left = filledSubtrees[i]; 135 | right = currentLevelHash; 136 | } 137 | 138 | currentLevelHash = hashLeftRight(left, right); 139 | 140 | // equivalent to currentIndex /= 2; 141 | currentIndex >>= 1; 142 | } 143 | 144 | root = currentLevelHash; 145 | rootHistory[root] = true; 146 | 147 | uint256 n = nextLeafIndex; 148 | nextLeafIndex += 1; 149 | 150 | emit LeafInsertion(_leaf, n); 151 | 152 | return currentIndex; 153 | } 154 | 155 | /* 156 | * Concatenates and hashes two `uint256` values (left and right) using 157 | * a combination of MiMCSponge and `addmod`. 158 | * @param _left The first value 159 | * @param _right The second value 160 | * @return The uint256 hash of _left and _right 161 | */ 162 | function hashLeftRight(uint256 _left, uint256 _right) internal pure returns (uint256) { 163 | 164 | // Solidity documentation states: 165 | // `addmod(uint x, uint y, uint k) returns (uint)`: 166 | // compute (x + y) % k where the addition is performed with arbitrary 167 | // precision and does not wrap around at 2**256. Assert that k != 0 168 | // starting from version 0.5.0. 169 | 170 | uint256 R = _left; 171 | uint256 C = 0; 172 | 173 | (R, C) = MiMC.MiMCSponge(R, 0); 174 | 175 | R = addmod(R, _right, SNARK_SCALAR_FIELD); 176 | (R, C) = MiMC.MiMCSponge(R, C); 177 | 178 | return R; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/MiMC.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Semaphore - Zero-knowledge signaling on Ethereum 3 | * Copyright (C) 2020 Barry WhiteHat , Kobi 4 | * Gurkan and Koh Wei Jie (contact@kohweijie.com) 5 | * 6 | * This file is part of Semaphore. 7 | * 8 | * Semaphore is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * Semaphore is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with Semaphore. If not, see . 20 | */ 21 | 22 | pragma solidity ^0.5.0; 23 | 24 | library MiMC { 25 | // Note that this function could also be called "MiMCFeistel", but we name 26 | // it "MiMCSponge" for consistency. 27 | function MiMCSponge(uint256 in_xL, uint256 in_xR) pure public 28 | returns (uint256 xL, uint256 xR); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | /** 4 | * @dev Contract module which provides a basic access control mechanism, where 5 | * there is an account (an owner) that can be granted exclusive access to 6 | * specific functions. 7 | * 8 | * This module is used through inheritance. It will make available the modifier 9 | * `onlyOwner`, which can be aplied to your functions to restrict their use to 10 | * the owner. 11 | */ 12 | contract Ownable { 13 | address private _owner; 14 | 15 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 16 | 17 | /** 18 | * @dev Initializes the contract setting the deployer as the initial owner. 19 | */ 20 | constructor () internal { 21 | _owner = msg.sender; 22 | emit OwnershipTransferred(address(0), _owner); 23 | } 24 | 25 | /** 26 | * @dev Returns the address of the current owner. 27 | */ 28 | function owner() public view returns (address) { 29 | return _owner; 30 | } 31 | 32 | /** 33 | * @dev Throws if called by any account other than the owner. 34 | */ 35 | modifier onlyOwner() { 36 | require(isOwner(), "Ownable: caller is not the owner"); 37 | _; 38 | } 39 | 40 | /** 41 | * @dev Returns true if the caller is the current owner. 42 | */ 43 | function isOwner() public view returns (bool) { 44 | return msg.sender == _owner; 45 | } 46 | 47 | /** 48 | * @dev Leaves the contract without owner. It will not be possible to call 49 | * `onlyOwner` functions anymore. Can only be called by the current owner. 50 | * 51 | * > Note: Renouncing ownership will leave the contract without an owner, 52 | * thereby removing any functionality that is only available to the owner. 53 | */ 54 | function renounceOwnership() public onlyOwner { 55 | emit OwnershipTransferred(_owner, address(0)); 56 | _owner = address(0); 57 | } 58 | 59 | /** 60 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 61 | * Can only be called by the current owner. 62 | */ 63 | function transferOwnership(address newOwner) public onlyOwner { 64 | _transferOwnership(newOwner); 65 | } 66 | 67 | /** 68 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 69 | */ 70 | function _transferOwnership(address newOwner) internal { 71 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 72 | emit OwnershipTransferred(_owner, newOwner); 73 | _owner = newOwner; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/Semaphore.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Semaphore - Zero-knowledge signaling on Ethereum 3 | * Copyright (C) 2020 Barry WhiteHat , Kobi 4 | * Gurkan and Koh Wei Jie (contact@kohweijie.com) 5 | * 6 | * This file is part of Semaphore. 7 | * 8 | * Semaphore is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * Semaphore is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with Semaphore. If not, see . 20 | */ 21 | 22 | pragma solidity ^0.5.0; 23 | 24 | import "./verifier.sol"; 25 | import { IncrementalMerkleTree } from "./IncrementalMerkleTree.sol"; 26 | import "./Ownable.sol"; 27 | 28 | contract Semaphore is Verifier, IncrementalMerkleTree, Ownable { 29 | // The external nullifier helps to prevent double-signalling by the same 30 | // user. An external nullifier can be active or deactivated. 31 | 32 | // Each node in the linked list 33 | struct ExternalNullifierNode { 34 | uint232 next; 35 | bool exists; 36 | bool isActive; 37 | } 38 | 39 | // We store the external nullifiers using a mapping of the form: 40 | // enA => { next external nullifier; if enA exists; if enA is active } 41 | // Think of it as a linked list. 42 | mapping (uint232 => ExternalNullifierNode) public 43 | externalNullifierLinkedList; 44 | 45 | uint256 public numExternalNullifiers = 0; 46 | 47 | // First and last external nullifiers for linked list enumeration 48 | uint232 public firstExternalNullifier = 0; 49 | uint232 public lastExternalNullifier = 0; 50 | 51 | // Whether broadcastSignal() can only be called by the owner of this 52 | // contract. This is the case as a safe default. 53 | bool public isBroadcastPermissioned = true; 54 | 55 | // Whether the contract has already seen a particular nullifier hash 56 | mapping (uint256 => bool) public nullifierHashHistory; 57 | 58 | event PermissionSet(bool indexed newPermission); 59 | event ExternalNullifierAdd(uint232 indexed externalNullifier); 60 | event ExternalNullifierChangeStatus( 61 | uint232 indexed externalNullifier, 62 | bool indexed active 63 | ); 64 | 65 | // This value should be equal to 66 | // 0x7d10c03d1f7884c85edee6353bd2b2ffbae9221236edde3778eac58089912bc0 67 | // which you can calculate using the following ethersjs code: 68 | // ethers.utils.solidityKeccak256(['bytes'], [ethers.utils.toUtf8Bytes('Semaphore')]) 69 | // By setting the value of unset (empty) tree leaves to this 70 | // nothing-up-my-sleeve value, the authors hope to demonstrate that they do 71 | // not have its preimage and therefore cannot spend funds they do not own. 72 | 73 | uint256 public NOTHING_UP_MY_SLEEVE_ZERO = 74 | uint256(keccak256(abi.encodePacked('Semaphore'))) % SNARK_SCALAR_FIELD; 75 | 76 | /* 77 | * If broadcastSignal is permissioned, check if msg.sender is the contract 78 | * owner 79 | */ 80 | modifier onlyOwnerIfPermissioned() { 81 | require( 82 | !isBroadcastPermissioned || isOwner(), 83 | "Semaphore: broadcast permission denied" 84 | ); 85 | 86 | _; 87 | } 88 | 89 | /* 90 | * @param _treeLevels The depth of the identity tree. 91 | * @param _firstExternalNullifier The first identity nullifier to add. 92 | */ 93 | constructor(uint8 _treeLevels, uint232 _firstExternalNullifier) 94 | IncrementalMerkleTree(_treeLevels, NOTHING_UP_MY_SLEEVE_ZERO) 95 | Ownable() 96 | public { 97 | addEn(_firstExternalNullifier, true); 98 | } 99 | 100 | /* 101 | * Registers a new user. 102 | * @param _identity_commitment The user's identity commitment, which is the 103 | * hash of their public key and their identity 104 | * nullifier (a random 31-byte value). It should 105 | * be the output of a Pedersen hash. It is the 106 | * responsibility of the caller to verify this. 107 | */ 108 | function insertIdentity(uint256 _identityCommitment) public onlyOwner 109 | returns (uint256) { 110 | // Ensure that the given identity commitment is not the zero value 111 | require( 112 | _identityCommitment != NOTHING_UP_MY_SLEEVE_ZERO, 113 | "Semaphore: identity commitment cannot be the nothing-up-my-sleeve-value" 114 | ); 115 | 116 | return insertLeaf(_identityCommitment); 117 | } 118 | 119 | /* 120 | * Checks if all values within pi_a, pi_b, and pi_c of a zk-SNARK are less 121 | * than the scalar field. 122 | * @param _a The corresponding `a` parameter to verifier.sol's 123 | * verifyProof() 124 | * @param _b The corresponding `b` parameter to verifier.sol's 125 | * verifyProof() 126 | * @param _c The corresponding `c` parameter to verifier.sol's 127 | verifyProof() 128 | */ 129 | function areAllValidFieldElements( 130 | uint256[8] memory _proof 131 | ) internal pure returns (bool) { 132 | return 133 | _proof[0] < SNARK_SCALAR_FIELD && 134 | _proof[1] < SNARK_SCALAR_FIELD && 135 | _proof[2] < SNARK_SCALAR_FIELD && 136 | _proof[3] < SNARK_SCALAR_FIELD && 137 | _proof[4] < SNARK_SCALAR_FIELD && 138 | _proof[5] < SNARK_SCALAR_FIELD && 139 | _proof[6] < SNARK_SCALAR_FIELD && 140 | _proof[7] < SNARK_SCALAR_FIELD; 141 | } 142 | 143 | /* 144 | * Produces a keccak256 hash of the given signal, shifted right by 8 bits. 145 | * @param _signal The signal to hash 146 | */ 147 | function hashSignal(bytes memory _signal) internal pure returns (uint256) { 148 | return uint256(keccak256(_signal)) >> 8; 149 | } 150 | 151 | /* 152 | * A convenience function which returns a uint256 array of 8 elements which 153 | * comprise a Groth16 zk-SNARK proof's pi_a, pi_b, and pi_c values. 154 | * @param _a The corresponding `a` parameter to verifier.sol's 155 | * verifyProof() 156 | * @param _b The corresponding `b` parameter to verifier.sol's 157 | * verifyProof() 158 | * @param _c The corresponding `c` parameter to verifier.sol's 159 | * verifyProof() 160 | */ 161 | function packProof ( 162 | uint256[2] memory _a, 163 | uint256[2][2] memory _b, 164 | uint256[2] memory _c 165 | ) public pure returns (uint256[8] memory) { 166 | 167 | return [ 168 | _a[0], 169 | _a[1], 170 | _b[0][0], 171 | _b[0][1], 172 | _b[1][0], 173 | _b[1][1], 174 | _c[0], 175 | _c[1] 176 | ]; 177 | } 178 | 179 | /* 180 | * A convenience function which converts an array of 8 elements, generated 181 | * by packProof(), into a format which verifier.sol's verifyProof() 182 | * accepts. 183 | * @param _proof The proof elements. 184 | */ 185 | function unpackProof( 186 | uint256[8] memory _proof 187 | ) public pure returns ( 188 | uint256[2] memory, 189 | uint256[2][2] memory, 190 | uint256[2] memory 191 | ) { 192 | 193 | return ( 194 | [_proof[0], _proof[1]], 195 | [ 196 | [_proof[2], _proof[3]], 197 | [_proof[4], _proof[5]] 198 | ], 199 | [_proof[6], _proof[7]] 200 | ); 201 | } 202 | 203 | 204 | /* 205 | * A convenience view function which helps operators to easily verify all 206 | * inputs to broadcastSignal() using a single contract call. This helps 207 | * them to save gas by detecting invalid inputs before they invoke 208 | * broadcastSignal(). Note that this function does the same checks as 209 | * `isValidSignalAndProof` but returns a bool instead of using require() 210 | * statements. 211 | * @param _signal The signal to broadcast 212 | * @param _proof The proof elements. 213 | * @param _root The Merkle tree root 214 | * @param _nullifiersHash The nullifiers hash 215 | * @param _signalHash The signal hash. This is included so as to verify in 216 | * Solidity that the signal hash computed off-chain 217 | * matches. 218 | * @param _externalNullifier The external nullifier 219 | */ 220 | function preBroadcastCheck ( 221 | bytes memory _signal, 222 | uint256[8] memory _proof, 223 | uint256 _root, 224 | uint256 _nullifiersHash, 225 | uint256 _signalHash, 226 | uint232 _externalNullifier 227 | ) public view returns (bool) { 228 | 229 | uint256[4] memory publicSignals = 230 | [_root, _nullifiersHash, _signalHash, _externalNullifier]; 231 | 232 | (uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c) = 233 | unpackProof(_proof); 234 | 235 | return nullifierHashHistory[_nullifiersHash] == false && 236 | hashSignal(_signal) == _signalHash && 237 | _signalHash == hashSignal(_signal) && 238 | isExternalNullifierActive(_externalNullifier) && 239 | rootHistory[_root] && 240 | areAllValidFieldElements(_proof) && 241 | _root < SNARK_SCALAR_FIELD && 242 | _nullifiersHash < SNARK_SCALAR_FIELD && 243 | verifyProof(a, b, c, publicSignals); 244 | } 245 | 246 | /* 247 | * A modifier which ensures that the signal and proof are valid. 248 | * @param _signal The signal to broadcast 249 | * @param _proof The proof elements. 250 | * @param _root The Merkle tree root 251 | * @param _nullifiersHash The nullifiers hash 252 | * @param _signalHash The signal hash 253 | * @param _externalNullifier The external nullifier 254 | */ 255 | modifier isValidSignalAndProof ( 256 | bytes memory _signal, 257 | uint256[8] memory _proof, 258 | uint256 _root, 259 | uint256 _nullifiersHash, 260 | uint232 _externalNullifier 261 | ) { 262 | // Check whether each element in _proof is a valid field element. Even 263 | // if verifier.sol does this check too, it is good to do so here for 264 | // the sake of good protocol design. 265 | require( 266 | areAllValidFieldElements(_proof), 267 | "Semaphore: invalid field element(s) in proof" 268 | ); 269 | 270 | // Check whether the nullifier hash has been seen 271 | require( 272 | nullifierHashHistory[_nullifiersHash] == false, 273 | "Semaphore: nullifier already seen" 274 | ); 275 | 276 | // Check whether the nullifier hash is active 277 | require( 278 | isExternalNullifierActive(_externalNullifier), 279 | "Semaphore: external nullifier not found" 280 | ); 281 | 282 | // Check whether the given Merkle root has been seen previously 283 | require(rootHistory[_root], "Semaphore: root not seen"); 284 | 285 | uint256 signalHash = hashSignal(_signal); 286 | 287 | // Check whether _nullifiersHash is a valid field element. 288 | require( 289 | _nullifiersHash < SNARK_SCALAR_FIELD, 290 | "Semaphore: the nullifiers hash must be lt the snark scalar field" 291 | ); 292 | 293 | uint256[4] memory publicSignals = 294 | [_root, _nullifiersHash, signalHash, _externalNullifier]; 295 | 296 | (uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c) = 297 | unpackProof(_proof); 298 | 299 | require( 300 | verifyProof(a, b, c, publicSignals), 301 | "Semaphore: invalid proof" 302 | ); 303 | 304 | // Note that we don't need to check if signalHash is less than 305 | // SNARK_SCALAR_FIELD because it always holds true due to the 306 | // definition of hashSignal() 307 | 308 | _; 309 | } 310 | 311 | /* 312 | * Broadcasts the signal. 313 | * @param _signal The signal to broadcast 314 | * @param _proof The proof elements. 315 | * @param _root The root of the Merkle tree (the 1st public signal) 316 | * @param _nullifiersHash The nullifiers hash (the 2nd public signal) 317 | * @param _externalNullifier The nullifiers hash (the 4th public signal) 318 | */ 319 | function broadcastSignal( 320 | bytes memory _signal, 321 | uint256[8] memory _proof, 322 | uint256 _root, 323 | uint256 _nullifiersHash, 324 | uint232 _externalNullifier 325 | ) public 326 | onlyOwnerIfPermissioned 327 | isValidSignalAndProof( 328 | _signal, _proof, _root, _nullifiersHash, _externalNullifier 329 | ) 330 | { 331 | // Client contracts should be responsible for storing the signal and/or 332 | // emitting it as an event 333 | 334 | // Store the nullifiers hash to prevent double-signalling 335 | nullifierHashHistory[_nullifiersHash] = true; 336 | } 337 | 338 | /* 339 | * A private helper function which adds an external nullifier. 340 | * @param _externalNullifier The external nullifier to add. 341 | * @param _isFirst Whether _externalNullifier is the first external 342 | * nullifier. Only the constructor should set _isFirst to true when it 343 | * calls addEn(). 344 | */ 345 | function addEn(uint232 _externalNullifier, bool isFirst) private { 346 | if (isFirst) { 347 | firstExternalNullifier = _externalNullifier; 348 | } else { 349 | // The external nullifier must not have already been set 350 | require( 351 | externalNullifierLinkedList[_externalNullifier].exists == false, 352 | "Semaphore: external nullifier already set" 353 | ); 354 | 355 | // Connect the previously added external nullifier node to this one 356 | externalNullifierLinkedList[lastExternalNullifier].next = 357 | _externalNullifier; 358 | } 359 | 360 | // Add a new external nullifier 361 | externalNullifierLinkedList[_externalNullifier].next = 0; 362 | externalNullifierLinkedList[_externalNullifier].isActive = true; 363 | externalNullifierLinkedList[_externalNullifier].exists = true; 364 | 365 | // Set the last external nullifier to this one 366 | lastExternalNullifier = _externalNullifier; 367 | 368 | numExternalNullifiers ++; 369 | 370 | emit ExternalNullifierAdd(_externalNullifier); 371 | } 372 | 373 | /* 374 | * Adds an external nullifier to the contract. This external nullifier is 375 | * active once it is added. Only the owner can do this. 376 | * @param _externalNullifier The new external nullifier to set. 377 | */ 378 | function addExternalNullifier(uint232 _externalNullifier) public 379 | onlyOwner { 380 | addEn(_externalNullifier, false); 381 | } 382 | 383 | /* 384 | * Deactivate an external nullifier. The external nullifier must already be 385 | * active for this function to work. Only the owner can do this. 386 | * @param _externalNullifier The new external nullifier to deactivate. 387 | */ 388 | function deactivateExternalNullifier(uint232 _externalNullifier) public 389 | onlyOwner { 390 | // The external nullifier must already exist 391 | require( 392 | externalNullifierLinkedList[_externalNullifier].exists, 393 | "Semaphore: external nullifier not found" 394 | ); 395 | 396 | // The external nullifier must already be active 397 | require( 398 | externalNullifierLinkedList[_externalNullifier].isActive == true, 399 | "Semaphore: external nullifier already deactivated" 400 | ); 401 | 402 | // Deactivate the external nullifier. Note that we don't change the 403 | // value of nextEn. 404 | externalNullifierLinkedList[_externalNullifier].isActive = false; 405 | 406 | emit ExternalNullifierChangeStatus(_externalNullifier, false); 407 | } 408 | 409 | /* 410 | * Reactivate an external nullifier. The external nullifier must already be 411 | * inactive for this function to work. Only the owner can do this. 412 | * @param _externalNullifier The new external nullifier to reactivate. 413 | */ 414 | function reactivateExternalNullifier(uint232 _externalNullifier) public 415 | onlyOwner { 416 | // The external nullifier must already exist 417 | require( 418 | externalNullifierLinkedList[_externalNullifier].exists, 419 | "Semaphore: external nullifier not found" 420 | ); 421 | 422 | // The external nullifier must already have been deactivated 423 | require( 424 | externalNullifierLinkedList[_externalNullifier].isActive == false, 425 | "Semaphore: external nullifier is already active" 426 | ); 427 | 428 | // Reactivate the external nullifier 429 | externalNullifierLinkedList[_externalNullifier].isActive = true; 430 | 431 | emit ExternalNullifierChangeStatus(_externalNullifier, true); 432 | } 433 | 434 | /* 435 | * Returns true if and only if the specified external nullifier is active 436 | * @param _externalNullifier The specified external nullifier. 437 | */ 438 | function isExternalNullifierActive(uint232 _externalNullifier) public view 439 | returns (bool) { 440 | return externalNullifierLinkedList[_externalNullifier].isActive; 441 | } 442 | 443 | /* 444 | * Returns the next external nullifier after the specified external 445 | * nullifier in the linked list. 446 | * @param _externalNullifier The specified external nullifier. 447 | */ 448 | function getNextExternalNullifier(uint232 _externalNullifier) public view 449 | returns (uint232) { 450 | 451 | require( 452 | externalNullifierLinkedList[_externalNullifier].exists, 453 | "Semaphore: no such external nullifier" 454 | ); 455 | 456 | uint232 n = externalNullifierLinkedList[_externalNullifier].next; 457 | 458 | require( 459 | numExternalNullifiers > 1 && externalNullifierLinkedList[n].exists, 460 | "Semaphore: no external nullifier exists after the specified one" 461 | ); 462 | 463 | return n; 464 | } 465 | 466 | /* 467 | * Returns the number of inserted identity commitments. 468 | */ 469 | function getNumIdentityCommitments() public view returns (uint256) { 470 | return nextLeafIndex; 471 | } 472 | 473 | /* 474 | * Sets the `isBroadcastPermissioned` storage variable, which determines 475 | * whether broadcastSignal can or cannot be called by only the contract 476 | * owner. 477 | * @param _newPermission True if the broadcastSignal can only be called by 478 | * the contract owner; and False otherwise. 479 | */ 480 | function setPermissioning(bool _newPermission) public onlyOwner { 481 | 482 | isBroadcastPermissioned = _newPermission; 483 | 484 | emit PermissionSet(_newPermission); 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/SnarkConstants.sol: -------------------------------------------------------------------------------- 1 | /* 2 | * Semaphore - Zero-knowledge signaling on Ethereum 3 | * Copyright (C) 2020 Barry WhiteHat , Kobi 4 | * Gurkan and Koh Wei Jie (contact@kohweijie.com) 5 | * 6 | * This file is part of Semaphore. 7 | * 8 | * Semaphore is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * Semaphore is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with Semaphore. If not, see . 20 | */ 21 | 22 | pragma solidity ^0.5.0; 23 | 24 | contract SnarkConstants { 25 | // The scalar field 26 | uint256 internal constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; 27 | } 28 | -------------------------------------------------------------------------------- /packages/contracts/contracts/semaphore/verifier.sol: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Christian Reitwiessner 2 | // Permission is hereby granted, free of charge, to any person obtaining a copy 3 | // of this software and associated documentation files (the "Software"), to 4 | // deal in the Software without restriction, including without limitation the 5 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | // sell copies of the Software, and to permit persons to whom the Software is 7 | // furnished to do so, subject to the following conditions: 8 | // The above copyright notice and this permission notice shall be included in 9 | // all copies or substantial portions of the Software. 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 15 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 16 | // IN THE SOFTWARE. 17 | 18 | // 2019 OKIMS 19 | 20 | pragma solidity ^0.5.0; 21 | 22 | library Pairing { 23 | 24 | uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; 25 | 26 | struct G1Point { 27 | uint256 X; 28 | uint256 Y; 29 | } 30 | 31 | // Encoding of field elements is: X[0] * z + X[1] 32 | struct G2Point { 33 | uint256[2] X; 34 | uint256[2] Y; 35 | } 36 | 37 | /* 38 | * @return The negation of p, i.e. p.plus(p.negate()) should be zero. 39 | */ 40 | function negate(G1Point memory p) internal pure returns (G1Point memory) { 41 | 42 | // The prime q in the base field F_q for G1 43 | if (p.X == 0 && p.Y == 0) { 44 | return G1Point(0, 0); 45 | } else { 46 | return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q)); 47 | } 48 | } 49 | 50 | /* 51 | * @return The sum of two points of G1 52 | */ 53 | function plus( 54 | G1Point memory p1, 55 | G1Point memory p2 56 | ) internal view returns (G1Point memory r) { 57 | 58 | uint256[4] memory input; 59 | input[0] = p1.X; 60 | input[1] = p1.Y; 61 | input[2] = p2.X; 62 | input[3] = p2.Y; 63 | bool success; 64 | 65 | // solium-disable-next-line security/no-inline-assembly 66 | assembly { 67 | success := staticcall(sub(gas, 2000), 6, input, 0xc0, r, 0x60) 68 | // Use "invalid" to make gas estimation work 69 | switch success case 0 { invalid() } 70 | } 71 | 72 | require(success,"pairing-add-failed"); 73 | } 74 | 75 | /* 76 | * @return The product of a point on G1 and a scalar, i.e. 77 | * p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all 78 | * points p. 79 | */ 80 | function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) { 81 | 82 | uint256[3] memory input; 83 | input[0] = p.X; 84 | input[1] = p.Y; 85 | input[2] = s; 86 | bool success; 87 | // solium-disable-next-line security/no-inline-assembly 88 | assembly { 89 | success := staticcall(sub(gas, 2000), 7, input, 0x80, r, 0x60) 90 | // Use "invalid" to make gas estimation work 91 | switch success case 0 { invalid() } 92 | } 93 | require (success,"pairing-mul-failed"); 94 | } 95 | 96 | /* @return The result of computing the pairing check 97 | * e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 98 | * For example, 99 | * pairing([P1(), P1().negate()], [P2(), P2()]) should return true. 100 | */ 101 | function pairing( 102 | G1Point memory a1, 103 | G2Point memory a2, 104 | G1Point memory b1, 105 | G2Point memory b2, 106 | G1Point memory c1, 107 | G2Point memory c2, 108 | G1Point memory d1, 109 | G2Point memory d2 110 | ) internal view returns (bool) { 111 | 112 | G1Point[4] memory p1 = [a1, b1, c1, d1]; 113 | G2Point[4] memory p2 = [a2, b2, c2, d2]; 114 | 115 | uint256 inputSize = 24; 116 | uint256[] memory input = new uint256[](inputSize); 117 | 118 | for (uint256 i = 0; i < 4; i++) { 119 | uint256 j = i * 6; 120 | input[j + 0] = p1[i].X; 121 | input[j + 1] = p1[i].Y; 122 | input[j + 2] = p2[i].X[0]; 123 | input[j + 3] = p2[i].X[1]; 124 | input[j + 4] = p2[i].Y[0]; 125 | input[j + 5] = p2[i].Y[1]; 126 | } 127 | 128 | uint256[1] memory out; 129 | bool success; 130 | 131 | // solium-disable-next-line security/no-inline-assembly 132 | assembly { 133 | success := staticcall(sub(gas, 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20) 134 | // Use "invalid" to make gas estimation work 135 | switch success case 0 { invalid() } 136 | } 137 | 138 | require(success,"pairing-opcode-failed"); 139 | 140 | return out[0] != 0; 141 | } 142 | } 143 | 144 | contract Verifier { 145 | 146 | using Pairing for *; 147 | 148 | uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; 149 | uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; 150 | 151 | struct VerifyingKey { 152 | Pairing.G1Point alfa1; 153 | Pairing.G2Point beta2; 154 | Pairing.G2Point gamma2; 155 | Pairing.G2Point delta2; 156 | Pairing.G1Point[5] IC; 157 | } 158 | 159 | struct Proof { 160 | Pairing.G1Point A; 161 | Pairing.G2Point B; 162 | Pairing.G1Point C; 163 | } 164 | 165 | function verifyingKey() internal pure returns (VerifyingKey memory vk) { 166 | vk.alfa1 = Pairing.G1Point(uint256(19412864166662840704199897530865176132379551878490098268546775855212807511623), uint256(1659653118858063231936623514142744009632862245288902525823221677276210935161)); 167 | vk.beta2 = Pairing.G2Point([uint256(21642731927166283717597040076624334182856179384944638206172042877912623746964), uint256(10167148937945084930725321596284210462590725859059519349238552658864205847292)], [uint256(1224040963938177736215304045934094846196618140310043882848453181887293487598), uint256(21818148805990469610055209831928752212083869699635755409679256612234566494214)]); 168 | vk.gamma2 = Pairing.G2Point([uint256(7084583264208126476050882701870582484117653569260992588151213710616665494315), uint256(21338791141815158629032141160990160038215366570564838558882493662897305673845)], [uint256(20409386432685531109985133572396335050408469502427908880430269312598850603009), uint256(21694931277671378411802527161940275869759588185961914055258162012593400433477)]); 169 | vk.delta2 = Pairing.G2Point([uint256(15974226699330331350608610622797702188540365463638301508490182464179306746479), uint256(10800304862034523735970868610105048512813625407055208260881866244104890739413)], [uint256(3115757193545898321493679843214264410358333980282409841160781532582592563749), uint256(20585865237865669840907249885451244426128970908885747090346049467936130099745)]); 170 | vk.IC[0] = Pairing.G1Point(uint256(15132217740731663181077706894312753465210500809816534743630331656993829080728), uint256(110196461348215931979632312103651461241391911014889357659299988542624772231)); 171 | vk.IC[1] = Pairing.G1Point(uint256(10128725078198782996699361178651605009720713215878609701039758932704577595075), uint256(6404467707897071196443816328081887791672216217394045289711692279719912978002)); 172 | vk.IC[2] = Pairing.G1Point(uint256(10522674726928807143308489533204590506138344033395366996236503798983177262637), uint256(4729387380831061502587298076566203099165040519549972799644442785786542168149)); 173 | vk.IC[3] = Pairing.G1Point(uint256(17764548214378097040054627843654571919160111811296549132642271073668911279441), uint256(18047600752595374913303954050610046390396466710244400595444056973195374265781)); 174 | vk.IC[4] = Pairing.G1Point(uint256(7483741608009854810379599415076882430339556147495509532415486196559753628073), uint256(15133497018284592127224880003457371607602553122563204208188040273053737050972)); 175 | 176 | } 177 | 178 | /* 179 | * @returns Whether the proof is valid given the hardcoded verifying key 180 | * above and the public inputs 181 | */ 182 | function verifyProof( 183 | uint256[2] memory a, 184 | uint256[2][2] memory b, 185 | uint256[2] memory c, 186 | uint256[4] memory input 187 | ) public view returns (bool r) { 188 | 189 | Proof memory proof; 190 | proof.A = Pairing.G1Point(a[0], a[1]); 191 | proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); 192 | proof.C = Pairing.G1Point(c[0], c[1]); 193 | 194 | VerifyingKey memory vk = verifyingKey(); 195 | 196 | // Compute the linear combination vk_x 197 | Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); 198 | 199 | // Make sure that proof.A, B, and C are each less than the prime q 200 | require(proof.A.X < PRIME_Q, "verifier-aX-gte-prime-q"); 201 | require(proof.A.Y < PRIME_Q, "verifier-aY-gte-prime-q"); 202 | 203 | require(proof.B.X[0] < PRIME_Q, "verifier-bX0-gte-prime-q"); 204 | require(proof.B.Y[0] < PRIME_Q, "verifier-bY0-gte-prime-q"); 205 | 206 | require(proof.B.X[1] < PRIME_Q, "verifier-bX1-gte-prime-q"); 207 | require(proof.B.Y[1] < PRIME_Q, "verifier-bY1-gte-prime-q"); 208 | 209 | require(proof.C.X < PRIME_Q, "verifier-cX-gte-prime-q"); 210 | require(proof.C.Y < PRIME_Q, "verifier-cY-gte-prime-q"); 211 | 212 | // Make sure that every input is less than the snark scalar field 213 | for (uint256 i = 0; i < input.length; i++) { 214 | require(input[i] < SNARK_SCALAR_FIELD,"verifier-gte-snark-scalar-field"); 215 | vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); 216 | } 217 | 218 | vk_x = Pairing.plus(vk_x, vk.IC[0]); 219 | 220 | return Pairing.pairing( 221 | Pairing.negate(proof.A), 222 | proof.B, 223 | vk.alfa1, 224 | vk.beta2, 225 | vk_x, 226 | vk.gamma2, 227 | proof.C, 228 | vk.delta2 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/contracts", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "download_snarks": "sh ./scripts/downloadSnarks.sh", 8 | "build_verifier": "rm semaphore/semaphorejs/contracts/verifier.sol && npx snarkjs generateverifier --vk semaphore/semaphorejs/build/verification_key.json -v semaphore/semaphorejs/contracts/verifier.sol", 9 | "deploy": "node ./scripts/deploy.js --verbose", 10 | "test": "ava", 11 | "copyABIs": "node ./scripts/copyABIs.js", 12 | "ganache": "ganache-cli -a 10 -m='candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' --gasLimit=8800000 --port 8545 -i 4" 13 | }, 14 | "devDependencies": { 15 | "ava": "^3.4.0", 16 | "ganache-cli": "^6.9.1", 17 | "libsemaphore": "^1.0.15" 18 | }, 19 | "dependencies": { 20 | "circomlib": "https://github.com/kobigurk/circomlib.git#347822604996bf25f659f96ee0f02810a1f71bb0", 21 | "node-fetch": "^2.6.0", 22 | "solc": "^0.6.4" 23 | }, 24 | "ava": { 25 | "files": [ 26 | "test/*" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/contracts/scripts/copyABIs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { compileContracts } = require('../src/compile') 4 | 5 | const main = async () => { 6 | const solcs = await compileContracts() 7 | 8 | const SemaphoreABI = solcs['Semaphore.sol'].Semaphore.abi 9 | const ProofOfBurnABI = solcs['ProofOfBurn.sol'].ProofOfBurn.abi 10 | 11 | saveABI('Semaphore', SemaphoreABI) 12 | saveABI('ProofOfBurn', ProofOfBurnABI) 13 | } 14 | 15 | const saveABI = (contractName, abi) => { 16 | const toPath = path.join(__dirname, `../../common/abis/${contractName}.json`) 17 | 18 | fs.writeFileSync( 19 | toPath, 20 | JSON.stringify(abi, null, 2) // spacing level = 2 21 | ) 22 | console.log(`${contractName}: Saved ABI to ${toPath}`) 23 | } 24 | 25 | if (require.main === module) { 26 | main() 27 | .then(() => process.exit(0)) 28 | .catch(error => { 29 | console.error(error) 30 | process.exit(1) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /packages/contracts/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const { deployContracts } = require('../src/deploy') 2 | const ethers = require('ethers') 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const deployWithSigners = async ({ network }) => { 7 | const provider = ethers.getDefaultProvider(network) 8 | 9 | let signer 10 | if (process.env.PRIVATE_KEY) { 11 | signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider) 12 | } else if (process.env.JSONPATH && process.env.PASSWORD) { 13 | const json = fs.readFileSync(path.join(__dirname, process.env.JSONPATH)) 14 | signer = await ethers.Wallet.fromEncryptedJson(json, process.env.PASSWORD) 15 | signer = signer.connect(provider) 16 | } 17 | console.log(signer) 18 | 19 | await deployContracts({ signer, verbose: true }) 20 | } 21 | 22 | async function main () { 23 | const verbose = process.argv.includes('--verbose') 24 | 25 | const network = process.env.NETWORK 26 | 27 | if (!network) { 28 | await deployContracts({ verbose }) 29 | } else { 30 | await deployWithSigners({ network }) 31 | } 32 | } 33 | 34 | if (require.main === module) { 35 | main() 36 | .then(() => process.exit(0)) 37 | .catch(error => { 38 | console.error(error) 39 | process.exit(1) 40 | }) 41 | } 42 | 43 | module.exports = { 44 | deployContracts 45 | } 46 | -------------------------------------------------------------------------------- /packages/contracts/scripts/downloadSnarks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROVING_KEY_BIN="https://www.dropbox.com/s/qjlu6v125g7jkcq/proving_key.bin?dl=1" 4 | CIRCUIT_JSON="https://www.dropbox.com/s/3gzxjibqgb6ke13/circuit.json?dl=1" 5 | 6 | CIRCUIT_JSON_PATH="cache/circuit.json" 7 | PROVING_KEY_BIN_PATH="cache/proving_key.bin" 8 | 9 | mkdir -p cache 10 | 11 | if [ ! -f "$CIRCUIT_JSON_PATH" ]; then 12 | echo "Downloading circuit.json" 13 | wget $CIRCUIT_JSON -O $CIRCUIT_JSON_PATH 14 | fi 15 | 16 | if [ ! -f "$PROVING_KEY_BIN_PATH" ]; then 17 | echo "Downloading proving_key.bin" 18 | wget $PROVING_KEY_BIN -O $PROVING_KEY_BIN_PATH 19 | fi 20 | -------------------------------------------------------------------------------- /packages/contracts/src/compile.js: -------------------------------------------------------------------------------- 1 | const solc = require('solc') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const fetch = require('node-fetch') 6 | 7 | const SOLC_VERSION = 'v0.5.15+commit.6a57276f' 8 | const CACHE_DIR = path.join(__dirname, '../cache') 9 | const SOLC_CACHE_PATH = path.join(CACHE_DIR, 'soljson.js') 10 | const CONTRACT_DIRS = ['../contracts/', '../contracts/semaphore/'] 11 | 12 | const downloadSolc = async version => { 13 | console.log(`Fetching soljson-${version}.js`) 14 | return fetch( 15 | `https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/soljson-${version}.js` 16 | ).then(res => { 17 | const dest = fs.createWriteStream(SOLC_CACHE_PATH) 18 | res.body.pipe(dest) 19 | return new Promise(function (resolve, reject) { 20 | res.body.on('end', resolve) 21 | res.body.on('error', reject) 22 | }) 23 | }) 24 | } 25 | 26 | const loadSolc = async version => { 27 | if (!fs.existsSync(CACHE_DIR)) { 28 | fs.mkdirSync(CACHE_DIR) 29 | } 30 | 31 | if (!fs.existsSync(SOLC_CACHE_PATH)) { 32 | console.log('Solc not found, downloading') 33 | await downloadSolc(version) 34 | } 35 | return solc.setupMethods(require(SOLC_CACHE_PATH)) 36 | } 37 | 38 | const loadSources = async contractDirs => { 39 | const sources = {} 40 | for (const dirPath of contractDirs) { 41 | const dirAbsolutePath = path.join(__dirname, dirPath) 42 | const sols = fs.readdirSync(dirAbsolutePath) 43 | for (const filename of sols) { 44 | const filePath = path.join(dirAbsolutePath, filename) 45 | if (fs.lstatSync(filePath).isFile()) { 46 | const content = fs.readFileSync(filePath).toString() 47 | sources[filename] = { content } 48 | } 49 | } 50 | } 51 | return sources 52 | } 53 | 54 | const compileContracts = async () => { 55 | const mySolc = await loadSolc(SOLC_VERSION) 56 | const sources = await loadSources(CONTRACT_DIRS) 57 | const input = { 58 | language: 'Solidity', 59 | sources, 60 | settings: { 61 | outputSelection: { 62 | '*': { 63 | '*': ['*'] 64 | } 65 | } 66 | } 67 | } 68 | function findImports (solPath) { 69 | const basename = path.basename(solPath) 70 | return { contents: sources[basename].content } 71 | } 72 | 73 | const output = JSON.parse( 74 | mySolc.compile(JSON.stringify(input), { import: findImports }) 75 | ) 76 | return output.contracts 77 | } 78 | 79 | module.exports = { compileContracts } 80 | -------------------------------------------------------------------------------- /packages/contracts/src/deploy.js: -------------------------------------------------------------------------------- 1 | const { compileContracts } = require('./compile') 2 | const ethers = require('ethers') 3 | const { 4 | REGISTRATION_FEE, 5 | SEMAPHORE_TREE_DEPTH, 6 | MIMC_SEED 7 | } = require('../constants') 8 | 9 | const linker = require('solc/linker') 10 | const mimcGenContract = require('circomlib/src/mimcsponge_gencontract.js') 11 | 12 | const linkBytecode = (solcToLink, deployed) => { 13 | // Assuming only one reference 14 | const oldBytecode = solcToLink.evm.bytecode.object 15 | const references = linker.findLinkReferences(oldBytecode) 16 | const libraryName = Object.keys(references)[0] 17 | const newBytecode = linker.linkBytecode(oldBytecode, { 18 | [libraryName]: deployed.address 19 | }) 20 | return newBytecode 21 | } 22 | 23 | const defaultSigner = () => { 24 | const provider = new ethers.providers.JsonRpcProvider() 25 | return provider.getSigner() 26 | } 27 | 28 | async function deployContracts ({ 29 | signer = defaultSigner(), 30 | registrationFee = REGISTRATION_FEE, 31 | verbose = false 32 | }) { 33 | if (verbose) console.log('deploying') 34 | const solcs = await compileContracts() 35 | if (verbose) console.log('compiled') 36 | 37 | const SemaphoreSolc = solcs['Semaphore.sol'].Semaphore 38 | const ProofOfBurnSolc = solcs['ProofOfBurn.sol'].ProofOfBurn 39 | 40 | const MiMCContract = new ethers.ContractFactory( 41 | mimcGenContract.abi, 42 | mimcGenContract.createCode(MIMC_SEED, 220), 43 | signer 44 | ) 45 | 46 | const ProofOfBurnContract = ethers.ContractFactory.fromSolidity( 47 | ProofOfBurnSolc, 48 | signer 49 | ) 50 | const mimcTx = await MiMCContract.deploy() 51 | if (verbose) console.log('mimcTx', mimcTx.deployTransaction.hash) 52 | const mimcInstance = await mimcTx.deployed() 53 | 54 | SemaphoreSolc.evm.bytecode.object = linkBytecode(SemaphoreSolc, mimcInstance) 55 | 56 | const SemaphoreContract = ethers.ContractFactory.fromSolidity( 57 | SemaphoreSolc, 58 | signer 59 | ) 60 | 61 | const semaphoreTx = await SemaphoreContract.deploy(SEMAPHORE_TREE_DEPTH, 0, 0) 62 | if (verbose) console.log('semaphoreTx', semaphoreTx.deployTransaction.hash) 63 | const semaphoreInstance = await semaphoreTx.deployed() 64 | 65 | const proofOfBurnTx = await ProofOfBurnContract.deploy( 66 | semaphoreInstance.address, 67 | ethers.utils.parseEther(registrationFee.toString()) 68 | ) 69 | if (verbose) 70 | console.log('proofOfBurnTx', proofOfBurnTx.deployTransaction.hash) 71 | const proofOfBurnInstance = await proofOfBurnTx.deployed() 72 | 73 | await semaphoreInstance.transferOwnership(proofOfBurnInstance.address) 74 | 75 | if (verbose) { 76 | console.log(`SEMAPHORE_ADDRESS=${semaphoreInstance.address}`) 77 | console.log(`PROOF_OF_BURN_ADDRESS=${proofOfBurnInstance.address}`) 78 | } 79 | 80 | return { 81 | MiMC: mimcInstance, 82 | Semaphore: semaphoreInstance, 83 | ProofOfBurn: proofOfBurnInstance 84 | } 85 | } 86 | 87 | module.exports = { deployContracts } 88 | -------------------------------------------------------------------------------- /packages/contracts/test/proofOfBurn.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const libsemaphore = require('libsemaphore') 3 | const deploy = require('../src/deploy') 4 | const ethers = require('ethers') 5 | const { REGISTRATION_FEE } = require('../constants') 6 | 7 | const registrationFee = REGISTRATION_FEE 8 | 9 | const setup = async () => { 10 | const contracts = await deploy.deployContracts({ 11 | registrationFee 12 | }) 13 | const identity = libsemaphore.genIdentity() 14 | const identityCommitment = libsemaphore.genIdentityCommitment(identity) 15 | 16 | return { contracts, identityCommitment } 17 | } 18 | 19 | test('register should emit an event', async t => { 20 | t.timeout(25000) 21 | const { contracts, identityCommitment } = await setup() 22 | 23 | const tx = await contracts.ProofOfBurn.register( 24 | identityCommitment.toString(), 25 | { 26 | value: ethers.utils.parseEther(registrationFee.toString()) 27 | } 28 | ) 29 | const receipt = await tx.wait() 30 | const _identityCommitment = receipt.events.pop().args[0] 31 | t.is(_identityCommitment.toString(), identityCommitment.toString()) 32 | }) 33 | 34 | test('Should fail when not enough registration fee is sent', async t => { 35 | t.timeout(25000) 36 | const { contracts, identityCommitment } = await setup() 37 | const fee = registrationFee * 0.9 38 | try { 39 | await contracts.ProofOfBurn.register(identityCommitment.toString(), { 40 | value: ethers.utils.parseEther(fee.toString()) 41 | }) 42 | } catch (err) { 43 | t.true(err.message.includes('Not sending the right amount of ether')) 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /packages/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .cache/ 3 | .parcel-cache/ 4 | -------------------------------------------------------------------------------- /packages/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.1-alpine AS hojicha-frontend 2 | 3 | COPY --from=hojicha-build /hojicha/packages/frontend/dist /static 4 | COPY nginx.conf /etc/nginx/nginx.conf 5 | 6 | WORKDIR / 7 | 8 | CMD nginx -c /etc/nginx/nginx.conf -g 'daemon off;' 9 | -------------------------------------------------------------------------------- /packages/frontend/externals/worker_threads.js: -------------------------------------------------------------------------------- 1 | module.exports = window.worker_thread 2 | -------------------------------------------------------------------------------- /packages/frontend/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/frontend/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/frontend/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/frontend/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /packages/frontend/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /packages/frontend/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/frontend/img/og_hojicha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChihChengLiang/semaphore_auth/d48e2f80301b6b4b515d25c14c520ed310a2893e/packages/frontend/img/og_hojicha.png -------------------------------------------------------------------------------- /packages/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hojicha 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | worker_rlimit_nofile 8192; 8 | events { 9 | worker_connections 8000; 10 | } 11 | 12 | http { 13 | server_tokens off; 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | charset_types 17 | text/css 18 | text/plain 19 | text/vnd.wap.wml 20 | application/javascript 21 | application/json 22 | application/rss+xml 23 | application/xml; 24 | 25 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 26 | '$status $body_bytes_sent "$http_referer" ' 27 | '"$http_user_agent" "$http_x_forwarded_for"'; 28 | 29 | access_log /var/log/nginx/access.log main; 30 | tcp_nopush on; 31 | 32 | sendfile on; 33 | keepalive_timeout 20s; 34 | 35 | gzip on; 36 | gzip_disable "msie6"; 37 | gzip_comp_level 6; 38 | gzip_vary on; 39 | gzip_proxied any; 40 | gzip_buffers 16 8k; 41 | gzip_http_version 1.1; 42 | gzip_min_length 256; 43 | gzip_types 44 | application/octet-stream 45 | application/atom+xml 46 | application/javascript 47 | application/json 48 | application/ld+json 49 | application/manifest+json 50 | application/rss+xml 51 | application/vnd.geo+json 52 | application/vnd.ms-fontobject 53 | application/x-font-ttf 54 | application/x-web-app-manifest+json 55 | application/xhtml+xml 56 | application/xml 57 | font/opentype 58 | image/bmp 59 | image/svg+xml 60 | image/x-icon 61 | text/cache-manifest 62 | text/css 63 | text/plain 64 | text/vcard 65 | text/vnd.rim.location.xloc 66 | text/vtt 67 | text/x-component 68 | text/x-cross-domain-policy; 69 | 70 | limit_req_zone $binary_remote_addr zone=mylimit:10m rate=30r/s; 71 | 72 | server { 73 | server_tokens off; 74 | listen 8001; 75 | server_name hojicha-frontend; 76 | real_ip_header X-Real-IP; 77 | index index.html; 78 | 79 | location / { 80 | root /static; 81 | try_files $uri /index.html; 82 | gzip_static on; 83 | } 84 | 85 | location /api { 86 | limit_req zone=mylimit burst=30 nodelay; 87 | proxy_set_header Host $host; 88 | proxy_set_header X-Real-IP $remote_addr; 89 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 90 | rewrite ^/api(/.*)$ $1 break; 91 | proxy_pass http://hojicha-backend:5566/; 92 | } 93 | } 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hojicha/frontend", 3 | "version": "0.0.3", 4 | "description": "", 5 | "scripts": { 6 | "build": "npm run configure && parcel build index.html --no-autoinstall", 7 | "start": "npm run configure && node server.js", 8 | "configure": "node ../config/export-config.js > ./src/exported-config.json" 9 | }, 10 | "alias": { 11 | "worker_threads": "./externals/worker_threads.js" 12 | }, 13 | "browserslist": [ 14 | "last 2 Chrome versions" 15 | ], 16 | "devDependencies": { 17 | "express": "^4.17.1", 18 | "http-proxy-middleware": "^1.0.3", 19 | "parcel-bundler": "^1.12.4", 20 | "sass": "^1.26.3" 21 | }, 22 | "dependencies": { 23 | "@hojicha/common": "^0.0.3", 24 | "@web3-react/core": "^6.0.7", 25 | "@web3-react/injected-connector": "^6.0.7", 26 | "bulma": "^0.8.0", 27 | "ethers": "^4", 28 | "fetch-progress": "^1.2.0", 29 | "libsemaphore": "^1.0.15", 30 | "react": "^16.8", 31 | "react-countdown-circle-timer": "^1.1.1", 32 | "react-dom": "^16.8", 33 | "react-router": "^5.1.2", 34 | "react-router-dom": "^5.1.2", 35 | "react-toast-notifications": "^2.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/frontend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const Bundler = require('parcel-bundler') 3 | const path = require('path') 4 | const { createProxyMiddleware } = require('http-proxy-middleware') 5 | 6 | const entryFile = path.join(__dirname, './index.html') 7 | 8 | const parcelOptions = { 9 | // provide parcel options here 10 | // these are directly passed into the 11 | // parcel bundler 12 | // 13 | // More info on supported options are documented at 14 | // https://parceljs.org/api 15 | https: false, 16 | autoInstall: false 17 | } 18 | 19 | const app = express() 20 | 21 | app.use( 22 | '/api', 23 | createProxyMiddleware({ 24 | target: 'http://localhost:5566', 25 | pathRewrite: { '^/api': '' }, 26 | changeOrigin: true 27 | }) 28 | ) 29 | 30 | app.use('/circuit', express.static('../contracts/cache/circuit.json')) 31 | app.use('/provingKey', express.static('../contracts/cache/proving_key.bin')) 32 | 33 | const bundler = new Bundler(entryFile, parcelOptions) 34 | bundler.on('buildEnd', () => { 35 | console.log('Build completed!') 36 | }) 37 | 38 | app.use(bundler.middleware()) 39 | 40 | const port = 3000 41 | 42 | // start up the server 43 | app.listen(port, () => { 44 | console.log('Parcel proxy server has started at:', `http://localhost:${port}`) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/frontend/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"Hojicha","short_name":"Hojicha","icons":[{"src":"/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /packages/frontend/src/components/contracts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ethers } from 'ethers' 3 | import { useProofOfBurnData } from '../hooks' 4 | 5 | const ProofOfBurn = () => { 6 | const data = useProofOfBurnData() 7 | 8 | return data.isLoaded ? ( 9 |
10 |
11 | Group managed by contract {data.address} 12 |
13 | 14 | 30 |
31 | ) : ( 32 |

Waiting contract data...

33 | ) 34 | } 35 | 36 | export { ProofOfBurn } 37 | -------------------------------------------------------------------------------- /packages/frontend/src/components/download_snarks.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { fetchCircuit, fetchProvingKey } from '../utils/fetch' 3 | 4 | const formatMB = rawSize => (rawSize / 1024 / 1024).toFixed(2) 5 | 6 | const ProgressBar = ({ label, progress }) => { 7 | const percentage = progress ? progress.percentage : 0 8 | 9 | const detail = 10 | progress && progress.transferred && progress.total 11 | ? `${formatMB(progress.transferred)}/${formatMB(progress.total)} MB` 12 | : progress && progress.transferred 13 | ? `Downloaded ${formatMB(progress.transferred)} MB` 14 | : '' 15 | 16 | return ( 17 | <> 18 |
19 |
20 |
21 | {label} 22 |
23 |
24 |
25 |
26 |

{detail}

27 |
28 |
29 |
30 | 31 | 32 | ) 33 | } 34 | 35 | const DownloadSnarks = ({ onComplete }) => { 36 | const [on, toggle] = useState(true) 37 | const [circuitProgress, setCircuitProgress] = useState(null) 38 | const [provingKeyProgress, setProvingKeyProgress] = useState(null) 39 | 40 | const startDownload = async () => { 41 | toggle(false) 42 | await Promise.all([ 43 | fetchCircuit(progress => setCircuitProgress(progress)), 44 | fetchProvingKey(progress => setProvingKeyProgress(progress)) 45 | ]) 46 | toggle(true) 47 | onComplete() 48 | } 49 | 50 | return ( 51 | <> 52 |

53 | To use the snark magic, we'll have to download a circuit and proving 54 | key. These are public and has a total size 200 MB -ish. 55 |

56 | 57 | 58 |
59 | 65 | 66 | ) 67 | } 68 | 69 | export default DownloadSnarks 70 | -------------------------------------------------------------------------------- /packages/frontend/src/components/github.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/frontend/src/configs.js: -------------------------------------------------------------------------------- 1 | import config from './exported-config.json' 2 | 3 | export const supportedNetwork = config.frontend.supportedNetwork 4 | 5 | export const supportedNetworkName = config.frontend.supportedNetworkName 6 | 7 | export const proofOfBurnAddress = config.chain.contracts.proofOfBurn 8 | 9 | export const circuitUrl = config.snarks.circuitUrl 10 | 11 | export const provingKeyUrl = config.snarks.provingKeyUrl 12 | -------------------------------------------------------------------------------- /packages/frontend/src/custom.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | // Import a Google Font 4 | @import url('https://fonts.googleapis.com/css?family=Nunito:400,700'); 5 | 6 | // Set your brand colors 7 | $purple: #C4524A; 8 | $pink: #F69888; 9 | $brown: #5A3D42; 10 | $beige-light: #C1A343; 11 | $beige-lighter: #F4F6ED; 12 | 13 | // Update Bulma's global variables 14 | $family-sans-serif: "Nunito", sans-serif; 15 | $grey-dark: $brown; 16 | $grey-light: $beige-light; 17 | $primary: $purple; 18 | $link: $pink; 19 | $widescreen-enabled: false; 20 | $fullhd-enabled: false; 21 | 22 | // Update some of Bulma's component variables 23 | $body-background-color: $beige-lighter; 24 | $control-border-width: 2px; 25 | $input-border-color: transparent; 26 | $input-shadow: none; 27 | 28 | // Import only what you need from Bulma 29 | @import "../node_modules/bulma/sass/utilities/_all.sass"; 30 | @import "../node_modules/bulma/sass/base/_all.sass"; 31 | @import "../node_modules/bulma/sass/elements/button.sass"; 32 | @import "../node_modules/bulma/sass/elements/container.sass"; 33 | @import "../node_modules/bulma/sass/elements/title.sass"; 34 | @import "../node_modules/bulma/sass/form/_all.sass"; 35 | @import "../node_modules/bulma/sass/components/navbar.sass"; 36 | @import "../node_modules/bulma/sass/layout/hero.sass"; 37 | @import "../node_modules/bulma/sass/layout/section.sass"; -------------------------------------------------------------------------------- /packages/frontend/src/hooks.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { checkRegistered } from './web3/registration' 3 | import { ProofOfBurnABI } from '@hojicha/common' 4 | import { ethers } from 'ethers' 5 | import { proofOfBurnAddress } from './configs' 6 | import { useWeb3React } from '@web3-react/core' 7 | 8 | const useProofOfBurn = () => { 9 | const { library, active } = useWeb3React() 10 | 11 | if (active) { 12 | return new ethers.Contract( 13 | proofOfBurnAddress, 14 | ProofOfBurnABI, 15 | library.getSigner() 16 | ) 17 | } else { 18 | return null 19 | } 20 | } 21 | 22 | const useIsRegistered = () => { 23 | const contract = useProofOfBurn() 24 | 25 | const [isRegistered, setIsRegistered] = useState(false) 26 | useEffect(() => { 27 | let didCancel = false 28 | if (contract !== null) { 29 | checkRegistered(contract).then(result => { 30 | if (!didCancel) { 31 | setIsRegistered(result) 32 | } 33 | }) 34 | } 35 | return () => { 36 | didCancel = true 37 | } 38 | }, [contract]) 39 | return isRegistered 40 | } 41 | 42 | const useProofOfBurnData = () => { 43 | const contract = useProofOfBurn() 44 | const [data, setData] = useState({ 45 | isLoaded: false, 46 | address: null, 47 | registrationFee: null, 48 | commitments: null 49 | }) 50 | 51 | useEffect(() => { 52 | let didCancel = false 53 | const fetchContractData = async () => { 54 | if (contract) { 55 | const registrationFee = (await contract.registration_fee()).toString() 56 | const commitments = (await contract.getIdentityCommitments()).length 57 | if (!didCancel) 58 | setData({ 59 | isLoaded: true, 60 | address: contract.address, 61 | registrationFee, 62 | commitments 63 | }) 64 | } 65 | } 66 | fetchContractData() 67 | return () => { 68 | didCancel = true 69 | } 70 | }, [contract]) 71 | 72 | return data 73 | } 74 | 75 | export { useProofOfBurn, useIsRegistered, useProofOfBurnData} 76 | -------------------------------------------------------------------------------- /packages/frontend/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ReactDOM from 'react-dom' 4 | import bulma from 'bulma' 5 | import { initStorage } from './storage' 6 | import { Web3ReactProvider } from '@web3-react/core' 7 | import { getLibrary } from './web3' 8 | 9 | import { IdentityPage } from './pages/identity' 10 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' 11 | import './custom.scss' 12 | 13 | import { ToastProvider } from 'react-toast-notifications' 14 | import Group from './pages/group' 15 | import About from './pages/about' 16 | 17 | 18 | const links = [ 19 | { path: '/about', title: 'About', component: }, 20 | { path: '/', title: 'Posts', component: }, 21 | { 22 | path: '/identity', 23 | title: 'Identity', 24 | component: 25 | } 26 | ] 27 | 28 | const RouteTabs = () => ( 29 | 30 |
31 |
    32 | {links.map((link, key) => ( 33 | 34 | {({ match }) => ( 35 |
  • 36 | {link.title} 37 |
  • 38 | )} 39 |
    40 | ))} 41 |
42 |
43 | 44 | {links.map((link, key) => ( 45 | 46 | {link.component} 47 | 48 | ))} 49 | 50 |
51 | ) 52 | 53 | const Layout = ({ children }) => ( 54 |
55 |
56 |
57 |
{children}
58 |
59 |
60 |
61 | ) 62 | 63 | const App = () => { 64 | initStorage() 65 | return ( 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | 76 | const root = document.getElementById('root') 77 | 78 | ReactDOM.render(, root) 79 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/about.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = () => { 4 | return ( 5 |
6 |

Hi there, let's grab some Hojicha

7 |

What is this?

8 |

9 | This is a forum that allows you to publish message without showing who 10 | you are. You are anonymous not only to public but also to the server 11 | hosting this forum. 12 |

13 |

How it works?

14 |

15 | The server authenticates a user by verifying a Zero Knowledge Proof they 16 | sent. 17 |

18 |

Learn more and contributing

19 |

20 | Please visit the{' '} 21 | 25 | Github 26 | {' '} 27 | page. 28 |

29 |
30 | ) 31 | } 32 | 33 | export default About 34 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/group.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useWeb3React } from '@web3-react/core' 3 | import { NewPost, Posts } from './posts' 4 | import DownloadSnarks from '../components/download_snarks' 5 | 6 | import { useToasts } from 'react-toast-notifications' 7 | 8 | import { CreateIdentity } from './identity' 9 | import { hasId, retrieveId } from '../storage' 10 | import { Activation } from '../web3' 11 | import { Registration } from '../pages/identity' 12 | 13 | import { genIdentityCommitment } from 'libsemaphore' 14 | import { register } from '../web3/registration' 15 | import { ProofOfBurn } from '../components/contracts' 16 | import { useProofOfBurn, useIsRegistered } from '../hooks' 17 | 18 | const Group = () => { 19 | const { active } = useWeb3React() 20 | const { addToast } = useToasts() 21 | 22 | const [idExists, setIdExists] = useState(hasId()) 23 | const contract = useProofOfBurn() 24 | const [newPostId, setNewPostId] = useState(null) 25 | const isRegistered = useIsRegistered() 26 | const forceRerender = useState()[1] 27 | 28 | const [snarksDownloaded, setSnarksDownloaded] = useState( 29 | window.circuit && window.provingKey 30 | ) 31 | 32 | const onIdCreated = () => { 33 | addToast('Identity created Successfully!', { appearance: 'success' }) 34 | setIdExists(true) 35 | } 36 | 37 | const _register = async () => { 38 | const identityCommitment = genIdentityCommitment(retrieveId()) 39 | let tx 40 | try { 41 | tx = await register(contract, identityCommitment) 42 | } catch (err) { 43 | addToast(`Registration failed: ${err.message}`, { 44 | appearance: 'error' 45 | }) 46 | return 47 | } 48 | 49 | addToast(`Registration transaction sent! Transaction ID: ${tx.hash}`, { 50 | appearance: 'info' 51 | }) 52 | 53 | const receipt = await tx.wait() 54 | if (receipt.status === 1) { 55 | addToast('Registration Success!', { appearance: 'success' }) 56 | forceRerender() 57 | } else { 58 | addToast(`Registration failed`, { 59 | appearance: 'error' 60 | }) 61 | } 62 | } 63 | 64 | const onSnarkDownloaded = () => { 65 | addToast('Circuit and proving key downloaded', { appearance: 'success' }) 66 | setSnarksDownloaded(true) 67 | } 68 | const onPublish = result => { 69 | console.log(result) 70 | if (!result.error) { 71 | setNewPostId(result.postId) 72 | } 73 | } 74 | 75 | let onboarding = null 76 | if (!idExists) { 77 | onboarding = 78 | } else if (!active) { 79 | onboarding = 80 | } else if (!isRegistered) { 81 | onboarding = 82 | } else if (!snarksDownloaded) { 83 | onboarding = 84 | } else { 85 | onboarding =

Loading

86 | } 87 | return ( 88 |
89 | {isRegistered && snarksDownloaded ? ( 90 | 91 | ) : ( 92 | onboarding 93 | )} 94 |
95 | 96 | 97 |
98 | ) 99 | } 100 | 101 | export default Group 102 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/identity.jsx: -------------------------------------------------------------------------------- 1 | import { genIdentity, serialiseIdentity } from 'libsemaphore' 2 | import { hasId, retrieveId, storeId } from '../storage' 3 | import React from 'react' 4 | 5 | import { ProofOfBurn } from '../components/contracts' 6 | import { useIsRegistered } from '../hooks' 7 | 8 | const Identity = ({ identity }) => { 9 | return ( 10 |
11 |
12 |

This is your identity

13 | 14 | {serialiseIdentity(identity)} 15 | 16 |
17 |
18 | ) 19 | } 20 | 21 | const IdentityPage = () => { 22 | if (hasId()) { 23 | return 24 | } else { 25 | return

No Identity Yet

26 | } 27 | } 28 | 29 | const CreateIdentity = ({ onCreated }) => { 30 | function createIdentity () { 31 | const identity = genIdentity() 32 | storeId(identity) 33 | onCreated() 34 | } 35 | 36 | return ( 37 |
38 |

39 | Let's create an identity. It contains private information only you can 40 | know. Guard it with your life 41 |

42 | 45 |
46 | ) 47 | } 48 | 49 | const Registration = ({ register }) => { 50 | const isRegistered = useIsRegistered() 51 | 52 | return ( 53 |
54 |

Register your identity to join a Semaphore group

55 |
56 |
57 | 58 | {isRegistered ? ( 59 |

You are registered

60 | ) : ( 61 | 64 | )} 65 |
66 |
67 |
68 | ) 69 | } 70 | 71 | export { IdentityPage, Registration, CreateIdentity } 72 | -------------------------------------------------------------------------------- /packages/frontend/src/pages/posts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | 3 | import genAuth from '../web3/semaphore' 4 | import { EpochbasedExternalNullifier } from '@hojicha/common' 5 | import { retrieveId } from '../storage' 6 | import { ethers } from 'ethers' 7 | import { fetchGetPosts, fetchPostNewPost } from '../utils/fetch' 8 | import { useToasts } from 'react-toast-notifications' 9 | import { CountdownCircleTimer } from 'react-countdown-circle-timer' 10 | import { useProofOfBurn } from '../hooks' 11 | 12 | const newPostENGen = new EpochbasedExternalNullifier( 13 | '/posts/new', 14 | 300 * 1000 // ExternalNullifier epoch length 15 | ) 16 | 17 | const ExternalNullifierIndicator = () => { 18 | const remainingTime = newPostENGen.timeBeforeNextEpoch() / 1000 19 | return ( 20 | [true, 0]} // repeat animation after 0 second 24 | durationSeconds={newPostENGen.epochLength / 1000} 25 | initialRemainingTime={remainingTime} 26 | colors={[['#0A0A0A', 0.8], ['#0A0A0A', 0.1], ['#FE3860']]} 27 | /> 28 | ) 29 | } 30 | 31 | const SemaphoreDescriptions = { 32 | externalNullifierStr: 33 | 'This limits the rate the author can publish a content in a period of time.', 34 | nullifierHash: 35 | 'Is unique given the same externalNullifier and identity, meaning the author can publish at most once between the time period', 36 | signalHash: "Is the content's hash and protects the content's authenticity", 37 | root: 38 | 'Is the root of the identity hash tree and is stored on chain. The author proves being a member of this root represents', 39 | proof: 'Claims the correctness of the above statements.' 40 | } 41 | 42 | const Semaphore = ({ post }) => { 43 | return ( 44 |
45 |
49 |

50 | The proof is validated at backend, and frontend validation will be 51 | implemented in the next version. For now, information is displayed for 52 | the demonstration of the usage. 53 |

54 | {Object.keys(SemaphoreDescriptions).map(key => ( 55 |
56 |
{key}
57 |

{SemaphoreDescriptions[key]}

58 | {post[key]} 59 |
60 |
61 | ))} 62 |
63 |
64 | ) 65 | } 66 | 67 | const Post = ({ post, isNew }) => { 68 | const [on, toggle] = useState(false) 69 | return ( 70 |
71 |
72 |

{post.id}

73 |
74 |
75 |
76 | {isNew ? new : ''} 77 |

{post.postBody}

78 |
79 |
80 |
81 | {post.createdAt} 82 |
83 |
84 | 87 |
88 |
89 |
90 | 91 | {on ? : ''} 92 |
93 |
94 |
95 | ) 96 | } 97 | 98 | const NewPost = ({ onPublish }) => { 99 | const contract = useProofOfBurn() 100 | const [postBody, setPostBody] = useState('') 101 | const [isLoading, setIsLoading] = useState(false) 102 | 103 | const { addToast, updateToast } = useToasts() 104 | 105 | let toastId = null 106 | const progressCallback = ({ text, appearance = 'info' }) => { 107 | const setToastId = id => (toastId = id) 108 | if (toastId == null) { 109 | addToast(text, { appearance }, setToastId) 110 | } else { 111 | updateToast(toastId, { content: text, appearance }, setToastId) 112 | } 113 | } 114 | 115 | const publishPost = async () => { 116 | console.log(postBody) 117 | 118 | setIsLoading(true) 119 | 120 | const signalStr = ethers.utils.hashMessage(postBody) 121 | 122 | const identity = retrieveId() 123 | const externalNullifierStr = newPostENGen.getString() 124 | 125 | // Assume by the time we call this function the contract is not null already 126 | const { proof, publicSignals } = await genAuth( 127 | externalNullifierStr, 128 | signalStr, 129 | identity, 130 | contract, 131 | progressCallback 132 | ) 133 | 134 | const result = await fetchPostNewPost(postBody, proof, publicSignals) 135 | if (result.error) { 136 | progressCallback({ text: result.error, appearance: 'error' }) 137 | } else { 138 | progressCallback({ text: result.message, appearance: 'success' }) 139 | } 140 | onPublish(result) 141 | setIsLoading(false) 142 | } 143 | return ( 144 |
145 |
146 |
147 |

148 |