├── backend ├── config │ ├── test.json │ ├── production.json │ └── default.json ├── public │ ├── favicon.ico │ └── index.html ├── .env.example ├── src │ ├── zk │ │ ├── check_guess.wasm │ │ ├── check_stats.wasm │ │ ├── check_guess_final.zkey │ │ └── check_stats_final.zkey │ ├── types │ │ ├── circomlibjs.d.ts │ │ └── snarkjs.d.ts │ ├── utils │ │ ├── proof.ts │ │ ├── asAsciiArray.ts │ │ └── words.ts │ ├── middleware │ │ └── index.ts │ ├── declarations.d.ts │ ├── services │ │ ├── salt-storage │ │ │ ├── salt-storage.class.ts │ │ │ ├── salt-storage.hooks.ts │ │ │ └── salt-storage.service.ts │ │ ├── salt │ │ │ ├── salt.hooks.ts │ │ │ ├── salt.service.ts │ │ │ └── salt.class.ts │ │ ├── game-round │ │ │ ├── game-round.hooks.ts │ │ │ ├── game-round.class.ts │ │ │ └── game-round.service.ts │ │ ├── index.ts │ │ ├── clue │ │ │ ├── clue.service.ts │ │ │ ├── clue.hooks.ts │ │ │ └── clue.class.ts │ │ ├── stats │ │ │ ├── stats.service.ts │ │ │ ├── stats.hooks.ts │ │ │ └── stats.class.ts │ │ └── commitment │ │ │ ├── commitment.service.ts │ │ │ ├── commitment.hooks.ts │ │ │ └── commitment.class.ts │ ├── index.ts │ ├── logger.ts │ ├── app.hooks.ts │ ├── models │ │ └── salt-storage.model.ts │ ├── sequelize.ts │ ├── hooks │ │ └── connect-to-contract.ts │ ├── app.ts │ └── channels.ts ├── jest.config.js ├── test │ ├── services │ │ ├── clue.test.ts │ │ ├── salt.test.ts │ │ ├── stats.test.ts │ │ ├── game-round.test.ts │ │ ├── commitment.test.ts │ │ └── salt-storage.test.ts │ └── app.test.ts ├── .editorconfig ├── tsconfig.json ├── .eslintrc.json ├── README.md ├── package.json └── .gitignore ├── frontend ├── .husky │ ├── .gitignore │ └── pre-commit ├── .prettierrc ├── src │ ├── react-app-env.d.ts │ ├── App.css │ ├── types │ │ ├── ffjavascript.d.ts │ │ └── snarkjs.d.ts │ ├── components │ │ ├── progress │ │ │ ├── Spinner.tsx │ │ │ └── spinner.css │ │ ├── alerts │ │ │ ├── AlertContainer.tsx │ │ │ └── Alert.tsx │ │ ├── grid │ │ │ ├── EmptyRow.tsx │ │ │ ├── CurrentRow.tsx │ │ │ ├── CompletedRow.tsx │ │ │ ├── Grid.tsx │ │ │ └── Cell.tsx │ │ ├── stats │ │ │ ├── Progress.tsx │ │ │ ├── Histogram.tsx │ │ │ └── StatBar.tsx │ │ ├── modals │ │ │ ├── SettingsToggle.tsx │ │ │ ├── ConnectWalletModal.tsx │ │ │ ├── SettingsModal.tsx │ │ │ ├── InfoModal.tsx │ │ │ ├── BaseModal.tsx │ │ │ └── StatsModal.tsx │ │ ├── navbar │ │ │ └── Navbar.tsx │ │ ├── profile │ │ │ └── Profile.tsx │ │ └── keyboard │ │ │ ├── Key.tsx │ │ │ └── Keyboard.tsx │ ├── setupTests.ts │ ├── lib │ │ ├── asAsciiArray.ts │ │ ├── share.test.ts │ │ ├── stats.ts │ │ ├── statuses.ts │ │ ├── localStorage.ts │ │ ├── words.ts │ │ └── share.ts │ ├── constants │ │ ├── settings.ts │ │ └── strings.ts │ ├── reportWebVitals.ts │ ├── zk │ │ └── prove.ts │ ├── App.test.tsx │ ├── index.tsx │ ├── logo.svg │ ├── context │ │ └── AlertContext.tsx │ └── index.css ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── postcss.config.js ├── tailwind.config.js ├── .env.example ├── .github │ ├── workflows │ │ ├── test.yml │ │ └── lint.yml │ └── ISSUE_TEMPLATE │ │ ├── feature_request.md │ │ └── bug_report.md ├── .gitignore ├── tsconfig.json ├── config-overrides.js ├── LICENSE ├── package.json └── CODE_OF_CONDUCT.md ├── blockchain ├── .env.example ├── script │ ├── copy-broadcast.sh │ └── ZKWordle.s.sol ├── foundry.toml ├── src │ ├── ZKWordle.sol │ ├── check_guess.sol │ └── check_stats.sol └── test │ └── ZKWordle.t.sol ├── remappings.txt ├── .gitmodules ├── .gitignore ├── package.json ├── circuits ├── src │ ├── check_guess.circom │ ├── check_stats.circom │ └── guess │ │ └── guess_single.circom └── scripts │ ├── rename-verifiers.js │ └── compile-circuit.sh ├── .github └── workflows │ └── node.js.yml └── README.md /backend/config/test.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /frontend/.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | -------------------------------------------------------------------------------- /blockchain/.env.example: -------------------------------------------------------------------------------- 1 | GOERLI_PRIVATE_KEY=abcdef01234567890... -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | html.dark { 2 | background-color: rgb(15, 23, 42); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/backend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | SIGNER_PRIVATE_KEY=abcdef01234567890... 2 | RPC_URL=http://localhost:8545 3 | CHAIN_ID=31337 -------------------------------------------------------------------------------- /backend/config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "https://zk-wordle.herokuapp.com", 3 | "port": "PORT" 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/zk/check_guess.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/backend/src/zk/check_guess.wasm -------------------------------------------------------------------------------- /backend/src/zk/check_stats.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/backend/src/zk/check_stats.wasm -------------------------------------------------------------------------------- /backend/src/types/circomlibjs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "circomlibjs" { 2 | export function buildPoseidon(): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/zk/check_guess_final.zkey: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/backend/src/zk/check_guess_final.zkey -------------------------------------------------------------------------------- /backend/src/zk/check_stats_final.zkey: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxkzmn/zk-wordle/HEAD/backend/src/zk/check_stats_final.zkey -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/types/ffjavascript.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ffjavascript' { 2 | export = ffjavascript 3 | const ffjavascript: { 4 | utils: any 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=blockchain/lib/forge-std/lib/ds-test/src/ 2 | forge-std/=blockchain/lib/forge-std/src/ 3 | openzeppelin-contracts/=blockchain/lib/openzeppelin-contracts -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 3 | darkMode: 'class', 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | diagnostics: false 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | CI= npm run build 2 | REACT_APP_GAME_NAME=ZK-Wordle 3 | REACT_APP_GAME_DESCRIPTION=A daily word ZK-puzzle 4 | REACT_APP_LOCALE_STRING= 5 | REACT_APP_SERVER_URL=http://localhost:3030 -------------------------------------------------------------------------------- /blockchain/script/copy-broadcast.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ./../frontend/src/contracts/ZKWordle.sol 2 | cp ./../backend/src/blockchain/ZKWordle.sol/ZKWordle.json ./../frontend/src/contracts/ZKWordle.sol/ZKWordle.json 3 | -------------------------------------------------------------------------------- /blockchain/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = './../backend/src/blockchain' 4 | libs = ['lib'] 5 | broadcast = './../backend/src/blockchain' 6 | 7 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /backend/test/services/clue.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'clue\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('clue'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/test/services/salt.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'salt\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('salt'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/test/services/stats.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'stats\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('stats'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/components/progress/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import './spinner.css' 2 | 3 | export default function LoadingSpinner() { 4 | return ( 5 |
6 |
7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/utils/proof.ts: -------------------------------------------------------------------------------- 1 | interface Proof { 2 | pi_a: string[]; 3 | pi_b: string[][]; 4 | pi_c: string[]; 5 | protocol: string; 6 | curve: string; 7 | } 8 | 9 | export interface Groth16Proof { 10 | proof: Proof; 11 | publicSignals: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /backend/test/services/game-round.test.ts: -------------------------------------------------------------------------------- 1 | import app from "../../src/app"; 2 | 3 | describe("'GameRound' service", () => { 4 | it("registered the service", () => { 5 | const service = app.service("game-round"); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/test/services/commitment.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'commitment\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('commitment'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/test/services/salt-storage.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'SaltStorage\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('salt-storage'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /frontend/src/lib/asAsciiArray.ts: -------------------------------------------------------------------------------- 1 | export const asAsciiArray = (word: string) => { 2 | let wordAsAscii: number[] = [] 3 | for (let i = 0; i < word.length; i++) { 4 | let code = word.charCodeAt(i) 5 | wordAsAscii.push(code) 6 | } 7 | 8 | return wordAsAscii 9 | } 10 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /backend/src/utils/asAsciiArray.ts: -------------------------------------------------------------------------------- 1 | export const asAsciiArray = (word: string) => { 2 | const wordAsAscii: number[] = []; 3 | for (let i = 0; i < word.length; i++) { 4 | const code = word.charCodeAt(i); 5 | wordAsAscii.push(code); 6 | } 7 | 8 | return wordAsAscii; 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/constants/settings.ts: -------------------------------------------------------------------------------- 1 | export const MAX_WORD_LENGTH = 5 2 | export const MAX_CHALLENGES = 6 3 | export const ALERT_TIME_MS = 2000 4 | export const REVEAL_TIME_MS = 350 5 | export const GAME_LOST_INFO_DELAY = (MAX_WORD_LENGTH + 1) * REVEAL_TIME_MS 6 | export const WELCOME_INFO_MODAL_MS = 350 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "blockchain/lib/forge-std"] 2 | path = blockchain/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "blockchain/lib/openzeppelin-contracts"] 5 | path = blockchain/lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .vscode 4 | *.o 5 | circuits/compiled/* 6 | node_modules 7 | blockchain/cache 8 | blockchain/out 9 | blockchain/broadcast 10 | circuits/powers-of-tau 11 | frontend/.env 12 | circuits/cache 13 | backend/src/blockchain 14 | !backend/src/blockchain/ZKWordle.sol/ZKWordle.json 15 | -------------------------------------------------------------------------------- /backend/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 5 | export default function (app: Application): void { 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/alerts/AlertContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from './Alert' 2 | import { useAlert } from '../../context/AlertContext' 3 | 4 | export const AlertContainer = () => { 5 | const { message, status, isVisible } = useAlert() 6 | 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["test"] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: | 14 | npm install 15 | - run: | 16 | npm run test 17 | -------------------------------------------------------------------------------- /backend/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | import { Application as ExpressFeathers } from '@feathersjs/express'; 2 | 3 | // A mapping of service names to types. Will be extended in service files. 4 | export interface ServiceTypes {} 5 | // The application instance type that will be used everywhere else 6 | export type Application = ExpressFeathers; 7 | -------------------------------------------------------------------------------- /frontend/.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Prettify code 14 | uses: creyD/prettier_action@v4.2 15 | with: 16 | prettier_options: --check src 17 | -------------------------------------------------------------------------------- /backend/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "public": "../public/", 5 | "paginate": { 6 | "default": 10, 7 | "max": 50 8 | }, 9 | "accountPrivateKey": "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 10 | "rpcUrl": "http://localhost:8545", 11 | "chainId": 31337, 12 | "postgres": "postgres://postgres:postgres@localhost:5432/postgres" 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/services/salt-storage/salt-storage.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize'; 2 | import { Application } from '../../declarations'; 3 | 4 | export class SaltStorage extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super(options); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | import app from './app'; 3 | 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /frontend/src/components/grid/EmptyRow.tsx: -------------------------------------------------------------------------------- 1 | import { MAX_WORD_LENGTH } from '../../constants/settings' 2 | import { Cell } from './Cell' 3 | 4 | export const EmptyRow = () => { 5 | const emptyCells = Array.from(Array(MAX_WORD_LENGTH)) 6 | 7 | return ( 8 |
9 | {emptyCells.map((_, i) => ( 10 | 11 | ))} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .idea 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/src/components/progress/spinner.css: -------------------------------------------------------------------------------- 1 | @keyframes spinner { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | .loading-spinner { 10 | width: 24px; 11 | height: 24px; 12 | border: 4px solid #f3f3f3; /* Light grey */ 13 | border-top: 4px solid #383636; /* Black */ 14 | border-radius: 50%; 15 | animation: spinner 1.5s linear infinite; 16 | } -------------------------------------------------------------------------------- /backend/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | export default logger; 17 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Game", 3 | "name": "Game", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/services/salt/salt.hooks.ts: -------------------------------------------------------------------------------- 1 | import { disallow } from "feathers-hooks-common"; 2 | 3 | export default { 4 | before: { 5 | all: disallow("external"), 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [], 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [], 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/services/salt-storage/salt-storage.hooks.ts: -------------------------------------------------------------------------------- 1 | import { disallow } from "feathers-hooks-common"; 2 | 3 | export default { 4 | before: { 5 | all: [disallow("external")], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [], 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [], 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/services/game-round/game-round.hooks.ts: -------------------------------------------------------------------------------- 1 | import { disallow } from "feathers-hooks-common"; 2 | 3 | export default { 4 | before: { 5 | all: [], 6 | find: [], 7 | get: [], 8 | create: [disallow()], 9 | update: [disallow()], 10 | patch: [disallow()], 11 | remove: [disallow()], 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [], 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/app.hooks.ts: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default { 5 | before: { 6 | all: [], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/src/services/game-round/game-round.class.ts: -------------------------------------------------------------------------------- 1 | import { Params } from "@feathersjs/feathers"; 2 | import { Service, MemoryServiceOptions } from "feathers-memory"; 3 | import { Application } from "../../declarations"; 4 | import { tomorrow, solutionIndex } from "../../utils/words"; 5 | 6 | export class GameRound extends Service { 7 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | constructor(options: Partial, app: Application) { 9 | super(options); 10 | } 11 | 12 | async find(params?: Params): Promise { 13 | return Promise.resolve({ 14 | solutionIndex: solutionIndex, 15 | tomorrow: tomorrow, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "../declarations"; 2 | import clue from "./clue/clue.service"; 3 | import stats from "./stats/stats.service"; 4 | import salt from "./salt/salt.service"; 5 | import commitment from "./commitment/commitment.service"; 6 | import saltStorage from "./salt-storage/salt-storage.service"; 7 | import gameRound from "./game-round/game-round.service"; 8 | // Don't remove this comment. It's needed to format import lines nicely. 9 | 10 | export default function (app: Application): void { 11 | app.configure(clue); 12 | app.configure(stats); 13 | app.configure(salt); 14 | app.configure(commitment); 15 | app.configure(saltStorage); 16 | app.configure(gameRound); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zk-wordle", 3 | "version": "1.0.0", 4 | "description": "Wordle game implemented using the Zero-Knowledge Proofs", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "dependencies": { 9 | "circomlib": "^2.0.5" 10 | }, 11 | "scripts": { 12 | "compile": ". circuits/scripts/compile-circuit.sh && node ./circuits/scripts/rename-verifiers.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/alxkzmn/zk-wordle.git" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/alxkzmn/zk-wordle/issues" 22 | }, 23 | "homepage": "https://github.com/alxkzmn/zk-wordle#readme" 24 | } 25 | -------------------------------------------------------------------------------- /circuits/src/check_guess.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.0.3; 2 | include "./guess/guess_single.circom"; 3 | 4 | template CheckGuess() { 5 | signal input solution[5]; 6 | signal input salt; 7 | signal input guess[5]; 8 | signal input commitment; 9 | signal output clue[5]; 10 | component check = SingleGuessCheck(); 11 | check.salt <== salt; 12 | check.commitment <== commitment; 13 | for (var i = 0; i < 5; i++) { 14 | check.solution[i] <== solution[i]; 15 | check.guess[i] <== guess[i]; 16 | } 17 | //Constraining the output after all the inputs were assigned 18 | for (var i = 0; i < 5; i++) { 19 | clue[i] <== check.clue[i]; 20 | } 21 | } 22 | 23 | component main{public [guess, commitment]} = CheckGuess(); -------------------------------------------------------------------------------- /frontend/src/zk/prove.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '@feathersjs/feathers' 2 | 3 | interface Proof { 4 | pi_a: string[] 5 | pi_b: string[][] 6 | pi_c: string[] 7 | protocol: string 8 | curve: string 9 | } 10 | 11 | export interface Groth16Proof { 12 | proof: Proof 13 | publicSignals: string[] 14 | } 15 | 16 | export const requestProof = async ( 17 | feathersClient: Application, 18 | asciiGuess: number[] 19 | ): Promise => { 20 | console.log(`Guess: ${asciiGuess}`) 21 | 22 | let result: Groth16Proof 23 | try { 24 | result = await feathersClient.service('clue').create({ guess: asciiGuess }) 25 | console.log(result) 26 | } catch (e) { 27 | throw Error(e as any) 28 | } 29 | 30 | return result 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/grid/CurrentRow.tsx: -------------------------------------------------------------------------------- 1 | import { MAX_WORD_LENGTH } from '../../constants/settings' 2 | import { Cell } from './Cell' 3 | import { unicodeSplit } from '../../lib/words' 4 | 5 | type Props = { 6 | guess: string 7 | className: string 8 | } 9 | 10 | export const CurrentRow = ({ guess, className }: Props) => { 11 | const splitGuess = unicodeSplit(guess) 12 | const emptyCells = Array.from(Array(MAX_WORD_LENGTH - splitGuess.length)) 13 | const classes = `flex justify-center mb-1 ${className}` 14 | 15 | return ( 16 |
17 | {splitGuess.map((letter, i) => ( 18 | 19 | ))} 20 | {emptyCells.map((_, i) => ( 21 | 22 | ))} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "parser": "@typescript-eslint/parser", 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "@typescript-eslint" 14 | ], 15 | "extends": [ 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 2 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ], 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-empty-interface": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/stats/Progress.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | 3 | type Props = { 4 | index: number 5 | size: number 6 | label: string 7 | currentDayStatRow: boolean 8 | } 9 | 10 | export const Progress = ({ index, size, label, currentDayStatRow }: Props) => { 11 | const currentRowClass = classNames( 12 | 'text-xs font-medium text-blue-100 text-center p-0.5', 13 | { 'bg-blue-600': currentDayStatRow, 'bg-gray-600': !currentDayStatRow } 14 | ) 15 | return ( 16 |
17 |
{index + 1}
18 |
19 |
20 | {label} 21 |
22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/stats/Histogram.tsx: -------------------------------------------------------------------------------- 1 | import { GameStats } from '../../lib/localStorage' 2 | import { Progress } from './Progress' 3 | 4 | type Props = { 5 | gameStats: GameStats 6 | numberOfGuessesMade: number 7 | } 8 | 9 | export const Histogram = ({ gameStats, numberOfGuessesMade }: Props) => { 10 | const winDistribution = gameStats.winDistribution 11 | const maxValue = Math.max(...winDistribution) 12 | 13 | return ( 14 |
15 | {winDistribution.map((value, i) => ( 16 | 23 | ))} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/utils/words.ts: -------------------------------------------------------------------------------- 1 | import { WORDS } from "../constants/wordlist"; 2 | 3 | export const localeAwareUpperCase = (text: string) => { 4 | return process.env.REACT_APP_LOCALE_STRING 5 | ? text.toLocaleUpperCase(process.env.REACT_APP_LOCALE_STRING) 6 | : text.toUpperCase(); 7 | }; 8 | 9 | export const getWordOfDay = () => { 10 | // January 1, 2022 Game Epoch 11 | const epochMs = new Date(2022, 0).valueOf(); 12 | const now = Date.now(); 13 | const msInDay = 86400000; 14 | const index = Math.floor((now - epochMs) / msInDay); 15 | const nextday = (index + 1) * msInDay + epochMs; 16 | 17 | return { 18 | solutionIndex: index, 19 | solution: localeAwareUpperCase(WORDS[index % WORDS.length]), 20 | tomorrow: nextday, 21 | }; 22 | }; 23 | 24 | export const { solution, solutionIndex, tomorrow } = getWordOfDay(); 25 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import App from './App' 4 | import { GAME_TITLE } from './constants/strings' 5 | 6 | beforeEach(() => { 7 | Object.defineProperty(window, 'matchMedia', { 8 | writable: true, 9 | value: jest.fn().mockImplementation((query) => ({ 10 | matches: false, 11 | media: query, 12 | onchange: null, 13 | addListener: jest.fn(), // deprecated 14 | removeListener: jest.fn(), // deprecated 15 | addEventListener: jest.fn(), 16 | removeEventListener: jest.fn(), 17 | dispatchEvent: jest.fn(), 18 | })), 19 | }) 20 | }) 21 | 22 | test('renders App component', () => { 23 | render() 24 | const linkElement = screen.getByText(GAME_TITLE) 25 | expect(linkElement).toBeInTheDocument() 26 | }) 27 | -------------------------------------------------------------------------------- /backend/src/services/salt/salt.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `salt` service on path `/salt` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { Salt } from "./salt.class"; 5 | import hooks from "./salt.hooks"; 6 | // Add this service to the service type index 7 | declare module "../../declarations" { 8 | interface ServiceTypes { 9 | salt: Salt & ServiceAddons; 10 | } 11 | } 12 | 13 | export default function (app: Application): void { 14 | const options = { 15 | paginate: app.get("paginate"), 16 | }; 17 | 18 | // Initialize our service with any options it requires 19 | app.use("/salt", new Salt(options, app)); 20 | 21 | // Get our initialized service so that we can register hooks 22 | const service = app.service("salt"); 23 | 24 | service.hooks(hooks); 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/services/clue/clue.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `clue` service on path `/clue` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { Clue } from "./clue.class"; 5 | import hooks from "./clue.hooks"; 6 | 7 | // Add this service to the service type index 8 | declare module "../../declarations" { 9 | interface ServiceTypes { 10 | clue: Clue & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application): void { 15 | const options = { 16 | paginate: app.get("paginate"), 17 | }; 18 | // Initialize our service with any options it requires 19 | app.use("/clue", new Clue(options, app)); 20 | 21 | // Get our initialized service so that we can register hooks 22 | const service = app.service("clue"); 23 | 24 | service.hooks(hooks(app)); 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/services/stats/stats.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `stats` service on path `/stats` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { Stats } from "./stats.class"; 5 | import hooks from "./stats.hooks"; 6 | 7 | // Add this service to the service type index 8 | declare module "../../declarations" { 9 | interface ServiceTypes { 10 | stats: Stats & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application): void { 15 | const options = { 16 | paginate: app.get("paginate"), 17 | }; 18 | 19 | // Initialize our service with any options it requires 20 | app.use("/stats", new Stats(options, app)); 21 | 22 | // Get our initialized service so that we can register hooks 23 | const service = app.service("stats"); 24 | 25 | service.hooks(hooks(app)); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = function override(config) { 4 | const fallback = config.resolve.fallback || {} 5 | Object.assign(fallback, { 6 | crypto: require.resolve('crypto-browserify'), 7 | stream: require.resolve('stream-browserify'), 8 | assert: require.resolve('assert'), 9 | http: require.resolve('stream-http'), 10 | https: require.resolve('https-browserify'), 11 | os: require.resolve('os-browserify'), 12 | url: require.resolve('url'), 13 | fs: require.resolve('browserify-fs'), 14 | path: require.resolve('path-browserify'), 15 | constants: false, 16 | }) 17 | config.resolve.fallback = fallback 18 | config.plugins = (config.plugins || []).concat([ 19 | new webpack.ProvidePlugin({ 20 | process: 'process/browser.js', 21 | Buffer: ['buffer', 'Buffer'], 22 | }), 23 | ]) 24 | return config 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/services/game-round/game-round.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `GameRound` service on path `/game-round` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { GameRound } from "./game-round.class"; 5 | import hooks from "./game-round.hooks"; 6 | 7 | // Add this service to the service type index 8 | declare module "../../declarations" { 9 | interface ServiceTypes { 10 | "game-round": GameRound & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application): void { 15 | const options = { 16 | paginate: app.get("paginate"), 17 | }; 18 | 19 | // Initialize our service with any options it requires 20 | app.use("/game-round", new GameRound(options, app)); 21 | 22 | // Get our initialized service so that we can register hooks 23 | const service = app.service("game-round"); 24 | 25 | service.hooks(hooks); 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/services/commitment/commitment.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `commitment` service on path `/commitment` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { Commitment } from "./commitment.class"; 5 | import hooks from "./commitment.hooks"; 6 | 7 | // Add this service to the service type index 8 | declare module "../../declarations" { 9 | interface ServiceTypes { 10 | commitment: Commitment & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application): void { 15 | const options = { 16 | paginate: app.get("paginate"), 17 | }; 18 | 19 | // Initialize our service with any options it requires 20 | app.use("/commitment", new Commitment(options, app)); 21 | 22 | // Get our initialized service so that we can register hooks 23 | const service = app.service("commitment"); 24 | 25 | service.hooks(hooks(app)); 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/services/clue/clue.hooks.ts: -------------------------------------------------------------------------------- 1 | import { Application, HooksObject } from "@feathersjs/feathers"; 2 | import { disallow } from "feathers-hooks-common"; 3 | 4 | import connectToContract from "../../hooks/connect-to-contract"; 5 | 6 | export default (app: Application): Partial => { 7 | return { 8 | before: { 9 | all: [], 10 | find: [disallow()], 11 | get: [disallow()], 12 | create: [connectToContract(app)], 13 | update: [disallow()], 14 | patch: [disallow()], 15 | remove: [disallow()], 16 | }, 17 | 18 | after: { 19 | all: [], 20 | find: [], 21 | get: [], 22 | create: [], 23 | update: [], 24 | patch: [], 25 | remove: [], 26 | }, 27 | 28 | error: { 29 | all: [], 30 | find: [], 31 | get: [], 32 | create: [], 33 | update: [], 34 | patch: [], 35 | remove: [], 36 | }, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/services/commitment/commitment.hooks.ts: -------------------------------------------------------------------------------- 1 | import { disallow } from "feathers-hooks-common"; 2 | import { Application, HooksObject } from "@feathersjs/feathers"; 3 | 4 | import connectToContract from "../../hooks/connect-to-contract"; 5 | 6 | export default (app: Application): Partial => { 7 | return { 8 | before: { 9 | all: [], 10 | find: [disallow()], 11 | get: [disallow()], 12 | create: [connectToContract(app)], 13 | update: [disallow()], 14 | patch: [disallow()], 15 | remove: [disallow()], 16 | }, 17 | 18 | after: { 19 | all: [], 20 | find: [], 21 | get: [], 22 | create: [], 23 | update: [], 24 | patch: [], 25 | remove: [], 26 | }, 27 | 28 | error: { 29 | all: [], 30 | find: [], 31 | get: [], 32 | create: [], 33 | update: [], 34 | patch: [], 35 | remove: [], 36 | }, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /blockchain/script/ZKWordle.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/ZKWordle.sol"; 6 | import "../src/check_guess.sol"; 7 | import "../src/check_stats.sol"; 8 | import "forge-std/console.sol"; 9 | 10 | contract ZKWordleScript is Script { 11 | function run() public { 12 | vm.startBroadcast(); 13 | GuessVerifier guessVerifier = new GuessVerifier(); 14 | StatsVerifier statsVerifier = new StatsVerifier(); 15 | ZKWordle zkWordle = new ZKWordle( 16 | address(guessVerifier), 17 | address(statsVerifier), 18 | msg.sender 19 | ); 20 | vm.stopBroadcast(); 21 | _copyBroadcast(); 22 | } 23 | 24 | function _copyBroadcast() internal { 25 | string[] memory inputs = new string[](1); 26 | inputs[0] = "script/copy-broadcast.sh"; 27 | vm.ffi(inputs); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/services/stats/stats.hooks.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "@feathersjs/express"; 2 | import { HooksObject } from "@feathersjs/feathers"; 3 | import { disallow } from "feathers-hooks-common"; 4 | 5 | import connectToContract from "../../hooks/connect-to-contract"; 6 | 7 | export default (app: Application): Partial => { 8 | return { 9 | before: { 10 | all: [], 11 | find: [disallow()], 12 | get: [disallow()], 13 | create: [connectToContract(app)], 14 | update: [disallow()], 15 | patch: [disallow()], 16 | remove: [disallow()], 17 | }, 18 | 19 | after: { 20 | all: [], 21 | find: [], 22 | get: [], 23 | create: [], 24 | update: [], 25 | patch: [], 26 | remove: [], 27 | }, 28 | 29 | error: { 30 | all: [], 31 | find: [], 32 | get: [], 33 | create: [], 34 | update: [], 35 | patch: [], 36 | remove: [], 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /circuits/src/check_stats.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.0.3; 2 | include "./guess/guess_single.circom"; 3 | 4 | template CheckWordleStats() { 5 | signal input guesses[6][5]; 6 | //"Word of the day", private input 7 | signal input solution[5]; 8 | signal input salt; 9 | //Solution commitment 10 | signal input commitment; 11 | signal output clues[6][5]; 12 | 13 | component checkGuess[6]; 14 | for (var i = 0; i < 6; i++) { 15 | checkGuess[i] = SingleGuessCheck(); 16 | checkGuess[i].salt <== salt; 17 | checkGuess[i].commitment <== commitment; 18 | for (var j = 0; j < 5; j++) { 19 | checkGuess[i].solution[j] <== solution[j]; 20 | checkGuess[i].guess[j] <== guesses[i][j]; 21 | } 22 | //Constraining the output after all the inputs were assigned 23 | for (var j = 0; j < 5; j++) { 24 | clues[i][j] <== checkGuess[i].clue[j]; 25 | } 26 | } 27 | } 28 | 29 | component main{public [commitment]} = CheckWordleStats(); -------------------------------------------------------------------------------- /circuits/scripts/rename-verifiers.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const verifierRegex = /contract Verifier/; 4 | const solidityRegex = /pragma solidity \^\d+\.\d+\.\d+/; 5 | const pairingRegex = "Pairing"; 6 | 7 | let guessVerifier = fs.readFileSync("./blockchain/src/check_guess.sol", { 8 | encoding: "utf-8", 9 | }); 10 | guessVerifier = guessVerifier.replace(solidityRegex, "pragma solidity ^0.8.13"); 11 | guessVerifier = guessVerifier.replace(verifierRegex, "contract GuessVerifier"); 12 | fs.writeFileSync("./blockchain/src/check_guess.sol", guessVerifier); 13 | 14 | let statsVerifier = fs.readFileSync("./blockchain/src/check_stats.sol", { 15 | encoding: "utf-8", 16 | }); 17 | statsVerifier = statsVerifier.replace(solidityRegex, "pragma solidity ^0.8.13"); 18 | statsVerifier = statsVerifier.replace(verifierRegex, "contract StatsVerifier"); 19 | statsVerifier = statsVerifier.replaceAll(pairingRegex, "Pairing2"); 20 | fs.writeFileSync("./blockchain/src/check_stats.sol", statsVerifier); 21 | -------------------------------------------------------------------------------- /frontend/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /backend/src/services/salt-storage/salt-storage.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `SaltStorage` service on path `/salt-storage` 2 | import { ServiceAddons } from "@feathersjs/feathers"; 3 | import { Application } from "../../declarations"; 4 | import { SaltStorage } from "./salt-storage.class"; 5 | import createModel from "../../models/salt-storage.model"; 6 | import hooks from "./salt-storage.hooks"; 7 | 8 | // Add this service to the service type index 9 | declare module "../../declarations" { 10 | interface ServiceTypes { 11 | "salt-storage": SaltStorage & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application): void { 16 | const saltModel = createModel(app); 17 | const options = { 18 | Model: saltModel, 19 | paginate: app.get("paginate"), 20 | }; 21 | 22 | // Initialize our service with any options it requires 23 | app.use("/salt-storage", new SaltStorage(options, app)); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service("salt-storage"); 27 | 28 | service.hooks(hooks); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/components/stats/StatBar.tsx: -------------------------------------------------------------------------------- 1 | import { GameStats } from '../../lib/localStorage' 2 | import { 3 | TOTAL_TRIES_TEXT, 4 | SUCCESS_RATE_TEXT, 5 | CURRENT_STREAK_TEXT, 6 | BEST_STREAK_TEXT, 7 | } from '../../constants/strings' 8 | 9 | type Props = { 10 | gameStats: GameStats 11 | } 12 | 13 | const StatItem = ({ 14 | label, 15 | value, 16 | }: { 17 | label: string 18 | value: string | number 19 | }) => { 20 | return ( 21 |
22 |
{value}
23 |
{label}
24 |
25 | ) 26 | } 27 | 28 | export const StatBar = ({ gameStats }: Props) => { 29 | return ( 30 |
31 | 32 | 33 | 34 | 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hannah Park 4 | Copyright (c) 2022 Alex Kuzmin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /backend/src/models/salt-storage.model.ts: -------------------------------------------------------------------------------- 1 | // See https://sequelize.org/master/manual/model-basics.html 2 | // for more of what you can do here. 3 | import { Sequelize, DataTypes, Model } from "sequelize"; 4 | import { Application } from "../declarations"; 5 | import { HookReturn } from "sequelize/types/hooks"; 6 | 7 | export default function (app: Application): typeof Model { 8 | const sequelizeClient: Sequelize = app.get("sequelizeClient"); 9 | const saltStorage = sequelizeClient.define( 10 | "salt_storage", 11 | { 12 | solutionIndex: { 13 | type: DataTypes.INTEGER, 14 | allowNull: false, 15 | }, 16 | salt: { 17 | type: DataTypes.BIGINT, 18 | allowNull: false, 19 | }, 20 | }, 21 | { 22 | hooks: { 23 | beforeCount(options: any): HookReturn { 24 | options.raw = true; 25 | }, 26 | }, 27 | } 28 | ); 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | (saltStorage as any).associate = function (models: any): void { 32 | // Define associations here 33 | // See https://sequelize.org/master/manual/assocs.html 34 | }; 35 | 36 | return saltStorage; 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | permissions: 7 | contents: write 8 | jobs: 9 | build-and-deploy: 10 | environment: github-pages 11 | concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession. 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 🛎️ 15 | uses: actions/checkout@v3 16 | 17 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 18 | run: | 19 | cd frontend 20 | npm ci 21 | npm run build 22 | env: 23 | REACT_APP_SERVER_URL: ${{ secrets.REACT_APP_SERVER_URL }} 24 | REACT_APP_GAME_NAME: ${{ secrets.REACT_APP_GAME_NAME }} 25 | REACT_APP_GAME_DESCRIPTION: ${{ secrets.REACT_APP_GAME_DESCRIPTION }} 26 | 27 | - name: Deploy 🚀 28 | uses: JamesIves/github-pages-deploy-action@v4 29 | with: 30 | folder: frontend/build # The folder the action should deploy. 31 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # ZK-Wordle Backend 2 | 3 | > ZK-Wordle Backend 4 | 5 | ## About 6 | 7 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 8 | 9 | ## Getting Started 10 | 11 | Getting up and running is as easy as 1, 2, 3. 12 | 13 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 14 | 2. Install your dependencies 15 | 16 | ``` 17 | cd path/to/ZK-Wordle Backend 18 | npm install 19 | ``` 20 | 21 | 3. Start your app 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Testing 28 | 29 | Simply run `npm test` and all your tests in the `test/` directory will be run. 30 | 31 | ## Scaffolding 32 | 33 | Feathers has a powerful command line interface. Here are a few things it can do: 34 | 35 | ``` 36 | $ npm install -g @feathersjs/cli # Install Feathers CLI 37 | 38 | $ feathers generate service # Generate a new Service 39 | $ feathers generate hook # Generate a new Hook 40 | $ feathers help # Show all commands 41 | ``` 42 | 43 | ## Help 44 | 45 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 46 | -------------------------------------------------------------------------------- /backend/src/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize"; 2 | import { Application } from "./declarations"; 3 | 4 | export default function (app: Application): void { 5 | const connectionString = app.get("postgres"); 6 | console.log("Connection string:", connectionString); 7 | const PRODUCTION = process.env.NODE_ENV === "production"; 8 | const sequelize = new Sequelize(connectionString, { 9 | dialect: "postgres", 10 | dialectOptions: { 11 | ssl: PRODUCTION && { 12 | require: true, 13 | rejectUnauthorized: false, 14 | }, 15 | }, 16 | logging: console.log, 17 | define: { 18 | freezeTableName: true, 19 | }, 20 | ssl: PRODUCTION, 21 | }); 22 | const oldSetup = app.setup; 23 | 24 | app.set("sequelizeClient", sequelize); 25 | 26 | app.setup = function (...args): Application { 27 | const result = oldSetup.apply(this, args); 28 | 29 | // Set up data relationships 30 | const models = sequelize.models; 31 | Object.keys(models).forEach((name) => { 32 | if ("associate" in models[name]) { 33 | (models[name] as any).associate(models); 34 | } 35 | }); 36 | 37 | // Sync to the database 38 | app.set("sequelizeSync", sequelize.sync()); 39 | 40 | return result; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/alerts/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import { Transition } from '@headlessui/react' 3 | import classNames from 'classnames' 4 | 5 | type Props = { 6 | isOpen: boolean 7 | message: string 8 | variant?: 'success' | 'error' 9 | topMost?: boolean 10 | } 11 | 12 | export const Alert = ({ 13 | isOpen, 14 | message, 15 | variant = 'error', 16 | topMost = false, 17 | }: Props) => { 18 | const classes = classNames( 19 | 'fixed z-20 top-14 left-1/2 transform -translate-x-1/2 max-w-sm shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden', 20 | { 21 | 'bg-rose-500 text-white': variant === 'error', 22 | 'bg-blue-500 text-white': variant === 'success', 23 | } 24 | ) 25 | 26 | return ( 27 | 37 |
38 |
39 |

{message}

40 |
41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/lib/share.test.ts: -------------------------------------------------------------------------------- 1 | import { generateEmojiGrid } from './share' 2 | 3 | const mockSolutionGetter = jest.fn() 4 | jest.mock('./words', () => ({ 5 | ...jest.requireActual('./words'), 6 | get solution() { 7 | return mockSolutionGetter() 8 | }, 9 | })) 10 | 11 | // TODO fix tests 12 | // describe('generateEmojiGrid', () => { 13 | // test('generates grid for ascii', () => { 14 | // const guesses = ['EDCBA', 'VWXYZ', 'ABCDE'] 15 | // const tiles = ['C', 'P', 'A'] // Correct, Present, Absent 16 | // mockSolutionGetter.mockReturnValue('ABCDE') 17 | 18 | // const grid = generateEmojiGrid(guesses, tiles) 19 | // const gridParts = grid.split('\n') 20 | // expect(gridParts[0]).toBe('PPCPP') 21 | // expect(gridParts[1]).toBe('AAAAA') 22 | // expect(gridParts[2]).toBe('CCCCC') 23 | // }) 24 | // test('generates grid for ascii', () => { 25 | // const guesses = ['5️⃣4️⃣3️⃣2️⃣1️⃣', '♠️♥️♦️♣️🔔', '1️⃣2️⃣3️⃣4️⃣5️⃣'] 26 | // const tiles = ['C', 'P', 'A'] // Correct, Present, Absemt 27 | // mockSolutionGetter.mockReturnValue('1️⃣2️⃣3️⃣4️⃣5️⃣') 28 | 29 | // const grid = generateEmojiGrid(guesses, tiles) 30 | // const gridParts = grid.split('\n') 31 | // expect(gridParts[0]).toBe('PPCPP') 32 | // expect(gridParts[1]).toBe('AAAAA') 33 | // expect(gridParts[2]).toBe('CCCCC') 34 | // }) 35 | // }) 36 | -------------------------------------------------------------------------------- /frontend/src/types/snarkjs.d.ts: -------------------------------------------------------------------------------- 1 | /** Declaration file generated by dts-gen */ 2 | 3 | declare module 'snarkjs' { 4 | export = snarkjs 5 | 6 | const snarkjs: { 7 | groth16: { 8 | exportSolidityCallData: any 9 | fullProve: any 10 | prove: any 11 | verify: any 12 | } 13 | plonk: { 14 | exportSolidityCallData: any 15 | fullProve: any 16 | prove: any 17 | setup: any 18 | verify: any 19 | } 20 | powersOfTau: { 21 | beacon: any 22 | challengeContribute: any 23 | contribute: any 24 | convert: any 25 | exportChallenge: any 26 | exportJson: any 27 | importResponse: any 28 | newAccumulator: any 29 | preparePhase2: any 30 | truncate: any 31 | verify: any 32 | } 33 | r1cs: { 34 | exportJson: any 35 | info: any 36 | print: any 37 | } 38 | wtns: { 39 | calculate: any 40 | debug: any 41 | exportJson: any 42 | } 43 | zKey: { 44 | beacon: any 45 | bellmanContribute: any 46 | contribute: any 47 | exportBellman: any 48 | exportJson: any 49 | exportSolidityVerifier: any 50 | exportVerificationKey: any 51 | importBellman: any 52 | newZKey: any 53 | verifyFromInit: any 54 | verifyFromR1cs: any 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/components/grid/CompletedRow.tsx: -------------------------------------------------------------------------------- 1 | import { CharStatus } from '../../lib/statuses' 2 | import { Cell } from './Cell' 3 | import { unicodeSplit } from '../../lib/words' 4 | import { ShieldCheckIcon } from '@heroicons/react/outline' 5 | import { GUESS_WAS_VERIFIED } from '../../constants/strings' 6 | 7 | type Props = { 8 | guess: string 9 | status: CharStatus[] 10 | isRevealing?: boolean 11 | guessProven: boolean 12 | } 13 | 14 | export const CompletedRow = ({ 15 | guess, 16 | status, 17 | isRevealing, 18 | guessProven, 19 | }: Props) => { 20 | const splitGuess = unicodeSplit(guess) 21 | return ( 22 |
28 |
29 |
30 | {splitGuess.map((letter, i) => ( 31 | 39 | ))} 40 |
41 | {guessProven && ( 42 |
43 | 44 |
45 | )} 46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/components/modals/SettingsToggle.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames' 2 | 3 | type Props = { 4 | settingName: string 5 | flag: boolean 6 | handleFlag: Function 7 | description?: string 8 | } 9 | 10 | export const SettingsToggle = ({ 11 | settingName, 12 | flag, 13 | handleFlag, 14 | description, 15 | }: Props) => { 16 | const toggleHolder = classnames( 17 | 'w-14 h-8 flex shrink-0 items-center bg-gray-300 rounded-full p-1 duration-300 ease-in-out cursor-pointer', 18 | { 19 | 'bg-green-400': flag, 20 | } 21 | ) 22 | const toggleButton = classnames( 23 | 'bg-white w-6 h-6 rounded-full shadow-md transform duration-300 ease-in-out cursor-pointer', 24 | { 25 | 'translate-x-6': flag, 26 | } 27 | ) 28 | 29 | return ( 30 | <> 31 |
32 |
33 |

{settingName}

34 | {description && ( 35 |

36 | {description} 37 |

38 | )} 39 |
40 |
handleFlag(!flag)}> 41 |
42 |
43 |
44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/types/snarkjs.d.ts: -------------------------------------------------------------------------------- 1 | /** Declaration file generated by dts-gen */ 2 | 3 | declare module "snarkjs" { 4 | export = snarkjs; 5 | 6 | const snarkjs: { 7 | groth16: { 8 | exportSolidityCallData: any; 9 | fullProve: any; 10 | prove: any; 11 | verify: any; 12 | }; 13 | plonk: { 14 | exportSolidityCallData: any; 15 | fullProve: any; 16 | prove: any; 17 | setup: any; 18 | verify: any; 19 | }; 20 | powersOfTau: { 21 | beacon: any; 22 | challengeContribute: any; 23 | contribute: any; 24 | convert: any; 25 | exportChallenge: any; 26 | exportJson: any; 27 | importResponse: any; 28 | newAccumulator: any; 29 | preparePhase2: any; 30 | truncate: any; 31 | verify: any; 32 | }; 33 | r1cs: { 34 | exportJson: any; 35 | info: any; 36 | print: any; 37 | }; 38 | wtns: { 39 | calculate: any; 40 | debug: any; 41 | exportJson: any; 42 | }; 43 | zKey: { 44 | beacon: any; 45 | bellmanContribute: any; 46 | contribute: any; 47 | exportBellman: any; 48 | exportJson: any; 49 | exportSolidityVerifier: any; 50 | exportVerificationKey: any; 51 | importBellman: any; 52 | newZKey: any; 53 | verifyFromInit: any; 54 | verifyFromR1cs: any; 55 | }; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/hooks/connect-to-contract.ts: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | import { Application, Hook, HookContext } from "@feathersjs/feathers"; 4 | import { ethers } from "ethers"; 5 | import contractAbi from "../blockchain/ZKWordle.sol/ZKWordle.json"; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | export default (app: Application): Hook => { 9 | let wallet: ethers.Wallet; 10 | 11 | if (!app.get("accountPrivateKey")) { 12 | throw new Error("Private key cannot be empty"); 13 | } else if (!app.get("chainId")) { 14 | throw new Error("chainId cannot be empty"); 15 | } else { 16 | wallet = new ethers.Wallet( 17 | app.get("accountPrivateKey"), 18 | new ethers.providers.JsonRpcProvider(app.get("rpcUrl")) 19 | ); 20 | } 21 | //TODO make compatible with local deployment 22 | const zkWordleContract = new ethers.Contract( 23 | process.env.NODE_ENV === "production" 24 | ? "0xD2936b30A608F63C925bF19f3da44EC8fA4C6170" 25 | : "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", 26 | contractAbi.abi, 27 | wallet 28 | ); 29 | return async (context: HookContext): Promise => { 30 | context.params.zkWordleContract = zkWordleContract; 31 | context.params.wallet = wallet; 32 | 33 | return context; 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/src/components/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChartBarIcon, 3 | CogIcon, 4 | InformationCircleIcon, 5 | } from '@heroicons/react/outline' 6 | import { GAME_TITLE } from '../../constants/strings' 7 | import { Profile } from '../profile/Profile' 8 | 9 | type Props = { 10 | setIsInfoModalOpen: (value: boolean) => void 11 | setIsStatsModalOpen: (value: boolean) => void 12 | setIsSettingsModalOpen: (value: boolean) => void 13 | } 14 | 15 | export const Navbar = ({ 16 | setIsInfoModalOpen, 17 | setIsStatsModalOpen, 18 | setIsSettingsModalOpen, 19 | }: Props) => { 20 | return ( 21 |
22 |
23 | setIsInfoModalOpen(true)} 26 | /> 27 |

{GAME_TITLE}

28 |
29 | setIsStatsModalOpen(true)} 32 | /> 33 | setIsSettingsModalOpen(true)} 36 | /> 37 | 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /circuits/scripts/compile-circuit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd circuits 4 | if [ -f ./powers-of-tau/powersOfTau28_hez_final_12.ptau ]; then 5 | echo "powersOfTau28_hez_final_12.ptau already exists. Skipping." 6 | else 7 | echo 'Downloading powersOfTau28_hez_final_12.ptau' 8 | wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_12.ptau -O ./powers-of-tau/powersOfTau28_hez_final_12.ptau 9 | fi 10 | 11 | echo "Compiling circuit.circom..." 12 | find ./src -type f -name "*.circom" -maxdepth 1 13 | for circuit in `find ./src -type f -name "*.circom" -maxdepth 1`; do 14 | name=`basename $circuit .circom` 15 | # compile circuit 16 | circom ./src/${name}.circom --r1cs --wasm --sym --output compiled 17 | snarkjs r1cs info compiled/${name}.r1cs 18 | cp compiled/${name}_js/${name}.wasm ./../backend/src/zk/${name}.wasm 19 | 20 | # Create and export the zkey 21 | 22 | snarkjs groth16 setup compiled/${name}.r1cs powers-of-tau/powersOfTau28_hez_final_12.ptau compiled/${name}_0000.zkey 23 | snarkjs zkey contribute compiled/${name}_0000.zkey compiled/${name}_final.zkey --name="1st Contributor Name" -v 24 | cp compiled/${name}_final.zkey ./../backend/src/zk/${name}_final.zkey 25 | cd compiled 26 | snarkjs zkey export verificationkey ${name}_final.zkey ${name}_verification_key.json 27 | 28 | # generate solidity contract 29 | snarkjs zkey export solidityverifier ${name}_final.zkey ./../../blockchain/src/${name}.sol 30 | cd .. 31 | done 32 | cd .. -------------------------------------------------------------------------------- /frontend/src/components/modals/ConnectWalletModal.tsx: -------------------------------------------------------------------------------- 1 | import { useConnect } from 'wagmi' 2 | import { 3 | CONNECT_WALLET_MSG, 4 | CONNECT_WALLET_TITLE, 5 | } from '../../constants/strings' 6 | import { BaseModal } from './BaseModal' 7 | 8 | type Props = { 9 | isOpen: boolean 10 | handleClose: () => void 11 | } 12 | 13 | export const ConnectWalletModal = ({ isOpen, handleClose }: Props) => { 14 | const { connect, connectors, error, isLoading, pendingConnector } = 15 | useConnect() 16 | 17 | return ( 18 | 24 |
{CONNECT_WALLET_MSG}
25 | {connectors.map((connector) => ( 26 | 39 | ))} 40 | 41 | {error &&
{error.message}
} 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { MAX_CHALLENGES } from '../../constants/settings' 2 | import { CharStatus } from '../../lib/statuses' 3 | import { CompletedRow } from './CompletedRow' 4 | import { CurrentRow } from './CurrentRow' 5 | import { EmptyRow } from './EmptyRow' 6 | 7 | type Props = { 8 | guesses: string[] 9 | statuses: Map 10 | currentGuess: string 11 | isRevealing?: boolean 12 | currentRowClassName: string 13 | guessesProven: Map 14 | } 15 | 16 | export const Grid = ({ 17 | guesses, 18 | statuses, 19 | currentGuess, 20 | isRevealing, 21 | currentRowClassName, 22 | guessesProven, 23 | }: Props) => { 24 | const empties = 25 | guesses.length < MAX_CHALLENGES - 1 26 | ? Array.from( 27 | Array(MAX_CHALLENGES - (!isRevealing ? 1 : 0) - guesses.length) 28 | ) 29 | : [] 30 | 31 | return ( 32 | <> 33 | {guesses.map((guess, i) => ( 34 | 0 ? statuses.get(guess) ?? [] : []} 38 | isRevealing={isRevealing && guesses.length - 1 === i} 39 | guessProven={guessesProven.get(guess) ?? false} 40 | /> 41 | ))} 42 | {!isRevealing && guesses.length < MAX_CHALLENGES && ( 43 | 44 | )} 45 | {empties.map((_, i) => ( 46 | 47 | ))} 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/lib/stats.ts: -------------------------------------------------------------------------------- 1 | import { MAX_CHALLENGES } from '../constants/settings' 2 | import { 3 | GameStats, 4 | loadStatsFromLocalStorage, 5 | saveStatsToLocalStorage, 6 | } from './localStorage' 7 | 8 | // In stats array elements 0-5 are successes in 1-6 trys 9 | 10 | export const addStatsForCompletedGame = ( 11 | gameStats: GameStats, 12 | count: number 13 | ) => { 14 | // Count is number of incorrect guesses before end. 15 | const stats = { ...gameStats } 16 | 17 | stats.totalGames += 1 18 | 19 | if (count >= MAX_CHALLENGES) { 20 | // A fail situation 21 | stats.currentStreak = 0 22 | stats.gamesFailed += 1 23 | } else { 24 | stats.winDistribution[count] += 1 25 | stats.currentStreak += 1 26 | 27 | if (stats.bestStreak < stats.currentStreak) { 28 | stats.bestStreak = stats.currentStreak 29 | } 30 | } 31 | 32 | stats.successRate = getSuccessRate(stats) 33 | 34 | saveStatsToLocalStorage(stats) 35 | return stats 36 | } 37 | 38 | const defaultStats: GameStats = { 39 | winDistribution: Array.from(new Array(MAX_CHALLENGES), () => 0), 40 | gamesFailed: 0, 41 | currentStreak: 0, 42 | bestStreak: 0, 43 | totalGames: 0, 44 | successRate: 0, 45 | } 46 | 47 | export const loadStats = () => { 48 | return loadStatsFromLocalStorage() || defaultStats 49 | } 50 | 51 | const getSuccessRate = (gameStats: GameStats) => { 52 | const { totalGames, gamesFailed } = gameStats 53 | 54 | return Math.round( 55 | (100 * (totalGames - gamesFailed)) / Math.max(totalGames, 1) 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/components/modals/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import { BaseModal } from './BaseModal' 2 | import { SettingsToggle } from './SettingsToggle' 3 | import { 4 | HARD_MODE_DESCRIPTION, 5 | HIGH_CONTRAST_MODE_DESCRIPTION, 6 | } from '../../constants/strings' 7 | 8 | type Props = { 9 | isOpen: boolean 10 | handleClose: () => void 11 | isHardMode: boolean 12 | handleHardMode: Function 13 | isDarkMode: boolean 14 | handleDarkMode: Function 15 | isHighContrastMode: boolean 16 | handleHighContrastMode: Function 17 | } 18 | 19 | export const SettingsModal = ({ 20 | isOpen, 21 | handleClose, 22 | isHardMode, 23 | handleHardMode, 24 | isDarkMode, 25 | handleDarkMode, 26 | isHighContrastMode, 27 | handleHighContrastMode, 28 | }: Props) => { 29 | return ( 30 | 31 |
32 | 38 | 43 | 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/lib/statuses.ts: -------------------------------------------------------------------------------- 1 | import { asAsciiArray } from './asAsciiArray' 2 | import { Groth16Proof, requestProof } from './../zk/prove' 3 | import { unicodeSplit } from './words' 4 | import { Application } from '@feathersjs/feathers' 5 | 6 | export type CharStatus = 'absent' | 'present' | 'correct' 7 | 8 | export const getStatuses = ( 9 | guesses: string[], 10 | statuses: Map 11 | ): { [key: string]: CharStatus } => { 12 | const charObj: { [key: string]: CharStatus } = {} 13 | 14 | guesses.forEach((guess) => { 15 | let status = statuses.get(guess) ?? [] 16 | unicodeSplit(guess).forEach((letter, i) => { 17 | if (status[i] === 'correct') charObj[letter] = 'correct' 18 | if (!charObj[letter] && status[i] === 'present') 19 | charObj[letter] = 'present' 20 | if (!charObj[letter]) charObj[letter] = 'absent' 21 | }) 22 | }) 23 | 24 | return charObj 25 | } 26 | 27 | export interface StatusesAndProof { 28 | statuses: CharStatus[] 29 | proof: Groth16Proof 30 | } 31 | 32 | export const getGuessStatuses = async ( 33 | feathersClient: Application, 34 | guess: string 35 | ): Promise => { 36 | return requestProof(feathersClient, asAsciiArray(guess)).then((proof) => { 37 | let clue = proof.publicSignals.slice(0, 5).map((ascii) => Number(ascii)) 38 | return { 39 | statuses: Array.from( 40 | clue 41 | .map((status) => 42 | status === 0 ? 'absent' : status === 1 ? 'correct' : 'present' 43 | ) 44 | .values() 45 | ), 46 | proof: proof, 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/components/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useAccount, useDisconnect, useEnsAvatar, useEnsName } from 'wagmi' 3 | import { ConnectWalletModal } from '../modals/ConnectWalletModal' 4 | 5 | export function Profile() { 6 | const [isConnectionModalOpen, setConnectionModalOpen] = useState(false) 7 | const { address, isConnected } = useAccount() 8 | const { data: ensName } = useEnsName({ address }) 9 | const { data: ensAvatar } = useEnsAvatar({ addressOrName: address }) 10 | const { disconnect } = useDisconnect() 11 | 12 | useEffect(() => { 13 | if (!isConnected) { 14 | setConnectionModalOpen(true) 15 | } 16 | }, [isConnected]) 17 | 18 | if (isConnected) { 19 | return ( 20 |
21 | {ensName && } 22 |
23 | {ensName 24 | ? `${ensName} (${ 25 | address?.slice(0, 4) + '...' + address?.slice(-2) 26 | })` 27 | : address?.slice(0, 4) + '...' + address?.slice(-2)} 28 |
29 | 36 |
37 | ) 38 | } 39 | 40 | return ( 41 |
42 | {!isConnected && ( 43 | 49 | )} 50 | { 53 | if (isConnected) setConnectionModalOpen(false) 54 | }} 55 | /> 56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZK-Wordle 2 | 3 | Wordle game implemented using the Zero-Knowledge Proofs. This project is my first exploration of zero-knowledge proofs. Check out [this series of articles](https://alexkuzmin.io/posts/zk-wordle-1/) to learn the story behind it. 4 | 5 | ## Compile the circuit, generate the reference zKey and verifier smart contract 6 | 7 | ``` 8 | npm install 9 | npm run compile 10 | ``` 11 | 12 | You will be asked to provide random entropy text during the circuits compilation. 13 | 14 | ## Get local chain up and deploy the contract 15 | 16 | Start the local chain: 17 | 18 | ```bash 19 | cd blockchain 20 | anvil 21 | ``` 22 | 23 | Rename `blockchain/.env.example` to `blockchain/.env` and paste the private key of an Ethereum account you intend to use as a deployer. 24 | 25 | Deploy the contract in another terminal: 26 | 27 | ```bash 28 | forge script script/ZKWordle.s.sol --ffi --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -vvv --broadcast 29 | ``` 30 | 31 | This is an Anvil test account's private key - don't use it in production, everyone else knows it! 32 | If you are deploying on a public chain, don't forget to verify the contract on Etherscan for others' convenience: 33 | 34 | ```bash 35 | forge script script/ZKWordle.s.sol --ffi --rpc-url --private-key -vvv --broadcast --etherscan-api-key --verify 36 | ``` 37 | 38 | ## Run the backend 39 | 40 | 1. Rename `backend/.env.example` to `backend/.env` and paste the private key of an Ethereum account you intend to use as a signer in the backend. 41 | 2. Run 42 | 43 | ```bash 44 | cd backend 45 | npm install 46 | npm run start-dev 47 | ``` 48 | 49 | ## Run the frontend 50 | 51 | 1. Rename `frontend/.env.example` to `frontend/.env` 52 | 2. Run 53 | 54 | ```bash 55 | cd frontend 56 | npm install 57 | npm run start 58 | ``` 59 | -------------------------------------------------------------------------------- /backend/test/app.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Server } from 'http'; 3 | import url from 'url'; 4 | import axios from 'axios'; 5 | 6 | import app from '../src/app'; 7 | 8 | const port = app.get('port') || 8998; 9 | const getUrl = (pathname?: string): string => url.format({ 10 | hostname: app.get('host') || 'localhost', 11 | protocol: 'http', 12 | port, 13 | pathname 14 | }); 15 | 16 | describe('Feathers application tests (with jest)', () => { 17 | let server: Server; 18 | 19 | beforeAll(done => { 20 | server = app.listen(port); 21 | server.once('listening', () => done()); 22 | }); 23 | 24 | afterAll(done => { 25 | server.close(done); 26 | }); 27 | 28 | it('starts and shows the index page', async () => { 29 | expect.assertions(1); 30 | 31 | const { data } = await axios.get(getUrl()); 32 | 33 | expect(data.indexOf('')).not.toBe(-1); 34 | }); 35 | 36 | describe('404', () => { 37 | it('shows a 404 HTML page', async () => { 38 | expect.assertions(2); 39 | 40 | try { 41 | await axios.get(getUrl('path/to/nowhere'), { 42 | headers: { 43 | 'Accept': 'text/html' 44 | } 45 | }); 46 | } catch (error: any) { 47 | const { response } = error; 48 | 49 | expect(response.status).toBe(404); 50 | expect(response.data.indexOf('')).not.toBe(-1); 51 | } 52 | }); 53 | 54 | it('shows a 404 JSON error without stack trace', async () => { 55 | expect.assertions(4); 56 | 57 | try { 58 | await axios.get(getUrl('path/to/nowhere')); 59 | } catch (error: any) { 60 | const { response } = error; 61 | 62 | expect(response.status).toBe(404); 63 | expect(response.data.code).toBe(404); 64 | expect(response.data.message).toBe('Page not found'); 65 | expect(response.data.name).toBe('NotFound'); 66 | } 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /backend/src/services/commitment/commitment.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, MemoryServiceOptions } from "feathers-memory"; 2 | import { Application } from "../../declarations"; 3 | import { Params } from "@feathersjs/feathers"; 4 | import { BigNumber, ethers } from "ethers"; 5 | import { solution, solutionIndex } from "../../utils/words"; 6 | import { buildPoseidon } from "circomlibjs"; 7 | import { asAsciiArray } from "../../utils/asAsciiArray"; 8 | 9 | export class Commitment extends Service { 10 | app: Application; 11 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | constructor(options: Partial, app: Application) { 13 | super(options); 14 | this.app = app; 15 | } 16 | 17 | async create(data: any, params?: Params) { 18 | const salt = ( 19 | await this.app 20 | .service("salt") 21 | .create({ solutionIndex: solutionIndex }, {}) 22 | ).salt; 23 | 24 | const asciiSolution = asAsciiArray(solution); 25 | const poseidon = await buildPoseidon(); 26 | //Converting solution to a single number in the same way as the circuit does. 27 | let solutionAsNum = 0; 28 | for (let i = 0; i < asciiSolution.length; i++) { 29 | solutionAsNum += asciiSolution[i] * Math.pow(100, i); 30 | } 31 | const hashedSolution = BigNumber.from( 32 | poseidon.F.toObject(poseidon([solutionAsNum, salt])) 33 | ); 34 | console.log("Commitment: " + hashedSolution); 35 | 36 | const hashedTxData = ethers.utils.defaultAbiCoder.encode( 37 | ["uint256", "uint256"], 38 | [solutionIndex, hashedSolution] 39 | ); 40 | const message = ethers.utils.solidityKeccak256(["bytes"], [hashedTxData]); 41 | const signature = await params?.wallet.signMessage( 42 | ethers.utils.arrayify(message) 43 | ); 44 | 45 | const response = { 46 | solutionIndex: solutionIndex, 47 | hashedSolution: hashedSolution, 48 | signature: signature, 49 | }; 50 | return Promise.resolve(response); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/services/salt/salt.class.ts: -------------------------------------------------------------------------------- 1 | import { Paginated, Params } from "@feathersjs/feathers"; 2 | import { Service, MemoryServiceOptions } from "feathers-memory"; 3 | import { Application } from "../../declarations"; 4 | 5 | interface SaltRequest { 6 | solutionIndex: number; 7 | } 8 | 9 | interface SaltResponse { 10 | solutionIndex: number; 11 | salt: bigint; 12 | } 13 | 14 | export class Salt extends Service { 15 | private app: Application; 16 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | constructor(options: Partial, app: Application) { 18 | super(options); 19 | this.app = app; 20 | } 21 | 22 | async get(id: number, params?: Params): Promise { 23 | console.log("Retrieving salt from DB"); 24 | const dbSalt = (await this.app.service("salt-storage").find({ 25 | query: { 26 | solutionIndex: id, 27 | }, 28 | })) as Paginated; 29 | const saltResponse = dbSalt ? dbSalt.data[0] : undefined; 30 | console.log("Retrieved salt:", saltResponse); 31 | return Promise.resolve(saltResponse); 32 | } 33 | 34 | async create(data: SaltRequest, params: Params): Promise { 35 | let saltResponse = await this.get(data.solutionIndex); 36 | if (!saltResponse) { 37 | const salt = Math.random() * 1e18; 38 | console.log( 39 | `Set new salt ${salt} for the solution index ${data.solutionIndex}` 40 | ); 41 | saltResponse = { 42 | solutionIndex: data.solutionIndex, 43 | salt: BigInt(salt), 44 | }; 45 | console.log("Writing salt to DB"); 46 | this.app 47 | .service("salt-storage") 48 | ?.create(saltResponse) 49 | .then((message) => console.log("Created salt", message)); 50 | } else { 51 | console.log( 52 | `Fetched salt ${saltResponse.salt} for the solution index ${data.solutionIndex} from DB` 53 | ); 54 | } 55 | return Promise.resolve(saltResponse); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import favicon from "serve-favicon"; 3 | import compress from "compression"; 4 | import helmet from "helmet"; 5 | import cors from "cors"; 6 | 7 | import feathers from "@feathersjs/feathers"; 8 | import configuration from "@feathersjs/configuration"; 9 | import express from "@feathersjs/express"; 10 | 11 | import { Application } from "./declarations"; 12 | import logger from "./logger"; 13 | import middleware from "./middleware"; 14 | import services from "./services"; 15 | import appHooks from "./app.hooks"; 16 | import channels from "./channels"; 17 | import { HookContext as FeathersHookContext } from "@feathersjs/feathers"; 18 | import sequelize from "./sequelize"; 19 | // Don't remove this comment. It's needed to format import lines nicely. 20 | 21 | const app: Application = express(feathers()); 22 | export type HookContext = { 23 | app: Application; 24 | } & FeathersHookContext; 25 | 26 | // Load app configuration 27 | app.configure(configuration()); 28 | // Enable security, CORS, compression, favicon and body parsing 29 | app.use( 30 | helmet({ 31 | contentSecurityPolicy: false, 32 | }) 33 | ); 34 | app.use(cors()); 35 | app.use(compress()); 36 | app.use(express.json()); 37 | app.use(express.urlencoded({ extended: true })); 38 | app.use(favicon(path.join(app.get("public"), "favicon.ico"))); 39 | // Host the public folder 40 | app.use("/", express.static(app.get("public"))); 41 | 42 | // Set up Plugins and providers 43 | app.configure(express.rest()); 44 | 45 | app.configure(sequelize); 46 | 47 | // Configure other middleware (see `middleware/index.ts`) 48 | app.configure(middleware); 49 | // Set up our services (see `services/index.ts`) 50 | app.configure(services); 51 | // Set up event channels (see channels.ts) 52 | app.configure(channels); 53 | 54 | // Configure a middleware for 404s and the error handler 55 | app.use(express.notFound()); 56 | app.use(express.errorHandler({ logger } as any)); 57 | 58 | app.hooks(appHooks); 59 | 60 | export default app; 61 | -------------------------------------------------------------------------------- /frontend/src/components/grid/Cell.tsx: -------------------------------------------------------------------------------- 1 | import { CharStatus } from '../../lib/statuses' 2 | import classnames from 'classnames' 3 | import { REVEAL_TIME_MS } from '../../constants/settings' 4 | import { getStoredIsHighContrastMode } from '../../lib/localStorage' 5 | 6 | type Props = { 7 | value?: string 8 | status?: CharStatus 9 | isRevealing?: boolean 10 | isCompleted?: boolean 11 | position?: number 12 | } 13 | 14 | export const Cell = ({ 15 | value, 16 | status, 17 | isRevealing, 18 | isCompleted, 19 | position = 0, 20 | }: Props) => { 21 | const isFilled = value && !isCompleted 22 | const shouldReveal = isRevealing && isCompleted 23 | const animationDelay = `${position * REVEAL_TIME_MS}ms` 24 | const isHighContrast = getStoredIsHighContrastMode() 25 | 26 | const classes = classnames( 27 | 'w-14 h-14 border-solid border-2 flex items-center justify-center mx-0.5 text-4xl font-bold rounded dark:text-white', 28 | { 29 | 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-600': 30 | !status, 31 | 'border-black dark:border-slate-100': value && !status, 32 | 'absent shadowed bg-slate-400 dark:bg-slate-700 text-white border-slate-400 dark:border-slate-700': 33 | status === 'absent', 34 | 'correct shadowed bg-orange-500 text-white border-orange-500': 35 | status === 'correct' && isHighContrast, 36 | 'present shadowed bg-cyan-500 text-white border-cyan-500': 37 | status === 'present' && isHighContrast, 38 | 'correct shadowed bg-green-500 text-white border-green-500': 39 | status === 'correct' && !isHighContrast, 40 | 'present shadowed bg-yellow-500 text-white border-yellow-500': 41 | status === 'present' && !isHighContrast, 42 | 'cell-fill-animation': isFilled, 43 | 'cell-reveal': shouldReveal, 44 | } 45 | ) 46 | 47 | return ( 48 |
49 |
50 | {value} 51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/constants/strings.ts: -------------------------------------------------------------------------------- 1 | export const GAME_TITLE = process.env.REACT_APP_GAME_NAME! 2 | 3 | export const WIN_MESSAGES = ['Great Job!', 'Awesome', 'Well done!'] 4 | export const GAME_COPIED_MESSAGE = 'Game copied to clipboard' 5 | export const NOT_ENOUGH_LETTERS_MESSAGE = 'Not enough letters' 6 | export const WORD_NOT_FOUND_MESSAGE = 'Word not found' 7 | export const HARD_MODE_ALERT_MESSAGE = 8 | 'Hard Mode can only be enabled at the start!' 9 | export const HARD_MODE_DESCRIPTION = 10 | 'Any revealed hints must be used in subsequent guesses' 11 | export const HIGH_CONTRAST_MODE_DESCRIPTION = 'For improved color vision' 12 | export const CORRECT_WORD_MESSAGE = (solution: string) => 13 | `The word was ${solution}` 14 | export const WRONG_SPOT_MESSAGE = (guess: string, position: number) => 15 | `Must use ${guess} in position ${position}` 16 | export const NOT_CONTAINED_MESSAGE = (letter: string) => 17 | `Guess must contain ${letter}` 18 | export const ENTER_TEXT = 'Enter' 19 | export const DELETE_TEXT = 'Delete' 20 | export const STATISTICS_TITLE = 'Statistics' 21 | export const GUESS_DISTRIBUTION_TEXT = 'Guess Distribution' 22 | export const NEW_WORD_TEXT = 'New word in' 23 | export const SHARE_TEXT = 'Share' 24 | export const TOTAL_TRIES_TEXT = 'Total tries' 25 | export const SUCCESS_RATE_TEXT = 'Success rate' 26 | export const CURRENT_STREAK_TEXT = 'Current streak' 27 | export const BEST_STREAK_TEXT = 'Best streak' 28 | export const INCORRECT_PROOF_TEXT = 29 | 'Please make sure that the proof is included in the pasted text' 30 | export const PROOF_VERIFICATION_HINT = "Paste other player's stats to verify" 31 | export const VERIFY_BTN_TEXT = 'Verify stats' 32 | export const GUESS_WAS_VERIFIED = 'The validity of this clue is proven with ZKP' 33 | export const VERIFYING = 'Verifying...' 34 | export const STATS_VALID = 'Stats valid' 35 | export const STATS_INVALID = 'Stats invalid' 36 | export const STATS_WAS_VERIFIED = 'Verified with ZKP' 37 | export const CONNECT_WALLET_TITLE = 'Connect Wallet' 38 | export const CONNECT_WALLET_MSG = 39 | 'Please connect to Görli Ethereum testnet to play ZK-Wordle' 40 | -------------------------------------------------------------------------------- /frontend/src/components/keyboard/Key.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import classnames from 'classnames' 3 | import { CharStatus } from '../../lib/statuses' 4 | import { MAX_WORD_LENGTH, REVEAL_TIME_MS } from '../../constants/settings' 5 | import { getStoredIsHighContrastMode } from '../../lib/localStorage' 6 | 7 | type Props = { 8 | children?: ReactNode 9 | value: string 10 | width?: number 11 | status?: CharStatus 12 | onClick: (value: string) => void 13 | isRevealing?: boolean 14 | } 15 | 16 | export const Key = ({ 17 | children, 18 | status, 19 | width = 40, 20 | value, 21 | onClick, 22 | isRevealing, 23 | }: Props) => { 24 | const keyDelayMs = REVEAL_TIME_MS * MAX_WORD_LENGTH 25 | const isHighContrast = getStoredIsHighContrastMode() 26 | 27 | const classes = classnames( 28 | 'flex items-center justify-center rounded mx-0.5 text-xs font-bold cursor-pointer select-none dark:text-white', 29 | { 30 | 'transition ease-in-out': isRevealing, 31 | 'bg-slate-200 dark:bg-slate-600 hover:bg-slate-300 active:bg-slate-400': 32 | !status, 33 | 'bg-slate-400 dark:bg-slate-800 text-white': status === 'absent', 34 | 'bg-orange-500 hover:bg-orange-600 active:bg-orange-700 text-white': 35 | status === 'correct' && isHighContrast, 36 | 'bg-cyan-500 hover:bg-cyan-600 active:bg-cyan-700 text-white': 37 | status === 'present' && isHighContrast, 38 | 'bg-green-500 hover:bg-green-600 active:bg-green-700 text-white': 39 | status === 'correct' && !isHighContrast, 40 | 'bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white': 41 | status === 'present' && !isHighContrast, 42 | } 43 | ) 44 | 45 | const styles = { 46 | transitionDelay: isRevealing ? `${keyDelayMs}ms` : 'unset', 47 | width: `${width}px`, 48 | height: '58px', 49 | } 50 | 51 | const handleClick: React.MouseEventHandler = (event) => { 52 | onClick(value) 53 | event.currentTarget.blur() 54 | } 55 | 56 | return ( 57 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/lib/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { CharStatus } from './statuses' 2 | const gameStateKey = 'gameState' 3 | const highContrastKey = 'highContrast' 4 | 5 | type StoredGameState = { 6 | guesses: string[] 7 | statuses: Map 8 | solutionIndex: number 9 | } 10 | 11 | export const saveGameStateToLocalStorage = (gameState: StoredGameState) => { 12 | localStorage.setItem( 13 | gameStateKey, 14 | JSON.stringify(gameState, function replacer(key, value) { 15 | if (value instanceof Map) { 16 | return { 17 | dataType: 'Map', 18 | value: Array.from(value.entries()), 19 | } 20 | } else { 21 | return value 22 | } 23 | }) 24 | ) 25 | } 26 | 27 | export const loadGameStateFromLocalStorage = () => { 28 | const state = localStorage.getItem(gameStateKey) 29 | return state 30 | ? (JSON.parse(state, function reviver(key, value) { 31 | if (typeof value === 'object' && value !== null) { 32 | if (value.dataType === 'Map') { 33 | return new Map(value.value) 34 | } 35 | } 36 | return value 37 | }) as StoredGameState) 38 | : null 39 | } 40 | 41 | const gameStatKey = 'gameStats' 42 | 43 | export type GameStats = { 44 | winDistribution: number[] 45 | gamesFailed: number 46 | currentStreak: number 47 | bestStreak: number 48 | totalGames: number 49 | successRate: number 50 | } 51 | 52 | export const saveStatsToLocalStorage = (gameStats: GameStats) => { 53 | localStorage.setItem(gameStatKey, JSON.stringify(gameStats)) 54 | } 55 | 56 | export const loadStatsFromLocalStorage = () => { 57 | const stats = localStorage.getItem(gameStatKey) 58 | return stats ? (JSON.parse(stats) as GameStats) : null 59 | } 60 | 61 | export const setStoredIsHighContrastMode = (isHighContrast: boolean) => { 62 | if (isHighContrast) { 63 | localStorage.setItem(highContrastKey, '1') 64 | } else { 65 | localStorage.removeItem(highContrastKey) 66 | } 67 | } 68 | 69 | export const getStoredIsHighContrastMode = () => { 70 | const highContrast = localStorage.getItem(highContrastKey) 71 | return highContrast === '1' 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import reportWebVitals from './reportWebVitals' 6 | import { AlertProvider } from './context/AlertContext' 7 | import { configureChains, chain, createClient, WagmiConfig } from 'wagmi' 8 | import { alchemyProvider } from 'wagmi/providers/alchemy' 9 | import { infuraProvider } from 'wagmi/providers/infura' 10 | import { publicProvider } from 'wagmi/providers/public' 11 | 12 | import { CoinbaseWalletConnector } from 'wagmi/connectors/coinbaseWallet' 13 | import { InjectedConnector } from 'wagmi/connectors/injected' 14 | import { MetaMaskConnector } from 'wagmi/connectors/metaMask' 15 | import { WalletConnectConnector } from 'wagmi/connectors/walletConnect' 16 | 17 | const { chains, provider, webSocketProvider } = configureChains( 18 | [process.env.NODE_ENV === 'production' ? chain.goerli : chain.localhost], 19 | [ 20 | alchemyProvider({ apiKey: process.env.REACT_APP_ALCHEMY_KEY, priority: 0 }), 21 | infuraProvider({ apiKey: process.env.REACT_APP_INFURA_KEY, priority: 0 }), 22 | publicProvider({ priority: 1 }), 23 | ] 24 | ) 25 | 26 | const client = createClient({ 27 | autoConnect: true, 28 | connectors: [ 29 | new MetaMaskConnector({ chains }), 30 | new CoinbaseWalletConnector({ 31 | chains, 32 | options: { 33 | appName: 'ZK-Wordle', 34 | }, 35 | }), 36 | new WalletConnectConnector({ 37 | chains, 38 | options: { 39 | qrcode: true, 40 | }, 41 | }), 42 | new InjectedConnector({ 43 | chains, 44 | options: { 45 | name: 'Injected', 46 | shimDisconnect: true, 47 | }, 48 | }), 49 | ], 50 | provider, 51 | webSocketProvider, 52 | }) 53 | 54 | ReactDOM.render( 55 | 56 | 57 | 58 | 59 | 60 | 61 | , 62 | document.getElementById('root') 63 | ) 64 | 65 | // If you want to start measuring performance in your app, pass a function 66 | // to log results (for example: reportWebVitals(console.log)) 67 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 68 | reportWebVitals() 69 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zkWordleBackend", 3 | "description": "ZK-Wordle Backend", 4 | "version": "0.0.0", 5 | "homepage": "", 6 | "private": true, 7 | "main": "src", 8 | "keywords": [ 9 | "feathers" 10 | ], 11 | "contributors": [], 12 | "bugs": {}, 13 | "directories": { 14 | "lib": "src", 15 | "test": "test/", 16 | "config": "config/" 17 | }, 18 | "engines": { 19 | "node": "^14.0.0", 20 | "npm": ">= 3.0.0" 21 | }, 22 | "scripts": { 23 | "test": "npm run lint && npm run compile && npm run jest", 24 | "lint": "eslint src/. test/. --config .eslintrc.json --ext .ts --fix", 25 | "dev": "ts-node-dev --no-notify src/", 26 | "start": "node lib/", 27 | "start-dev": "npm run compile && node lib/", 28 | "jest": "jest --forceExit", 29 | "compile": "shx rm -rf lib/ && tsc", 30 | "tsc": "./node_modules/typescript/bin/tsc", 31 | "postinstall": "npm run tsc" 32 | }, 33 | "standard": { 34 | "env": [ 35 | "jest" 36 | ], 37 | "ignore": [] 38 | }, 39 | "types": "lib/", 40 | "dependencies": { 41 | "@feathersjs/configuration": "^4.5.15", 42 | "@feathersjs/errors": "^4.5.15", 43 | "@feathersjs/express": "^4.5.15", 44 | "@feathersjs/feathers": "^4.5.15", 45 | "@feathersjs/transport-commons": "^4.5.15", 46 | "circomlibjs": "^0.1.6", 47 | "compression": "^1.7.4", 48 | "cors": "^2.8.5", 49 | "ethers": "^5.6.9", 50 | "feathers-hooks-common": "^6.1.5", 51 | "feathers-memory": "^4.1.0", 52 | "feathers-sequelize": "^6.3.4", 53 | "helmet": "^4.6.0", 54 | "pg": "^8.8.0", 55 | "sequelize": "^6.23.1", 56 | "serve-favicon": "^2.5.0", 57 | "snarkjs": "^0.5.0", 58 | "winston": "^3.8.1" 59 | }, 60 | "devDependencies": { 61 | "@types/bluebird": "^3.5.36", 62 | "@types/compression": "^1.7.2", 63 | "@types/cors": "^2.8.12", 64 | "@types/jest": "^28.1.4", 65 | "@types/serve-favicon": "^2.5.3", 66 | "@types/validator": "^13.7.6", 67 | "@typescript-eslint/eslint-plugin": "^5.30.0", 68 | "@typescript-eslint/parser": "^5.30.0", 69 | "axios": "^0.27.2", 70 | "eslint": "^8.18.0", 71 | "jest": "^28.1.2", 72 | "shx": "^0.3.4", 73 | "ts-jest": "^28.0.5", 74 | "ts-node-dev": "^2.0.0", 75 | "typescript": "^4.7.4" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | 26 | %REACT_APP_GAME_NAME% 27 | 28 | <% if (process.env.NODE_ENV==='production' && process.env.REACT_APP_GOOGLE_MEASUREMENT_ID !=null) { %> 29 | 30 | 31 | 38 | <% } %> 39 | 40 | 41 | 42 | 43 |
44 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | ### Linux ### 47 | *~ 48 | 49 | # temporary files which can be created if a process still has a handle open of a deleted file 50 | .fuse_hidden* 51 | 52 | # KDE directory preferences 53 | .directory 54 | 55 | # Linux trash folder which might appear on any partition or disk 56 | .Trash-* 57 | 58 | # .nfs files are created when an open file is removed but is still being accessed 59 | .nfs* 60 | 61 | ### OSX ### 62 | *.DS_Store 63 | .AppleDouble 64 | .LSOverride 65 | 66 | # Icon must end with two \r 67 | Icon 68 | 69 | 70 | # Thumbnails 71 | ._* 72 | 73 | # Files that might appear in the root of a volume 74 | .DocumentRevisions-V100 75 | .fseventsd 76 | .Spotlight-V100 77 | .TemporaryItems 78 | .Trashes 79 | .VolumeIcon.icns 80 | .com.apple.timemachine.donotpresent 81 | 82 | # Directories potentially created on remote AFP share 83 | .AppleDB 84 | .AppleDesktop 85 | Network Trash Folder 86 | Temporary Items 87 | .apdisk 88 | 89 | ### Windows ### 90 | # Windows thumbnail cache files 91 | Thumbs.db 92 | ehthumbs.db 93 | ehthumbs_vista.db 94 | 95 | # Folder config file 96 | Desktop.ini 97 | 98 | # Recycle Bin used on file shares 99 | $RECYCLE.BIN/ 100 | 101 | # Windows Installer files 102 | *.cab 103 | *.msi 104 | *.msm 105 | *.msp 106 | 107 | # Windows shortcuts 108 | *.lnk 109 | 110 | # Others 111 | lib/ 112 | data/ 113 | -------------------------------------------------------------------------------- /backend/src/services/clue/clue.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, MemoryServiceOptions } from "feathers-memory"; 2 | import { Application } from "../../declarations"; 3 | import { Params } from "@feathersjs/feathers"; 4 | import { groth16 } from "snarkjs"; 5 | import { solution, solutionIndex } from "../../utils/words"; 6 | import { asAsciiArray } from "../../utils/asAsciiArray"; 7 | import { BigNumber } from "ethers"; 8 | 9 | const CIRCUIT_WASM_PATH = "src/zk/check_guess.wasm"; 10 | const CIRCUIT_ZKEY_PATH = "src/zk/check_guess_final.zkey"; 11 | 12 | interface Guess { 13 | guess: number[]; 14 | } 15 | 16 | export class Clue extends Service { 17 | private app: Application; 18 | private salt: bigint | undefined; 19 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | constructor(options: Partial, app: Application) { 21 | super(options); 22 | this.app = app; 23 | } 24 | 25 | async create(data: Guess, params?: Params) { 26 | const { guess } = data; 27 | console.log("Received guess:", guess); 28 | console.log("Solution:", solution); 29 | console.log("Solution index:", solutionIndex); 30 | 31 | //Poseidon hash is a BigInt 32 | const solutionCommitment = BigNumber.from( 33 | await params?.zkWordleContract?.solutionCommitment(solutionIndex) 34 | ); 35 | const asciiSolution = asAsciiArray(solution); 36 | //If the mapping in a smart contract returns zero, it means that either the day has changed and the solution index is different, 37 | //or the game hasn't yet started 38 | if (solutionCommitment.isZero()) { 39 | throw new Error( 40 | "Solution commitment not found, impossible to verify clue" 41 | ); 42 | } else { 43 | console.log("Solution commitment found: ", solutionCommitment.toString()); 44 | this.salt = (await this.app.service("salt").get(solutionIndex, {})).salt; 45 | } 46 | 47 | const args = { 48 | solution: asciiSolution, 49 | salt: this.salt, 50 | guess: guess, 51 | commitment: solutionCommitment.toString(), 52 | }; 53 | console.log("Args:", args); 54 | let proof; 55 | try { 56 | proof = await groth16.fullProve( 57 | args, 58 | CIRCUIT_WASM_PATH, 59 | CIRCUIT_ZKEY_PATH 60 | ); 61 | } catch (e: any) { 62 | console.log(e); 63 | throw new Error("Could not generate proof"); 64 | } 65 | console.log("Clue proof generated"); 66 | console.log(proof); 67 | 68 | return Promise.resolve(proof); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zk-wordle-frontend", 3 | "version": "0.1.0", 4 | "homepage": "https://alxkzmn.github.io/zk-wordle/", 5 | "dependencies": { 6 | "@feathersjs/client": "^4.5.15", 7 | "@feathersjs/feathers": "^4.5.15", 8 | "@feathersjs/rest-client": "^4.5.15", 9 | "@headlessui/react": "^1.4.2", 10 | "@heroicons/react": "^1.0.5", 11 | "@testing-library/jest-dom": "^5.16.1", 12 | "@testing-library/react": "^12.1.2", 13 | "@testing-library/user-event": "^13.5.0", 14 | "@types/jest": "^27.4.0", 15 | "@types/node": "^16.11.19", 16 | "@types/react": "^17.0.38", 17 | "@types/react-dom": "^17.0.11", 18 | "@types/ua-parser-js": "^0.7.36", 19 | "axios": "^0.27.2", 20 | "classnames": "^2.3.1", 21 | "ethers": "^5.6.9", 22 | "grapheme-splitter": "^1.0.4", 23 | "react": "^17.0.2", 24 | "react-countdown": "^2.3.2", 25 | "react-dom": "^17.0.2", 26 | "react-scripts": "^5.0.1", 27 | "snarkjs": "^0.5.0", 28 | "typescript": "^4.5.4", 29 | "ua-parser-js": "^1.0.2", 30 | "wagmi": "^0.6.4", 31 | "web-vitals": "^2.1.3" 32 | }, 33 | "scripts": { 34 | "start": "react-app-rewired start", 35 | "build": "react-app-rewired build", 36 | "test": "react-app-rewired test", 37 | "eject": "react-scripts eject", 38 | "fix": "prettier --write src", 39 | "lint": "prettier --check src", 40 | "prepare": "cd .. && husky install frontend/.husky" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "assert": "^2.0.0", 62 | "autoprefixer": "^10.4.2", 63 | "browserify-fs": "^1.0.0", 64 | "buffer": "^6.0.3", 65 | "crypto-browserify": "^3.12.0", 66 | "https-browserify": "^1.0.0", 67 | "husky": "^7.0.4", 68 | "lint-staged": "^12.3.2", 69 | "os-browserify": "^0.3.0", 70 | "path-browserify": "^1.0.1", 71 | "postcss": "^8.4.5", 72 | "prettier": "2.5.1", 73 | "process": "^0.11.10", 74 | "react-app-rewired": "^2.2.1", 75 | "stream-browserify": "^3.0.0", 76 | "stream-http": "^3.2.0", 77 | "tailwindcss": "^3.0.12", 78 | "url": "^0.11.0" 79 | }, 80 | "lint-staged": { 81 | "src/*.{ts,tsx,js,jsx,css,md}": "prettier --write" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/src/services/stats/stats.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, MemoryServiceOptions } from "feathers-memory"; 2 | import { Application } from "../../declarations"; 3 | import { solution, solutionIndex } from "../../utils/words"; 4 | import { Params } from "@feathersjs/feathers"; 5 | import { BigNumber } from "ethers"; 6 | import { asAsciiArray } from "../../utils/asAsciiArray"; 7 | import { groth16 } from "snarkjs"; 8 | 9 | const CIRCUIT_WASM_PATH = "src/zk/check_stats.wasm"; 10 | const CIRCUIT_ZKEY_PATH = "src/zk/check_stats_final.zkey"; 11 | 12 | interface Guesses { 13 | guesses: number[][]; 14 | } 15 | 16 | export class Stats extends Service { 17 | app: Application; 18 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | constructor(options: Partial, app: Application) { 20 | super(options); 21 | this.app = app; 22 | } 23 | 24 | async create(data: Guesses, params?: Params) { 25 | const { guesses } = data; 26 | console.log("Received guess:", guesses); 27 | console.log("Solution:", solution); 28 | console.log("Solution index:", solutionIndex); 29 | 30 | //Poseidon hash is a BigInt 31 | const solutionCommitment = BigNumber.from( 32 | await params?.zkWordleContract?.solutionCommitment(solutionIndex) 33 | ); 34 | const asciiSolution = asAsciiArray(solution); 35 | //If the mapping in a smart contract returns zero, it means that either the day has changed and the solution index is different, 36 | //or the game hasn't yet started 37 | if (solutionCommitment.isZero()) { 38 | throw new Error( 39 | "Solution commitment not found, impossible to verify stats" 40 | ); 41 | } else { 42 | console.log("Solution commitment found: ", solutionCommitment.toString()); 43 | } 44 | 45 | const salt = (await this.app.service("salt").get(solutionIndex, {})).salt; 46 | 47 | const args = { 48 | solution: asciiSolution, 49 | salt: salt, 50 | guesses: guesses, 51 | commitment: solutionCommitment.toString(), 52 | }; 53 | console.log("Args:", args); 54 | const PRODUCTION = process.env.NODE_ENV === "production"; 55 | let start; 56 | if (!PRODUCTION) { 57 | start = performance.now(); 58 | } 59 | const proof = await groth16.fullProve( 60 | args, 61 | CIRCUIT_WASM_PATH, 62 | CIRCUIT_ZKEY_PATH 63 | ); 64 | if (!PRODUCTION) { 65 | console.log( 66 | `Proof took ${(performance.now() - (start ?? 0)).toFixed(3)}ms` 67 | ); 68 | } 69 | console.log("Stats proof generated"); 70 | console.log(proof); 71 | 72 | return super.create(proof, params); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/components/modals/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import { Cell } from '../grid/Cell' 2 | import { BaseModal } from './BaseModal' 3 | 4 | type Props = { 5 | isOpen: boolean 6 | handleClose: () => void 7 | } 8 | 9 | export const InfoModal = ({ isOpen, handleClose }: Props) => { 10 | return ( 11 | 12 |

13 | Guess the word in 6 tries. After each guess, the color of the tiles will 14 | change to show how close your guess was to the word. 15 |

16 | 17 |
18 | 24 | 25 | 26 | 27 | 28 |
29 |

30 | The letter W is in the word and in the correct spot. 31 |

32 | 33 |
34 | 35 | 36 | 42 | 43 | 44 |
45 |

46 | The letter L is in the word but in the wrong spot. 47 |

48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 |
56 |

57 | The letter U is not in the word in any spot. 58 |

59 | 60 |

61 | This is an Zero-Knowledge-Proof-enabled version of the word guessing 62 | game we all know and love -{' '} 63 | 67 | check out the code here 68 | 69 | . To learn more about the ZK-Wordle implementation, check out{' '} 70 | 74 | this series of articles 75 | 76 | . 77 |

78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/context/AlertContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | ReactNode, 4 | useCallback, 5 | useContext, 6 | useState, 7 | } from 'react' 8 | import { ALERT_TIME_MS } from '../constants/settings' 9 | 10 | type AlertStatus = 'success' | 'error' | undefined 11 | 12 | type ShowOptions = { 13 | persist?: boolean 14 | delayMs?: number 15 | durationMs?: number 16 | onClose?: () => void 17 | } 18 | 19 | type AlertContextValue = { 20 | status: AlertStatus 21 | message: string | null 22 | isVisible: boolean 23 | showSuccess: (message: string, options?: ShowOptions) => void 24 | showError: (message: string, options?: ShowOptions) => void 25 | } 26 | 27 | export const AlertContext = createContext({ 28 | status: 'success', 29 | message: null, 30 | isVisible: false, 31 | showSuccess: () => null, 32 | showError: () => null, 33 | }) 34 | AlertContext.displayName = 'AlertContext' 35 | 36 | export const useAlert = () => useContext(AlertContext) as AlertContextValue 37 | 38 | type Props = { 39 | children?: ReactNode 40 | } 41 | 42 | export const AlertProvider = ({ children }: Props) => { 43 | const [status, setStatus] = useState('success') 44 | const [message, setMessage] = useState(null) 45 | const [isVisible, setIsVisible] = useState(false) 46 | 47 | const show = useCallback( 48 | (showStatus: AlertStatus, newMessage: string, options?: ShowOptions) => { 49 | const { 50 | delayMs = 0, 51 | persist, 52 | onClose, 53 | durationMs = ALERT_TIME_MS, 54 | } = options || {} 55 | 56 | setTimeout(() => { 57 | setStatus(showStatus) 58 | setMessage(newMessage) 59 | setIsVisible(true) 60 | 61 | if (!persist) { 62 | setTimeout(() => { 63 | setIsVisible(false) 64 | if (onClose) { 65 | onClose() 66 | } 67 | }, durationMs) 68 | } 69 | }, delayMs) 70 | }, 71 | [setStatus, setMessage, setIsVisible] 72 | ) 73 | 74 | const showError = useCallback( 75 | (newMessage: string, options?: ShowOptions) => { 76 | show('error', newMessage, options) 77 | }, 78 | [show] 79 | ) 80 | 81 | const showSuccess = useCallback( 82 | (newMessage: string, options?: ShowOptions) => { 83 | show('success', newMessage, options) 84 | }, 85 | [show] 86 | ) 87 | 88 | return ( 89 | 98 | {children} 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /frontend/src/lib/words.ts: -------------------------------------------------------------------------------- 1 | import { VALID_GUESSES } from '../constants/validGuesses' 2 | import { WRONG_SPOT_MESSAGE, NOT_CONTAINED_MESSAGE } from '../constants/strings' 3 | import { default as GraphemeSplitter } from 'grapheme-splitter' 4 | import { CharStatus } from './statuses' 5 | 6 | export const isValidGuess = (word: string) => { 7 | return VALID_GUESSES.includes(localeAwareLowerCase(word)) 8 | } 9 | 10 | export const isWinningWord = ( 11 | word: string, 12 | statuses: Map 13 | ) => { 14 | let isWinning = true 15 | statuses.get(word)?.forEach((status) => { 16 | if (status !== 'correct') { 17 | isWinning = false 18 | } 19 | }) 20 | return isWinning 21 | } 22 | 23 | // build a set of previously revealed letters - present and correct 24 | // guess must use correct letters in that space and any other revealed letters 25 | // also check if all revealed instances of a letter are used (i.e. two C's) 26 | export const findFirstUnusedReveal = async ( 27 | word: string, 28 | guesses: string[], 29 | statuses: Map 30 | ) => { 31 | if (guesses.length === 0) { 32 | return false 33 | } 34 | 35 | const lettersLeftArray = new Array() 36 | const guess = guesses[guesses.length - 1] 37 | const splitWord = unicodeSplit(word) 38 | const splitGuess = unicodeSplit(guess) 39 | const status = statuses.get(guess) ?? [] 40 | for (let i = 0; i < splitGuess.length; i++) { 41 | if (status[i] === 'correct' || status[i] === 'present') { 42 | lettersLeftArray.push(splitGuess[i]) 43 | } 44 | if (status[i] === 'correct' && splitWord[i] !== splitGuess[i]) { 45 | return WRONG_SPOT_MESSAGE(splitGuess[i], i + 1) 46 | } 47 | } 48 | 49 | // check for the first unused letter, taking duplicate letters 50 | // into account - see issue #198 51 | let n 52 | for (const letter of splitWord) { 53 | n = lettersLeftArray.indexOf(letter) 54 | if (n !== -1) { 55 | lettersLeftArray.splice(n, 1) 56 | } 57 | } 58 | 59 | if (lettersLeftArray.length > 0) { 60 | return NOT_CONTAINED_MESSAGE(lettersLeftArray[0]) 61 | } 62 | return false 63 | } 64 | 65 | export const unicodeSplit = (word: string) => { 66 | return new GraphemeSplitter().splitGraphemes(word) 67 | } 68 | 69 | export const unicodeLength = (word: string) => { 70 | return unicodeSplit(word).length 71 | } 72 | 73 | export const localeAwareLowerCase = (text: string) => { 74 | return process.env.REACT_APP_LOCALE_STRING 75 | ? text.toLocaleLowerCase(process.env.REACT_APP_LOCALE_STRING) 76 | : text.toLowerCase() 77 | } 78 | 79 | export const localeAwareUpperCase = (text: string) => { 80 | return process.env.REACT_APP_LOCALE_STRING 81 | ? text.toLocaleUpperCase(process.env.REACT_APP_LOCALE_STRING) 82 | : text.toUpperCase() 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/components/keyboard/Keyboard.tsx: -------------------------------------------------------------------------------- 1 | import { CharStatus, getStatuses } from '../../lib/statuses' 2 | import { Key } from './Key' 3 | import { useEffect } from 'react' 4 | import { ENTER_TEXT, DELETE_TEXT } from '../../constants/strings' 5 | import { localeAwareUpperCase } from '../../lib/words' 6 | 7 | type Props = { 8 | onChar: (value: string) => void 9 | onDelete: () => void 10 | onEnter: () => void 11 | guesses: string[] 12 | statuses: Map 13 | isRevealing?: boolean 14 | } 15 | 16 | export const Keyboard = ({ 17 | onChar, 18 | onDelete, 19 | onEnter, 20 | guesses, 21 | statuses, 22 | isRevealing, 23 | }: Props) => { 24 | const charStatuses = getStatuses(guesses, statuses) 25 | 26 | const onClick = (value: string) => { 27 | if (value === 'ENTER') { 28 | onEnter() 29 | } else if (value === 'DELETE') { 30 | onDelete() 31 | } else { 32 | onChar(value) 33 | } 34 | } 35 | 36 | useEffect(() => { 37 | const listener = (e: KeyboardEvent) => { 38 | if (e.code === 'Enter') { 39 | onEnter() 40 | } else if (e.code === 'Backspace') { 41 | onDelete() 42 | } else { 43 | const key = localeAwareUpperCase(e.key) 44 | if (key.length === 1 && key >= 'A' && key <= 'Z') { 45 | onChar(key) 46 | } 47 | } 48 | } 49 | window.addEventListener('keyup', listener) 50 | return () => { 51 | window.removeEventListener('keyup', listener) 52 | } 53 | }, [onEnter, onDelete, onChar]) 54 | 55 | return ( 56 |
57 |
58 | {['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'].map((key) => ( 59 | 66 | ))} 67 |
68 |
69 | {['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'].map((key) => ( 70 | 77 | ))} 78 |
79 |
80 | 81 | {ENTER_TEXT} 82 | 83 | {['Z', 'X', 'C', 'V', 'B', 'N', 'M'].map((key) => ( 84 | 91 | ))} 92 | 93 | {DELETE_TEXT} 94 | 95 |
96 |
97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /backend/src/channels.ts: -------------------------------------------------------------------------------- 1 | import "@feathersjs/transport-commons"; 2 | import { HookContext } from "@feathersjs/feathers"; 3 | import { Application } from "./declarations"; 4 | 5 | export default function (app: Application): void { 6 | if (typeof app.channel !== "function") { 7 | // If no real-time functionality has been configured just return 8 | return; 9 | } 10 | 11 | app.on("connection", (connection: any): void => { 12 | // On a new real-time connection, add it to the anonymous channel 13 | app.channel("anonymous").join(connection); 14 | }); 15 | 16 | app.on("login", (authResult: any, { connection }: any): void => { 17 | // connection can be undefined if there is no 18 | // real-time connection, e.g. when logging in via REST 19 | if (connection) { 20 | // Obtain the logged in user from the connection 21 | // const user = connection.user; 22 | 23 | // The connection is no longer anonymous, remove it 24 | app.channel("anonymous").leave(connection); 25 | 26 | // Add it to the authenticated user channel 27 | app.channel("authenticated").join(connection); 28 | 29 | // Channels can be named anything and joined on any condition 30 | 31 | // E.g. to send real-time events only to admins use 32 | // if(user.isAdmin) { app.channel('admins').join(connection); } 33 | 34 | // If the user has joined e.g. chat rooms 35 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 36 | 37 | // Easily organize users by email and userid for things like messaging 38 | // app.channel(`emails/${user.email}`).join(connection); 39 | // app.channel(`userIds/${user.id}`).join(connection); 40 | } 41 | }); 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | app.publish((data: any, hook: HookContext) => { 45 | // Here you can add event publishers to channels set up in `channels.ts` 46 | // To publish only for a specific event use `app.publish(eventname, () => {})` 47 | 48 | console.log( 49 | "Publishing all events to all authenticated users. See `channels.ts` and https://docs.feathersjs.com/api/channels.html for more information." 50 | ); // eslint-disable-line 51 | 52 | // e.g. to publish all service events to all authenticated users use 53 | return app.channel("authenticated"); 54 | }); 55 | 56 | // Here you can also add service specific event publishers 57 | // e.g. the publish the `users` service `created` event to the `admins` channel 58 | // app.service('users').publish('created', () => app.channel('admins')); 59 | 60 | // With the userid and email organization from above you can easily select involved users 61 | // app.service('messages').publish(() => { 62 | // return [ 63 | // app.channel(`userIds/${data.createdBy}`), 64 | // app.channel(`emails/${data.recipientEmail}`) 65 | // ]; 66 | // }); 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/lib/share.ts: -------------------------------------------------------------------------------- 1 | import { CharStatus } from './statuses' 2 | import { unicodeSplit } from './words' 3 | import { GAME_TITLE } from '../constants/strings' 4 | import { MAX_CHALLENGES } from '../constants/settings' 5 | import { UAParser } from 'ua-parser-js' 6 | import { Groth16Proof } from '../zk/prove' 7 | 8 | const webShareApiDeviceTypes: string[] = ['mobile', 'smarttv', 'wearable'] 9 | const parser = new UAParser() 10 | const browser = parser.getBrowser() 11 | const device = parser.getDevice() 12 | 13 | export const shareStatus = ( 14 | solutionIndex: number, 15 | proof: Groth16Proof, 16 | guesses: string[], 17 | statuses: Map, 18 | lost: boolean, 19 | isHardMode: boolean, 20 | isDarkMode: boolean, 21 | isHighContrastMode: boolean, 22 | handleShareToClipboard: () => void 23 | ) => { 24 | const textToShare = 25 | `${GAME_TITLE} ${solutionIndex} ${ 26 | lost ? 'X' : guesses.length 27 | }/${MAX_CHALLENGES}${isHardMode ? '*' : ''}\n\n` + 28 | generateEmojiGrid( 29 | guesses, 30 | statuses, 31 | getEmojiTiles(isDarkMode, isHighContrastMode) 32 | ) + 33 | '\n' + 34 | JSON.stringify(proof) 35 | 36 | const shareData = { text: textToShare } 37 | 38 | let shareSuccess = false 39 | 40 | try { 41 | if (attemptShare(shareData)) { 42 | navigator.share(shareData) 43 | shareSuccess = true 44 | } 45 | } catch (error) { 46 | shareSuccess = false 47 | } 48 | 49 | if (!shareSuccess) { 50 | navigator.clipboard.writeText(textToShare) 51 | handleShareToClipboard() 52 | } 53 | } 54 | 55 | export const generateEmojiGrid = ( 56 | guesses: string[], 57 | statuses: Map, 58 | tiles: string[] 59 | ) => { 60 | return guesses 61 | .map((guess) => { 62 | const splitGuess = unicodeSplit(guess) 63 | const status = statuses.get(guess) ?? '' 64 | return splitGuess 65 | .map((_, i) => { 66 | switch (status[i]) { 67 | case 'correct': 68 | return tiles[0] 69 | case 'present': 70 | return tiles[1] 71 | default: 72 | return tiles[2] 73 | } 74 | }) 75 | .join('') 76 | }) 77 | .join('\n') 78 | } 79 | 80 | const attemptShare = (shareData: object) => { 81 | return ( 82 | // Deliberately exclude Firefox Mobile, because its Web Share API isn't working correctly 83 | browser.name?.toUpperCase().indexOf('FIREFOX') === -1 && 84 | webShareApiDeviceTypes.indexOf(device.type ?? '') !== -1 && 85 | navigator.canShare && 86 | navigator.canShare(shareData) && 87 | navigator.share 88 | ) 89 | } 90 | 91 | const getEmojiTiles = (isDarkMode: boolean, isHighContrastMode: boolean) => { 92 | let tiles: string[] = [] 93 | tiles.push(isHighContrastMode ? '🟧' : '🟩') 94 | tiles.push(isHighContrastMode ? '🟦' : '🟨') 95 | tiles.push(isDarkMode ? '⬛' : '⬜') 96 | return tiles 97 | } 98 | -------------------------------------------------------------------------------- /frontend/src/components/modals/BaseModal.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import { Dialog, Transition } from '@headlessui/react' 3 | import { XCircleIcon } from '@heroicons/react/outline' 4 | 5 | type Props = { 6 | title: string 7 | children: React.ReactNode 8 | isOpen: boolean 9 | handleClose: () => void 10 | isCloseable?: boolean 11 | } 12 | 13 | export const BaseModal = ({ 14 | title, 15 | children, 16 | isOpen, 17 | handleClose, 18 | isCloseable = true, 19 | }: Props) => { 20 | return ( 21 | 22 | 27 |
28 | 37 | 38 | 39 | 40 | {/* This element is to trick the browser into centering the modal contents. */} 41 | 47 | 56 |
57 |
58 | {isCloseable && ( 59 | handleClose()} 62 | /> 63 | )} 64 |
65 |
66 |
67 | 71 | {title} 72 | 73 |
{children}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /blockchain/src/ZKWordle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.13; 2 | 3 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 4 | import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; 5 | 6 | interface IGuessVerifier { 7 | function verifyProof( 8 | uint256[2] memory a, 9 | uint256[2][2] memory b, 10 | uint256[2] memory c, 11 | uint256[11] memory input 12 | ) external view returns (bool); 13 | } 14 | 15 | interface IStatsVerifier { 16 | function verifyProof( 17 | uint256[2] memory a, 18 | uint256[2][2] memory b, 19 | uint256[2] memory c, 20 | uint256[31] memory input 21 | ) external view returns (bool); 22 | } 23 | 24 | contract ZKWordle is Ownable { 25 | using ECDSA for bytes32; 26 | 27 | address signer = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; 28 | mapping(uint256 => uint256) public solutionCommitment; 29 | IGuessVerifier guessVerifier; 30 | IStatsVerifier statsVerifier; 31 | 32 | constructor( 33 | address _guessVerifier, 34 | address _statsVerifier, 35 | address _signer 36 | ) { 37 | guessVerifier = IGuessVerifier(_guessVerifier); 38 | statsVerifier = IStatsVerifier(_statsVerifier); 39 | signer = _signer; 40 | } 41 | 42 | function commitSolution( 43 | uint256 _solutionIndex, 44 | uint256 _solutionCommitment, 45 | bytes memory _signature 46 | ) external { 47 | bytes32 hash = hashTransaction(_solutionIndex, _solutionCommitment); 48 | require( 49 | solutionCommitment[_solutionIndex] == 0, 50 | "Solution already committed" 51 | ); 52 | require(matchSigner(hash, _signature), "Signature mismatch"); 53 | _commitSolution(_solutionIndex, _solutionCommitment); 54 | } 55 | 56 | function _commitSolution( 57 | uint256 _solutionIndex, 58 | uint256 _solutionCommitment 59 | ) internal { 60 | solutionCommitment[_solutionIndex] = _solutionCommitment; 61 | } 62 | 63 | function verifyClues( 64 | uint256[2] memory a, 65 | uint256[2][2] memory b, 66 | uint256[2] memory c, 67 | uint256[11] memory input 68 | ) public view returns (bool) { 69 | return guessVerifier.verifyProof(a, b, c, input); 70 | } 71 | 72 | function verifyStats( 73 | uint256[2] memory a, 74 | uint256[2][2] memory b, 75 | uint256[2] memory c, 76 | uint256[31] memory input 77 | ) public view returns (bool) { 78 | return statsVerifier.verifyProof(a, b, c, input); 79 | } 80 | 81 | function setSigner(address _newSigner) external onlyOwner { 82 | signer = _newSigner; 83 | } 84 | 85 | function hashTransaction( 86 | uint256 _solutionIndex, 87 | uint256 _solutionCommitment 88 | ) public pure returns (bytes32) { 89 | bytes32 _hash = keccak256( 90 | abi.encode(_solutionIndex, _solutionCommitment) 91 | ); 92 | return _hash.toEthSignedMessageHash(); 93 | } 94 | 95 | function matchSigner(bytes32 _payload, bytes memory _signature) 96 | public 97 | view 98 | returns (bool) 99 | { 100 | return signer == _payload.recover(_signature); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --animation-speed: 1000ms; 7 | --animation-speed-fast: 250ms; 8 | --default-cell-bg-color: theme('colors.white'); 9 | --default-cell-border-color: theme('colors.black'); 10 | --default-cell-text-color: theme('colors.black'); 11 | --absent-cell-bg-color: theme('colors.slate.400'); 12 | --correct-cell-bg-color: theme('colors.green.400'); 13 | --present-cell-bg-color: theme('colors.yellow.400'); 14 | } 15 | 16 | .dark { 17 | --default-cell-bg-color: theme('colors.slate.900'); 18 | --default-cell-border-color: theme('colors.white'); 19 | --default-cell-text-color: theme('colors.white'); 20 | --absent-cell-bg-color: theme('colors.slate.700'); 21 | } 22 | 23 | .high-contrast { 24 | --correct-cell-bg-color: theme('colors.orange.400'); 25 | --present-cell-bg-color: theme('colors.cyan.400'); 26 | } 27 | 28 | .cell-fill-animation { 29 | animation: onTypeCell linear; 30 | animation-duration: 0.35s; 31 | } 32 | 33 | .cell-reveal { 34 | animation-duration: 0.35s; 35 | animation-timing-function: linear; 36 | animation-fill-mode: backwards; 37 | } 38 | 39 | .cell-reveal.absent { 40 | animation-name: revealAbsentCharCell; 41 | } 42 | 43 | .cell-reveal.correct { 44 | animation-name: revealCorrectCharCell; 45 | } 46 | 47 | .cell-reveal.present { 48 | animation-name: revealPresentCharCell; 49 | } 50 | 51 | .cell-reveal > .letter-container { 52 | animation: offsetLetterFlip 0.35s linear; 53 | animation-fill-mode: backwards; 54 | } 55 | 56 | svg.cursor-pointer { 57 | transition: all var(--animation-speed-fast); 58 | } 59 | 60 | svg.cursor-pointer:hover { 61 | transform: scale(1.2); 62 | } 63 | 64 | .jiggle { 65 | animation: jiggle linear; 66 | animation-duration: var(--animation-speed-fast); 67 | } 68 | 69 | .navbar { 70 | margin-bottom: 2%; 71 | } 72 | 73 | .navbar-content { 74 | display: flex; 75 | height: 3rem; 76 | align-items: center; 77 | justify-content: space-between; 78 | } 79 | 80 | .right-icons { 81 | display: flex; 82 | align-items: center; 83 | } 84 | 85 | @keyframes revealAbsentCharCell { 86 | 0% { 87 | transform: rotateX(0deg); 88 | background-color: var(--default-cell-bg-color); 89 | border-color: var(--default-cell-border-color); 90 | color: var(--default-cell-text-color); 91 | } 92 | 50% { 93 | background-color: var(--default-cell-bg-color); 94 | border-color: var(--default-cell-border-color); 95 | color: var(--default-cell-text-color); 96 | } 97 | 50.1% { 98 | background-color: var(--absent-cell-bg-color); 99 | border-color: var(--absent-cell-bg-color); 100 | } 101 | 100% { 102 | transform: rotateX(180deg); 103 | } 104 | } 105 | 106 | @keyframes revealCorrectCharCell { 107 | 0% { 108 | transform: rotateX(0deg); 109 | background-color: var(--default-cell-bg-color); 110 | border-color: var(--default-cell-border-color); 111 | color: var(--default-cell-text-color); 112 | } 113 | 50% { 114 | background-color: var(--default-cell-bg-color); 115 | border-color: var(--default-cell-border-color); 116 | color: var(--default-cell-text-color); 117 | } 118 | 50.1% { 119 | background-color: var(--correct-cell-bg-color); 120 | border-color: var(--correct-cell-bg-color); 121 | } 122 | 100% { 123 | transform: rotateX(180deg); 124 | } 125 | } 126 | 127 | @keyframes revealPresentCharCell { 128 | 0% { 129 | transform: rotateX(0deg); 130 | background-color: var(--default-cell-bg-color); 131 | border-color: var(--default-cell-border-color); 132 | color: var(--default-cell-text-color); 133 | } 134 | 50% { 135 | background-color: var(--default-cell-bg-color); 136 | border-color: var(--default-cell-border-color); 137 | color: var(--default-cell-text-color); 138 | } 139 | 50.1% { 140 | background-color: var(--present-cell-bg-color); 141 | border-color: var(--present-cell-bg-color); 142 | } 143 | 100% { 144 | transform: rotateX(180deg); 145 | } 146 | } 147 | 148 | /* Additional animation on the child div to avoid letters turning upside down/snapping back to upright visual glitch */ 149 | @keyframes offsetLetterFlip { 150 | 0% { 151 | transform: rotateX(0deg); 152 | } 153 | 100% { 154 | transform: rotateX(180deg); 155 | } 156 | } 157 | 158 | @keyframes onTypeCell { 159 | 0% { 160 | transform: scale(1); 161 | } 162 | 163 | 50% { 164 | transform: scale(1.1); 165 | } 166 | 167 | 100% { 168 | transform: scale(1); 169 | } 170 | } 171 | 172 | .shadowed { 173 | text-shadow: 1px 1px 1px #000000; 174 | } 175 | 176 | @keyframes jiggle { 177 | 0% { 178 | transform: translate(0, 0); 179 | } 180 | 25% { 181 | transform: translate(-0.5rem, 0); 182 | } 183 | 50% { 184 | transform: translate(0.5rem, 0); 185 | } 186 | 75% { 187 | transform: translate(-0.5rem, 0); 188 | } 189 | 100% { 190 | transform: translate(0, 0); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /frontend/src/components/modals/StatsModal.tsx: -------------------------------------------------------------------------------- 1 | import Countdown from 'react-countdown' 2 | import { StatBar } from '../stats/StatBar' 3 | import { Histogram } from '../stats/Histogram' 4 | import { GameStats } from '../../lib/localStorage' 5 | import { shareStatus } from '../../lib/share' 6 | import { BaseModal } from './BaseModal' 7 | import { 8 | STATISTICS_TITLE, 9 | GUESS_DISTRIBUTION_TEXT, 10 | NEW_WORD_TEXT, 11 | SHARE_TEXT, 12 | } from '../../constants/strings' 13 | import { CharStatus } from '../../lib/statuses' 14 | import LoadingSpinner from '../progress/Spinner' 15 | import { useState } from 'react' 16 | import { MAX_CHALLENGES } from '../../constants/settings' 17 | import { asAsciiArray } from '../../lib/asAsciiArray' 18 | import { Application } from '@feathersjs/feathers' 19 | 20 | type Props = { 21 | isOpen: boolean 22 | handleClose: () => void 23 | guesses: string[] 24 | statuses: Map 25 | gameStats: GameStats 26 | isGameLost: boolean 27 | isGameWon: boolean 28 | handleShareToClipboard: () => void 29 | isHardMode: boolean 30 | isDarkMode: boolean 31 | isHighContrastMode: boolean 32 | numberOfGuessesMade: number 33 | solutionIndex: number 34 | tomorrow: number 35 | feathersClient: Application 36 | } 37 | 38 | export const StatsModal = ({ 39 | isOpen, 40 | handleClose, 41 | guesses, 42 | statuses, 43 | gameStats, 44 | isGameLost, 45 | isGameWon, 46 | handleShareToClipboard, 47 | isHardMode, 48 | isDarkMode, 49 | isHighContrastMode, 50 | numberOfGuessesMade, 51 | solutionIndex, 52 | tomorrow, 53 | feathersClient, 54 | }: Props) => { 55 | const [isProving, setIsProving] = useState(false) 56 | 57 | if (gameStats.totalGames <= 0) { 58 | return ( 59 | 64 | 65 | 66 | ) 67 | } 68 | return ( 69 | 74 | 75 |

76 | {GUESS_DISTRIBUTION_TEXT} 77 |

78 | 82 | {(isGameLost || isGameWon) && ( 83 |
84 |
85 |
{NEW_WORD_TEXT}
86 | 91 |
92 | {!isProving && ( 93 | 139 | )} 140 | {isProving && } 141 |
142 | )} 143 |
144 | ) 145 | } 146 | -------------------------------------------------------------------------------- /frontend/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | cwackerfuss@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /circuits/src/guess/guess_single.circom: -------------------------------------------------------------------------------- 1 | pragma circom 2.0.3; 2 | include "../../../node_modules/circomlib/circuits/comparators.circom"; 3 | include "../../../node_modules/circomlib/circuits/gates.circom"; 4 | include "../../../node_modules/circomlib/circuits/poseidon.circom"; 5 | 6 | template SingleGuessCheck() { 7 | //"Word of the day", private input 8 | signal input solution[5]; 9 | signal input salt; 10 | //Current guess 11 | signal input guess[5]; 12 | //Solution commitment 13 | signal input commitment; 14 | //Clue output (typically represented using colored squares ⬜🟩⬜🟨🟨) 15 | //"0" - the letter is absent (gray), "1" - the letter matches correctly (green) 16 | //"2" - the letter is present in solution but is located at a different position (yellow) 17 | signal output clue[5]; 18 | 19 | signal correct[5]; 20 | component eq[5]; 21 | for (var i=0; i<5; i++) { 22 | eq[i] = IsEqual(); 23 | eq[i].in[0] <== guess[i]; 24 | eq[i].in[1] <== solution[i]; 25 | correct[i] <== eq[i].out; 26 | } 27 | 28 | var i=0; 29 | var j=0; 30 | //Unfortunately, Circom (as of v2.0.3) does not allow dynamic component declaration inside loops. 31 | //It leads to the following error: error[T2011]: Signal or component declaration outside initial scope. 32 | //It means that we have to declare how many instances of a component we would like to have in advance. 33 | component match[5][5]; 34 | component alreadyPresent[5][5]; 35 | component wasOrIsTaken[5][5]; 36 | component matchAndNotCorrect[5][5]; 37 | component matchAndNotTaken[5][5]; 38 | component andNotYetPresent[5][5]; 39 | component present[5][5]; 40 | signal solutionCharsTaken[5][5]; 41 | for (i=0; i<5; i++) { // guess index 42 | for (j=0; j<5; j++) { // solution index 43 | //True if the i-th guess letter is the same as j-th solution letter. 44 | match[i][j] = IsEqual(); 45 | match[i][j].in[0] <== guess[i]; 46 | match[i][j].in[1] <== solution[j]; 47 | //True if guess letter has a match but it didn't match with 48 | //solution letter exactly in this (i-th) position. 49 | //Let's call it an "elsewhere match". 50 | matchAndNotCorrect[i][j] = AndNotB(); 51 | matchAndNotCorrect[i][j].a <== match[i][j].out; 52 | matchAndNotCorrect[i][j].b <== correct[i]; 53 | //True if there is an elsewhere match but the matching letter of the solution hasn't been taken 54 | //Let's call it a vacant elsewhere match. 55 | matchAndNotTaken[i][j] = AndNotB(); 56 | matchAndNotTaken[i][j].a <== matchAndNotCorrect[i][j].out; 57 | if (i==0){ 58 | matchAndNotTaken[i][j].b <== correct[j]; 59 | } else { 60 | matchAndNotTaken[i][j].b <== solutionCharsTaken[i-1][j]; 61 | } 62 | 63 | //True if there has been at least one vacant elsewhere match with any previous solution letter 64 | alreadyPresent[i][j] = OR(); 65 | if (j==0){ 66 | alreadyPresent[i][j].a <== 0; 67 | } else { 68 | alreadyPresent[i][j].a <== alreadyPresent[i][j-1].out; 69 | } 70 | alreadyPresent[i][j].b <== matchAndNotTaken[i][j].out; 71 | 72 | //Marking "present" only once (only for the first occurence). 73 | //True if it is a vacant match but it hasn't happened for any previous solution letter 74 | andNotYetPresent[i][j] = AndNotB(); 75 | andNotYetPresent[i][j].a <== matchAndNotTaken[i][j].out; 76 | if (j==0){ 77 | andNotYetPresent[i][j].b <== 0; 78 | } else { 79 | andNotYetPresent[i][j].b <== alreadyPresent[i][j-1].out; 80 | } 81 | 82 | //"Flattening" the 2d array by carrying the previous "presents" over to the top layer 83 | present[i][j] = OR(); 84 | if (j==0){ 85 | present[i][j].a <== 0; 86 | } else { 87 | present[i][j].a <== present[i][j-1].out; 88 | } 89 | present[i][j].b <== andNotYetPresent[i][j].out; 90 | 91 | //Merging the previously taken solution letters with the newly taken ones 92 | wasOrIsTaken[i][j] = OR(); 93 | wasOrIsTaken[i][j].a <== andNotYetPresent[i][j].out; 94 | if (i==0){ 95 | wasOrIsTaken[i][j].b <== correct[j]; 96 | } else { 97 | wasOrIsTaken[i][j].b <== solutionCharsTaken[i-1][j]; 98 | } 99 | solutionCharsTaken[i][j] <== wasOrIsTaken[i][j].out; 100 | } 101 | //"0" if the letter is absent, "1" if it's an exact match 102 | //and "2" if it's present elsewhere in the solution. 103 | clue[i] <== correct[i] + present[i][4].out * 2; 104 | } 105 | 106 | /* 107 | * Converting the ASCII solution to a single number. 108 | * Alternatively, we could make a Poseidon hasher with 6 inputs, 109 | * but it would almost double the circuit constraint number. 110 | * We are confident that none of the chars has a code above 99, 111 | * so we can safely "stick" them together as two-digit decimals. 112 | */ 113 | signal solutionAsNumber[5]; 114 | solutionAsNumber[0] <== solution[0]; 115 | for (i=1; i<5; i++){ 116 | solutionAsNumber[i] <== solutionAsNumber[i-1] + solution[i] * (100 ** i); 117 | } 118 | //Hashing the solution 119 | component solutionHash = Poseidon(2); 120 | solutionHash.inputs[0] <== solutionAsNumber[4]; 121 | solutionHash.inputs[1] <== salt; 122 | //Constrain the hash to a publicly committed one 123 | commitment === solutionHash.out; 124 | } 125 | 126 | //Convenience component that inverts the "b" input 127 | //and performs && of the result with the "a" input 128 | template AndNotB(){ 129 | signal input a; 130 | signal input b; 131 | signal output out; 132 | 133 | component not = NOT(); 134 | not.in <== b; 135 | 136 | component and = AND(); 137 | and.a <== a; 138 | and.b <== not.out; 139 | out <== and.out; 140 | } 141 | 142 | //For ZK-REPL (https://zkrepl.dev/) 143 | /* INPUT = { 144 | "solution": [ 83, 84, 69, 69, 76 ], 145 | "salt": 362986289847779600, 146 | "guess": [ 83, 84, 65, 82, 84 ], 147 | "commitment": "15057754752634756475908235894514270422456734783907164695964318985994495471810" 148 | } */ -------------------------------------------------------------------------------- /blockchain/test/ZKWordle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/ZKWordle.sol"; 6 | import "../src/check_guess.sol"; 7 | import "../src/check_stats.sol"; 8 | 9 | contract VerifierTest is Test { 10 | address constant DEFAULT_SIGNER = 11 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; 12 | address constant NEW_SIGNER = 0xF39fD6e51Aad88F6f4CE6AB8827279CFffb92267; 13 | //Forge deployer, see https://book.getfoundry.sh/forge/writing-tests for reference 14 | address constant DEPLOYER = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; 15 | ZKWordle public zkWordle; 16 | GuessVerifier public guessVerifier; 17 | StatsVerifier public statsVerifier; 18 | 19 | function setUp() public { 20 | guessVerifier = new GuessVerifier(); 21 | statsVerifier = new StatsVerifier(); 22 | zkWordle = new ZKWordle( 23 | address(guessVerifier), 24 | address(statsVerifier), 25 | DEPLOYER 26 | ); 27 | } 28 | 29 | function testSignerChange() public { 30 | zkWordle.setSigner(NEW_SIGNER); 31 | vm.prank(address(0)); 32 | vm.expectRevert("Ownable: caller is not the owner"); 33 | zkWordle.setSigner(DEPLOYER); 34 | } 35 | 36 | function testCommitmentWithWrongSignatureNotMade() public { 37 | bytes 38 | memory dummySignature = hex"1cf53d2ab3c22d43becf449b21978fbb8d160df0d9eeecb3f6e66dbf5ec132fc302e031ec65b59a34cb4ac0720b4f5d97637cf5171e0712f7001fb89469b76871c"; 39 | vm.expectRevert("Signature mismatch"); 40 | zkWordle.commitSolution( 41 | 1, 42 | 0x5201afa16d758c3e75e63639fb20371463d34973b5d9d04cf20d12c80b11979, 43 | dummySignature 44 | ); 45 | } 46 | 47 | function testCommitmentMade() public { 48 | zkWordle.setSigner(DEFAULT_SIGNER); 49 | assertEq(zkWordle.solutionCommitment(269), 0); 50 | bytes 51 | memory signature = hex"1cf53d2ab3c22d43becf449b21978fbb8d160df0d9eeecb3f6e66dbf5ec132fc302e031ec65b59a34cb4ac0720b4f5d97637cf5171e0712f7001fb89469b76871c"; 52 | zkWordle.commitSolution( 53 | 269, 54 | 2318289536786382509651858531288693186368073365867419991124997557363993024889, 55 | signature 56 | ); 57 | assertEq( 58 | zkWordle.solutionCommitment(269), 59 | 2318289536786382509651858531288693186368073365867419991124997557363993024889 60 | ); 61 | } 62 | 63 | function testGuessVerified() public { 64 | assertEq( 65 | zkWordle.verifyClues( 66 | [ 67 | 2129687194795867968006967515041007785934044452952050877498151689790762668813, 68 | 4659446366458619975187937442535888644804526490563369374839107478524455225940 69 | ], 70 | [ 71 | [ 72 | 4687978534181292353714354484544244982664077573227074934397088608926561457370, 73 | 3758361400296785606805990361406388590231945719858290498991471290600796461392 74 | ], 75 | [ 76 | 21733610163656725103082645438776870226283543882840358231835223117009124437413, 77 | 18201434579992038494193446832660243705307882861699268814817312529016742994746 78 | ] 79 | ], 80 | [ 81 | 21741109911522375072463394811344229741426969275354605739908542122052590624395, 82 | 14285313199406863956784005156298227536725647389977686558301124713583278901502 83 | ], 84 | [ 85 | 2, 86 | 0, 87 | 2, 88 | 2, 89 | 0, 90 | 83, 91 | 84, 92 | 65, 93 | 82, 94 | 84, 95 | 12712306925672885864262427677183129153604184585548733491246491168260041610099 96 | ] 97 | ), 98 | true 99 | ); 100 | } 101 | 102 | function testWrongGuessNotVerified() public { 103 | uint256 o = 0; 104 | uint256[11] memory input; 105 | for (uint256 i = 0; i < 11; i++) { 106 | input[i] = 0; 107 | } 108 | assertEq( 109 | zkWordle.verifyClues([o, o], [[o, o], [o, o]], [o, o], input), 110 | false 111 | ); 112 | } 113 | 114 | function testStatsVerified() public { 115 | assertEq( 116 | zkWordle.verifyStats( 117 | [ 118 | 1798977333436471117382024826469814851138758551221354393508916101069409399542, 119 | 14924595529700676805989765089208688211438235032036457115293376458283675988746 120 | ], 121 | [ 122 | [ 123 | 7485299579872628514350836707902657723638531788673458541475739811263838575180, 124 | 3014478729772173852838655120797605364421127624916045766073312053546987411501 125 | ], 126 | [ 127 | 1588525158811465386764102811943691554067841456782838273758680779102146702100, 128 | 2296273619629026851578569836381070553323654537892862911695837229412590962388 129 | ] 130 | ], 131 | [ 132 | 12386820492295888712550011536338157983564086210731308306105953460027456963137, 133 | 14478000093748443130776078322509905711658887159161800376218655455467008171923 134 | ], 135 | [ 136 | 0, 137 | 1, 138 | 0, 139 | 0, 140 | 0, 141 | 2, 142 | 1, 143 | 1, 144 | 0, 145 | 1, 146 | 1, 147 | 1, 148 | 1, 149 | 1, 150 | 1, 151 | 0, 152 | 0, 153 | 0, 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 0, 159 | 0, 160 | 0, 161 | 0, 162 | 0, 163 | 0, 164 | 0, 165 | 0, 166 | 12712306925672885864262427677183129153604184585548733491246491168260041610099 167 | ] 168 | ), 169 | true 170 | ); 171 | } 172 | 173 | function testWrongStatsNotVerified() public { 174 | uint256 o = 0; 175 | uint256[31] memory input; 176 | for (uint256 i = 0; i < 31; i++) { 177 | input[i] = 0; 178 | } 179 | assertEq( 180 | zkWordle.verifyStats([o, o], [[o, o], [o, o]], [o, o], input), 181 | false 182 | ); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /backend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A FeathersJS application 5 | 6 | 7 | 65 | 66 | 67 |
68 | 69 | 70 | 73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /blockchain/src/check_guess.sol: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Christian Reitwiessner 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | // 7 | // 2019 OKIMS 8 | // ported to solidity 0.6 9 | // fixed linter warnings 10 | // added requiere error messages 11 | // 12 | // 13 | // SPDX-License-Identifier: GPL-3.0 14 | pragma solidity ^0.8.13; 15 | library Pairing { 16 | struct G1Point { 17 | uint X; 18 | uint Y; 19 | } 20 | // Encoding of field elements is: X[0] * z + X[1] 21 | struct G2Point { 22 | uint[2] X; 23 | uint[2] Y; 24 | } 25 | /// @return the generator of G1 26 | function P1() internal pure returns (G1Point memory) { 27 | return G1Point(1, 2); 28 | } 29 | /// @return the generator of G2 30 | function P2() internal pure returns (G2Point memory) { 31 | // Original code point 32 | return G2Point( 33 | [11559732032986387107991004021392285783925812861821192530917403151452391805634, 34 | 10857046999023057135944570762232829481370756359578518086990519993285655852781], 35 | [4082367875863433681332203403145435568316851327593401208105741076214120093531, 36 | 8495653923123431417604973247489272438418190587263600148770280649306958101930] 37 | ); 38 | 39 | /* 40 | // Changed by Jordi point 41 | return G2Point( 42 | [10857046999023057135944570762232829481370756359578518086990519993285655852781, 43 | 11559732032986387107991004021392285783925812861821192530917403151452391805634], 44 | [8495653923123431417604973247489272438418190587263600148770280649306958101930, 45 | 4082367875863433681332203403145435568316851327593401208105741076214120093531] 46 | ); 47 | */ 48 | } 49 | /// @return r the negation of p, i.e. p.addition(p.negate()) should be zero. 50 | function negate(G1Point memory p) internal pure returns (G1Point memory r) { 51 | // The prime q in the base field F_q for G1 52 | uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; 53 | if (p.X == 0 && p.Y == 0) 54 | return G1Point(0, 0); 55 | return G1Point(p.X, q - (p.Y % q)); 56 | } 57 | /// @return r the sum of two points of G1 58 | function addition(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) { 59 | uint[4] memory input; 60 | input[0] = p1.X; 61 | input[1] = p1.Y; 62 | input[2] = p2.X; 63 | input[3] = p2.Y; 64 | bool success; 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 | require(success,"pairing-add-failed"); 72 | } 73 | /// @return r the product of a point on G1 and a scalar, i.e. 74 | /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p. 75 | function scalar_mul(G1Point memory p, uint s) internal view returns (G1Point memory r) { 76 | uint[3] memory input; 77 | input[0] = p.X; 78 | input[1] = p.Y; 79 | input[2] = s; 80 | bool success; 81 | // solium-disable-next-line security/no-inline-assembly 82 | assembly { 83 | success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) 84 | // Use "invalid" to make gas estimation work 85 | switch success case 0 { invalid() } 86 | } 87 | require (success,"pairing-mul-failed"); 88 | } 89 | /// @return the result of computing the pairing check 90 | /// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 91 | /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should 92 | /// return true. 93 | function pairing(G1Point[] memory p1, G2Point[] memory p2) internal view returns (bool) { 94 | require(p1.length == p2.length,"pairing-lengths-failed"); 95 | uint elements = p1.length; 96 | uint inputSize = elements * 6; 97 | uint[] memory input = new uint[](inputSize); 98 | for (uint i = 0; i < elements; i++) 99 | { 100 | input[i * 6 + 0] = p1[i].X; 101 | input[i * 6 + 1] = p1[i].Y; 102 | input[i * 6 + 2] = p2[i].X[0]; 103 | input[i * 6 + 3] = p2[i].X[1]; 104 | input[i * 6 + 4] = p2[i].Y[0]; 105 | input[i * 6 + 5] = p2[i].Y[1]; 106 | } 107 | uint[1] memory out; 108 | bool success; 109 | // solium-disable-next-line security/no-inline-assembly 110 | assembly { 111 | success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20) 112 | // Use "invalid" to make gas estimation work 113 | switch success case 0 { invalid() } 114 | } 115 | require(success,"pairing-opcode-failed"); 116 | return out[0] != 0; 117 | } 118 | /// Convenience method for a pairing check for two pairs. 119 | function pairingProd2(G1Point memory a1, G2Point memory a2, G1Point memory b1, G2Point memory b2) internal view returns (bool) { 120 | G1Point[] memory p1 = new G1Point[](2); 121 | G2Point[] memory p2 = new G2Point[](2); 122 | p1[0] = a1; 123 | p1[1] = b1; 124 | p2[0] = a2; 125 | p2[1] = b2; 126 | return pairing(p1, p2); 127 | } 128 | /// Convenience method for a pairing check for three pairs. 129 | function pairingProd3( 130 | G1Point memory a1, G2Point memory a2, 131 | G1Point memory b1, G2Point memory b2, 132 | G1Point memory c1, G2Point memory c2 133 | ) internal view returns (bool) { 134 | G1Point[] memory p1 = new G1Point[](3); 135 | G2Point[] memory p2 = new G2Point[](3); 136 | p1[0] = a1; 137 | p1[1] = b1; 138 | p1[2] = c1; 139 | p2[0] = a2; 140 | p2[1] = b2; 141 | p2[2] = c2; 142 | return pairing(p1, p2); 143 | } 144 | /// Convenience method for a pairing check for four pairs. 145 | function pairingProd4( 146 | G1Point memory a1, G2Point memory a2, 147 | G1Point memory b1, G2Point memory b2, 148 | G1Point memory c1, G2Point memory c2, 149 | G1Point memory d1, G2Point memory d2 150 | ) internal view returns (bool) { 151 | G1Point[] memory p1 = new G1Point[](4); 152 | G2Point[] memory p2 = new G2Point[](4); 153 | p1[0] = a1; 154 | p1[1] = b1; 155 | p1[2] = c1; 156 | p1[3] = d1; 157 | p2[0] = a2; 158 | p2[1] = b2; 159 | p2[2] = c2; 160 | p2[3] = d2; 161 | return pairing(p1, p2); 162 | } 163 | } 164 | contract GuessVerifier { 165 | using Pairing for *; 166 | struct VerifyingKey { 167 | Pairing.G1Point alfa1; 168 | Pairing.G2Point beta2; 169 | Pairing.G2Point gamma2; 170 | Pairing.G2Point delta2; 171 | Pairing.G1Point[] IC; 172 | } 173 | struct Proof { 174 | Pairing.G1Point A; 175 | Pairing.G2Point B; 176 | Pairing.G1Point C; 177 | } 178 | function verifyingKey() internal pure returns (VerifyingKey memory vk) { 179 | vk.alfa1 = Pairing.G1Point( 180 | 20491192805390485299153009773594534940189261866228447918068658471970481763042, 181 | 9383485363053290200918347156157836566562967994039712273449902621266178545958 182 | ); 183 | 184 | vk.beta2 = Pairing.G2Point( 185 | [4252822878758300859123897981450591353533073413197771768651442665752259397132, 186 | 6375614351688725206403948262868962793625744043794305715222011528459656738731], 187 | [21847035105528745403288232691147584728191162732299865338377159692350059136679, 188 | 10505242626370262277552901082094356697409835680220590971873171140371331206856] 189 | ); 190 | vk.gamma2 = Pairing.G2Point( 191 | [11559732032986387107991004021392285783925812861821192530917403151452391805634, 192 | 10857046999023057135944570762232829481370756359578518086990519993285655852781], 193 | [4082367875863433681332203403145435568316851327593401208105741076214120093531, 194 | 8495653923123431417604973247489272438418190587263600148770280649306958101930] 195 | ); 196 | vk.delta2 = Pairing.G2Point( 197 | [17007011518383691906649686045513740143965206596105703750033336214364349766795, 198 | 15280321403301723278187704337171410779026621118136537558643821468757154293978], 199 | [15655299096097306296013088199688952197546930730890447880537731202915697824418, 200 | 13837255459185566992557142985343023465638826258153796842042912954657263013618] 201 | ); 202 | vk.IC = new Pairing.G1Point[](12); 203 | 204 | vk.IC[0] = Pairing.G1Point( 205 | 10423115800191011551409856091737950305461150542257725701393261857308935387292, 206 | 12799529256810853619250062724344054250589910980067732732754807351442147591940 207 | ); 208 | 209 | vk.IC[1] = Pairing.G1Point( 210 | 16931891069789386941141383134720865417362077368001257824403333444413404279635, 211 | 12965169269504370696874976431331294690720071111727077696654551700833803006569 212 | ); 213 | 214 | vk.IC[2] = Pairing.G1Point( 215 | 6194292528304589983314256652669499665586716941670230565976589385247886819012, 216 | 1619183963733222422698149966161273300454654792367523365832618517351231910687 217 | ); 218 | 219 | vk.IC[3] = Pairing.G1Point( 220 | 804559034655049031994330132455842147116833284467582178748268618333906666246, 221 | 3264125383170781541087939691297099629781453000978840338867888610878455446093 222 | ); 223 | 224 | vk.IC[4] = Pairing.G1Point( 225 | 19235369382332645184592544454409928291248801652739059837472496179943784023274, 226 | 158580650156211612502968158809076032395012147485302104513827784957376565638 227 | ); 228 | 229 | vk.IC[5] = Pairing.G1Point( 230 | 11606545125361780052879638794494047964324737550726053099950684491143640604662, 231 | 1054490576773231695122716334991934541381905600599631049774618720772330540474 232 | ); 233 | 234 | vk.IC[6] = Pairing.G1Point( 235 | 697930122766037432910290722459935851995032072308588277292774811150287047773, 236 | 19050709934188754946764676411906884780770765403160903168569630321702519494442 237 | ); 238 | 239 | vk.IC[7] = Pairing.G1Point( 240 | 16746621189677665436849184384854006747292433069742835084820207135542709805094, 241 | 20964062313253469519505698816141127542302697536896195234210409637363369798310 242 | ); 243 | 244 | vk.IC[8] = Pairing.G1Point( 245 | 509702880017233829352380428836447385537805339609633012957510491218935031837, 246 | 13103376915438909796687967530994785416699728166579591330402061439542816591880 247 | ); 248 | 249 | vk.IC[9] = Pairing.G1Point( 250 | 18918504832233429078990273691997240921615025298634850248860641919984047573281, 251 | 3936887477449688129940151050064694602138865902135428448159252831765808412455 252 | ); 253 | 254 | vk.IC[10] = Pairing.G1Point( 255 | 9240227569637538881319244389031294482330654543213865681420337944348521845608, 256 | 519902960029710976316887740538720216386490373163588661964180073120651789322 257 | ); 258 | 259 | vk.IC[11] = Pairing.G1Point( 260 | 2212175617118040693004068680731558205049515035678814311899332130313296206027, 261 | 4824644776537046219101801238463221295726415210073513631921068066342541395908 262 | ); 263 | 264 | } 265 | function verify(uint[] memory input, Proof memory proof) internal view returns (uint) { 266 | uint256 snark_scalar_field = 21888242871839275222246405745257275088548364400416034343698204186575808495617; 267 | VerifyingKey memory vk = verifyingKey(); 268 | require(input.length + 1 == vk.IC.length,"verifier-bad-input"); 269 | // Compute the linear combination vk_x 270 | Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0); 271 | for (uint i = 0; i < input.length; i++) { 272 | require(input[i] < snark_scalar_field,"verifier-gte-snark-scalar-field"); 273 | vk_x = Pairing.addition(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i])); 274 | } 275 | vk_x = Pairing.addition(vk_x, vk.IC[0]); 276 | if (!Pairing.pairingProd4( 277 | Pairing.negate(proof.A), proof.B, 278 | vk.alfa1, vk.beta2, 279 | vk_x, vk.gamma2, 280 | proof.C, vk.delta2 281 | )) return 1; 282 | return 0; 283 | } 284 | /// @return r bool true if proof is valid 285 | function verifyProof( 286 | uint[2] memory a, 287 | uint[2][2] memory b, 288 | uint[2] memory c, 289 | uint[11] memory input 290 | ) public view returns (bool r) { 291 | Proof memory proof; 292 | proof.A = Pairing.G1Point(a[0], a[1]); 293 | proof.B = Pairing.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); 294 | proof.C = Pairing.G1Point(c[0], c[1]); 295 | uint[] memory inputValues = new uint[](input.length); 296 | for(uint i = 0; i < input.length; i++){ 297 | inputValues[i] = input[i]; 298 | } 299 | if (verify(inputValues, proof) == 0) { 300 | return true; 301 | } else { 302 | return false; 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /blockchain/src/check_stats.sol: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Christian Reitwiessner 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | // 7 | // 2019 OKIMS 8 | // ported to solidity 0.6 9 | // fixed linter warnings 10 | // added requiere error messages 11 | // 12 | // 13 | // SPDX-License-Identifier: GPL-3.0 14 | pragma solidity ^0.8.13; 15 | library Pairing2 { 16 | struct G1Point { 17 | uint X; 18 | uint Y; 19 | } 20 | // Encoding of field elements is: X[0] * z + X[1] 21 | struct G2Point { 22 | uint[2] X; 23 | uint[2] Y; 24 | } 25 | /// @return the generator of G1 26 | function P1() internal pure returns (G1Point memory) { 27 | return G1Point(1, 2); 28 | } 29 | /// @return the generator of G2 30 | function P2() internal pure returns (G2Point memory) { 31 | // Original code point 32 | return G2Point( 33 | [11559732032986387107991004021392285783925812861821192530917403151452391805634, 34 | 10857046999023057135944570762232829481370756359578518086990519993285655852781], 35 | [4082367875863433681332203403145435568316851327593401208105741076214120093531, 36 | 8495653923123431417604973247489272438418190587263600148770280649306958101930] 37 | ); 38 | 39 | /* 40 | // Changed by Jordi point 41 | return G2Point( 42 | [10857046999023057135944570762232829481370756359578518086990519993285655852781, 43 | 11559732032986387107991004021392285783925812861821192530917403151452391805634], 44 | [8495653923123431417604973247489272438418190587263600148770280649306958101930, 45 | 4082367875863433681332203403145435568316851327593401208105741076214120093531] 46 | ); 47 | */ 48 | } 49 | /// @return r the negation of p, i.e. p.addition(p.negate()) should be zero. 50 | function negate(G1Point memory p) internal pure returns (G1Point memory r) { 51 | // The prime q in the base field F_q for G1 52 | uint q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; 53 | if (p.X == 0 && p.Y == 0) 54 | return G1Point(0, 0); 55 | return G1Point(p.X, q - (p.Y % q)); 56 | } 57 | /// @return r the sum of two points of G1 58 | function addition(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) { 59 | uint[4] memory input; 60 | input[0] = p1.X; 61 | input[1] = p1.Y; 62 | input[2] = p2.X; 63 | input[3] = p2.Y; 64 | bool success; 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 | require(success,"pairing-add-failed"); 72 | } 73 | /// @return r the product of a point on G1 and a scalar, i.e. 74 | /// p == p.scalar_mul(1) and p.addition(p) == p.scalar_mul(2) for all points p. 75 | function scalar_mul(G1Point memory p, uint s) internal view returns (G1Point memory r) { 76 | uint[3] memory input; 77 | input[0] = p.X; 78 | input[1] = p.Y; 79 | input[2] = s; 80 | bool success; 81 | // solium-disable-next-line security/no-inline-assembly 82 | assembly { 83 | success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) 84 | // Use "invalid" to make gas estimation work 85 | switch success case 0 { invalid() } 86 | } 87 | require (success,"pairing-mul-failed"); 88 | } 89 | /// @return the result of computing the pairing check 90 | /// e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 91 | /// For example pairing([P1(), P1().negate()], [P2(), P2()]) should 92 | /// return true. 93 | function pairing(G1Point[] memory p1, G2Point[] memory p2) internal view returns (bool) { 94 | require(p1.length == p2.length,"pairing-lengths-failed"); 95 | uint elements = p1.length; 96 | uint inputSize = elements * 6; 97 | uint[] memory input = new uint[](inputSize); 98 | for (uint i = 0; i < elements; i++) 99 | { 100 | input[i * 6 + 0] = p1[i].X; 101 | input[i * 6 + 1] = p1[i].Y; 102 | input[i * 6 + 2] = p2[i].X[0]; 103 | input[i * 6 + 3] = p2[i].X[1]; 104 | input[i * 6 + 4] = p2[i].Y[0]; 105 | input[i * 6 + 5] = p2[i].Y[1]; 106 | } 107 | uint[1] memory out; 108 | bool success; 109 | // solium-disable-next-line security/no-inline-assembly 110 | assembly { 111 | success := staticcall(sub(gas(), 2000), 8, add(input, 0x20), mul(inputSize, 0x20), out, 0x20) 112 | // Use "invalid" to make gas estimation work 113 | switch success case 0 { invalid() } 114 | } 115 | require(success,"pairing-opcode-failed"); 116 | return out[0] != 0; 117 | } 118 | /// Convenience method for a pairing check for two pairs. 119 | function pairingProd2(G1Point memory a1, G2Point memory a2, G1Point memory b1, G2Point memory b2) internal view returns (bool) { 120 | G1Point[] memory p1 = new G1Point[](2); 121 | G2Point[] memory p2 = new G2Point[](2); 122 | p1[0] = a1; 123 | p1[1] = b1; 124 | p2[0] = a2; 125 | p2[1] = b2; 126 | return pairing(p1, p2); 127 | } 128 | /// Convenience method for a pairing check for three pairs. 129 | function pairingProd3( 130 | G1Point memory a1, G2Point memory a2, 131 | G1Point memory b1, G2Point memory b2, 132 | G1Point memory c1, G2Point memory c2 133 | ) internal view returns (bool) { 134 | G1Point[] memory p1 = new G1Point[](3); 135 | G2Point[] memory p2 = new G2Point[](3); 136 | p1[0] = a1; 137 | p1[1] = b1; 138 | p1[2] = c1; 139 | p2[0] = a2; 140 | p2[1] = b2; 141 | p2[2] = c2; 142 | return pairing(p1, p2); 143 | } 144 | /// Convenience method for a pairing check for four pairs. 145 | function pairingProd4( 146 | G1Point memory a1, G2Point memory a2, 147 | G1Point memory b1, G2Point memory b2, 148 | G1Point memory c1, G2Point memory c2, 149 | G1Point memory d1, G2Point memory d2 150 | ) internal view returns (bool) { 151 | G1Point[] memory p1 = new G1Point[](4); 152 | G2Point[] memory p2 = new G2Point[](4); 153 | p1[0] = a1; 154 | p1[1] = b1; 155 | p1[2] = c1; 156 | p1[3] = d1; 157 | p2[0] = a2; 158 | p2[1] = b2; 159 | p2[2] = c2; 160 | p2[3] = d2; 161 | return pairing(p1, p2); 162 | } 163 | } 164 | contract StatsVerifier { 165 | using Pairing2 for *; 166 | struct VerifyingKey { 167 | Pairing2.G1Point alfa1; 168 | Pairing2.G2Point beta2; 169 | Pairing2.G2Point gamma2; 170 | Pairing2.G2Point delta2; 171 | Pairing2.G1Point[] IC; 172 | } 173 | struct Proof { 174 | Pairing2.G1Point A; 175 | Pairing2.G2Point B; 176 | Pairing2.G1Point C; 177 | } 178 | function verifyingKey() internal pure returns (VerifyingKey memory vk) { 179 | vk.alfa1 = Pairing2.G1Point( 180 | 20491192805390485299153009773594534940189261866228447918068658471970481763042, 181 | 9383485363053290200918347156157836566562967994039712273449902621266178545958 182 | ); 183 | 184 | vk.beta2 = Pairing2.G2Point( 185 | [4252822878758300859123897981450591353533073413197771768651442665752259397132, 186 | 6375614351688725206403948262868962793625744043794305715222011528459656738731], 187 | [21847035105528745403288232691147584728191162732299865338377159692350059136679, 188 | 10505242626370262277552901082094356697409835680220590971873171140371331206856] 189 | ); 190 | vk.gamma2 = Pairing2.G2Point( 191 | [11559732032986387107991004021392285783925812861821192530917403151452391805634, 192 | 10857046999023057135944570762232829481370756359578518086990519993285655852781], 193 | [4082367875863433681332203403145435568316851327593401208105741076214120093531, 194 | 8495653923123431417604973247489272438418190587263600148770280649306958101930] 195 | ); 196 | vk.delta2 = Pairing2.G2Point( 197 | [21202840743514253315933271572249505425454722607656947449290521984700388767620, 198 | 13482396594239990267307324287211745348633431480462499851730009766383282370341], 199 | [3901697140919890325635705403066884241756851120918152428998624244667241409886, 200 | 5844052804296807340691086574680613213355012646846186119090274672950962159475] 201 | ); 202 | vk.IC = new Pairing2.G1Point[](32); 203 | 204 | vk.IC[0] = Pairing2.G1Point( 205 | 4824097385034124576426229567791807706070762077745790741439891080049276906977, 206 | 8906501212922373993246790669289175821742771925467810834241402132711872641959 207 | ); 208 | 209 | vk.IC[1] = Pairing2.G1Point( 210 | 1688554140148414207531946058159561306869867950473636756758378530784409232850, 211 | 16272743001726012192627523122534964089699339069260441762885880297652303116579 212 | ); 213 | 214 | vk.IC[2] = Pairing2.G1Point( 215 | 21115701265905377737951421486224183704578075352599766472456112820678184931607, 216 | 6111625150883125208328807389679547243494774837394725485875193319204943897932 217 | ); 218 | 219 | vk.IC[3] = Pairing2.G1Point( 220 | 19978490366764774816174741298875354308820455107568957720774866306102980426239, 221 | 10810562905347231728411602550763224918417660513104135268125615085628874005211 222 | ); 223 | 224 | vk.IC[4] = Pairing2.G1Point( 225 | 21192129521851816727960912394254871021528048289790873427992408578907039803977, 226 | 20974759307692295500989392099920465159429006835317688535268358484857746872901 227 | ); 228 | 229 | vk.IC[5] = Pairing2.G1Point( 230 | 21522423808948962833179657535552904251648926681757042862293502844925507041153, 231 | 3345246915513779404687639210920897062272562680646442884312711923154763823308 232 | ); 233 | 234 | vk.IC[6] = Pairing2.G1Point( 235 | 18192190647178722376593892718254805270094328737484371082294439035495345644349, 236 | 10460490688709244996047949104868263383882135417067600117388978521286831569482 237 | ); 238 | 239 | vk.IC[7] = Pairing2.G1Point( 240 | 4743853020240984589719043461621464963986406515749477129645477914799535674147, 241 | 16556854087536624459029916363764131836912225180685165352866113131303204727705 242 | ); 243 | 244 | vk.IC[8] = Pairing2.G1Point( 245 | 19592732072787242993390059027330351182748003477480626569436415166066721080504, 246 | 1764897457266232511215651949734982008465067454087933159625192912820819582339 247 | ); 248 | 249 | vk.IC[9] = Pairing2.G1Point( 250 | 4672168778917917719180331877964072094548070428535822976350657841322455220368, 251 | 1943712250817981896405729322002249095573106377078658401552481263589920376150 252 | ); 253 | 254 | vk.IC[10] = Pairing2.G1Point( 255 | 16835652544544231609043488979694397413377705899676365634615724756945567158583, 256 | 3959254858119258398003418284591328007227066046870887297855437028238368583710 257 | ); 258 | 259 | vk.IC[11] = Pairing2.G1Point( 260 | 11453675543597003168861390351523035369122218905963826144672287715755061463689, 261 | 12542230406051839943215610824238078500353846731637894078398219350035991709618 262 | ); 263 | 264 | vk.IC[12] = Pairing2.G1Point( 265 | 9471652200187838842102558751796889680114888304668946654438225175077968677219, 266 | 4986805264915748914601280537284238484796083592123581719463240140870671466120 267 | ); 268 | 269 | vk.IC[13] = Pairing2.G1Point( 270 | 14590211984082491259113090548456874738794634948326085199925337938910472715948, 271 | 13354297900276531062415014788925844145436723254458954361462051500541572704849 272 | ); 273 | 274 | vk.IC[14] = Pairing2.G1Point( 275 | 19850641374752491415753330062488354332934820719241446450703437538206412961541, 276 | 1299716949796839596427329659845609731895014818467448650222195579288853639236 277 | ); 278 | 279 | vk.IC[15] = Pairing2.G1Point( 280 | 4047710651084358182206041178379049451348328396098618788827822756094035812093, 281 | 12512192345544048919355432199903352221357715838986571279158774494573970680421 282 | ); 283 | 284 | vk.IC[16] = Pairing2.G1Point( 285 | 10976564305103794036291254683061620242910965097451178405120470605385277149545, 286 | 14438513798597123256348591592468135309860634645435552360048508648899553095770 287 | ); 288 | 289 | vk.IC[17] = Pairing2.G1Point( 290 | 21026056226914007745773892945904818590418321426695060826311327857738121345740, 291 | 1207748265095954553517710719437637888236177427750223091635380665496152645922 292 | ); 293 | 294 | vk.IC[18] = Pairing2.G1Point( 295 | 5335972904360619966977236729013272085930982429049868216316350988364509818842, 296 | 18361296610271552801455851287629160344482167228809694454115770426303182020739 297 | ); 298 | 299 | vk.IC[19] = Pairing2.G1Point( 300 | 17288542892797138452088494839951073954504748003100249869271962871532940864154, 301 | 11532991406555189800414477183720888220884177303182202174860478928342363545682 302 | ); 303 | 304 | vk.IC[20] = Pairing2.G1Point( 305 | 16656899985844638635662532012427576794985273090521741124429436524111249432541, 306 | 7062357802303730393473464597586544010800424760397667428897317441480921676464 307 | ); 308 | 309 | vk.IC[21] = Pairing2.G1Point( 310 | 7859057772268849684713194939078015410216075063094240303112110536164042687116, 311 | 14391100026310923148751619368130347252149679821868029778289107188986421402679 312 | ); 313 | 314 | vk.IC[22] = Pairing2.G1Point( 315 | 21669717279350763220856210199344767501850716703240304676040061980136430008883, 316 | 18141662175367137375822138290342436245383689821971380736148418601021841105433 317 | ); 318 | 319 | vk.IC[23] = Pairing2.G1Point( 320 | 5147886789564016786648929013490815488721070009805246315594775573322027987871, 321 | 19744367773021866193988053683936232737556344064292084985111294690171568487935 322 | ); 323 | 324 | vk.IC[24] = Pairing2.G1Point( 325 | 18836354201610472338855671297077832533157105757202590750600400384755493873543, 326 | 15539648420730476509165962530921526257882644450128507778299744116849520771890 327 | ); 328 | 329 | vk.IC[25] = Pairing2.G1Point( 330 | 21299149283355114916990007536910701509549777715340853583353046064622964836520, 331 | 9550213359635958847017550917510996669688458576232560332632642390040211867263 332 | ); 333 | 334 | vk.IC[26] = Pairing2.G1Point( 335 | 15675340952720637657650554159681273058636382008766652621184227800846767295788, 336 | 9283747118614067112207525492306586951308009403736991713727876780939796851681 337 | ); 338 | 339 | vk.IC[27] = Pairing2.G1Point( 340 | 21796576014332826912824351309852689834376038595587259418758789576619264988494, 341 | 18606426584461858222741662031091802216538332552940059289989897795803712731493 342 | ); 343 | 344 | vk.IC[28] = Pairing2.G1Point( 345 | 346194942545022307094433460120165482948410055600410174259351328960247389314, 346 | 20990256153332551354738235758108185194860784944936710540730975761119526870035 347 | ); 348 | 349 | vk.IC[29] = Pairing2.G1Point( 350 | 20387859242663777303204410152605153526271515712419666160679072839981730550264, 351 | 1016245967509344336029767264664243797219481176976428972111922185451503580095 352 | ); 353 | 354 | vk.IC[30] = Pairing2.G1Point( 355 | 387124767353954742041913371855946493496218534749506324001056192281882129069, 356 | 10321974597725459940745933853836992635692890870719362973734877069619014131296 357 | ); 358 | 359 | vk.IC[31] = Pairing2.G1Point( 360 | 126418064824618766674502603836279692794544897695228285722993675384275802158, 361 | 14748388087378171507324646580083818016542168662344032857703587670513567335524 362 | ); 363 | 364 | } 365 | function verify(uint[] memory input, Proof memory proof) internal view returns (uint) { 366 | uint256 snark_scalar_field = 21888242871839275222246405745257275088548364400416034343698204186575808495617; 367 | VerifyingKey memory vk = verifyingKey(); 368 | require(input.length + 1 == vk.IC.length,"verifier-bad-input"); 369 | // Compute the linear combination vk_x 370 | Pairing2.G1Point memory vk_x = Pairing2.G1Point(0, 0); 371 | for (uint i = 0; i < input.length; i++) { 372 | require(input[i] < snark_scalar_field,"verifier-gte-snark-scalar-field"); 373 | vk_x = Pairing2.addition(vk_x, Pairing2.scalar_mul(vk.IC[i + 1], input[i])); 374 | } 375 | vk_x = Pairing2.addition(vk_x, vk.IC[0]); 376 | if (!Pairing2.pairingProd4( 377 | Pairing2.negate(proof.A), proof.B, 378 | vk.alfa1, vk.beta2, 379 | vk_x, vk.gamma2, 380 | proof.C, vk.delta2 381 | )) return 1; 382 | return 0; 383 | } 384 | /// @return r bool true if proof is valid 385 | function verifyProof( 386 | uint[2] memory a, 387 | uint[2][2] memory b, 388 | uint[2] memory c, 389 | uint[31] memory input 390 | ) public view returns (bool r) { 391 | Proof memory proof; 392 | proof.A = Pairing2.G1Point(a[0], a[1]); 393 | proof.B = Pairing2.G2Point([b[0][0], b[0][1]], [b[1][0], b[1][1]]); 394 | proof.C = Pairing2.G1Point(c[0], c[1]); 395 | uint[] memory inputValues = new uint[](input.length); 396 | for(uint i = 0; i < input.length; i++){ 397 | inputValues[i] = input[i]; 398 | } 399 | if (verify(inputValues, proof) == 0) { 400 | return true; 401 | } else { 402 | return false; 403 | } 404 | } 405 | } 406 | --------------------------------------------------------------------------------