├── .gitignore ├── .gitmodules ├── .prettierrc ├── README.md ├── build-all.sh ├── build-client.sh ├── build-server.sh ├── setup-mpc-client-bash ├── .gitignore ├── README.md ├── contribute ├── download.sh ├── sign_and_upload.sh └── upload │ ├── index.js │ ├── package-lock.json │ └── package.json ├── setup-mpc-client ├── .dockerignore ├── .mbt.yml ├── Dockerfile ├── Dockerfile.production ├── README.md ├── build-prod-client.sh ├── package.json ├── run-client.sh ├── src │ ├── app.ts │ ├── compute.ts │ ├── downloader.ts │ ├── exports.ts │ ├── index.ts │ ├── terminal-interface.ts │ ├── terminal-kit │ │ └── index.ts │ └── uploader.ts ├── tsconfig.json ├── tsconfig.prod.json └── yarn.lock ├── setup-mpc-common ├── .dockerignore ├── Dockerfile ├── package.json ├── src │ ├── fifo.ts │ ├── hash-files.test.ts │ ├── hash-files.ts │ ├── http-client.ts │ ├── index.ts │ ├── mpc-server.ts │ └── mpc-state.ts ├── tsconfig.json ├── tsconfig.prod.json └── yarn.lock ├── setup-mpc-server ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── initial │ ├── circuit_example.json │ └── radix │ │ └── phase1radix2m9_example ├── package.json ├── run-server.sh ├── src │ ├── app.test.ts │ ├── app.ts │ ├── fs-async.ts │ ├── index.ts │ ├── maxmind │ │ ├── GeoLite2-City.mmdb │ │ └── index.ts │ ├── participant-selector.ts │ ├── s3-explorer │ │ └── index.html │ ├── server.ts │ ├── state-store.ts │ ├── state │ │ ├── advance-state.test.ts │ │ ├── advance-state.ts │ │ ├── create-participant.ts │ │ ├── default-state.ts │ │ ├── order-waiting-participants.test.ts │ │ ├── order-waiting-participants.ts │ │ ├── reset-participant.ts │ │ ├── select-participants.test.ts │ │ └── select-participants.ts │ ├── transcript-store.ts │ └── verifier.ts ├── tsconfig.json ├── tsconfig.prod.json └── yarn.lock ├── setup-tools ├── .dockerignore ├── .gitignore ├── Dockerfile └── README.md └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | .terraform 4 | *.log 5 | *.ignore 6 | dest 7 | dist 8 | .env 9 | .idea 10 | **/.DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "setup-tools/phase2-bn254"] 2 | path = setup-tools/phase2-bn254 3 | url = https://github.com/kobigurk/phase2-bn254.git 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 120 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trusted Setup MPC Tools 2 | 3 | This repository contains several tools to help coordinators of trusted SNARK setup multi-party computation. It is based on the AZTEC [Ignition Ceremony](https://github.com/AztecProtocol/Setup/). 4 | 5 | - [setup-tools](/setup-tools) - Codebase of the actual computation code, verification code etc. These are taken from Kobi Gurkan's [phase2](https://github.com/kobigurk/phase2-bn254) repository, which itself is a modified version of the ZCash team's phase2 code. 6 | - [setup-mpc-server](/setup-mpc-server) - Coordination server for participants partaking in the MPC. 7 | - [setup-mpc-client](/setup-mpc-client) - Client terminal application for execution of client side tools and reporting to server. 8 | - [setup-mpc-client-bash](/setup-mpc-client-bash) - Utility scripts for clients wanting more fine-grained control over their contribution process. 9 | - [setup-mpc-common](/setup-mpc-common) - Shared code between server and client applications (i.e. common TS interfaces). 10 | 11 | Instructions for ceremony coordinators are in [setup-mpc-server](/setup-mpc-server). 12 | 13 | Instructions for ceremony participants are in [setup-mpc-client](/setup-mpc-client). 14 | 15 | We are also working on a webapp (zkparty.io) that can serve as a portal to different ceremonies + host attestations of participants and transcript files from any ceremonies run with this tool chain. 16 | 17 | All of this code is highly experimental and has not been audited. Use at your own risk. 18 | -------------------------------------------------------------------------------- /build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git submodule init && git submodule update 3 | cd ./setup-tools 4 | docker build -t setup-tools:latest . 5 | cd ../setup-mpc-common 6 | docker build -t setup-mpc-common:latest . 7 | cd ../setup-mpc-server 8 | docker build -t setup-mpc-server:latest . 9 | cd ../setup-mpc-client 10 | docker build -t setup-mpc-client:latest . -------------------------------------------------------------------------------- /build-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git submodule init && git submodule update 3 | cd ./setup-tools 4 | docker build -t setup-tools:latest . 5 | cd ../setup-mpc-common 6 | docker build -t setup-mpc-common:latest . 7 | cd ../setup-mpc-client 8 | docker build -t setup-mpc-client:latest . -------------------------------------------------------------------------------- /build-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd setup-tools/phase2-bn254/phase2 4 | cargo build --release --bin verify_contribution 5 | cargo build --release --bin new 6 | mv target/release/verify_contribution ../../ 7 | mv target/release/new ../../ 8 | cd ../../../setup-mpc-common 9 | yarn install 10 | yarn build 11 | yarn link 12 | cd ../setup-mpc-server 13 | ../setup-tools/new ./initial/circuit.json ./initial/initial_params ./initial/radix 14 | yarn install 15 | yarn link setup-mpc-common 16 | yarn build 17 | -------------------------------------------------------------------------------- /setup-mpc-client-bash/.gitignore: -------------------------------------------------------------------------------- 1 | upload/node_modules 2 | -------------------------------------------------------------------------------- /setup-mpc-client-bash/README.md: -------------------------------------------------------------------------------- 1 | # Run a client (offline / manual mode) 2 | 3 | If you'd like to have more control over how you contribute to the ceremony, you can contribute in OFFLINE/CUSTOM mode. You'll still have to run an "empty" client that signals to the server that you're indeed in the process of contributing (else the server will skip over you - see "Notes"), but the actual work of generating the contribution can be done by yourself. 4 | 5 | This directory contains scripts that will help you download, compute, and upload your contribution. Documentation is in the "Run a client (custom / extra security" section of the [participant guide](https://hackmd.io/AFqIQYGCQDmNCXNVmA54-Q?both). 6 | -------------------------------------------------------------------------------- /setup-mpc-client-bash/contribute: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gubsheep/Setup/92e972163552e3efa1402a73c7adf0c5a0660969/setup-mpc-client-bash/contribute -------------------------------------------------------------------------------- /setup-mpc-client-bash/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -z "$API_URL" ]]; then 3 | echo "USAGE: API_URL= PREV_ADDRESS=<0xPREVIOUS_ADDRESS> ./download.sh" 4 | exit 1 5 | fi 6 | 7 | if [[ -z "$PREV_ADDRESS" ]]; then 8 | echo "USAGE: API_URL= PREV_ADDRESS=<0xPREVIOUS_ADDRESS> ./download.sh" 9 | exit 1 10 | fi 11 | 12 | echo Downloading latest contribution from $PREV_ADDRESS... 13 | curl -s -S $API_URL/api/data/$PREV_ADDRESS/0 > params.params -------------------------------------------------------------------------------- /setup-mpc-client-bash/sign_and_upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -z "$PRIVATE_KEY" ]]; then 3 | echo "USAGE: API_URL= PARAMS_PATH= PRIVATE_KEY=<0xPRIVATE_KEY> ./sign_and_upload.sh" 4 | exit 1 5 | fi 6 | 7 | if [[ -z "$PARAMS_PATH" ]]; then 8 | echo "USAGE: API_URL= PARAMS_PATH= PRIVATE_KEY=<0xPRIVATE_KEY> ./sign_and_upload.sh" 9 | exit 1 10 | fi 11 | 12 | if [[ -z "$API_URL" ]]; then 13 | echo "USAGE: API_URL= PARAMS_PATH= PRIVATE_KEY=<0xPRIVATE_KEY> ./sign_and_upload.sh" 14 | exit 1 15 | fi 16 | 17 | node upload "$PRIVATE_KEY" "$PARAMS_PATH" "$API_URL" -------------------------------------------------------------------------------- /setup-mpc-client-bash/upload/index.js: -------------------------------------------------------------------------------- 1 | const { Account } = require('web3x/account'); 2 | const { hexToBuffer, bufferToHex } = require('web3x/utils'); 3 | const { createReadStream, existsSync, statSync } = require('fs'); 4 | const { createHash } = require('crypto'); 5 | const http = require('http'); 6 | const https = require('https'); 7 | const fetch = require('isomorphic-fetch'); 8 | const progress = require('progress-stream'); 9 | 10 | // USAGE: node index.js <0xPRIVATE_KEY_HEX> 11 | 12 | // code modified from setup-mpc-common 13 | function hashFile(path) { 14 | const stream = createReadStream(path); 15 | const hash = createHash('sha256'); 16 | 17 | return new Promise(resolve => { 18 | stream.on('end', () => { 19 | hash.end(); 20 | resolve(hash.read()); 21 | }); 22 | 23 | stream.pipe(hash); 24 | }); 25 | } 26 | 27 | async function main(privateKey, paramsPath, apiUrl) { 28 | const myAccount = Account.fromPrivate(hexToBuffer(privateKey)); 29 | 30 | if (!existsSync(paramsPath)) { 31 | throw new Error('Params file not found.'); 32 | } 33 | const hash = await hashFile(paramsPath); 34 | const { signature: pingSig } = myAccount.sign('ping'); 35 | const { signature: dataSig } = myAccount.sign(bufferToHex(hash)); 36 | 37 | const paramsStream = createReadStream(paramsPath); 38 | paramsStream.on('error', error => { 39 | console.error('Params file read error: ', error); 40 | reject(new Error('Failed to read params file.')); 41 | }); 42 | 43 | const stats = statSync(paramsPath); 44 | const progStream = progress({ length: stats.size, time: 1000 }); 45 | progStream.on('progress', progress => { 46 | console.log(`transferred ${progress.transferred}`); 47 | }); 48 | paramsStream.pipe(progStream); 49 | 50 | const agent = /^https/.test(apiUrl) ? new https.Agent({ keepAlive: true }) : new http.Agent({ keepAlive: true }); 51 | 52 | const response = await fetch(`${apiUrl}/api/data/${myAccount.address.toString().toLowerCase()}/0`, { 53 | keepalive: true, 54 | agent, 55 | method: 'PUT', 56 | body: progStream, 57 | headers: { 58 | 'X-Signature': `${pingSig},${dataSig}`, 59 | 'Content-Type': 'application/octet-stream', 60 | 'Content-Length': `${stats.size}`, 61 | }, 62 | }); 63 | 64 | if (response.status !== 200) { 65 | throw new Error(`Upload failed, bad status code: ${response.status}`); 66 | } else { 67 | console.log('Upload successful!'); 68 | } 69 | } 70 | 71 | const args = process.argv.slice(2); 72 | if (args.length < 3) { 73 | console.error('USAGE: node index.js <0xPRIVATE_KEY_HEX> '); 74 | throw new Error('Invalid args.'); 75 | } 76 | 77 | main(...args); 78 | -------------------------------------------------------------------------------- /setup-mpc-client-bash/upload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upload", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "isomorphic-fetch": "^2.2.1", 13 | "progress-stream": "^2.0.0", 14 | "web3x": "^4.0.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /setup-mpc-client/.dockerignore: -------------------------------------------------------------------------------- 1 | dest 2 | node_modules -------------------------------------------------------------------------------- /setup-mpc-client/.mbt.yml: -------------------------------------------------------------------------------- 1 | name: setup-mpc-client 2 | build: 3 | default: 4 | cmd: ../ci-scripts/build.sh 5 | args: 6 | - aztecprotocol/setup-mpc-client 7 | - '278380418400.dkr.ecr.eu-west-2.amazonaws.com/setup-mpc-common 278380418400.dkr.ecr.eu-west-2.amazonaws.com/setup-tools' 8 | commands: 9 | deploy-public: 10 | cmd: ../ci-scripts/deploy-public.sh 11 | args: 12 | - setup-mpc-client 13 | dependencies: 14 | - setup-mpc-common 15 | - setup-tools 16 | -------------------------------------------------------------------------------- /setup-mpc-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM setup-mpc-common:latest 2 | WORKDIR /usr/src/setup-mpc-common 3 | RUN yarn link 4 | WORKDIR /usr/src/setup-mpc-client 5 | COPY . ./ 6 | RUN yarn link "setup-mpc-common" && yarn install && yarn build && mkdir -p /usr/src/setup_db/new && mkdir -p /usr/src/setup_db/old 7 | CMD ["yarn", "--silent", "start"] -------------------------------------------------------------------------------- /setup-mpc-client/Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM setup-mpc-common:latest 2 | 3 | FROM ubuntu:latest 4 | RUN apt update && \ 5 | apt install -y curl && \ 6 | curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ 7 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ 8 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ 9 | apt update && \ 10 | apt install -y nodejs yarn build-essential && \ 11 | apt clean 12 | COPY --from=0 /usr/src/setup-tools/contribute /usr/src/setup-tools/contribute 13 | WORKDIR /usr/src/setup-mpc-common 14 | COPY --from=0 /usr/src/setup-mpc-common . 15 | RUN yarn link 16 | WORKDIR /usr/src/setup-mpc-client 17 | COPY . . 18 | RUN yarn link "setup-mpc-common" && yarn install && yarn build && mkdir -p /usr/src/setup_db/new && mkdir -p /usr/src/setup_db/old 19 | CMD ["yarn", "--silent", "start"] -------------------------------------------------------------------------------- /setup-mpc-client/README.md: -------------------------------------------------------------------------------- 1 | # zkparty MPC Participant Guide 2 | **All of this code is highly experimental and has not been audited. Use at your own risk.** 3 | 4 | The participant guide will walk you through how to participate in a trusted setup ceremony: [HackMD link](https://hackmd.io/@bgu33/BJ9jcRerU). 5 | 6 | In the guide: 7 | - **Register for a ceremony**: How to join an upcoming or ongoing MPC ceremony. 8 | - **Build the client image**: Build the software you need to participate in a ceremony. 9 | - **Run a client (no frills)**: The easiest way to run the client software and contribute to MPC. 10 | - **Run a client (custom / extra security)**: For advanced users who want to be especially careful about the integrity of their ceremony contribution. Read this if you want to control the computation / toxic waste generation process. 11 | - **Spectator mode**: How to spectate on a ceremony you aren't participating in. 12 | - **Attestations**: Send a signed attestation or message regarding your participation. 13 | - **Verifying ceremony integrity**: Verify that all steps of an MPC ceremony have been performed properly. 14 | - **Notes on participant ordering**: When your client needs to be online and running. 15 | -------------------------------------------------------------------------------- /setup-mpc-client/build-prod-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t bgu33/zkparty:latest -f Dockerfile.production . -------------------------------------------------------------------------------- /setup-mpc-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-mpc-client", 3 | "version": "1.0.0", 4 | "main": "dest/exports.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node ./dest 2> ./err.log", 8 | "start:dev": "tsc-watch -p tsconfig.prod.json --onSuccess 'yarn start' 2> ./err.log", 9 | "build": "tsc -p tsconfig.prod.json", 10 | "postinstall": "yarn link setup-mpc-common" 11 | }, 12 | "devDependencies": { 13 | "@types/humanize-duration": "^3.18.0", 14 | "@types/isomorphic-fetch": "^0.0.35", 15 | "@types/node": "^12.6.2", 16 | "@types/progress-stream": "^2.0.0", 17 | "@types/terminal-kit": "^1.28.0", 18 | "tsc-watch": "^2.2.1", 19 | "tslint": "^5.18.0", 20 | "tslint-config-prettier": "^1.18.0", 21 | "typescript": "^3.5.3" 22 | }, 23 | "dependencies": { 24 | "chalk": "^2.4.2", 25 | "form-data": "^2.5.0", 26 | "humanize-duration": "^3.20.1", 27 | "isomorphic-fetch": "^2.2.1", 28 | "moment": "^2.24.0", 29 | "progress-stream": "^2.0.0", 30 | "terminal-kit": "^1.28.7", 31 | "web3x": "^4.0.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /setup-mpc-client/run-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -z "$API_URL" ]]; then 3 | echo "USAGE: API_URL= PRIVATE_KEY=<0xPRIVATE_KEY> [optional COMPUTE_OFFLINE=1] ./run-client.sh" 4 | exit 1 5 | fi 6 | 7 | if [[ -z "$PRIVATE_KEY" ]]; then 8 | echo "USAGE: API_URL= PRIVATE_KEY=<0xPRIVATE_KEY> [optional COMPUTE_OFFLINE=1] ./run-client.sh" 9 | exit 1 10 | fi 11 | 12 | if [[ -z "$COMPUTE_OFFLINE" ]]; then 13 | COMPUTE_OFFLINE="0" 14 | fi 15 | 16 | docker run -ti -e API_URL="$API_URL"/api -e PRIVATE_KEY="$PRIVATE_KEY" -e COMPUTE_OFFLINE="$COMPUTE_OFFLINE" setup-mpc-client:latest -------------------------------------------------------------------------------- /setup-mpc-client/src/app.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { applyDelta, cloneParticipant, MpcServer, MpcState } from 'setup-mpc-common'; 3 | import { Writable } from 'stream'; 4 | import { Account } from 'web3x/account'; 5 | import { Address } from 'web3x/address'; 6 | import { Compute } from './compute'; 7 | import { TerminalInterface } from './terminal-interface'; 8 | import { TerminalKit } from './terminal-kit'; 9 | 10 | export class App { 11 | private interval!: NodeJS.Timeout; 12 | private uiInterval!: NodeJS.Timeout; 13 | private terminalInterface!: TerminalInterface; 14 | private compute?: Compute; 15 | private state?: MpcState; 16 | 17 | constructor( 18 | private server: MpcServer, 19 | private account: Account | undefined, 20 | stream: Writable, 21 | height: number, 22 | width: number, 23 | private computeOffline = false, 24 | private exitOnComplete = false 25 | ) { 26 | console.error(`Exit on complete: ${exitOnComplete}`); 27 | const termKit = new TerminalKit(stream, height, width); 28 | this.terminalInterface = new TerminalInterface(termKit, this.account); 29 | } 30 | 31 | public start() { 32 | this.updateState(); 33 | this.updateUi(); 34 | } 35 | 36 | public stop() { 37 | clearTimeout(this.interval); 38 | clearTimeout(this.uiInterval); 39 | this.terminalInterface.hideCursor(false); 40 | } 41 | 42 | public resize(width: number, height: number) { 43 | this.terminalInterface.resize(width, height); 44 | } 45 | 46 | private updateState = async () => { 47 | try { 48 | // Then get the latest state from the server. 49 | const remoteStateDelta = await this.server.getState(this.state ? this.state.sequence : undefined); 50 | 51 | // this.state updates atomically in this code block, allowing the ui to update independently. 52 | if (!this.state) { 53 | if (this.account && this.participantIsOnline(remoteStateDelta, this.account.address)) { 54 | this.terminalInterface.error = 'Participant is already online. Is another container already running?'; 55 | throw new Error('Participant is already online.'); 56 | } 57 | this.state = remoteStateDelta; 58 | } else if (this.state.startSequence !== remoteStateDelta.startSequence) { 59 | this.state = await this.server.getState(); 60 | } else { 61 | this.state = applyDelta(this.state, remoteStateDelta); 62 | } 63 | 64 | this.terminalInterface.lastUpdate = moment(); 65 | 66 | // Start or stop computation. 67 | await this.processRemoteState(this.state); 68 | 69 | // Send any local state changes to the server. 70 | await this.updateRemoteState(); 71 | } catch (err) { 72 | console.error(err); 73 | } finally { 74 | this.scheduleUpdate(); 75 | } 76 | }; 77 | 78 | private participantIsOnline(state: MpcState, address: Address) { 79 | const p = state.participants.find(p => p.address.equals(address)); 80 | return p && p.online; 81 | } 82 | 83 | private updateUi = () => { 84 | if (this.compute && this.state) { 85 | const local = this.compute.getParticipant(); 86 | const remote = this.state.participants.find(p => p.address.equals(local.address))!; 87 | remote.runningState = local.runningState; 88 | remote.transcripts = local.transcripts; 89 | remote.computeProgress = local.computeProgress; 90 | } 91 | this.terminalInterface.updateState(this.state); 92 | this.uiInterval = setTimeout(this.updateUi, 1000); 93 | }; 94 | 95 | private scheduleUpdate = () => { 96 | if (this.exitOnComplete && this.account) { 97 | const p = this.state!.participants.find(p => p.address.equals(this.account!.address)); 98 | if (p && p.state === 'COMPLETE') { 99 | this.stop(); 100 | return; 101 | } 102 | } 103 | this.interval = setTimeout(this.updateState, 1000); 104 | }; 105 | 106 | private async processRemoteState(remoteState: MpcState) { 107 | if (!this.account) { 108 | // We are in spectator mode. 109 | return; 110 | } 111 | 112 | const myIndex = remoteState.participants.findIndex(p => p.address.equals(this.account!.address)); 113 | if (myIndex < 0) { 114 | // We're an unknown participant. 115 | return; 116 | } 117 | const myRemoteState = remoteState.participants[myIndex]; 118 | 119 | // Either launch or destroy the computation based on remote state. 120 | if (myRemoteState.state === 'RUNNING' && !this.compute) { 121 | // Compute takes a copy of the participants state and modifies it with local telemetry. 122 | this.compute = new Compute( 123 | remoteState, 124 | cloneParticipant(myRemoteState), 125 | this.server, 126 | this.computeOffline && myRemoteState.tier < 2 127 | ); 128 | 129 | this.compute.start().catch(err => { 130 | console.error(`Compute failed: `, err); 131 | this.compute = undefined; 132 | }); 133 | } else if (myRemoteState.state !== 'RUNNING' && this.compute) { 134 | this.compute.cancel(); 135 | this.compute = undefined; 136 | } 137 | } 138 | 139 | private updateRemoteState = async () => { 140 | if (!this.account || !this.state!.participants.find(p => p.address.equals(this.account!.address))) { 141 | return; 142 | } 143 | 144 | if (this.compute) { 145 | const myState = this.compute.getParticipant(); 146 | await this.server.updateParticipant(myState); 147 | } else { 148 | await this.server.ping(this.account.address); 149 | } 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /setup-mpc-client/src/compute.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; 2 | import { EventEmitter } from 'events'; 3 | import readline from 'readline'; 4 | import { cloneParticipant, MemoryFifo, MpcServer, MpcState, Participant, Transcript } from 'setup-mpc-common'; 5 | import { Downloader } from './downloader'; 6 | import { Uploader } from './uploader'; 7 | import * as fs from 'fs'; 8 | 9 | // wrapper object for new and contribute processes 10 | class ComputeProcess extends EventEmitter { 11 | private proc?: ChildProcessWithoutNullStreams; 12 | 13 | constructor() { 14 | super(); 15 | } 16 | 17 | public startContribute() { 18 | const binPath = '../setup-tools/contribute'; 19 | console.error(`Computing with: ${binPath}`); 20 | const proc = spawn(binPath, ['../setup_db/old/params.params', '../setup_db/new/params.params', '', '-v', '100']); 21 | this.proc = proc; 22 | this.setupListeners(); 23 | } 24 | 25 | private setupListeners() { 26 | if (this.proc) { 27 | readline 28 | .createInterface({ 29 | input: this.proc.stdout, 30 | terminal: false, 31 | }) 32 | .on('line', input => { 33 | this.emit('line', input); 34 | }); 35 | 36 | this.proc.stderr.on('data', data => { 37 | this.emit('stderr', data); 38 | }); 39 | 40 | this.proc.on('close', code => { 41 | this.emit('close', code); 42 | }); 43 | 44 | this.proc.on('error', (...args) => { 45 | this.emit('error', ...args); 46 | }); 47 | } 48 | } 49 | 50 | public kill(...args: any[]) { 51 | if (this.proc) { 52 | this.proc.kill(...args); 53 | } 54 | } 55 | } 56 | 57 | export class Compute { 58 | private setupProc?: ComputeProcess; 59 | private computeQueue: MemoryFifo = new MemoryFifo(); 60 | private downloader: Downloader; 61 | private uploader: Uploader; 62 | private isFirstParticipant: boolean; 63 | 64 | constructor( 65 | private state: MpcState, 66 | private myState: Participant, 67 | server: MpcServer, 68 | private computeOffline: boolean 69 | ) { 70 | const previousParticipant = this.state.participants 71 | .slice() 72 | .reverse() 73 | .find(p => p.state === 'COMPLETE'); 74 | this.isFirstParticipant = !previousParticipant; 75 | this.downloader = new Downloader(server, this.isFirstParticipant); 76 | this.uploader = new Uploader(server, myState.address); 77 | } 78 | 79 | public async start() { 80 | if (this.computeOffline) { 81 | this.myState.runningState = 'OFFLINE'; 82 | return; 83 | } else { 84 | this.myState.runningState = 'WAITING'; 85 | } 86 | 87 | if (this.myState.runningState === 'WAITING') { 88 | this.myState.runningState = 'RUNNING'; 89 | } 90 | 91 | await this.populateQueues(); 92 | 93 | await Promise.all([this.runDownloader(), this.compute(), this.runUploader()]).catch(err => { 94 | console.error(err); 95 | this.cancel(); 96 | throw err; 97 | }); 98 | 99 | this.myState.runningState = 'COMPLETE'; 100 | console.error('Compute ran to completion.'); 101 | } 102 | 103 | public cancel() { 104 | this.downloader.cancel(); 105 | this.uploader.cancel(); 106 | this.computeQueue.cancel(); 107 | if (this.setupProc) { 108 | this.setupProc.kill('SIGINT'); 109 | } 110 | } 111 | 112 | public getParticipant() { 113 | return cloneParticipant(this.myState); 114 | } 115 | 116 | private async populateQueues() { 117 | this.myState.computeProgress = 0; 118 | 119 | this.myState.transcripts.forEach(transcript => { 120 | // Reset download and upload progress as we are starting over. 121 | if (!this.downloader.isDownloaded(transcript)) { 122 | transcript.downloaded = 0; 123 | } 124 | transcript.uploaded = 0; 125 | 126 | // Add to downloaded queue regardless of if already downloaded. Will shortcut later in the downloader. 127 | this.downloader.put(transcript); 128 | }); 129 | } 130 | 131 | private async runDownloader() { 132 | this.downloader.on('downloaded', (transcript: Transcript) => { 133 | this.computeQueue.put(`process`); 134 | }); 135 | 136 | this.downloader.on('progress', (transcript: Transcript, transferred: number) => { 137 | transcript.downloaded = transferred; 138 | }); 139 | 140 | await this.downloader.run(); 141 | 142 | this.computeQueue.end(); 143 | } 144 | 145 | private async runUploader() { 146 | this.uploader.on('progress', (num: number, transferred: number) => { 147 | this.myState.transcripts[num].uploaded = transferred; 148 | }); 149 | 150 | await this.uploader.run(); 151 | } 152 | 153 | private async compute() { 154 | return new Promise(async (resolve, reject) => { 155 | const setupProcess = new ComputeProcess(); 156 | this.setupProc = setupProcess; 157 | 158 | setupProcess.on('line', this.handleSetupOutput); 159 | 160 | setupProcess.on('stderr', data => { 161 | console.error(data.toString()); 162 | }); 163 | 164 | setupProcess.on('close', code => { 165 | this.setupProc = undefined; 166 | this.uploader.end(); 167 | if (code === 0) { 168 | console.error(`Compute complete.`); 169 | resolve(); 170 | } else { 171 | reject(new Error(`setup exited with code ${code}`)); 172 | } 173 | }); 174 | 175 | setupProcess.on('error', reject); 176 | 177 | console.error(`Compute starting...`); 178 | while (true) { 179 | const cmd = await this.computeQueue.get(); 180 | if (!cmd) { 181 | break; 182 | } 183 | console.error(`Setup command: ${cmd}`); 184 | setupProcess.startContribute(); 185 | } 186 | }); 187 | } 188 | 189 | private handleSetupOutput = (data: Buffer) => { 190 | console.error('From contribute: ', data.toString()); 191 | const params = data 192 | .toString() 193 | .replace('\n', '') 194 | .split(' '); 195 | const cmd = params.shift()!; 196 | switch (cmd) { 197 | case 'starting': { 198 | // downloading done, starting process 199 | fs.stat('../setup_db/old/params.params', (_, stats) => { 200 | this.myState.transcripts[0].size = stats.size; 201 | this.myState.transcripts[0].downloaded = stats.size; 202 | }); 203 | break; 204 | } 205 | case 'progress': { 206 | const computedPoints = +params[0]; 207 | const totalPoints = +params[1]; 208 | this.myState.computeProgress += (100 * computedPoints) / totalPoints; 209 | break; 210 | } 211 | case 'wrote': { 212 | this.uploader.put(0); 213 | this.myState.computeProgress = 100; 214 | fs.stat('../setup_db/new/params.params', (_, stats) => { 215 | // note that there will be a brief interval before this is calculated where transcript.size is stale 216 | // however we cannot put this.uploader.put(0) in this callback, because the uploader queue gets ended 217 | // as soon as the process is closed 218 | this.myState.transcripts[0].size = stats.size; 219 | this.myState.transcripts[0].downloaded = stats.size; // in case size changed, else terminal won't show 100% downloaded 220 | }); 221 | break; 222 | } 223 | } 224 | }; 225 | } 226 | -------------------------------------------------------------------------------- /setup-mpc-client/src/downloader.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { createWriteStream, existsSync, statSync } from 'fs'; 3 | import progress from 'progress-stream'; 4 | import { MemoryFifo, MpcServer, Transcript } from 'setup-mpc-common'; 5 | 6 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 7 | 8 | export class Downloader extends EventEmitter { 9 | private cancelled = false; 10 | private queue: MemoryFifo = new MemoryFifo(); 11 | 12 | constructor(private server: MpcServer, private isFirstParticipant: boolean) { 13 | super(); 14 | } 15 | 16 | public async run() { 17 | console.error('Downloader starting...'); 18 | while (true) { 19 | const transcript = await this.queue.get(); 20 | if (!transcript) { 21 | break; 22 | } 23 | await this.downloadTranscriptWithRetry(transcript); 24 | } 25 | console.error('Downloader complete.'); 26 | } 27 | 28 | public put(transcript: Transcript) { 29 | this.queue.put(transcript); 30 | } 31 | 32 | public end() { 33 | this.queue.end(); 34 | } 35 | 36 | public cancel() { 37 | this.cancelled = true; 38 | this.queue.cancel(); 39 | } 40 | 41 | public isDownloaded(transcript: Transcript) { 42 | const filename = `../setup_db/old/params.params`; 43 | if (existsSync(filename)) { 44 | // const stat = statSync(filename); 45 | return true; 46 | // if (stat.size === transcript.size && transcript.downloaded === transcript.size) { 47 | // return true; 48 | // } 49 | } 50 | } 51 | 52 | private async downloadTranscriptWithRetry(transcript: Transcript) { 53 | while (!this.cancelled) { 54 | try { 55 | console.error(`Downloading transcript ${transcript.num}`); 56 | await this.downloadTranscript(transcript); 57 | this.emit('downloaded', transcript); 58 | break; 59 | } catch (err) { 60 | console.error(`Failed to download transcript ${transcript.num}: ${err.message}`); 61 | await sleep(1000); 62 | } 63 | } 64 | } 65 | 66 | private async downloadTranscript(transcript: Transcript) { 67 | const filename = `../setup_db/old/params.params`; 68 | if (this.isDownloaded(transcript)) { 69 | return; 70 | } 71 | const readStream = this.isFirstParticipant 72 | ? await this.server.downloadInitialParams() 73 | : await this.server.downloadData(transcript.fromAddress!, transcript.num); 74 | const progStream = progress({ length: transcript.size, time: 1000 }); 75 | const writeStream = createWriteStream(filename); 76 | 77 | progStream.on('progress', progress => { 78 | this.emit('progress', transcript, progress.transferred); 79 | }); 80 | 81 | return new Promise((resolve, reject) => { 82 | readStream.on('error', reject); 83 | progStream.on('error', reject); 84 | writeStream.on('error', reject); 85 | writeStream.on('finish', () => { 86 | if (this.isDownloaded(transcript)) { 87 | resolve(); 88 | } else { 89 | reject(new Error('File not fully downloaded.')); 90 | } 91 | }); 92 | readStream.pipe(progStream).pipe(writeStream); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /setup-mpc-client/src/exports.ts: -------------------------------------------------------------------------------- 1 | export * from './app'; 2 | // export * from './setup-mpc-common'; 3 | -------------------------------------------------------------------------------- /setup-mpc-client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from 'setup-mpc-common'; 2 | import { Account } from 'web3x/account'; 3 | import { hexToBuffer } from 'web3x/utils'; 4 | import { App } from './app'; 5 | 6 | async function main() { 7 | const { 8 | API_URL = 'https://ignition.aztecprotocol.com/api', 9 | PRIVATE_KEY = '', 10 | COMPUTE_OFFLINE = 0, 11 | EXIT_ON_COMPLETE = 0, 12 | } = process.env; 13 | 14 | if (!PRIVATE_KEY && !process.stdout.isTTY) { 15 | throw new Error('If spectating (no private key) you must run in interactive mode.'); 16 | } 17 | 18 | const myAccount = PRIVATE_KEY ? Account.fromPrivate(hexToBuffer(PRIVATE_KEY)) : undefined; 19 | const server = new HttpClient(API_URL, myAccount); 20 | const app = new App( 21 | server, 22 | myAccount, 23 | process.stdout, 24 | process.stdout.rows!, 25 | process.stdout.columns!, 26 | +COMPUTE_OFFLINE > 0, 27 | +EXIT_ON_COMPLETE > 0 || !process.stdout.isTTY 28 | ); 29 | 30 | app.start(); 31 | 32 | process.stdout.on('resize', () => app.resize(process.stdout.columns!, process.stdout.rows!)); 33 | 34 | const shutdown = () => { 35 | app.stop(); 36 | process.exit(0); 37 | }; 38 | process.once('SIGINT', shutdown); 39 | process.once('SIGTERM', shutdown); 40 | } 41 | 42 | main().catch(err => console.log(err.message)); 43 | -------------------------------------------------------------------------------- /setup-mpc-client/src/terminal-interface.ts: -------------------------------------------------------------------------------- 1 | import humanizeDuration from 'humanize-duration'; 2 | import moment, { Moment } from 'moment'; 3 | import { cloneMpcState, MpcState, Participant } from 'setup-mpc-common'; 4 | import { Account } from 'web3x/account'; 5 | import { Address } from 'web3x/address'; 6 | import { leftPad } from 'web3x/utils'; 7 | import { TerminalKit } from './terminal-kit'; 8 | 9 | export class TerminalInterface { 10 | private banner = false; 11 | private bannerY!: number; 12 | private listY!: number; 13 | private offset = 0; 14 | private state?: MpcState; 15 | public lastUpdate?: Moment; 16 | public error?: string; 17 | 18 | constructor(private term: TerminalKit, private myAccount?: Account) {} 19 | 20 | private render() { 21 | this.term.clear(); 22 | this.term.hideCursor(); 23 | this.term.cyan('Semaphore Phase 2 Trusted Setup Multi Party Computation\n'); 24 | this.term.cyan('Written by AZTEC Ignition Ceremony team, modified for use by Semaphore'); 25 | this.renderStatus(); 26 | this.renderList(); 27 | } 28 | 29 | public resize(width: number, height: number) { 30 | this.term.width = width; 31 | this.term.height = height; 32 | this.render(); 33 | } 34 | 35 | public hideCursor(hide: boolean = true) { 36 | this.term.hideCursor(hide); 37 | } 38 | 39 | private renderStatus() { 40 | this.term.moveTo(0, 2); 41 | this.term.eraseLine(); 42 | 43 | if (!this.state) { 44 | this.term.white(this.error || 'Awaiting update from server...'); 45 | return; 46 | } 47 | 48 | const { startTime, completedAt } = this.state; 49 | 50 | const { ceremonyState } = this.state; 51 | switch (ceremonyState) { 52 | case 'PRESELECTION': 53 | case 'SELECTED': { 54 | const startedStr = `${startTime.utc().format('MMM Do YYYY HH:mm:ss')} UTC`; 55 | this.term.white( 56 | `The ceremony will begin at ${startedStr} in T-${Math.max(startTime.diff(moment(), 's'), 0)}s.\n\n` 57 | ); 58 | break; 59 | } 60 | case 'RUNNING': 61 | this.term.white( 62 | `The ceremony is in progress and started at ${startTime.utc().format('MMM Do YYYY HH:mm:ss')} UTC.\n\n` 63 | ); 64 | break; 65 | case 'COMPLETE': { 66 | const completedStr = `${completedAt!.utc().format('MMM Do YYYY HH:mm:ss')} UTC`; 67 | const duration = completedAt!.diff(startTime); 68 | const durationText = humanizeDuration(duration, { largest: 2, round: true }); 69 | this.term.white(`The ceremony was completed at ${completedStr} taking ${durationText}.\n\n`); 70 | break; 71 | } 72 | } 73 | 74 | this.bannerY = this.term.getCursorLocation().y; 75 | this.renderBanner(true); 76 | 77 | this.term.nextLine(1); 78 | 79 | const { y } = this.term.getCursorLocation(); 80 | this.listY = y; 81 | } 82 | 83 | private renderBanner(force: boolean = false) { 84 | const banner = this.myAccount && new Date().getTime() % 20000 < 10000; 85 | 86 | if (banner && (!this.banner || force)) { 87 | this.term.moveTo(0, this.bannerY); 88 | this.term.eraseLine(); 89 | this.banner = true; 90 | this.renderYourStatus(); 91 | } else if (!banner && (this.banner || force)) { 92 | const { participants } = this.state!; 93 | this.term.moveTo(0, this.bannerY); 94 | this.term.eraseLine(); 95 | this.banner = false; 96 | const online = participants.reduce((a, p) => a + (p.online ? 1 : 0), 0); 97 | const offline = participants.length - online; 98 | this.term.white(`Server status: `); 99 | if (!this.lastUpdate || this.lastUpdate.isBefore(moment().subtract(10, 's'))) { 100 | this.term.red('DISCONNECTED'); 101 | } else { 102 | this.term 103 | .white(`(participants: ${participants.length}) (online: `) 104 | .green(`${online}`) 105 | .white(`) (offline: `) 106 | .red(`${offline}`) 107 | .white(`)\n`); 108 | } 109 | } 110 | } 111 | 112 | private renderYourStatus() { 113 | const { participants, selectBlock, ceremonyState, latestBlock } = this.state!; 114 | 115 | this.term.eraseLine(); 116 | 117 | const myIndex = participants.findIndex(p => p.address.equals(this.myAccount!.address)); 118 | if (myIndex === -1) { 119 | this.term.white(`Private key does not match an address. You are currently spectating.\n`); 120 | } else { 121 | const myState = participants[myIndex]; 122 | switch (myState.state) { 123 | case 'WAITING': 124 | if (ceremonyState === 'PRESELECTION') { 125 | const selectCountdown = selectBlock - latestBlock; 126 | this.term.white( 127 | `Your position in the queue will determined at block number ${selectBlock} (B-${selectCountdown}).\n` 128 | ); 129 | } else if (ceremonyState !== 'RUNNING' && ceremonyState !== 'SELECTED') { 130 | this.term.white('Participants are no longer being selected.\n'); 131 | } else { 132 | const first = participants.find(p => p.state === 'WAITING')!; 133 | const inFront = myState.position - first.position; 134 | this.term.white( 135 | `You are in position ${myState.position} (${inFront ? inFront + ' in front' : 'next in line'}).\n` 136 | ); 137 | } 138 | break; 139 | case 'RUNNING': 140 | if (myState.runningState === 'OFFLINE') { 141 | this.term.white(`It's your turn. You have opted to perform the computation externally.\n`); 142 | } else { 143 | this.term.white(`You are currently processing your part of the ceremony...\n`); 144 | } 145 | break; 146 | case 'COMPLETE': 147 | this.term.white( 148 | `Your part is complete and you can close the program at any time. Thank you for participating.\n` 149 | ); 150 | break; 151 | case 'INVALIDATED': 152 | this.term.white(`You failed to compute your part of the ceremony.\n`); 153 | break; 154 | } 155 | } 156 | } 157 | 158 | private async renderList() { 159 | if (!this.state) { 160 | return; 161 | } 162 | 163 | const { participants } = this.state; 164 | const linesLeft = this.term.height - this.listY; 165 | this.offset = this.getRenderOffset(linesLeft); 166 | 167 | const toRender = participants.slice(this.offset, this.offset + linesLeft); 168 | 169 | toRender.forEach((p, i) => { 170 | this.renderLine(p, i); 171 | this.term.nextLine(1); 172 | }); 173 | 174 | if (toRender.length < linesLeft) { 175 | this.term.eraseDisplayBelow(); 176 | } 177 | } 178 | 179 | private getRenderOffset(linesForList: number) { 180 | const { participants } = this.state!; 181 | // Find first RUNNING or WAITING. 182 | let selectedIndex = participants.findIndex(p => p.state !== 'COMPLETE' && p.state !== 'INVALIDATED'); 183 | if (selectedIndex < 0) { 184 | // None found, pick last in list. 185 | selectedIndex = this.state!.participants.length - 1; 186 | } 187 | const midLine = Math.floor(linesForList / 2); 188 | return Math.min(Math.max(0, selectedIndex - midLine), Math.max(0, this.state!.participants.length - linesForList)); 189 | } 190 | 191 | private renderLine(p: Participant, i: number) { 192 | if (i < 0 || this.listY + i >= this.term.height) { 193 | return; 194 | } 195 | this.term.moveTo(0, this.listY + i); 196 | this.term.eraseLine(); 197 | if (p.online) { 198 | this.term.green('\u25CF '); 199 | } else { 200 | this.term.red('\u25CF '); 201 | } 202 | this.term.white(`${leftPad(p.position.toString(), 2)}. `); 203 | switch (p.state) { 204 | case 'WAITING': 205 | this.term.grey(`${p.address.toString()}`); 206 | if (this.state!.ceremonyState !== 'PRESELECTION') { 207 | this.term.grey(` (${p.priority})`); 208 | } 209 | break; 210 | case 'RUNNING': 211 | this.renderRunningLine(p); 212 | break; 213 | case 'COMPLETE': 214 | this.term.green(p.address.toString()); 215 | this.term.grey(` (${p.priority}) (${p.completedAt!.diff(p.startedAt!, 's')}s)`); 216 | break; 217 | case 'INVALIDATED': 218 | this.term.red(p.address.toString()); 219 | if (p.error) { 220 | this.term.grey(` (${p.priority}) (${p.error})`); 221 | } 222 | break; 223 | } 224 | 225 | if (this.myAccount && p.address.equals(this.myAccount.address)) { 226 | this.term.white(' (you)'); 227 | } 228 | } 229 | 230 | private renderRunningLine(p: Participant) { 231 | const { term } = this; 232 | const addrString = p.address.toString(); 233 | const progIndex = addrString.length * ((p.runningState === 'OFFLINE' ? p.verifyProgress : p.computeProgress) / 100); 234 | term.yellow(addrString.slice(0, progIndex)).grey(addrString.slice(progIndex)); 235 | term.grey(` (${p.priority})`); 236 | 237 | term.red(' <'); 238 | if (p.lastUpdate || p.runningState === 'OFFLINE') { 239 | switch (p.runningState) { 240 | case 'OFFLINE': 241 | term 242 | .white(' (') 243 | .blue('computing offline') 244 | .white(') (') 245 | .blue('\u2714') 246 | .white(` ${p.verifyProgress.toFixed(p.verifyProgress < 100 ? 2 : 0)}%`) 247 | .white(`)`); 248 | break; 249 | case 'RUNNING': 250 | case 'COMPLETE': { 251 | const totalData = p.transcripts.reduce((a, t) => a + t.size, 0); 252 | const totalDownloaded = p.transcripts.reduce((a, t) => a + t.downloaded, 0); 253 | const totalUploaded = p.transcripts.reduce((a, t) => a + t.uploaded, 0); 254 | const downloadProgress = totalData ? (totalDownloaded / totalData) * 100 : 0; 255 | const uploadProgress = totalData ? (totalUploaded / totalData) * 100 : 0; 256 | const computeProgress = p.computeProgress; 257 | const verifyProgress = p.verifyProgress; 258 | term 259 | .white(` (`) 260 | .blue('\u2b07\ufe0e') 261 | .white(` ${downloadProgress.toFixed(downloadProgress < 100 ? 2 : 0)}%`) 262 | .white(`)`); 263 | term 264 | .white(` (`) 265 | .blue('\u2699\ufe0e') 266 | .white(` ${computeProgress.toFixed(computeProgress < 100 ? 2 : 0)}%`) 267 | .white(`)`); 268 | term 269 | .white(` (`) 270 | .blue('\u2b06\ufe0e') 271 | .white(` ${uploadProgress.toFixed(uploadProgress < 100 ? 2 : 0)}%`) 272 | .white(`)`); 273 | term 274 | .white(` (`) 275 | .blue('\u2714\ufe0e') 276 | .white(` ${verifyProgress.toFixed(verifyProgress < 100 ? 2 : 0)}%`) 277 | .white(`)`); 278 | break; 279 | } 280 | } 281 | } 282 | 283 | const { invalidateAfter } = this.state!; 284 | const completeWithin = p.invalidateAfter || invalidateAfter; 285 | 286 | // TODO: verifyWithin will be used if we are splitting parameter files into chunks 287 | /* 288 | const verifyWithin = 2 * completeWithin; 289 | const verifyTimeout = Math.max( 290 | 0, 291 | moment(p.lastVerified || p.startedAt!) 292 | .add(verifyWithin, 's') 293 | .diff(moment(), 's') 294 | ); 295 | */ 296 | 297 | const timeout = Math.max( 298 | 0, 299 | moment(p.startedAt!) 300 | .add(completeWithin, 's') 301 | .diff(moment(), 's') 302 | ); 303 | 304 | term.white(` (`).blue('\u25b6\ufe0e\u25b6\ufe0e '); 305 | 306 | /* 307 | if (p.tier > 1) { 308 | term.white(`${verifyTimeout}/`); 309 | } 310 | */ 311 | 312 | term.white(`${timeout}s)`); 313 | } 314 | 315 | public async updateState(state?: MpcState) { 316 | const oldState = this.state; 317 | this.state = state ? cloneMpcState(state) : undefined; 318 | 319 | if (!oldState || !this.state || oldState.startSequence !== this.state.startSequence) { 320 | // If first time or reset render everything. 321 | this.render(); 322 | return; 323 | } 324 | 325 | if ( 326 | this.state.ceremonyState === 'PRESELECTION' || 327 | this.state.ceremonyState === 'SELECTED' || 328 | this.state.statusSequence !== oldState.statusSequence 329 | ) { 330 | // If the ceremony hasn't started, update the status line for the countdown. 331 | await this.renderStatus(); 332 | } else { 333 | await this.renderBanner(); 334 | } 335 | 336 | const linesLeft = this.term.height - this.listY; 337 | const offset = this.getRenderOffset(linesLeft); 338 | this.state.participants.forEach((p, i) => { 339 | // Update any new participants, participants that changed, and always the running participant (for the countdown). 340 | if ( 341 | offset !== this.offset || 342 | !oldState.participants[i] || 343 | p.sequence !== oldState.participants[i].sequence || 344 | p.state === 'RUNNING' 345 | ) { 346 | this.renderLine(p, i - offset); 347 | } 348 | }); 349 | this.offset = offset; 350 | } 351 | 352 | public getParticipant(address: Address) { 353 | return this.state!.participants.find(p => p.address.equals(address))!; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /setup-mpc-client/src/terminal-kit/index.ts: -------------------------------------------------------------------------------- 1 | import chalkmod from 'chalk'; 2 | import { Writable } from 'stream'; 3 | 4 | const options: any = { enabled: true, level: 2 }; 5 | const chalk = new chalkmod.constructor(options); 6 | 7 | export class TerminalKit { 8 | private x: number = 0; 9 | private y: number = 0; 10 | 11 | constructor(private stream: Writable, public height: number, public width: number) {} 12 | 13 | private getNewYPos(str: string) { 14 | let xPos = this.x; 15 | let yPos = this.y; 16 | for (const char of str) { 17 | if (char === '\n' || xPos === this.width) { 18 | xPos = 0; 19 | yPos += 1; 20 | } else { 21 | xPos += 1; 22 | } 23 | } 24 | return yPos; 25 | } 26 | 27 | public white(str: string = '') { 28 | this.y = this.getNewYPos(str); 29 | this.stream.write(chalk.white(str)); 30 | return this; 31 | } 32 | 33 | public yellow(str: string = '') { 34 | this.y = this.getNewYPos(str); 35 | this.stream.write(chalk.yellow(str)); 36 | return this; 37 | } 38 | 39 | public cyan(str: string = '') { 40 | this.y = this.getNewYPos(str); 41 | this.stream.write(chalk.cyan(str)); 42 | return this; 43 | } 44 | 45 | public red(str: string = '') { 46 | this.y = this.getNewYPos(str); 47 | this.stream.write(chalk.red(str)); 48 | return this; 49 | } 50 | 51 | public redBright(str: string = '') { 52 | this.y = this.getNewYPos(str); 53 | this.stream.write(chalk.redBright(str)); 54 | return this; 55 | } 56 | 57 | public blue(str: string = '') { 58 | this.y = this.getNewYPos(str); 59 | this.stream.write(chalk.blue(str)); 60 | return this; 61 | } 62 | 63 | public green(str: string = '') { 64 | this.y = this.getNewYPos(str); 65 | this.stream.write(chalk.green(str)); 66 | return this; 67 | } 68 | 69 | public magentaBright(str: string = '') { 70 | this.y = this.getNewYPos(str); 71 | this.stream.write(chalk.magentaBright(str)); 72 | return this; 73 | } 74 | 75 | public yellowBright(str: string = '') { 76 | this.y = this.getNewYPos(str); 77 | this.stream.write(chalk.yellowBright(str)); 78 | return this; 79 | } 80 | 81 | public grey(str: string = '') { 82 | this.y = this.getNewYPos(str); 83 | this.stream.write(chalk.gray(str)); 84 | return this; 85 | } 86 | 87 | public clear() { 88 | this.moveTo(0, 0); 89 | this.stream.write('\u001B[2J'); 90 | this.stream.write('\u001B[3J'); 91 | } 92 | 93 | public eraseLine() { 94 | this.moveTo(0, this.y); 95 | this.stream.write('\u001B[0K'); 96 | } 97 | 98 | public moveTo(x: number, y: number) { 99 | this.x = Math.min(x, this.width - 1); 100 | this.y = Math.min(y, this.height - 1); 101 | this.stream.write(`\u001B[${y + 1};${x + 1}H`); 102 | } 103 | 104 | public nextLine(n: number) { 105 | this.moveTo(0, this.y + n); 106 | } 107 | 108 | public eraseDisplayBelow() { 109 | this.stream.write('\u001B[0J'); 110 | } 111 | 112 | public hideCursor(hide: boolean = true) { 113 | if (hide) { 114 | this.stream.write('\u001B[?25l'); 115 | } else { 116 | this.stream.write('\u001B[?25h'); 117 | } 118 | } 119 | 120 | public getCursorLocation() { 121 | return { 122 | x: this.x, 123 | y: this.y, 124 | }; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /setup-mpc-client/src/uploader.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { unlink } from 'fs'; 3 | import { MemoryFifo, MpcServer } from 'setup-mpc-common'; 4 | import { Address } from 'web3x/address'; 5 | 6 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 7 | 8 | export class Uploader extends EventEmitter { 9 | private cancelled = false; 10 | private queue: MemoryFifo = new MemoryFifo(); 11 | 12 | constructor(private server: MpcServer, private address: Address) { 13 | super(); 14 | } 15 | 16 | public async run() { 17 | console.error('Uploader starting...'); 18 | while (true) { 19 | const num = await this.queue.get(); 20 | if (num === null) { 21 | break; 22 | } 23 | await this.uploadTranscriptWithRetry(num); 24 | } 25 | console.error('Uploader complete.'); 26 | } 27 | 28 | public put(transcriptNum: number) { 29 | this.queue.put(transcriptNum); 30 | } 31 | 32 | public cancel() { 33 | this.cancelled = true; 34 | this.queue.cancel(); 35 | } 36 | 37 | public end() { 38 | this.queue.end(); 39 | } 40 | 41 | private async uploadTranscriptWithRetry(num: number) { 42 | const filename = `../setup_db/new/params.params`; 43 | while (!this.cancelled) { 44 | try { 45 | console.error(`Uploading: `, filename); 46 | await this.server.uploadData(this.address, num, filename, undefined, transferred => { 47 | this.emit('progress', num, transferred); 48 | }); 49 | await new Promise(resolve => unlink(filename, resolve)); 50 | this.emit('uploaded', num); 51 | break; 52 | } catch (err) { 53 | console.error(`Failed to upload transcript ${num}: ${err.message}`); 54 | await sleep(1000); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /setup-mpc-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["dom", "esnext", "es2017.object"], 7 | "outDir": "dest", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": false, 11 | "esModuleInterop": true, 12 | "declaration": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /setup-mpc-client/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".", 3 | "exclude": ["**/*.test.*"] 4 | } 5 | -------------------------------------------------------------------------------- /setup-mpc-common/.dockerignore: -------------------------------------------------------------------------------- 1 | dest 2 | node_modules 3 | Dockerfile -------------------------------------------------------------------------------- /setup-mpc-common/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM setup-tools:latest 2 | WORKDIR /usr/src/setup-mpc-common 3 | COPY . . 4 | RUN yarn install && yarn test && yarn build -------------------------------------------------------------------------------- /setup-mpc-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-mpc-common", 3 | "version": "1.0.0", 4 | "main": "dest/index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "dest" 8 | ], 9 | "scripts": { 10 | "build": "tsc -p tsconfig.prod.json", 11 | "test": "jest" 12 | }, 13 | "jest": { 14 | "transform": { 15 | "^.+\\.ts$": "ts-jest" 16 | }, 17 | "testRegex": ".*\\.test\\.(tsx?|js)$", 18 | "moduleFileExtensions": [ 19 | "ts", 20 | "tsx", 21 | "js", 22 | "jsx", 23 | "json", 24 | "node" 25 | ], 26 | "rootDir": "./src" 27 | }, 28 | "dependencies": { 29 | "@types/progress-stream": "^2.0.0", 30 | "isomorphic-fetch": "^2.2.1", 31 | "moment": "^2.24.0", 32 | "progress-stream": "^2.0.0", 33 | "web3x": "^4.0.3" 34 | }, 35 | "devDependencies": { 36 | "@types/isomorphic-fetch": "^0.0.35", 37 | "@types/jest": "^24.0.15", 38 | "@types/node": "^12.6.8", 39 | "jest": "^24.8.0", 40 | "ts-jest": "^24.0.2", 41 | "tslint": "^5.18.0", 42 | "tslint-config-prettier": "^1.18.0", 43 | "typescript": "^3.5.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /setup-mpc-common/src/fifo.ts: -------------------------------------------------------------------------------- 1 | export class MemoryFifo { 2 | private waiting: ((item: T | null) => void)[] = []; 3 | private items: T[] = []; 4 | private flushing: boolean = false; 5 | 6 | public async length(): Promise { 7 | return this.items.length; 8 | } 9 | 10 | public async get(timeout?: number): Promise { 11 | if (this.items.length) { 12 | return Promise.resolve(this.items.shift()!); 13 | } 14 | 15 | if (this.items.length === 0 && this.flushing) { 16 | return Promise.resolve(null); 17 | } 18 | 19 | return new Promise((resolve, reject) => { 20 | this.waiting.push(resolve); 21 | 22 | if (timeout) { 23 | setTimeout(() => { 24 | const index = this.waiting.findIndex(r => r === resolve); 25 | if (index > -1) { 26 | this.waiting.splice(index, 1); 27 | const err = new Error('Timeout getting item from queue.'); 28 | reject(err); 29 | } 30 | }, timeout * 1000); 31 | } 32 | }); 33 | } 34 | 35 | public async put(item: T) { 36 | if (this.flushing) { 37 | return; 38 | } else if (this.waiting.length) { 39 | this.waiting.shift()!(item); 40 | } else { 41 | this.items.push(item); 42 | } 43 | } 44 | 45 | public end() { 46 | this.flushing = true; 47 | this.waiting.forEach(resolve => resolve(null)); 48 | } 49 | 50 | public cancel() { 51 | this.flushing = true; 52 | this.items = []; 53 | this.waiting.forEach(resolve => resolve(null)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /setup-mpc-common/src/hash-files.test.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { hashStreams } from './hash-files'; 3 | 4 | describe('hash-files', () => { 5 | it('should create correct hash', async () => { 6 | const file1 = new Readable(); 7 | const file2 = new Readable(); 8 | 9 | file1.push('somejunk1'); 10 | file1.push(null); 11 | 12 | file2.push('somejunk2'); 13 | file2.push(null); 14 | 15 | const hash = await hashStreams([file1, file2]); 16 | expect(hash.toString('hex')).toBe('227a2c8dc5f3e429ce95820c613385e9bf8b9e44092b5f89b887419198c50efa'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /setup-mpc-common/src/hash-files.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import { createReadStream } from 'fs'; 3 | import { Readable } from 'stream'; 4 | 5 | export function hashFiles(paths: string[]) { 6 | return hashStreams(paths.map(p => createReadStream(p))); 7 | } 8 | 9 | export function hashStreams(streams: Readable[]) { 10 | return new Promise(resolve => { 11 | const hash = createHash('sha256'); 12 | 13 | hash.on('readable', () => { 14 | resolve(hash.read() as Buffer); 15 | }); 16 | 17 | const pipeNext = () => { 18 | const s = streams.shift(); 19 | if (!s) { 20 | hash.end(); 21 | } else { 22 | s.pipe( 23 | hash, 24 | { end: false } 25 | ); 26 | s.on('end', pipeNext); 27 | } 28 | }; 29 | 30 | pipeNext(); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /setup-mpc-common/src/http-client.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, existsSync, statSync } from 'fs'; 2 | import http from 'http'; 3 | import https from 'https'; 4 | import fetch from 'isomorphic-fetch'; 5 | import progress from 'progress-stream'; 6 | import { Readable } from 'stream'; 7 | import { Account } from 'web3x/account'; 8 | import { Address } from 'web3x/address'; 9 | import { bufferToHex } from 'web3x/utils'; 10 | import { hashFiles } from './hash-files'; 11 | import { MpcServer, MpcState, Participant, PatchState, ResetState, MpcStateSummary } from './mpc-server'; 12 | import { mpcStateFromJSON, mpcStateSummaryFromJSON } from './mpc-state'; 13 | 14 | export class HttpClient implements MpcServer { 15 | private opts: any = { 16 | keepalive: true, 17 | }; 18 | constructor(private apiUrl: string, private account?: Account) { 19 | this.opts.agent = /^https/.test(apiUrl) 20 | ? new https.Agent({ keepAlive: true }) 21 | : new http.Agent({ keepAlive: true }); 22 | } 23 | 24 | public async resetState(resetState: ResetState) { 25 | throw new Error('Not implemented.'); 26 | } 27 | 28 | public async loadState(name: string) { 29 | throw new Error('Not implemented.'); 30 | } 31 | 32 | public async flushWaiting() { 33 | throw new Error('Not implemented.'); 34 | } 35 | 36 | public async patchState(state: PatchState): Promise { 37 | throw new Error('Not implemented.'); 38 | } 39 | 40 | public async addParticipant(address: Address, tier: number) { 41 | throw new Error('Not implemented.'); 42 | } 43 | 44 | public async getState(sequence?: number): Promise { 45 | const url = new URL(`${this.apiUrl}/state`); 46 | if (sequence !== undefined) { 47 | url.searchParams.append('sequence', `${sequence}`); 48 | } 49 | const response = await fetch(url.toString(), this.opts); 50 | if (response.status !== 200) { 51 | throw new Error(`Bad status code from server: ${response.status}`); 52 | } 53 | const json = await response.json(); 54 | 55 | return mpcStateFromJSON(json); 56 | } 57 | 58 | public async getStateSummary(): Promise { 59 | const url = new URL(`${this.apiUrl}/state_summary`); 60 | const response = await fetch(url.toString(), this.opts); 61 | if (response.status !== 200) { 62 | throw new Error(`Bad status code from server: ${response.status}`); 63 | } 64 | const json = await response.json(); 65 | 66 | return mpcStateSummaryFromJSON(json); 67 | } 68 | 69 | public async ping(address: Address) { 70 | if (!this.account) { 71 | throw new Error('No account provided. Can only request server state, not modify.'); 72 | } 73 | const { signature } = this.account.sign('ping'); 74 | const response = await fetch(`${this.apiUrl}/ping/${address.toString().toLowerCase()}`, { 75 | ...this.opts, 76 | method: 'GET', 77 | headers: { 78 | 'X-Signature': signature, 79 | }, 80 | }); 81 | if (response.status !== 200) { 82 | throw new Error(`Bad status code from server: ${response.status}`); 83 | } 84 | } 85 | 86 | public async updateParticipant(participant: Participant) { 87 | if (!this.account) { 88 | throw new Error('No account provided. Can only request server state, not modify.'); 89 | } 90 | const { transcripts, address, runningState, computeProgress, error } = participant; 91 | const body = JSON.stringify({ 92 | address: address.toString().toLowerCase(), 93 | runningState, 94 | computeProgress, 95 | transcripts, 96 | error, 97 | }); 98 | const { signature } = this.account.sign(body); 99 | const response = await fetch(`${this.apiUrl}/participant/${address.toString().toLowerCase()}`, { 100 | ...this.opts, 101 | method: 'PATCH', 102 | headers: { 103 | 'Content-Type': 'application/json', 104 | 'X-Signature': signature, 105 | }, 106 | body, 107 | }); 108 | if (response.status !== 200) { 109 | throw new Error(`Bad status code from server: ${response.status}`); 110 | } 111 | } 112 | 113 | public async downloadData(address: Address, transcriptNumber: number) { 114 | const response = await fetch( 115 | `${this.apiUrl}/data/${address.toString().toLowerCase()}/${transcriptNumber}`, 116 | this.opts 117 | ); 118 | if (response.status !== 200) { 119 | throw new Error(`Download failed, bad status code: ${response.status}`); 120 | } 121 | return (response.body! as any) as Readable; 122 | } 123 | 124 | public async downloadSignature(address: Address, transcriptNumber: number) { 125 | const response = await fetch( 126 | `${this.apiUrl}/signature/${address.toString().toLowerCase()}/${transcriptNumber}`, 127 | this.opts 128 | ); 129 | if (response.status !== 200) { 130 | throw new Error(`Download failed, bad status code: ${response.status}`); 131 | } 132 | return (response.body! as any) as string; 133 | } 134 | 135 | public async downloadInitialParams() { 136 | const response = await fetch(`${this.apiUrl}/data/initial_params`, this.opts); 137 | if (response.status !== 200) { 138 | throw new Error(`Download initial params failed, bad status code: ${response.status}`); 139 | } 140 | return (response.body! as any) as Readable; 141 | } 142 | 143 | public async uploadData( 144 | address: Address, 145 | transcriptNumber: number, 146 | transcriptPath: string, 147 | signaturePath?: string, 148 | progressCb?: (transferred: number) => void 149 | ) { 150 | return new Promise(async (resolve, reject) => { 151 | try { 152 | if (!this.account) { 153 | throw new Error('No account provided. Can only request server state, not modify.'); 154 | } 155 | 156 | if (!existsSync(transcriptPath)) { 157 | throw new Error('Transcript not found.'); 158 | } 159 | 160 | const hash = await hashFiles([transcriptPath]); 161 | 162 | const { signature: pingSig } = this.account.sign('ping'); 163 | const { signature: dataSig } = this.account.sign(bufferToHex(hash)); 164 | 165 | const transcriptStream = createReadStream(transcriptPath); 166 | transcriptStream.on('error', error => { 167 | console.error('Transcript read error: ', error); 168 | reject(new Error('Failed to read transcript.')); 169 | }); 170 | 171 | const stats = statSync(transcriptPath); 172 | const progStream = progress({ length: stats.size, time: 1000 }); 173 | if (progressCb) { 174 | progStream.on('progress', progress => progressCb(progress.transferred)); 175 | } 176 | transcriptStream.pipe(progStream); 177 | 178 | const response = await fetch(`${this.apiUrl}/data/${address.toString().toLowerCase()}/${transcriptNumber}`, { 179 | ...this.opts, 180 | method: 'PUT', 181 | body: progStream as any, 182 | headers: { 183 | 'X-Signature': `${pingSig},${dataSig}`, 184 | 'Content-Type': 'application/octet-stream', 185 | 'Content-Length': `${stats.size}`, 186 | }, 187 | }); 188 | 189 | if (response.status !== 200) { 190 | throw new Error(`Upload failed, bad status code: ${response.status}`); 191 | } 192 | 193 | resolve(); 194 | } catch (err) { 195 | if (progressCb) { 196 | progressCb(0); 197 | } 198 | reject(err); 199 | } 200 | }); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /setup-mpc-common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mpc-server'; 2 | export * from './http-client'; 3 | export * from './hash-files'; 4 | export * from './fifo'; 5 | export * from './mpc-state'; 6 | -------------------------------------------------------------------------------- /setup-mpc-common/src/mpc-server.ts: -------------------------------------------------------------------------------- 1 | import { Moment } from 'moment'; 2 | import { Readable } from 'stream'; 3 | import { Address } from 'web3x/address'; 4 | 5 | export type CeremonyState = 'PRESELECTION' | 'SELECTED' | 'RUNNING' | 'COMPLETE'; 6 | export type ParticipantState = 'WAITING' | 'RUNNING' | 'COMPLETE' | 'INVALIDATED'; 7 | export type ParticipantRunningState = 'OFFLINE' | 'WAITING' | 'RUNNING' | 'COMPLETE'; 8 | export type TranscriptState = 'WAITING' | 'VERIFYING' | 'COMPLETE'; 9 | 10 | export interface Transcript { 11 | // Server controlled data. 12 | state: TranscriptState; 13 | // Client controlled data. 14 | num: number; 15 | fromAddress?: Address; 16 | size: number; 17 | downloaded: number; 18 | uploaded: number; 19 | } 20 | 21 | export interface Participant { 22 | // Server controlled data. 23 | sequence: number; 24 | address: Address; 25 | state: ParticipantState; 26 | // Position in the queue (can vary due to online/offline changes), or position computation took place (fixed). 27 | position: number; 28 | // Priority is randomised at the selection date, after which it is fixed. It's used to determine position. 29 | priority: number; 30 | tier: number; 31 | verifyProgress: number; 32 | lastVerified?: Moment; 33 | addedAt: Moment; 34 | startedAt?: Moment; 35 | completedAt?: Moment; 36 | error?: string; 37 | online: boolean; 38 | lastUpdate?: Moment; 39 | location?: ParticipantLocation; 40 | invalidateAfter?: number; 41 | 42 | // Client controlled data. 43 | runningState: ParticipantRunningState; 44 | transcripts: Transcript[]; // Except 'complete'. 45 | computeProgress: number; 46 | } 47 | 48 | export interface ParticipantLocation { 49 | city?: string; 50 | country?: string; 51 | continent?: string; 52 | latitude?: number; 53 | longitude?: number; 54 | } 55 | 56 | export type EthNet = 'mainnet' | 'ropsten'; 57 | 58 | export interface MpcState { 59 | name: string; 60 | adminAddress: Address; 61 | sequence: number; 62 | startSequence: number; 63 | statusSequence: number; 64 | ceremonyState: CeremonyState; 65 | paused: boolean; 66 | maxTier2: number; 67 | minParticipants: number; 68 | invalidateAfter: number; 69 | startTime: Moment; 70 | endTime: Moment; 71 | network: EthNet; 72 | latestBlock: number; 73 | selectBlock: number; 74 | completedAt?: Moment; 75 | participants: Participant[]; 76 | } 77 | 78 | export interface MpcStateSummary { 79 | name: string; 80 | ceremonyState: CeremonyState; 81 | paused: boolean; 82 | maxTier2: number; 83 | minParticipants: number; 84 | startTime: Moment; 85 | endTime: Moment; 86 | selectBlock: number; 87 | completedAt?: Moment; 88 | numParticipants: number; 89 | ceremonyProgress: number; // total progress 90 | } 91 | 92 | export interface ResetState { 93 | name: string; 94 | startTime: Moment; 95 | endTime: Moment; 96 | network: EthNet; 97 | latestBlock: number; 98 | selectBlock: number; 99 | maxTier2: number; 100 | minParticipants: number; 101 | invalidateAfter: number; 102 | participants0: Address[]; 103 | participants1: Address[]; 104 | participants2: Address[]; 105 | } 106 | 107 | export interface PatchState { 108 | paused?: boolean; 109 | startTime?: Moment; 110 | endTime?: Moment; 111 | selectBlock?: number; 112 | maxTier2?: number; 113 | minParticipants?: number; 114 | invalidateAfter?: number; 115 | } 116 | 117 | export interface MpcServer { 118 | resetState(resetState: ResetState): Promise; 119 | loadState(name: string): Promise; 120 | patchState(state: PatchState): Promise; 121 | getState(sequence?: number): Promise; 122 | getStateSummary(): Promise; 123 | ping(address: Address, ip?: string): Promise; 124 | addParticipant(address: Address, tier: number): Promise; 125 | updateParticipant(participant: Participant, admin?: boolean): Promise; 126 | downloadData(address: Address, transcriptNumber: number): Promise; 127 | downloadSignature(address: Address, num: number): Promise; 128 | downloadInitialParams(): Promise; 129 | uploadData( 130 | address: Address, 131 | transcriptNumber: number, 132 | transcriptPath: string, 133 | signaturePath?: string, 134 | progressCb?: (transferred: number) => void 135 | ): Promise; 136 | flushWaiting(): Promise; 137 | } 138 | -------------------------------------------------------------------------------- /setup-mpc-common/src/mpc-state.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { Address } from 'web3x/address'; 3 | import { MpcState, Participant, MpcStateSummary } from './mpc-server'; 4 | 5 | export function mpcStateFromJSON(json: any): MpcState { 6 | const { startTime, endTime, completedAt, participants, ...rest } = json; 7 | 8 | return { 9 | ...rest, 10 | startTime: moment(startTime), 11 | endTime: moment(endTime), 12 | completedAt: completedAt ? moment(completedAt) : undefined, 13 | participants: participants.map(({ startedAt, lastUpdate, completedAt, address, transcripts, ...rest }: any) => ({ 14 | ...rest, 15 | startedAt: startedAt ? moment(startedAt) : undefined, 16 | lastUpdate: lastUpdate ? moment(lastUpdate) : undefined, 17 | completedAt: completedAt ? moment(completedAt) : undefined, 18 | address: Address.fromString(address), 19 | transcripts: transcripts.map(({ fromAddress, ...rest }: any) => ({ 20 | ...rest, 21 | fromAddress: fromAddress ? Address.fromString(fromAddress) : undefined, 22 | })), 23 | })), 24 | }; 25 | } 26 | 27 | export function mpcStateSummaryFromJSON(json: any): MpcStateSummary { 28 | const { startTime, endTime, completedAt, participants, ...rest } = json; 29 | return { 30 | ...rest, 31 | startTime: moment(startTime), 32 | endTime: moment(endTime), 33 | completedAt: completedAt ? moment(completedAt) : undefined, 34 | numParticipants: participants.length, 35 | }; 36 | } 37 | 38 | export function cloneParticipant(participant: Participant): Participant { 39 | return { 40 | ...participant, 41 | transcripts: participant.transcripts.map(t => ({ ...t })), 42 | }; 43 | } 44 | 45 | export function cloneMpcState(state: MpcState): MpcState { 46 | return { 47 | ...state, 48 | participants: state.participants.map(cloneParticipant), 49 | }; 50 | } 51 | 52 | export function applyDelta(state: MpcState, delta: MpcState): MpcState { 53 | const participants = [...state.participants]; 54 | delta.participants.forEach(p => (participants[p.position - 1] = p)); 55 | return { 56 | ...delta, 57 | participants, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /setup-mpc-common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["dom", "esnext", "es2017.object"], 7 | "outDir": "dest", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": false, 11 | "esModuleInterop": true, 12 | "declaration": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /setup-mpc-common/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".", 3 | "exclude": ["**/*.test.*"] 4 | } 5 | -------------------------------------------------------------------------------- /setup-mpc-server/.dockerignore: -------------------------------------------------------------------------------- 1 | dest 2 | node_modules 3 | store 4 | **/.terraform -------------------------------------------------------------------------------- /setup-mpc-server/.gitignore: -------------------------------------------------------------------------------- 1 | store 2 | initial/circuit.json 3 | initial/initial_params 4 | initial/radix/* 5 | !initial/radix/phase1radix2m9_example -------------------------------------------------------------------------------- /setup-mpc-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM setup-mpc-common:latest 2 | WORKDIR /usr/src/setup-mpc-common 3 | RUN yarn link 4 | WORKDIR /usr/src/setup-mpc-server 5 | COPY . ./ 6 | RUN ../setup-tools/new ./initial/circuit.json ./initial/initial_params ./initial/radix \ 7 | && yarn link "setup-mpc-common" \ 8 | && yarn install \ 9 | && yarn test \ 10 | && yarn build 11 | CMD [ "node", "./dest"] -------------------------------------------------------------------------------- /setup-mpc-server/README.md: -------------------------------------------------------------------------------- 1 | # zkparty MPC Coordinator Guide 2 | **All of this code is highly experimental and has not been audited. Use at your own risk.** 3 | 4 | The coordinator guide can be found here: [HackMD link](https://hackmd.io/@bgu33/H1ndttIBL) 5 | 6 | This set of tools allows you to run a trusted setup ceremony for zkSNARKs. We are using a fork of AZTEC's trusted setup repository. 7 | 8 | In the coordinator guide: 9 | - **Ceremony lifecycle**: An overview of how ceremonies work with `setup-mpc-server`. 10 | - **Get, build, and run a server**: How to build and run the trusted setup server code. 11 | - **Quickstart**: Get up and running locally once you've built the repo. 12 | - **Ceremony state guide**: The ceremony parameters (admin address, start time, end conditions, timeout conditions, more) and how to set and change them. 13 | - **Ceremony data guide**: Where ceremony data is stored, and how to reload or discard data. 14 | - **Selecting participants**: How to register and order participants in the ceremony. 15 | - **API**: Description of the coordinator server API. 16 | - **Setup binaries**: How to build and run the phase2 binaries used by the coordinator server on their own. 17 | -------------------------------------------------------------------------------- /setup-mpc-server/initial/radix/phase1radix2m9_example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gubsheep/Setup/92e972163552e3efa1402a73c7adf0c5a0660969/setup-mpc-server/initial/radix/phase1radix2m9_example -------------------------------------------------------------------------------- /setup-mpc-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-mpc-server", 3 | "version": "1.0.4", 4 | "main": "dest/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.prod.json && ln -f ./src/maxmind/GeoLite2-City.mmdb ./dest/maxmind/GeoLite2-City.mmdb", 8 | "start": "node ./dest", 9 | "start:dev": "tsc-watch -p tsconfig.prod.json --onSuccess 'yarn start'", 10 | "test": "jest --silent", 11 | "postinstall": "yarn link setup-mpc-common" 12 | }, 13 | "jest": { 14 | "transform": { 15 | "^.+\\.ts$": "ts-jest" 16 | }, 17 | "testRegex": ".*\\.test\\.(tsx?|js)$", 18 | "moduleFileExtensions": [ 19 | "ts", 20 | "tsx", 21 | "js", 22 | "jsx", 23 | "json", 24 | "node" 25 | ], 26 | "rootDir": "./src" 27 | }, 28 | "dependencies": { 29 | "@koa/cors": "^3.0.0", 30 | "@types/bn.js": "^4.11.5", 31 | "@types/node": "^12.6.8", 32 | "async-mutex": "^0.1.3", 33 | "aws-sdk": "^2.526.0", 34 | "bn.js": "^5.0.0", 35 | "form-data": "^2.5.0", 36 | "isomorphic-fetch": "^2.2.1", 37 | "koa": "^2.7.0", 38 | "koa-body": "^4.1.0", 39 | "koa-compress": "^3.0.0", 40 | "koa-router": "^7.4.0", 41 | "maxmind-db-reader": "^0.2.1", 42 | "moment": "^2.24.0", 43 | "path": "^0.12.7", 44 | "q": "^1.5.1", 45 | "seedrandom": "^3.0.3", 46 | "stream-meter": "^1.0.4", 47 | "tsc-watch": "^2.2.1", 48 | "typescript": "^3.5.3", 49 | "web3x": "^4.0.3" 50 | }, 51 | "devDependencies": { 52 | "@types/isomorphic-fetch": "^0.0.35", 53 | "@types/jest": "^24.0.15", 54 | "@types/koa": "^2.0.49", 55 | "@types/koa-compress": "^2.0.9", 56 | "@types/koa-router": "^7.0.42", 57 | "@types/seedrandom": "^2.4.28", 58 | "@types/stream-meter": "^0.0.21", 59 | "@types/supertest": "^2.0.8", 60 | "jest": "^24.8.0", 61 | "supertest": "^4.0.2", 62 | "ts-jest": "^24.0.2", 63 | "tslint": "^5.18.0", 64 | "tslint-config-prettier": "^1.18.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /setup-mpc-server/run-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -z "$PORT" ]]; then 3 | PORT="8081" # default port 8081 4 | fi 5 | 6 | if [[ -z "$VOLUME" ]]; then 7 | VOLUME="mpc-server-vol" 8 | fi 9 | 10 | if [[ -z "$ADMIN_ADDRESS" ]]; then 11 | ADMIN_ADDRESS="0x1aA18F5b595d87CC2C66d7b93367d8beabE203bB" 12 | fi 13 | 14 | if [[ $* == *--clear-state* ]]; then 15 | # remove all stopped setup-mpc-server containers 16 | # if you still get an Error: volume is in use error then try `docker system prune` 17 | docker ps -a | awk '{ print $1,$2 }' | grep setup-mpc-server:latest | awk '{print $1 }' | xargs -I {} docker rm {} 18 | # remove mpc-server-vol volume 19 | docker volume rm "$VOLUME" 20 | fi 21 | 22 | # first command mounts a volume; second does not 23 | docker run --mount 'type=volume,src='"$VOLUME"',dst=/usr/src/setup-mpc-server/store' -p "$PORT":80 -e ADMIN_ADDRESS="$ADMIN_ADDRESS" setup-mpc-server:latest 24 | # docker run -p "$PORT":80 -e ADMIN_ADDRESS="$ADMIN_ADDRESS" setup-mpc-server:latest 25 | -------------------------------------------------------------------------------- /setup-mpc-server/src/app.test.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | import moment from 'moment'; 3 | import { MpcServer } from 'setup-mpc-common'; 4 | import request from 'supertest'; 5 | import { Account } from 'web3x/account'; 6 | import { Address } from 'web3x/address'; 7 | import { bufferToHex, hexToBuffer } from 'web3x/utils'; 8 | import { appFactory } from './app'; 9 | import { createParticipant } from './state/create-participant'; 10 | import { defaultState } from './state/default-state'; 11 | 12 | type Mockify = { [P in keyof T]: jest.Mock }; 13 | 14 | describe('app', () => { 15 | const account = Account.fromPrivate( 16 | hexToBuffer('0xf94ac892bbe482ca01cc43cce0f467d63baef67e37428209f8193fdc0e6d9013') 17 | ); 18 | const { signature: pingSig } = account.sign('ping'); 19 | let app: any; 20 | let mockServer: Mockify; 21 | 22 | beforeEach(() => { 23 | mockServer = { 24 | getState: jest.fn(), 25 | getStateSummary: jest.fn(), 26 | resetState: jest.fn(), 27 | loadState: jest.fn(), 28 | patchState: jest.fn(), 29 | addParticipant: jest.fn(), 30 | updateParticipant: jest.fn(), 31 | downloadData: jest.fn(), 32 | downloadSignature: jest.fn(), 33 | downloadInitialParams: jest.fn(), 34 | uploadData: jest.fn(), 35 | ping: jest.fn(), 36 | flushWaiting: jest.fn(), 37 | }; 38 | 39 | const state = defaultState(1234, Address.fromString('0x1aA18F5b595d87CC2C66d7b93367d8beabE203bB')); 40 | const participant = createParticipant(0, moment(), 0, 1, account.address); 41 | participant.state = 'RUNNING'; 42 | state.participants.push(participant); 43 | mockServer.getState.mockResolvedValue(state); 44 | 45 | const mockParticipantSelector = { 46 | getCurrentBlockHeight: jest.fn(), 47 | }; 48 | app = appFactory(mockServer as any, account.address, mockParticipantSelector as any, undefined, '/tmp', 32); 49 | }); 50 | 51 | describe('GET /', () => { 52 | it('should return 200', async () => { 53 | const response = await request(app.callback()) 54 | .get('/') 55 | .send(); 56 | expect(response.status).toBe(200); 57 | }); 58 | }); 59 | 60 | describe('PUT /data', () => { 61 | it('should return 401 with no signature header', async () => { 62 | const response = await request(app.callback()) 63 | .put(`/data/${account.address}/0`) 64 | .send(); 65 | expect(response.status).toBe(401); 66 | expect(response.body.error).toMatch(/X-Signature/); 67 | }); 68 | 69 | it('should return 401 with transcript number out of range', async () => { 70 | const response = await request(app.callback()) 71 | .put(`/data/${account.address}/30`) 72 | .set('X-Signature', `${pingSig},placeholder2`) 73 | .send(); 74 | expect(response.status).toBe(401); 75 | expect(response.body.error).toMatch(/out of range/); 76 | }); 77 | 78 | it('should return 401 with bad signature', async () => { 79 | const body = 'hello world'; 80 | const badSig = `${pingSig},0x76195abb935b441f1553b2f6c60d272de5a56391dfcca8cf22399c4cb600dd26188a4f003176ccdf7f314cbe08740bf7414fadef0e74cb42e94745a836e9dd311d`; 81 | 82 | const response = await request(app.callback()) 83 | .put(`/data/${account.address}/0`) 84 | .set('X-Signature', badSig) 85 | .send(body); 86 | expect(response.status).toBe(401); 87 | expect(response.body.error).toMatch(/does not match X-Signature/); 88 | }); 89 | 90 | it('should return 429 with body length exceeding limit', async () => { 91 | const body = '000000000000000000000000000000000'; 92 | 93 | const response = await request(app.callback()) 94 | .put(`/data/${account.address}/0`) 95 | .set('X-Signature', `${pingSig},placeholder2`) 96 | .send(body); 97 | expect(response.status).toBe(429); 98 | expect(response.body.error).toMatch(/Stream exceeded/); 99 | }); 100 | 101 | it('should return 200 on success', async () => { 102 | const body = 'hello world'; 103 | const hash = createHash('sha256') 104 | .update(body) 105 | .digest(); 106 | const { signature: dataSig } = account.sign(bufferToHex(hash)); 107 | 108 | const response = await request(app.callback()) 109 | .put(`/data/${account.address}/0`) 110 | .set('X-Signature', `${pingSig},${dataSig}`) 111 | .send(body); 112 | expect(response.status).toBe(200); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /setup-mpc-server/src/app.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { createWriteStream, unlink } from 'fs'; 3 | import Koa from 'koa'; 4 | import koaBody from 'koa-body'; 5 | import compress from 'koa-compress'; 6 | import Router from 'koa-router'; 7 | import moment from 'moment'; 8 | import readline from 'readline'; 9 | import { hashFiles, MpcServer } from 'setup-mpc-common'; 10 | import { PassThrough } from 'stream'; 11 | import meter from 'stream-meter'; 12 | import { isNumber, isString } from 'util'; 13 | import { Address } from 'web3x/address'; 14 | import { bufferToHex, randomBuffer, recover } from 'web3x/utils'; 15 | import { writeFileAsync } from './fs-async'; 16 | import { ParticipantSelectorFactory } from './participant-selector'; 17 | import { defaultState } from './state/default-state'; 18 | 19 | const cors = require('@koa/cors'); 20 | 21 | // 1GB 22 | const MAX_UPLOAD_SIZE = 1024 * 1024 * 1024; 23 | 24 | function normaliseSettings(settings: any) { 25 | if (isString(settings.startTime)) { 26 | settings.startTime = moment(settings.startTime); 27 | } else if (isNumber(settings.startTime)) { 28 | settings.startTime = moment().add(settings.startTime, 's'); 29 | } 30 | 31 | if (isString(settings.endTime)) { 32 | settings.endTime = moment(settings.endTime); 33 | } else if (isNumber(settings.endTime)) { 34 | settings.endTime = moment(settings.startTime).add(settings.endTime, 's'); 35 | } 36 | 37 | if (settings.selectBlock < 0) { 38 | // If select block is negative, use it as an offset from the latest block. 39 | settings.selectBlock = settings.latestBlock - settings.selectBlock; 40 | } 41 | } 42 | 43 | export function appFactory( 44 | server: MpcServer, 45 | adminAddress: Address, 46 | participantSelectorFactory: ParticipantSelectorFactory, 47 | prefix?: string, 48 | tmpDir: string = '/tmp', 49 | maxUploadSize: number = MAX_UPLOAD_SIZE 50 | ) { 51 | const isAdmin = (ctx: Koa.Context) => { 52 | const signature = ctx.get('X-Signature'); 53 | return adminAddress.equals(recover('SignMeWithYourPrivateKey', signature)); 54 | }; 55 | 56 | const adminAuth = async (ctx: Koa.Context, next: any) => { 57 | if (!isAdmin(ctx)) { 58 | ctx.status = 401; 59 | return; 60 | } 61 | await next(ctx); 62 | }; 63 | 64 | const router = new Router({ prefix }); 65 | 66 | router.get('/', async (ctx: Koa.Context) => { 67 | ctx.body = 'OK\n'; 68 | }); 69 | 70 | router.post('/reset', adminAuth, koaBody(), async (ctx: Koa.Context) => { 71 | const network = ctx.request.body.network || 'ropsten'; 72 | const latestBlock = await participantSelectorFactory.getCurrentBlockHeight(network); 73 | const { adminAddress: prevAdminAddress, ...rest } = defaultState(latestBlock, adminAddress); 74 | const resetState = { 75 | ...rest, 76 | participants0: [], 77 | participants1: [], 78 | participants2: [], 79 | ...ctx.request.body, 80 | }; 81 | normaliseSettings(resetState); 82 | 83 | resetState.participants0 = resetState.participants0.map(Address.fromString); 84 | resetState.participants1 = resetState.participants1.map(Address.fromString); 85 | resetState.participants2 = resetState.participants2.map(Address.fromString); 86 | 87 | try { 88 | await server.resetState(resetState); 89 | ctx.status = 200; 90 | } catch (err) { 91 | ctx.body = { error: err.message }; 92 | ctx.status = 400; 93 | } 94 | }); 95 | 96 | router.post('/flush-waiting', adminAuth, async (ctx: Koa.Context) => { 97 | await server.flushWaiting(); 98 | ctx.status = 200; 99 | }); 100 | 101 | router.get('/state', async (ctx: Koa.Context) => { 102 | ctx.body = await server.getState(ctx.query.sequence); 103 | }); 104 | 105 | router.get('/state-summary', async (ctx: Koa.Context) => { 106 | ctx.body = await server.getStateSummary(); 107 | }); 108 | 109 | router.get('/state/load/:name', async (ctx: Koa.Context) => { 110 | ctx.body = await server.loadState(ctx.params.name); 111 | }); 112 | 113 | router.patch('/state', adminAuth, koaBody(), async (ctx: Koa.Context) => { 114 | normaliseSettings(ctx.request.body); 115 | ctx.body = await server.patchState(ctx.request.body); 116 | }); 117 | 118 | router.get('/ping/:address', koaBody(), async (ctx: Koa.Context) => { 119 | const signature = ctx.get('X-Signature'); 120 | const address = Address.fromString(ctx.params.address.toLowerCase()); 121 | if (!address.equals(recover('ping', signature))) { 122 | ctx.status = 401; 123 | return; 124 | } 125 | await server.ping(address, ctx.request.ip); 126 | ctx.status = 200; 127 | }); 128 | 129 | router.put('/participant/:address', adminAuth, async (ctx: Koa.Context) => { 130 | const address = Address.fromString(ctx.params.address.toLowerCase()); 131 | let tier = 2; 132 | if (ctx.query.tier === '0' || ctx.query.tier === '1' || ctx.query.tier === '2' || ctx.query.tier === '3') { 133 | tier = +ctx.query.tier; 134 | } 135 | server.addParticipant(address, tier); 136 | ctx.status = 204; 137 | }); 138 | 139 | router.patch('/participant/:address', koaBody(), async (ctx: Koa.Context) => { 140 | const signature = ctx.get('X-Signature'); 141 | const address = Address.fromString(ctx.params.address.toLowerCase()); 142 | let admin = false; 143 | if (!address.equals(recover(JSON.stringify(ctx.request.body), signature))) { 144 | admin = isAdmin(ctx); 145 | if (!admin) { 146 | ctx.status = 401; 147 | return; 148 | } 149 | } 150 | try { 151 | await server.updateParticipant( 152 | { 153 | ...ctx.request.body, 154 | address, 155 | }, 156 | admin 157 | ); 158 | } catch (err) { 159 | // This is a "not running" error. Just swallow it as the client need not be concerned with this. 160 | } 161 | ctx.status = 200; 162 | }); 163 | 164 | router.get('/signature/:address/:num', async (ctx: Koa.Context) => { 165 | const { address, num } = ctx.params; 166 | ctx.body = await server.downloadSignature(Address.fromString(address.toLowerCase()), num); 167 | }); 168 | 169 | router.get('/data/:address/:num', async (ctx: Koa.Context) => { 170 | const { address, num } = ctx.params; 171 | ctx.body = await server.downloadData(Address.fromString(address.toLowerCase()), num); 172 | }); 173 | 174 | router.get('/data/initial_params', async (ctx: Koa.Context) => { 175 | ctx.body = await server.downloadInitialParams(); 176 | }); 177 | 178 | router.put('/data/:address/:num', async (ctx: Koa.Context) => { 179 | const address = Address.fromString(ctx.params.address.toLowerCase()); 180 | const transcriptNum = +ctx.params.num; 181 | const [pingSig, dataSig] = ctx.get('X-Signature').split(','); 182 | 183 | console.log(`Receiving transcript: /${address.toString()}/${transcriptNum}`); 184 | 185 | // 500, unless we explicitly set it to 200 or something else. 186 | ctx.status = 500; 187 | 188 | try { 189 | if (!pingSig || !dataSig) { 190 | throw new Error('X-Signature header incomplete.'); 191 | } 192 | 193 | // Before reading body, pre-authenticate user. 194 | if (!address.equals(recover('ping', pingSig))) { 195 | throw new Error('Ping signature incorrect.'); 196 | } 197 | 198 | const { participants } = await server.getState(); 199 | const participant = participants.find(p => p.address.equals(address)); 200 | if (!participant || participant.state !== 'RUNNING') { 201 | throw new Error('Can only upload to currently running participant.'); 202 | } 203 | 204 | if (transcriptNum >= 30) { 205 | throw new Error('Transcript number out of range (max 0-29).'); 206 | } 207 | } catch (err) { 208 | console.log(`Rejecting: ${err.message}`); 209 | ctx.body = { error: err.message }; 210 | ctx.status = 401; 211 | return; 212 | } 213 | 214 | const nonce = randomBuffer(8).toString('hex'); 215 | const transcriptPath = `${tmpDir}/transcript_${ctx.params.address}_${transcriptNum}_${nonce}.dat`; 216 | const signaturePath = `${tmpDir}/transcript_${ctx.params.address}_${transcriptNum}_${nonce}.sig`; 217 | 218 | try { 219 | console.log(`Writing to temporary file: ${transcriptPath}`); 220 | await new Promise((resolve, reject) => { 221 | const writeStream = createWriteStream(transcriptPath); 222 | const meterStream = meter(maxUploadSize); 223 | ctx.req.setTimeout(180000, () => reject(new Error('Timeout reading data.'))); 224 | ctx.req 225 | .on('error', reject) 226 | .on('aborted', () => reject(new Error('Read was aborted.'))) 227 | .pipe(meterStream) 228 | .on('error', (err: Error) => { 229 | ctx.status = 429; 230 | reject(err || new Error('Upload too large.')); 231 | }) 232 | .pipe(writeStream) 233 | .on('error', reject) 234 | .on('finish', resolve); 235 | }); 236 | console.log(`Finished receiving transcript: ${transcriptPath}`); 237 | 238 | const hash = await hashFiles([transcriptPath]); 239 | if (!address.equals(recover(bufferToHex(hash), dataSig))) { 240 | ctx.status = 401; 241 | throw new Error('Body signature does not match X-Signature.'); 242 | } 243 | 244 | await writeFileAsync(signaturePath, dataSig); 245 | 246 | await server.uploadData(address, +ctx.params.num, transcriptPath, signaturePath); 247 | 248 | ctx.status = 200; 249 | } catch (err) { 250 | console.log(`Rejecting: ${err ? err.message : 'no error information'}`); 251 | ctx.body = { error: err.message }; 252 | unlink(transcriptPath, () => {}); 253 | unlink(signaturePath, () => {}); 254 | return; 255 | } 256 | }); 257 | 258 | const app = new Koa(); 259 | app.proxy = true; 260 | app.use(compress()); 261 | app.use(cors()); 262 | app.use(router.routes()); 263 | app.use(router.allowedMethods()); 264 | 265 | return app; 266 | } 267 | -------------------------------------------------------------------------------- /setup-mpc-server/src/fs-async.ts: -------------------------------------------------------------------------------- 1 | import { access, copyFile, exists, mkdir, readdir, readFile, rename, rmdir, stat, unlink, writeFile } from 'fs'; 2 | import { promisify } from 'util'; 3 | 4 | export const accessAsync = promisify(access); 5 | export const existsAsync = promisify(exists); 6 | export const renameAsync = promisify(rename); 7 | export const mkdirAsync = promisify(mkdir); 8 | export const unlinkAsync = promisify(unlink); 9 | export const writeFileAsync = promisify(writeFile); 10 | export const readFileAsync = promisify(readFile); 11 | export const readdirAsync = promisify(readdir); 12 | export const rmdirAsync = promisify(rmdir); 13 | export const statAsync = promisify(stat); 14 | export const copyFileAsync = promisify(copyFile); 15 | -------------------------------------------------------------------------------- /setup-mpc-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import { Address } from 'web3x/address'; 3 | import { appFactory } from './app'; 4 | import { mkdirAsync } from './fs-async'; 5 | import { ParticipantSelectorFactory } from './participant-selector'; 6 | import { Server } from './server'; 7 | import { DiskStateStore } from './state-store'; 8 | import { defaultState } from './state/default-state'; 9 | import { DiskTranscriptStoreFactory } from './transcript-store'; 10 | 11 | const { 12 | PORT = 80, 13 | STORE_PATH = './store', 14 | INFURA_API_KEY = 'fe576c8bab174752bd0d963c89d5d7a2', 15 | ADMIN_ADDRESS = '0x1aA18F5b595d87CC2C66d7b93367d8beabE203bB', 16 | } = process.env; 17 | 18 | async function main() { 19 | const shutdown = async () => process.exit(0); 20 | process.once('SIGINT', shutdown); 21 | process.once('SIGTERM', shutdown); 22 | 23 | await mkdirAsync(STORE_PATH, { recursive: true }); 24 | 25 | const adminAddress = Address.fromString(ADMIN_ADDRESS); 26 | const participantSelectorFactory = new ParticipantSelectorFactory(adminAddress, INFURA_API_KEY); 27 | const latestBlock = await participantSelectorFactory.getCurrentBlockHeight('ropsten'); 28 | const defaults = defaultState(latestBlock, adminAddress); 29 | const stateStore = new DiskStateStore(STORE_PATH + '/state', defaults); 30 | const transcriptStoreFactory = new DiskTranscriptStoreFactory(STORE_PATH); 31 | 32 | const server = new Server(transcriptStoreFactory, stateStore, participantSelectorFactory); 33 | await server.start(); 34 | 35 | const tmpPath = STORE_PATH + '/tmp'; 36 | await mkdirAsync(tmpPath, { recursive: true }); 37 | const app = appFactory(server, adminAddress, participantSelectorFactory, '/api', tmpPath); 38 | 39 | const httpServer = http.createServer(app.callback()); 40 | httpServer.listen(PORT); 41 | console.log(`Server listening.`); 42 | } 43 | 44 | main().catch(console.log); 45 | -------------------------------------------------------------------------------- /setup-mpc-server/src/maxmind/GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gubsheep/Setup/92e972163552e3efa1402a73c7adf0c5a0660969/setup-mpc-server/src/maxmind/GeoLite2-City.mmdb -------------------------------------------------------------------------------- /setup-mpc-server/src/maxmind/index.ts: -------------------------------------------------------------------------------- 1 | const mmdbreader = require('maxmind-db-reader'); 2 | const cities = mmdbreader.openSync(__dirname + '/GeoLite2-City.mmdb'); 3 | 4 | export interface GeoData { 5 | city?: string; 6 | country?: string; 7 | continent?: string; 8 | latitude?: number; 9 | longitude?: number; 10 | } 11 | 12 | export function getGeoData(ip: string) { 13 | try { 14 | const data = cities.getGeoDataSync(ip); 15 | if (!data) { 16 | return; 17 | } 18 | const geoData: GeoData = {}; 19 | if (data.city) { 20 | geoData.city = data.city.names.en; 21 | } 22 | if (data.country) { 23 | geoData.country = data.country.names.en; 24 | } 25 | if (data.continent) { 26 | geoData.continent = data.continent.names.en; 27 | } 28 | if (data.location) { 29 | geoData.latitude = data.location.latitude; 30 | geoData.longitude = data.location.longitude; 31 | } 32 | return geoData; 33 | } catch (e) { 34 | return; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /setup-mpc-server/src/participant-selector.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { EthNet } from 'setup-mpc-common'; 3 | import { Address } from 'web3x/address'; 4 | import { Eth } from 'web3x/eth'; 5 | import { HttpProvider } from 'web3x/providers'; 6 | 7 | export class ParticipantSelectorFactory { 8 | constructor(private signupAddress: Address, private projectId: string) {} 9 | 10 | public create(ethNet: EthNet, startBlock: number, selectBlock: number) { 11 | return new ParticipantSelector(ethNet, this.signupAddress, startBlock, selectBlock, this.projectId); 12 | } 13 | 14 | public async getCurrentBlockHeight(ethNet: EthNet) { 15 | const provider = new HttpProvider(`https://${ethNet}.infura.io/v3/${this.projectId}`); 16 | const eth = new Eth(provider); 17 | return await eth.getBlockNumber(); 18 | } 19 | } 20 | 21 | export class ParticipantSelector extends EventEmitter { 22 | private provider: HttpProvider; 23 | private eth: Eth; 24 | private cancelled = false; 25 | 26 | constructor( 27 | ethNet: EthNet, 28 | private signupAddress: Address, 29 | private startBlock: number, 30 | private selectBlock: number, 31 | private projectId: string 32 | ) { 33 | super(); 34 | 35 | this.provider = new HttpProvider(`https://${ethNet}.infura.io/v3/${this.projectId}`); 36 | this.eth = new Eth(this.provider); 37 | } 38 | 39 | public async run() { 40 | console.log('Block processor starting...'); 41 | let currentBlock = this.startBlock; 42 | while (!this.cancelled) { 43 | try { 44 | const block = await this.eth.getBlock(currentBlock, true); 45 | const participants = block.transactions 46 | .filter(t => (t.to ? t.to.equals(this.signupAddress) : false)) 47 | .map(t => t.from); 48 | this.emit('newParticipants', participants, currentBlock); 49 | if (currentBlock === this.selectBlock) { 50 | this.emit('selectParticipants', block.hash); 51 | } 52 | currentBlock += 1; 53 | } catch (err) { 54 | await new Promise(resolve => setTimeout(resolve, 10000)); 55 | } 56 | } 57 | console.log('Block processor complete.'); 58 | } 59 | 60 | public stop() { 61 | this.cancelled = true; 62 | this.removeAllListeners(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /setup-mpc-server/src/s3-explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | AWS S3 Explorer 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 | 79 | 80 |
81 | 82 |
83 | 84 |
85 | AWS S3 Explorer  86 |
87 | 88 |
89 | 94 |
95 |
96 | 97 | 127 |
128 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
ObjectFolderLast ModifiedTimestampSize
143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /setup-mpc-server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { Mutex } from 'async-mutex'; 2 | import moment, { Moment } from 'moment'; 3 | import { 4 | cloneMpcState, 5 | EthNet, 6 | MpcServer, 7 | MpcState, 8 | Participant, 9 | PatchState, 10 | ResetState, 11 | MpcStateSummary, 12 | } from 'setup-mpc-common'; 13 | import { Address } from 'web3x/address'; 14 | import { getGeoData } from './maxmind'; 15 | import { ParticipantSelector, ParticipantSelectorFactory } from './participant-selector'; 16 | import { StateStore } from './state-store'; 17 | import { advanceState } from './state/advance-state'; 18 | import { createParticipant } from './state/create-participant'; 19 | import { orderWaitingParticipants } from './state/order-waiting-participants'; 20 | import { resetParticipant } from './state/reset-participant'; 21 | import { selectParticipants } from './state/select-participants'; 22 | import { TranscriptStore, TranscriptStoreFactory } from './transcript-store'; 23 | import { Verifier } from './verifier'; 24 | 25 | export class Server implements MpcServer { 26 | private interval?: NodeJS.Timer; 27 | private verifier!: Verifier; 28 | private state!: MpcState; 29 | private readState!: MpcState; 30 | private mutex = new Mutex(); 31 | private participantSelector?: ParticipantSelector; 32 | private store!: TranscriptStore; 33 | 34 | constructor( 35 | private storeFactory: TranscriptStoreFactory, 36 | private stateStore: StateStore, 37 | private participantSelectorFactory: ParticipantSelectorFactory 38 | ) {} 39 | 40 | public async start() { 41 | // Take a copy of the state from the state store. 42 | const state = await this.stateStore.getState(); 43 | await this.resetWithState(state); 44 | } 45 | 46 | public stop() { 47 | clearInterval(this.interval!); 48 | 49 | if (this.participantSelector) { 50 | this.participantSelector.stop(); 51 | } 52 | 53 | if (this.verifier) { 54 | this.verifier.cancel(); 55 | } 56 | } 57 | 58 | private async createUniqueCeremonyName(name: string) { 59 | // Ensure name is unique. 60 | if (await this.stateStore.exists(name)) { 61 | let n = 1; 62 | while (await this.stateStore.exists(`${name}_${n}`)) { 63 | ++n; 64 | } 65 | return name + `_${n}`; 66 | } 67 | return name; 68 | } 69 | 70 | public async resetState(resetState: ResetState) { 71 | await this.stateStore.saveState(); 72 | 73 | const nextSequence = this.state.sequence + 1; 74 | const state: MpcState = { 75 | name: await this.createUniqueCeremonyName(resetState.name), 76 | adminAddress: this.state.adminAddress, 77 | sequence: nextSequence, 78 | statusSequence: nextSequence, 79 | startSequence: nextSequence, 80 | ceremonyState: 'PRESELECTION', 81 | paused: false, 82 | startTime: resetState.startTime, 83 | endTime: resetState.endTime, 84 | network: resetState.network, 85 | latestBlock: resetState.latestBlock, 86 | selectBlock: resetState.selectBlock, 87 | maxTier2: resetState.maxTier2, 88 | minParticipants: resetState.minParticipants, 89 | invalidateAfter: resetState.invalidateAfter, 90 | participants: [], 91 | }; 92 | 93 | if (resetState.participants0.length) { 94 | resetState.participants0.forEach(address => this.addNextParticipant(state, address, 0)); 95 | } 96 | if (resetState.participants1.length) { 97 | resetState.participants1.forEach(address => this.addNextParticipant(state, address, 1)); 98 | } 99 | if (resetState.participants2.length) { 100 | resetState.participants2.forEach(address => this.addNextParticipant(state, address, 2)); 101 | } 102 | 103 | await this.resetWithState(state); 104 | } 105 | 106 | public async flushWaiting() { 107 | const release = await this.mutex.acquire(); 108 | try { 109 | this.state.participants = this.state.participants.filter(p => p.state !== 'WAITING'); 110 | this.state.sequence += 1; 111 | // Force clients to re-request entire state. 112 | this.state.startSequence += this.state.sequence; 113 | this.state.latestBlock = -1; 114 | 115 | if (this.participantSelector) { 116 | this.participantSelector.stop(); 117 | this.participantSelector = undefined; 118 | } 119 | } finally { 120 | release(); 121 | } 122 | } 123 | 124 | public async loadState(name: string) { 125 | await this.stateStore.saveState(); 126 | const state = await this.stateStore.restoreState(name); 127 | await this.resetWithState(state); 128 | } 129 | 130 | public async patchState(state: PatchState) { 131 | const release = await this.mutex.acquire(); 132 | switch (this.state.ceremonyState) { 133 | case 'COMPLETE': 134 | case 'RUNNING': 135 | delete state.startTime; 136 | case 'SELECTED': 137 | delete state.selectBlock; 138 | delete state.maxTier2; 139 | } 140 | 141 | this.state = { 142 | ...this.state, 143 | ...state, 144 | }; 145 | 146 | if (this.state.latestBlock < 0 && this.participantSelector) { 147 | this.participantSelector.stop(); 148 | this.participantSelector = undefined; 149 | } 150 | 151 | this.readState = cloneMpcState(this.state); 152 | release(); 153 | return this.readState; 154 | } 155 | 156 | private async resetWithState(state: MpcState) { 157 | this.stop(); 158 | 159 | { 160 | const release = await this.mutex.acquire(); 161 | 162 | // If we have a running participant, reset their lastVerified time to give them additional 163 | // time on current chunk. Not fair to penalise them when we restarted the server. 164 | const running = state.participants.find(p => p.state === 'RUNNING'); 165 | if (running) { 166 | running.lastVerified = moment(); 167 | } 168 | 169 | this.state = state; 170 | this.readState = cloneMpcState(state); 171 | release(); 172 | } 173 | 174 | this.store = this.storeFactory.create(state.name); 175 | 176 | this.verifier = await this.createVerifier(); 177 | this.verifier.run(); 178 | 179 | if (state.latestBlock >= 0) { 180 | this.participantSelector = this.createParticipantSelector(state.network, state.latestBlock, state.selectBlock); 181 | this.participantSelector.run(); 182 | } 183 | 184 | this.scheduleAdvanceState(); 185 | } 186 | 187 | private createParticipantSelector(ethNet: EthNet, latestBlock: number, selectBlock: number) { 188 | const participantSelector = this.participantSelectorFactory.create(ethNet, latestBlock, selectBlock); 189 | participantSelector.on('newParticipants', (addresses, latestBlock) => this.addParticipants(addresses, latestBlock)); 190 | participantSelector.on('selectParticipants', blockHash => this.selectParticipants(blockHash)); 191 | return participantSelector; 192 | } 193 | 194 | private async createVerifier() { 195 | const verifier = new Verifier(this.store, this.verifierCallback.bind(this)); 196 | const lastCompleteParticipant = this.getLastCompleteParticipant(); 197 | const runningParticipant = this.getRunningParticipant(); 198 | verifier.lastCompleteAddress = lastCompleteParticipant && lastCompleteParticipant.address; 199 | verifier.runningAddress = runningParticipant && runningParticipant.address; 200 | 201 | // Get any files awaiting verification and add to the queue. 202 | if (runningParticipant) { 203 | const { address, transcripts } = runningParticipant; 204 | const unverified = await this.store.getUnverified(address); 205 | unverified 206 | .filter(uv => transcripts[uv.num].state !== 'COMPLETE') 207 | .forEach(item => verifier.put({ address, ...item })); 208 | } 209 | 210 | return verifier; 211 | } 212 | 213 | public async getState(sequence?: number): Promise { 214 | return { 215 | ...this.readState, 216 | participants: 217 | sequence === undefined 218 | ? this.readState.participants 219 | : this.readState.participants.filter(p => p.sequence > sequence), 220 | }; 221 | } 222 | 223 | public async getStateSummary(): Promise { 224 | const { participants, ...rest } = this.readState; 225 | const numParticipants = participants.length; 226 | let completedParticipants = 0; 227 | for (const participant of participants) { 228 | completedParticipants += participant.state === 'COMPLETE' ? 1 : 0; 229 | } 230 | let ceremonyProgress = Math.min(99, (100 * completedParticipants) / this.readState.minParticipants); 231 | if (this.readState.ceremonyState === 'COMPLETE') { 232 | ceremonyProgress = 100; 233 | } 234 | return { 235 | ...rest, 236 | numParticipants, 237 | ceremonyProgress, 238 | }; 239 | } 240 | 241 | public async addParticipant(address: Address, tier: number) { 242 | const release = await this.mutex.acquire(); 243 | this.state.sequence += 1; 244 | this.state.statusSequence = this.state.sequence; 245 | this.addNextParticipant(this.state, address, tier); 246 | this.state.participants = orderWaitingParticipants(this.state.participants, this.state.sequence); 247 | release(); 248 | } 249 | 250 | private async addParticipants(addresses: Address[], latestBlock: number) { 251 | const release = await this.mutex.acquire(); 252 | this.state.latestBlock = latestBlock; 253 | if (addresses.length || this.state.ceremonyState === 'PRESELECTION') { 254 | this.state.sequence += 1; 255 | this.state.statusSequence = this.state.sequence; 256 | if (addresses.length) { 257 | console.log( 258 | `Adding participants from block ${latestBlock}:`, 259 | addresses.map(a => a.toString()) 260 | ); 261 | addresses.forEach(address => this.addNextParticipant(this.state, address, 3)); 262 | } 263 | } 264 | release(); 265 | } 266 | 267 | private async selectParticipants(blockHash: Buffer) { 268 | const release = await this.mutex.acquire(); 269 | try { 270 | selectParticipants(this.state, blockHash); 271 | } finally { 272 | release(); 273 | } 274 | } 275 | 276 | private addNextParticipant(state: MpcState, address: Address, tier: number) { 277 | if (state.participants.find(p => p.address.equals(address))) { 278 | return; 279 | } 280 | const participant = createParticipant(state.sequence, moment(), state.participants.length + 1, tier, address); 281 | state.participants.push(participant); 282 | return participant; 283 | } 284 | 285 | private scheduleAdvanceState() { 286 | this.interval = setTimeout(() => this.advanceState(), 1000); 287 | } 288 | 289 | private async advanceState() { 290 | const release = await this.mutex.acquire(); 291 | 292 | try { 293 | await advanceState(this.state, this.store, this.verifier, moment()); 294 | } catch (err) { 295 | console.log(err); 296 | } finally { 297 | await this.stateStore.setState(this.state); 298 | this.readState = cloneMpcState(this.state); 299 | release(); 300 | } 301 | 302 | this.scheduleAdvanceState(); 303 | } 304 | 305 | public async ping(address: Address, ip?: string) { 306 | const release = await this.mutex.acquire(); 307 | try { 308 | const p = this.getParticipant(address); 309 | 310 | p.lastUpdate = moment(); 311 | 312 | if (p.online === false) { 313 | this.state.sequence += 1; 314 | this.state.statusSequence = this.state.sequence; 315 | if (ip && p.state === 'WAITING') { 316 | p.location = getGeoData(ip); 317 | } 318 | p.sequence = this.state.sequence; 319 | p.online = true; 320 | 321 | this.state.participants = orderWaitingParticipants(this.state.participants, this.state.sequence); 322 | } 323 | } finally { 324 | release(); 325 | } 326 | } 327 | 328 | public async updateParticipant(participantData: Participant, admin: boolean = false) { 329 | const release = await this.mutex.acquire(); 330 | try { 331 | const { state, transcripts, address, runningState, computeProgress, invalidateAfter } = participantData; 332 | const p = admin ? this.getParticipant(address) : this.getAndAssertRunningParticipant(address); 333 | this.state.sequence += 1; 334 | p.sequence = this.state.sequence; 335 | if (admin) { 336 | // Fields that administrator can adjust. 337 | if (invalidateAfter) { 338 | if (p.lastVerified) { 339 | p.lastVerified = moment(); 340 | } 341 | p.invalidateAfter = invalidateAfter; 342 | } 343 | if (state && state === 'WAITING' && p.state === 'INVALIDATED') { 344 | resetParticipant(this.state, p, invalidateAfter); 345 | } 346 | } else { 347 | if (transcripts) { 348 | // Only update transcript fields that are permitted. 349 | p.transcripts.forEach((t, i) => { 350 | t.size = transcripts[i].size; 351 | t.downloaded = transcripts[i].downloaded; 352 | t.uploaded = transcripts[i].uploaded; 353 | }); 354 | } 355 | p.runningState = runningState; 356 | p.computeProgress = computeProgress; 357 | p.lastUpdate = moment(); 358 | p.online = true; 359 | } 360 | } finally { 361 | release(); 362 | } 363 | } 364 | 365 | public async downloadInitialParams() { 366 | return this.store.loadInitialParams(); 367 | } 368 | 369 | public async downloadData(address: Address, num: number) { 370 | return this.store.loadTranscript(address, num); 371 | } 372 | 373 | public async downloadSignature(address: Address, num: number) { 374 | return this.store.getTranscriptSignature(address, num); 375 | } 376 | 377 | public async uploadData(address: Address, transcriptNumber: number, transcriptPath: string, signaturePath: string) { 378 | const p = this.getAndAssertRunningParticipant(address); 379 | 380 | // If we have any transcripts >= to this one, they must all be invalidated. If the verifier is running just reject 381 | // outright and client can try again once verifier is complete. Enables safe client restarts. 382 | const gteCurrent = p.transcripts.filter(t => t.num >= transcriptNumber); 383 | if (gteCurrent.some(t => t.state !== 'WAITING')) { 384 | if (await this.verifier.active()) { 385 | throw new Error('Upload of older transcript rejected until verifier inactive.'); 386 | } 387 | for (const t of gteCurrent) { 388 | console.log(`Setting transcript ${t.num} to be WAITING.`); 389 | await this.store.eraseUnverified(address, transcriptNumber); 390 | t.state = 'WAITING'; 391 | } 392 | } 393 | 394 | await this.store.save(address, transcriptNumber, transcriptPath, signaturePath); 395 | p.transcripts[transcriptNumber].state = 'VERIFYING'; 396 | if (p.runningState === 'OFFLINE') { 397 | // we know that this participant has uploaded and downloaded the transcript file! 398 | p.transcripts[transcriptNumber].downloaded = p.transcripts[transcriptNumber].size; 399 | p.transcripts[transcriptNumber].uploaded = p.transcripts[transcriptNumber].size; 400 | } 401 | this.verifier.put({ address, num: transcriptNumber }); 402 | } 403 | 404 | public getAndAssertRunningParticipant(address: Address) { 405 | const p = this.getRunningParticipant(); 406 | if (!p || !p.address.equals(address)) { 407 | throw new Error('Can only update a running participant.'); 408 | } 409 | return p; 410 | } 411 | 412 | private getRunningParticipant() { 413 | return this.state.participants.find(p => p.state === 'RUNNING'); 414 | } 415 | 416 | private getLastCompleteParticipant() { 417 | return [...this.state.participants].reverse().find(p => p.state === 'COMPLETE'); 418 | } 419 | 420 | private getParticipant(address: Address) { 421 | const p = this.state.participants.find(p => p.address.equals(address)); 422 | if (!p) { 423 | throw new Error(`Participant with address ${address} not found.`); 424 | } 425 | return p; 426 | } 427 | 428 | private async verifierCallback(address: Address, transcriptNumber: number, verified: boolean) { 429 | const release = await this.mutex.acquire(); 430 | try { 431 | const p = this.getParticipant(address); 432 | 433 | if (p.state !== 'RUNNING') { 434 | // Abort update if state changed during verification process (timed out). 435 | return; 436 | } 437 | 438 | if (verified) { 439 | await this.onVerified(p, transcriptNumber); 440 | } else { 441 | await this.onRejected(p, transcriptNumber); 442 | } 443 | } finally { 444 | release(); 445 | } 446 | } 447 | 448 | private async onVerified(p: Participant, transcriptNumber: number) { 449 | p.lastVerified = moment(); 450 | p.transcripts[transcriptNumber].state = 'COMPLETE'; 451 | p.verifyProgress = ((transcriptNumber + 1) / p.transcripts.length) * 100; 452 | 453 | if (p.transcripts.every(t => t.state === 'COMPLETE')) { 454 | await this.store.makeLive(p.address); 455 | p.state = 'COMPLETE'; 456 | p.runningState = 'COMPLETE'; 457 | // Don't need transcripts anymore. If we don't clear them, state size will increase over time. 458 | p.transcripts = []; 459 | // We may not have yet received final state update from the client, and once we're no longer 460 | // running we won't process the update. Force compute progress to 100%. 461 | p.computeProgress = 100; 462 | p.completedAt = moment(); 463 | this.verifier.lastCompleteAddress = p.address; 464 | } 465 | 466 | p.lastUpdate = moment(); 467 | this.state.sequence += 1; 468 | p.sequence = this.state.sequence; 469 | } 470 | 471 | private async onRejected(p: Participant, transcriptNumber: number) { 472 | console.log(`Verification failed: ${p.address.toString()} ${transcriptNumber}...`); 473 | if (p.runningState === 'OFFLINE') { 474 | // If participant is computing offline, we'll be more lenient and give them a chance to retry. 475 | p.transcripts[transcriptNumber].uploaded = 0; 476 | return; 477 | } 478 | // Otherwise, *bang*, you're dead. 479 | p.state = 'INVALIDATED'; 480 | p.runningState = 'COMPLETE'; 481 | p.transcripts = []; 482 | p.error = 'verify failed'; 483 | this.state.sequence += 1; 484 | p.sequence = this.state.sequence; 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state-store.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readFileSync } from 'fs'; 2 | import { MpcState, mpcStateFromJSON } from 'setup-mpc-common'; 3 | import { existsAsync, renameAsync, writeFileAsync } from './fs-async'; 4 | 5 | export interface StateStore { 6 | setState(state: MpcState): Promise; 7 | getState(): Promise; 8 | saveState(): Promise; 9 | restoreState(name: string): Promise; 10 | exists(name: string): Promise; 11 | } 12 | 13 | export class MemoryStateStore implements StateStore { 14 | private state!: MpcState; 15 | 16 | public async setState(state: MpcState) { 17 | this.state = state; 18 | } 19 | 20 | public async getState(): Promise { 21 | return this.state; 22 | } 23 | 24 | public async saveState() {} 25 | 26 | public async restoreState(name: string): Promise { 27 | return this.state; 28 | } 29 | 30 | public async exists(name: string) { 31 | return false; 32 | } 33 | } 34 | 35 | export class DiskStateStore implements StateStore { 36 | private state: MpcState; 37 | private storeFile: string; 38 | 39 | constructor(private storePath: string, defaultState: MpcState) { 40 | this.storeFile = `${storePath}/state.json`; 41 | mkdirSync(storePath, { recursive: true }); 42 | 43 | if (existsSync(this.storeFile)) { 44 | const buffer = readFileSync(this.storeFile); 45 | // In the event that new state is added, we merge in the defaults. 46 | this.state = { 47 | ...defaultState, 48 | ...this.migrate(mpcStateFromJSON(JSON.parse(buffer.toString()))), 49 | }; 50 | 51 | this.state.startSequence = this.state.sequence; 52 | } else { 53 | this.state = defaultState; 54 | } 55 | } 56 | 57 | public async setState(state: MpcState) { 58 | try { 59 | this.state = state; 60 | // Atomic file update. 61 | await writeFileAsync(`${this.storeFile}.new`, JSON.stringify(this.state)); 62 | await renameAsync(`${this.storeFile}.new`, this.storeFile); 63 | } catch (err) { 64 | console.log(err); 65 | } 66 | } 67 | 68 | public async getState(): Promise { 69 | return this.state; 70 | } 71 | 72 | public async saveState() { 73 | const id = this.state.name || this.state.startTime.format('YYYYMMDDHHmmss'); 74 | await writeFileAsync(this.getStatePath(id), JSON.stringify(this.state)); 75 | } 76 | 77 | public async restoreState(name: string): Promise { 78 | const buffer = readFileSync(this.getStatePath(name)); 79 | this.state = mpcStateFromJSON(JSON.parse(buffer.toString())); 80 | this.state.startSequence = this.state.sequence; 81 | return this.state; 82 | } 83 | 84 | public async exists(name: string) { 85 | return await existsAsync(this.getStatePath(name)); 86 | } 87 | 88 | private getStatePath = (id: string) => 89 | `${this.storePath}/state_${id 90 | .replace(/[^A-Za-z0-9_ ]/g, '') 91 | .replace(/ +/g, '_') 92 | .toLowerCase()}.json`; 93 | 94 | private migrate(state: any) { 95 | // 001 - Discarded transcript complete flag in favour of state. 96 | for (const p of state.participants) { 97 | for (const t of p.transcripts) { 98 | if (t.complete !== undefined) { 99 | t.state = t.complete ? 'COMPLETE' : 'WAITING'; 100 | t.complete = undefined; 101 | } 102 | } 103 | } 104 | 105 | return state; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/advance-state.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { cloneMpcState, MpcState } from 'setup-mpc-common'; 3 | import { Wallet } from 'web3x/wallet'; 4 | import { Address } from 'web3x/address'; 5 | import { TranscriptStore } from '../transcript-store'; 6 | import { Verifier } from '../verifier'; 7 | import { advanceState } from './advance-state'; 8 | import { createParticipant } from './create-participant'; 9 | import { defaultState } from './default-state'; 10 | 11 | type Mockify = { [P in keyof T]: jest.Mock }; 12 | 13 | describe('advance state', () => { 14 | const wallet = Wallet.fromMnemonic('alarm disagree index ridge tone outdoor betray pole forum source okay joy', 10); 15 | const addresses = wallet.currentAddresses(); 16 | const baseTime = moment('2019-01-01'); 17 | let state!: MpcState; 18 | const mockTranscriptStore: Mockify> = {}; 19 | const mockVerifier: Mockify> = {}; 20 | 21 | beforeEach(() => { 22 | state = defaultState(1234, Address.fromString('0x1aA18F5b595d87CC2C66d7b93367d8beabE203bB')); 23 | state.startTime = moment(baseTime).add(5, 's'); 24 | state.endTime = moment(baseTime).add(60, 's'); 25 | state.participants = addresses.map((a, i) => createParticipant(0, moment(baseTime), i + 1, 1, a)); 26 | mockTranscriptStore.eraseAll = jest.fn().mockResolvedValue(undefined); 27 | mockTranscriptStore.getVerified = jest.fn(); 28 | mockTranscriptStore.getInitialParamsSize = jest.fn().mockResolvedValue(20); 29 | }); 30 | 31 | it('should mark participants that have not pinged in 10s offline', async () => { 32 | state.participants.forEach((p, i) => { 33 | p.online = true; 34 | p.lastUpdate = moment(baseTime).add(i, 's'); 35 | }); 36 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(15, 's')); 37 | 38 | // Online participants are at the top of the list. 39 | expect(state.participants.slice(0, 5).every(p => p.online)).toBe(true); 40 | expect(state.participants.slice(5).every(p => !p.online)).toBe(true); 41 | expect(state.sequence).toBe(1); 42 | }); 43 | 44 | it('should not change state if ceremony not started', async () => { 45 | const preState = cloneMpcState(state); 46 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, baseTime); 47 | expect(state).toEqual(preState); 48 | }); 49 | 50 | it('should not change state if ceremony ended', async () => { 51 | state.completedAt = moment(baseTime.add(30, 's')); 52 | const preState = cloneMpcState(state); 53 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(60, 's')); 54 | expect(state).toEqual(preState); 55 | }); 56 | 57 | it('should not change state if ceremony is pre-selection', async () => { 58 | const preState = cloneMpcState(state); 59 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(10, 's')); 60 | expect(state).toEqual(preState); 61 | }); 62 | 63 | it('should shift ceremony to RUNNING state after selection', async () => { 64 | state.ceremonyState = 'SELECTED'; 65 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(10, 's')); 66 | expect(state.ceremonyState).toBe('RUNNING'); 67 | expect(state.sequence).toBe(1); 68 | }); 69 | 70 | it('should shift ceremony to COMPLETE state when min participants and end time met.', async () => { 71 | state.ceremonyState = 'RUNNING'; 72 | state.minParticipants = 5; 73 | state.endTime = moment(baseTime).add(2, 'h'); 74 | state.participants.forEach((p, i) => { 75 | if (i < 4) { 76 | p.state = 'COMPLETE'; 77 | } 78 | }); 79 | 80 | // Before end time. 81 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(10, 's')); 82 | expect(state.ceremonyState).toBe('RUNNING'); 83 | expect(state.sequence).toBe(0); 84 | 85 | // After end time but before min participants. 86 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(3, 'h')); 87 | expect(state.ceremonyState).toBe('RUNNING'); 88 | expect(state.sequence).toBe(0); 89 | 90 | // After end time and after min participants. 91 | state.participants[4].state = 'COMPLETE'; 92 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(3, 'h')); 93 | expect(state.ceremonyState).toBe('COMPLETE'); 94 | expect(state.sequence).toBe(1); 95 | }); 96 | 97 | it('should not shift ceremony to COMPLETE state with running participant.', async () => { 98 | state.ceremonyState = 'RUNNING'; 99 | state.minParticipants = 1; 100 | state.endTime = moment(baseTime).add(1, 'h'); 101 | state.participants[0].state = 'COMPLETE'; 102 | state.participants[1].state = 'RUNNING'; 103 | 104 | // After end time and with min participants, but one is still running. 105 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(2, 'h')); 106 | expect(state.ceremonyState).toBe('RUNNING'); 107 | expect(state.sequence).toBe(0); 108 | 109 | // After end time and after min participants. 110 | state.participants[1].state = 'COMPLETE'; 111 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(baseTime).add(2, 'h')); 112 | expect(state.ceremonyState).toBe('COMPLETE'); 113 | expect(state.sequence).toBe(1); 114 | }); 115 | 116 | it('should shift first waiting online participant to running state', async () => { 117 | state.ceremonyState = 'RUNNING'; 118 | state.participants[0].online = true; 119 | 120 | const now = moment(state.startTime).add(10, 's'); 121 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, now); 122 | 123 | expect(mockTranscriptStore.getVerified).not.toHaveBeenCalled(); 124 | expect(state.sequence).toBe(1); 125 | expect(state.participants[0].state).toBe('RUNNING'); 126 | expect(state.participants[0].startedAt).toEqual(now); 127 | expect(state.participants[0].transcripts).toEqual([ 128 | { 129 | num: 0, 130 | size: 20, 131 | downloaded: 0, 132 | uploaded: 0, 133 | state: 'WAITING', 134 | }, 135 | ]); 136 | }); 137 | 138 | it('should not shift first waiting participant to running state if not online', async () => { 139 | state.ceremonyState = 'RUNNING'; 140 | 141 | const now = moment(state.startTime).add(10, 's'); 142 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, now); 143 | 144 | expect(state.sequence).toBe(0); 145 | expect(state.participants.some(p => p.state === 'RUNNING')).toBe(false); 146 | }); 147 | 148 | it('should shift next waiting online participant to running state', async () => { 149 | mockTranscriptStore.getVerified!.mockResolvedValue([ 150 | { num: 0, size: 1000 }, 151 | { num: 1, size: 1005 }, 152 | ]); 153 | 154 | state.ceremonyState = 'RUNNING'; 155 | state.participants[0].state = 'COMPLETE'; 156 | state.participants[1].online = true; 157 | 158 | const now = moment(state.startTime).add(10, 's'); 159 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, now); 160 | 161 | expect(mockTranscriptStore.getVerified).toHaveBeenCalledWith(state.participants[0].address); 162 | expect(state.sequence).toBe(1); 163 | expect(state.participants[1].state).toBe('RUNNING'); 164 | expect(state.participants[1].startedAt).toEqual(now); 165 | expect(state.participants[1].transcripts).toEqual([ 166 | { 167 | fromAddress: state.participants[0].address, 168 | num: 0, 169 | size: 1000, 170 | downloaded: 0, 171 | uploaded: 0, 172 | state: 'WAITING', 173 | }, 174 | { 175 | fromAddress: state.participants[0].address, 176 | num: 1, 177 | size: 1005, 178 | downloaded: 0, 179 | uploaded: 0, 180 | state: 'WAITING', 181 | }, 182 | ]); 183 | }); 184 | 185 | it('should not shift to next waiting online participant when paused', async () => { 186 | state.ceremonyState = 'RUNNING'; 187 | state.paused = true; 188 | state.participants[0].state = 'COMPLETE'; 189 | state.participants[1].online = true; 190 | 191 | const now = moment(state.startTime).add(10, 's'); 192 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, now); 193 | 194 | expect(state.sequence).toBe(0); 195 | expect(state.participants[1].state).toBe('WAITING'); 196 | }); 197 | 198 | it('should invalidate running tier 1 participant after timeout', async () => { 199 | state.ceremonyState = 'RUNNING'; 200 | state.participants[0].startedAt = state.startTime; 201 | state.participants[0].state = 'RUNNING'; 202 | 203 | // Within time limit. 204 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(180, 's')); 205 | expect(state.sequence).toBe(0); 206 | expect(state.participants[0].state).toBe('RUNNING'); 207 | 208 | // After time limit. 209 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(181, 's')); 210 | expect(state.sequence).toBe(1); 211 | expect(state.participants[0].state).toBe('INVALIDATED'); 212 | }); 213 | 214 | it('should invalidate running participant and complete ceremony if after endTime', async () => { 215 | state.ceremonyState = 'RUNNING'; 216 | state.minParticipants = 1; 217 | state.participants[0].state = 'COMPLETE'; 218 | state.participants[1].startedAt = state.startTime; 219 | state.participants[1].state = 'RUNNING'; 220 | state.endTime = moment(state.startTime).add(100, 's'); 221 | 222 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(200, 's')); 223 | expect(state.participants[1].state).toBe('INVALIDATED'); 224 | expect(state.ceremonyState).toBe('COMPLETE'); 225 | }); 226 | 227 | it('should invalidate using participant timeout as priority over global timeout', async () => { 228 | state.ceremonyState = 'RUNNING'; 229 | state.participants[0].startedAt = state.startTime; 230 | state.participants[0].state = 'RUNNING'; 231 | state.participants[0].invalidateAfter = 190; 232 | 233 | // Within time limit. 234 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(190, 's')); 235 | expect(state.sequence).toBe(0); 236 | expect(state.participants[0].state).toBe('RUNNING'); 237 | 238 | // After time limit. 239 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(191, 's')); 240 | expect(state.sequence).toBe(1); 241 | expect(state.participants[0].state).toBe('INVALIDATED'); 242 | }); 243 | 244 | it('should invalidate running tier 2 participant after verify timeout', async () => { 245 | state.ceremonyState = 'RUNNING'; 246 | state.participants[0].startedAt = state.startTime; 247 | state.participants[0].state = 'RUNNING'; 248 | state.participants[0].tier = 2; 249 | 250 | // Within time limit. 251 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(180, 's')); 252 | expect(state.sequence).toBe(0); 253 | expect(state.participants[0].state).toBe('RUNNING'); 254 | 255 | // After time limit. 256 | await advanceState(state, mockTranscriptStore as any, mockVerifier as any, moment(state.startTime).add(181, 's')); 257 | expect(state.sequence).toBe(1); 258 | expect(state.participants[0].state).toBe('INVALIDATED'); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/advance-state.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from 'moment'; 2 | import { MpcState, Transcript } from 'setup-mpc-common'; 3 | import { TranscriptStore } from '../transcript-store'; 4 | import { Verifier } from '../verifier'; 5 | import { orderWaitingParticipants } from './order-waiting-participants'; 6 | 7 | const OFFLINE_AFTER = 10; 8 | 9 | export async function advanceState(state: MpcState, store: TranscriptStore, verifier: Verifier, now: Moment) { 10 | const { paused, sequence, startTime, completedAt, invalidateAfter } = state; 11 | const nextSequence = sequence + 1; 12 | 13 | // Shift any participants that haven't performed an update recently to offline state and reorder accordingly. 14 | if (markIdleParticipantsOffline(state, nextSequence, now)) { 15 | state.participants = orderWaitingParticipants(state.participants, nextSequence); 16 | state.sequence = nextSequence; 17 | } 18 | 19 | // Nothing to do if paused, not yet running, or already completed. 20 | if (now.isBefore(startTime) || completedAt) { 21 | return; 22 | } 23 | 24 | // If we've not yet hit our selection block, or are complete. Do nothing. 25 | if (state.ceremonyState !== 'SELECTED' && state.ceremonyState !== 'RUNNING') { 26 | return; 27 | } 28 | 29 | // Shift to running state if not already. 30 | if (state.ceremonyState !== 'RUNNING') { 31 | state.statusSequence = nextSequence; 32 | state.ceremonyState = 'RUNNING'; 33 | state.sequence = nextSequence; 34 | } 35 | 36 | let runningParticipant = state.participants.find(p => p.state === 'RUNNING'); 37 | 38 | // If we have a running participant, mark as invalidated if timed out. 39 | if (runningParticipant) { 40 | const { startedAt, tier, lastVerified } = runningParticipant; 41 | const completeWithin = runningParticipant.invalidateAfter || invalidateAfter; 42 | // TODO: not sure if 2 is enough? Verify is veryyyy slow 43 | const verifyWithin = 2 * completeWithin; 44 | if ( 45 | moment(now) 46 | .subtract(completeWithin, 's') 47 | .isAfter(startedAt!) || 48 | (tier > 1 && 49 | moment(now) 50 | .subtract(verifyWithin, 's') 51 | .isAfter(lastVerified || startedAt!)) // lastVerified is meaningless right now, so this second check doesn't do anything 52 | ) { 53 | runningParticipant.sequence = nextSequence; 54 | runningParticipant.state = 'INVALIDATED'; 55 | runningParticipant.transcripts = []; 56 | runningParticipant.error = 'timed out'; 57 | runningParticipant = undefined; 58 | state.sequence = nextSequence; 59 | } else { 60 | return; 61 | } 62 | } 63 | 64 | // If at least min participants reached and after end date, ceremony is completed 65 | if ( 66 | !runningParticipant && 67 | state.participants.reduce((a, p) => (p.state === 'COMPLETE' ? a + 1 : a), 0) >= state.minParticipants && 68 | now.isSameOrAfter(state.endTime) 69 | ) { 70 | state.statusSequence = nextSequence; 71 | state.ceremonyState = 'COMPLETE'; 72 | state.completedAt = moment(); 73 | state.sequence = nextSequence; 74 | return; 75 | } 76 | 77 | // Find next waiting, online participant and shift them to the running state. 78 | const waitingParticipant = state.participants.find(p => p.state === 'WAITING'); 79 | if (!paused && waitingParticipant && waitingParticipant.online) { 80 | await store.eraseAll(waitingParticipant.address); 81 | state.sequence = nextSequence; 82 | state.statusSequence = nextSequence; 83 | waitingParticipant.sequence = nextSequence; 84 | waitingParticipant.startedAt = now; 85 | waitingParticipant.state = 'RUNNING'; 86 | waitingParticipant.transcripts = await getRunningParticipantsTranscripts(state, store); 87 | verifier.runningAddress = waitingParticipant.address; 88 | } 89 | } 90 | 91 | async function getRunningParticipantsTranscripts(state: MpcState, store: TranscriptStore): Promise { 92 | const lastCompletedParticipant = state.participants 93 | .slice() 94 | .reverse() 95 | .find(p => p.state === 'COMPLETE'); 96 | 97 | if (!lastCompletedParticipant) { 98 | const initialParamsSize = await store.getInitialParamsSize(); 99 | return Array(1) 100 | .fill(0) 101 | .map((_, num) => ({ 102 | num, 103 | size: initialParamsSize, 104 | downloaded: 0, 105 | uploaded: 0, 106 | state: 'WAITING', 107 | })); 108 | } 109 | 110 | const transcripts = await store.getVerified(lastCompletedParticipant.address); 111 | return transcripts.map(t => ({ 112 | num: t.num, 113 | size: t.size, 114 | fromAddress: lastCompletedParticipant.address, 115 | downloaded: 0, 116 | uploaded: 0, 117 | state: 'WAITING', 118 | })); 119 | } 120 | 121 | function markIdleParticipantsOffline(state: MpcState, sequence: number, now: Moment) { 122 | const { participants } = state; 123 | let changed = false; 124 | participants.forEach(p => { 125 | if ( 126 | moment(now) 127 | .subtract(OFFLINE_AFTER, 's') 128 | .isAfter(p.lastUpdate!) && 129 | p.online 130 | ) { 131 | state.statusSequence = sequence; 132 | p.sequence = sequence; 133 | p.online = false; 134 | changed = true; 135 | } 136 | }); 137 | return changed; 138 | } 139 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/create-participant.ts: -------------------------------------------------------------------------------- 1 | import { Moment } from 'moment'; 2 | import { Participant } from 'setup-mpc-common'; 3 | import { Address } from 'web3x/address'; 4 | 5 | export function createParticipant( 6 | sequence: number, 7 | addedAt: Moment, 8 | position: number, 9 | tier: number, 10 | address: Address 11 | ): Participant { 12 | return { 13 | sequence, 14 | addedAt, 15 | online: false, 16 | state: 'WAITING', 17 | runningState: 'WAITING', 18 | position, 19 | priority: position, 20 | tier, 21 | computeProgress: 0, 22 | verifyProgress: 0, 23 | transcripts: [], 24 | address, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/default-state.ts: -------------------------------------------------------------------------------- 1 | import moment = require('moment'); 2 | import { MpcState } from 'setup-mpc-common'; 3 | import { Address } from 'web3x/address'; 4 | 5 | export function defaultState(latestBlock: number, adminAddress: Address): MpcState { 6 | return { 7 | name: 'default', 8 | adminAddress, 9 | sequence: 0, 10 | statusSequence: 0, 11 | startSequence: 0, 12 | ceremonyState: 'PRESELECTION', 13 | paused: false, 14 | startTime: moment().add(20, 'seconds'), 15 | endTime: moment().add(1, 'hour'), 16 | network: 'ropsten', 17 | latestBlock, 18 | selectBlock: latestBlock + 1, 19 | maxTier2: 0, 20 | minParticipants: 5, 21 | invalidateAfter: 180, 22 | participants: [], 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/order-waiting-participants.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { Participant } from 'setup-mpc-common'; 3 | import { Wallet } from 'web3x/wallet'; 4 | import { createParticipant } from './create-participant'; 5 | import { orderWaitingParticipants } from './order-waiting-participants'; 6 | 7 | describe('order waiting participants', () => { 8 | const wallet = Wallet.fromMnemonic('alarm disagree index ridge tone outdoor betray pole forum source okay joy', 10); 9 | const addresses = wallet.currentAddresses(); 10 | let participants: Participant[]; 11 | 12 | beforeEach(() => { 13 | participants = addresses.map((a, i) => createParticipant(0, moment().add(i, 's'), i + 1, 1, a)); 14 | }); 15 | 16 | it('should correctly order participants', () => { 17 | const result = orderWaitingParticipants(participants, 0); 18 | expect(result.map(p => p.address)).toEqual(addresses); 19 | }); 20 | 21 | it('should correctly order online participants', () => { 22 | participants[3].online = true; 23 | participants[7].online = true; 24 | const result = orderWaitingParticipants(participants, 0); 25 | expect(result[0].address).toEqual(addresses[3]); 26 | expect(result[1].address).toEqual(addresses[7]); 27 | expect(result[2].address).toEqual(addresses[0]); 28 | }); 29 | 30 | it('should correctly order iter 0 participants', () => { 31 | participants[3].tier = 0; 32 | participants[8].tier = 0; 33 | participants[7].online = true; 34 | const result = orderWaitingParticipants(participants, 0); 35 | expect(result[0].address).toEqual(addresses[3]); 36 | expect(result[1].address).toEqual(addresses[8]); 37 | expect(result[2].address).toEqual(addresses[7]); 38 | expect(result[3].address).toEqual(addresses[0]); 39 | }); 40 | 41 | it('should correctly order tiered participants', () => { 42 | participants[0].tier = 3; 43 | participants[1].tier = 2; 44 | participants[2].tier = 3; 45 | participants[2].online = true; 46 | 47 | const result = orderWaitingParticipants(participants, 0); 48 | 49 | expect(result[9].address).toEqual(addresses[0]); 50 | expect(result[8].address).toEqual(addresses[1]); 51 | expect(result[0].address).toEqual(addresses[2]); 52 | }); 53 | 54 | it('should correctly order priority participants', () => { 55 | participants[0].tier = 2; 56 | participants[0].priority = 5; 57 | participants[1].tier = 2; 58 | participants[2].tier = 2; 59 | participants[2].online = true; 60 | participants[3].tier = 2; 61 | participants[4].tier = 2; 62 | participants[4].priority = 1; 63 | 64 | const result = orderWaitingParticipants(participants, 0); 65 | 66 | expect(result[6].address).toEqual(addresses[4]); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/order-waiting-participants.ts: -------------------------------------------------------------------------------- 1 | import { cloneParticipant, Participant } from 'setup-mpc-common'; 2 | 3 | export function orderWaitingParticipants(participants: Participant[], sequence: number) { 4 | const indexOfFirstWaiting = participants.findIndex(p => p.state === 'WAITING'); 5 | 6 | const waiting = participants.slice(indexOfFirstWaiting).sort((a, b) => { 7 | if (a.tier === 0 || b.tier === 0) { 8 | return a.tier !== b.tier ? a.tier - b.tier : a.priority - b.priority; 9 | } 10 | if (a.online !== b.online) { 11 | return a.online ? -1 : 1; 12 | } 13 | if (a.tier !== b.tier) { 14 | return a.tier - b.tier; 15 | } 16 | return a.priority - b.priority; 17 | }); 18 | 19 | let orderedParticipants = [...participants.slice(0, indexOfFirstWaiting), ...waiting]; 20 | 21 | // Adjust positions based on new order and advance sequence numbers if position changed. 22 | orderedParticipants = orderedParticipants.map((p, i) => { 23 | if (p.position !== i + 1) { 24 | p = cloneParticipant(p); 25 | p.position = i + 1; 26 | p.sequence = sequence; 27 | } 28 | return p; 29 | }); 30 | 31 | return orderedParticipants; 32 | } 33 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/reset-participant.ts: -------------------------------------------------------------------------------- 1 | import { MpcState, Participant } from 'setup-mpc-common'; 2 | import { orderWaitingParticipants } from './order-waiting-participants'; 3 | 4 | export function resetParticipant(state: MpcState, p: Participant, invalidateAfter?: number) { 5 | // Reset participant. 6 | p.state = 'WAITING'; 7 | p.runningState = 'WAITING'; 8 | p.startedAt = undefined; 9 | p.lastVerified = undefined; 10 | p.error = undefined; 11 | p.invalidateAfter = invalidateAfter; 12 | p.computeProgress = 0; 13 | p.verifyProgress = 0; 14 | p.transcripts = []; 15 | 16 | const complete = state.participants 17 | .filter(p => p.state !== 'WAITING') 18 | .sort((a, b) => a.startedAt!.unix() - b.startedAt!.unix()); 19 | const waiting = state.participants.filter(p => p.state === 'WAITING'); 20 | 21 | state.participants = orderWaitingParticipants([...complete, ...waiting], state.sequence); 22 | } 23 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/select-participants.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { MpcState } from 'setup-mpc-common'; 3 | import { hexToBuffer } from 'web3x/utils'; 4 | import { Wallet } from 'web3x/wallet'; 5 | import { Address } from 'web3x/address'; 6 | import { createParticipant } from './create-participant'; 7 | import { defaultState } from './default-state'; 8 | import { orderWaitingParticipants } from './order-waiting-participants'; 9 | import { selectParticipants } from './select-participants'; 10 | 11 | describe('select participants', () => { 12 | const wallet = Wallet.fromMnemonic('alarm disagree index ridge tone outdoor betray pole forum source okay joy', 10); 13 | const addresses = wallet.currentAddresses(); 14 | let state: MpcState; 15 | 16 | beforeEach(() => { 17 | state = defaultState(1234, Address.fromString('0x1aA18F5b595d87CC2C66d7b93367d8beabE203bB')); 18 | state.participants = addresses.map((a, i) => createParticipant(0, moment().add(i, 's'), i + 1, i < 5 ? 1 : 2, a)); 19 | }); 20 | 21 | it('should correctly select participants', () => { 22 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 23 | for (const p of state.participants.slice(0, 5)) { 24 | expect(addresses.slice(0, 5).includes(p.address)).toBeTruthy(); 25 | } 26 | for (const p of state.participants.slice(5)) { 27 | expect(addresses.slice(5).includes(p.address)).toBeTruthy(); 28 | } 29 | }); 30 | 31 | it('should correctly select participants with tier 0', () => { 32 | state.participants[7].tier = 0; 33 | state.participants[9].tier = 0; 34 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 35 | expect(state.participants[0].address).toEqual(addresses[7]); 36 | expect(state.participants[1].address).toEqual(addresses[9]); 37 | for (const p of state.participants.slice(2, 7)) { 38 | expect(addresses.slice(0, 5).includes(p.address)).toBeTruthy(); 39 | } 40 | for (const p of state.participants.slice(7)) { 41 | expect(addresses.slice(5, 9).includes(p.address)).toBeTruthy(); 42 | } 43 | }); 44 | 45 | it('should correctly select participants with tier 3', () => { 46 | state.participants[9].tier = 3; 47 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 48 | for (const p of state.participants.slice(0, 5)) { 49 | expect(addresses.slice(0, 5).includes(p.address)).toBeTruthy(); 50 | } 51 | for (const p of state.participants.slice(5, 9)) { 52 | expect(addresses.slice(5).includes(p.address)).toBeTruthy(); 53 | } 54 | expect(addresses[9].equals(state.participants[9].address)).toBeTruthy(); 55 | }); 56 | 57 | it('should not affect priority with online status', () => { 58 | state.participants[9].online = true; 59 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 60 | expect(state.participants[0].address.equals(addresses[9])).toBeTruthy(); 61 | state.participants[0].online = false; 62 | state.participants = orderWaitingParticipants(state.participants, 0); 63 | expect(state.participants[0].address.equals(addresses[9])).toBeFalsy(); 64 | }); 65 | 66 | it('should change ordering', () => { 67 | expect(state.participants.map(p => p.address)).toEqual(addresses); 68 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 69 | expect(state.participants.map(p => p.address)).not.toEqual(addresses); 70 | }); 71 | 72 | it('should have same ordering with same hash', () => { 73 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 74 | const addresses = state.participants.map(p => p.address); 75 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 76 | expect(state.participants.map(p => p.address)).toEqual(addresses); 77 | }); 78 | 79 | it('should have different ordering with different hash', () => { 80 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 81 | const addresses = state.participants.map(p => p.address); 82 | state.ceremonyState = 'PRESELECTION'; 83 | selectParticipants(state, hexToBuffer('0x2aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 84 | expect(state.participants.map(p => p.address)).not.toEqual(addresses); 85 | }); 86 | 87 | it('should limit tier 2 participants, resulting tier 3 in time added order', () => { 88 | state.maxTier2 = 2; 89 | selectParticipants(state, hexToBuffer('0x1aeaff3366f816e1d0157664dcd7ffaeb8741c854e2575ec9d438fc42c83b870')); 90 | for (const p of state.participants.slice(0, 5)) { 91 | expect(p.tier).toBe(1); 92 | expect(addresses.slice(0, 5).includes(p.address)).toBeTruthy(); 93 | } 94 | for (const p of state.participants.slice(5, 7)) { 95 | expect(p.tier).toBe(2); 96 | expect(addresses.slice(5).includes(p.address)).toBeTruthy(); 97 | } 98 | for (const p of state.participants.slice(7, 10)) { 99 | expect(p.tier).toBe(3); 100 | expect(addresses.slice(5).includes(p.address)).toBeTruthy(); 101 | } 102 | expect(state.participants[7].addedAt.isBefore(state.participants[8].addedAt)).toBeTruthy(); 103 | expect(state.participants[8].addedAt.isBefore(state.participants[9].addedAt)).toBeTruthy(); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /setup-mpc-server/src/state/select-participants.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom'; 2 | import { MpcState } from 'setup-mpc-common'; 3 | import { orderWaitingParticipants } from './order-waiting-participants'; 4 | 5 | function shuffle(seed: Buffer, array: T[]) { 6 | const prng = seedrandom(seed.toString('hex')); 7 | let m = array.length; 8 | let t: T; 9 | let i: number; 10 | 11 | // Fisher-Yates shuffle. 12 | while (m) { 13 | // Pick a remaining element. 14 | const n = prng.double(); 15 | i = Math.floor(n * m--); 16 | t = array[m]; 17 | 18 | // And swap it with the current element. 19 | array[m] = array[i]; 20 | array[i] = t; 21 | } 22 | } 23 | 24 | export function selectParticipants(state: MpcState, blockHash: Buffer) { 25 | if (state.ceremonyState !== 'PRESELECTION') { 26 | return; 27 | } 28 | 29 | console.log('Selecting participants.'); 30 | 31 | state.sequence += 1; 32 | state.statusSequence = state.sequence; 33 | state.ceremonyState = 'SELECTED'; 34 | 35 | let { participants } = state; 36 | const tier0 = participants.filter(t => t.tier === 0); 37 | const tier1 = participants.filter(t => t.tier === 1); 38 | const earlyBirds = participants.filter(t => t.tier === 2); 39 | const tier3 = participants.filter(t => t.tier === 3); 40 | shuffle(blockHash, tier1); 41 | shuffle(blockHash, earlyBirds); 42 | const tier2 = earlyBirds.slice(0, state.maxTier2); 43 | const tier2rejects = earlyBirds.slice(state.maxTier2).sort((a, b) => a.addedAt.valueOf() - b.addedAt.valueOf()); 44 | tier2rejects.forEach(p => (p.tier = 3)); 45 | 46 | participants = [...tier0, ...tier1, ...tier2, ...tier2rejects, ...tier3]; 47 | 48 | participants.forEach((p, i) => { 49 | p.sequence = state.sequence; 50 | p.priority = i + 1; 51 | }); 52 | 53 | state.participants = orderWaitingParticipants(participants, state.sequence); 54 | } 55 | -------------------------------------------------------------------------------- /setup-mpc-server/src/transcript-store.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, mkdirSync } from 'fs'; 2 | import * as fs from 'fs'; 3 | import { Readable } from 'stream'; 4 | import { Address } from 'web3x/address'; 5 | import { 6 | copyFileAsync, 7 | existsAsync, 8 | mkdirAsync, 9 | readdirAsync, 10 | readFileAsync, 11 | renameAsync, 12 | rmdirAsync, 13 | statAsync, 14 | unlinkAsync, 15 | } from './fs-async'; 16 | 17 | export interface TranscriptStoreRecord { 18 | num: number; 19 | size: number; 20 | path: string; 21 | } 22 | 23 | export interface TranscriptStore { 24 | save(address: Address, num: number, transcriptPath: string, signaturePath: string): Promise; 25 | loadTranscript(address: Address, num: number): Readable; 26 | loadInitialParams(): Readable; 27 | getInitialParamsSize(): Promise; 28 | getTranscriptSignature(address: Address, num: number): Promise; 29 | makeLive(address: Address): Promise; 30 | getInitialParametersPath(): string; 31 | getVerifiedTranscriptPath(address: Address, num: number): string; 32 | getVerifiedSignaturePath(address: Address, num: number): string; 33 | getUnverifiedTranscriptPath(address: Address, num: number): string; 34 | getUnverifiedSignaturePath(address: Address, num: number): string; 35 | initialParamsExists(): Promise; 36 | getVerified(address: Address, includeSignatures?: boolean): Promise; 37 | getUnverified(address: Address, includeSignatures?: boolean): Promise; 38 | eraseAll(address: Address): Promise; 39 | eraseUnverified(address: Address, num?: number): Promise; 40 | copyVerifiedTo(address: Address, path: string): Promise; 41 | } 42 | 43 | export interface TranscriptStoreFactory { 44 | create(name: string): TranscriptStore; 45 | } 46 | 47 | export class DiskTranscriptStoreFactory implements TranscriptStoreFactory { 48 | constructor(private storePath: string) {} 49 | 50 | public create(name: string) { 51 | return new DiskTranscriptStore(`${this.storePath}/${name}`); 52 | } 53 | } 54 | 55 | export class DiskTranscriptStore implements TranscriptStore { 56 | private initialParamsPath: string; 57 | private unverifiedPath: string; 58 | private verifiedPath: string; 59 | private fileRegex = /transcript(\d+).(dat|sig)$/; 60 | 61 | constructor(private storePath: string) { 62 | this.initialParamsPath = storePath + '/../../initial'; 63 | this.verifiedPath = storePath + '/verified'; 64 | this.unverifiedPath = storePath + '/unverified'; 65 | mkdirSync(this.initialParamsPath, { recursive: true }); 66 | mkdirSync(this.verifiedPath, { recursive: true }); 67 | mkdirSync(this.unverifiedPath, { recursive: true }); 68 | } 69 | 70 | public async save(address: Address, num: number, transcriptPath: string, signaturePath: string) { 71 | await mkdirAsync(`${this.unverifiedPath}/${address.toString().toLowerCase()}`, { recursive: true }); 72 | await renameAsync(transcriptPath, this.getUnverifiedTranscriptPath(address, num)); 73 | await renameAsync(signaturePath, this.getUnverifiedSignaturePath(address, num)); 74 | } 75 | 76 | public async makeLive(address: Address) { 77 | await renameAsync(this.getUnverifiedBasePath(address), this.getVerifiedBasePath(address)); 78 | } 79 | 80 | public loadTranscript(address: Address, num: number) { 81 | return createReadStream(this.getVerifiedTranscriptPath(address, num)); 82 | } 83 | 84 | public loadInitialParams() { 85 | return createReadStream(this.getInitialParametersPath()); 86 | } 87 | 88 | public getInitialParamsSize() { 89 | return new Promise(resolve => { 90 | fs.stat(this.getInitialParametersPath(), (_, stats) => { 91 | resolve(stats.size); 92 | }); 93 | }); 94 | } 95 | 96 | public async getTranscriptSignature(address: Address, num: number) { 97 | return (await readFileAsync(this.getVerifiedSignaturePath(address, num))).toString(); 98 | } 99 | 100 | private getVerifiedBasePath(address: Address) { 101 | return `${this.verifiedPath}/${address.toString().toLowerCase()}`; 102 | } 103 | 104 | private getUnverifiedBasePath(address: Address) { 105 | return `${this.unverifiedPath}/${address.toString().toLowerCase()}`; 106 | } 107 | 108 | public getInitialParametersPath() { 109 | return `${this.initialParamsPath}/initial_params`; 110 | } 111 | 112 | public getVerifiedTranscriptPath(address: Address, num: number) { 113 | return `${this.getVerifiedBasePath(address)}/transcript${num}.dat`; 114 | } 115 | 116 | public getVerifiedSignaturePath(address: Address, num: number) { 117 | return `${this.getVerifiedBasePath(address)}/transcript${num}.sig`; 118 | } 119 | 120 | public getUnverifiedTranscriptPath(address: Address, num: number) { 121 | return `${this.getUnverifiedBasePath(address)}/transcript${num}.dat`; 122 | } 123 | 124 | public getUnverifiedSignaturePath(address: Address, num: number) { 125 | return `${this.getUnverifiedBasePath(address)}/transcript${num}.sig`; 126 | } 127 | 128 | private async getDirRecords(dir: string, includeSignatures: boolean) { 129 | if (!(await existsAsync(dir))) { 130 | return []; 131 | } 132 | let files = await readdirAsync(dir); 133 | if (!includeSignatures) { 134 | files = files.filter(path => path.endsWith('.dat')); 135 | } 136 | const results = await Promise.all( 137 | files.map(async file => { 138 | const path = `${dir}/${file}`; 139 | const match = file.match(this.fileRegex)!; 140 | const stats = await statAsync(path); 141 | return { 142 | path, 143 | size: stats.size, 144 | num: +match[1], 145 | }; 146 | }) 147 | ); 148 | return results.sort((a, b) => a.num - b.num); 149 | } 150 | 151 | public async initialParamsExists() { 152 | return new Promise(resolve => { 153 | fs.access(this.getInitialParametersPath(), fs.constants.F_OK, err => { 154 | resolve(!err); 155 | }); 156 | }); 157 | } 158 | 159 | public async getVerified(address: Address, includeSignatures?: boolean) { 160 | return this.getDirRecords(this.getVerifiedBasePath(address), !!includeSignatures); 161 | } 162 | 163 | public async getUnverified(address: Address, includeSignatures?: boolean) { 164 | return this.getDirRecords(this.getUnverifiedBasePath(address), !!includeSignatures); 165 | } 166 | 167 | public async eraseAll(address: Address) { 168 | await this.eraseVerified(address); 169 | await this.eraseUnverified(address); 170 | } 171 | 172 | private async eraseVerified(address: Address) { 173 | try { 174 | const dir = this.getVerifiedBasePath(address); 175 | if (!(await existsAsync(dir))) { 176 | return; 177 | } 178 | const files = await readdirAsync(dir); 179 | for (const file of files) { 180 | await unlinkAsync(`${dir}/${file}`); 181 | } 182 | await rmdirAsync(this.getVerifiedBasePath(address)); 183 | } catch (err) { 184 | console.log(err); 185 | } 186 | } 187 | 188 | public async eraseUnverified(address: Address, num?: number) { 189 | try { 190 | const dir = this.getUnverifiedBasePath(address); 191 | if (!(await existsAsync(dir))) { 192 | return; 193 | } 194 | if (num) { 195 | await unlinkAsync(this.getUnverifiedTranscriptPath(address, num)); 196 | } else { 197 | const files = await readdirAsync(dir); 198 | for (const file of await files) { 199 | await unlinkAsync(`${dir}/${file}`); 200 | } 201 | await rmdirAsync(this.getUnverifiedBasePath(address)); 202 | } 203 | } catch (err) { 204 | console.log(err); 205 | } 206 | } 207 | 208 | public async copyVerifiedTo(address: Address, path: string) { 209 | let num = 0; 210 | while (await existsAsync(this.getVerifiedTranscriptPath(address, num))) { 211 | await copyFileAsync(this.getVerifiedTranscriptPath(address, num), `${path}/transcript${num}.dat`); 212 | ++num; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /setup-mpc-server/src/verifier.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn } from 'child_process'; 2 | import { MemoryFifo } from 'setup-mpc-common'; 3 | import { Address } from 'web3x/address'; 4 | import { TranscriptStore } from './transcript-store'; 5 | 6 | export interface VerifyItem { 7 | address: Address; 8 | num: number; 9 | } 10 | 11 | export class Verifier { 12 | private queue: MemoryFifo = new MemoryFifo(); 13 | public lastCompleteAddress?: Address; 14 | public runningAddress?: Address; 15 | private proc?: ChildProcess; 16 | private cancelled = false; 17 | 18 | constructor( 19 | private store: TranscriptStore, 20 | private cb: (address: Address, num: number, verified: boolean) => Promise 21 | ) {} 22 | 23 | public async active() { 24 | return this.proc || (await this.queue.length()); 25 | } 26 | 27 | public async run() { 28 | console.log('Verifier started...'); 29 | while (true) { 30 | const item = await this.queue.get(); 31 | if (!item) { 32 | break; 33 | } 34 | const { address, num } = item; 35 | const transcriptPath = this.store.getUnverifiedTranscriptPath(address, num); 36 | 37 | try { 38 | if (!this.runningAddress) { 39 | // If we dequeued an item, someone should be running. 40 | throw new Error('No running address set.'); 41 | } 42 | 43 | if (!this.runningAddress.equals(address)) { 44 | // This address is no longer running. Just skip. 45 | continue; 46 | } 47 | 48 | if (await this.verifyTranscript(address, num, transcriptPath)) { 49 | console.log(`Verification succeeded: ${transcriptPath}...`); 50 | 51 | await this.cb(address, num, true); 52 | } else { 53 | await this.store.eraseUnverified(address, num); 54 | if (!this.cancelled) { 55 | await this.cb(address, num, false); 56 | } 57 | } 58 | } catch (err) { 59 | console.log(err); 60 | } 61 | } 62 | console.log('Verifier completed.'); 63 | } 64 | 65 | public put(item: VerifyItem) { 66 | this.queue.put(item); 67 | } 68 | 69 | public cancel() { 70 | this.cancelled = true; 71 | this.queue.cancel(); 72 | if (this.proc) { 73 | this.proc.kill(); 74 | } 75 | } 76 | 77 | private async verifyTranscript(address: Address, transcriptNumber: number, transcriptPath: string) { 78 | console.log(`Verifiying transcript ${transcriptNumber}...`); 79 | return new Promise(resolve => { 80 | // call verify_contribution if this is not the first transcript 81 | const args = [ 82 | 'initial/circuit.json', 83 | this.lastCompleteAddress 84 | ? this.store.getVerifiedTranscriptPath(this.lastCompleteAddress, 0) 85 | : this.store.getInitialParametersPath(), 86 | this.store.getUnverifiedTranscriptPath(address, 0), 87 | 'initial/radix', 88 | ]; 89 | const binPath = '../setup-tools/verify_contribution'; 90 | const verify = spawn(binPath, args); 91 | this.proc = verify; 92 | 93 | verify.stdout.on('data', data => { 94 | console.log(data.toString()); 95 | }); 96 | 97 | verify.stderr.on('data', data => { 98 | console.log(data.toString()); 99 | }); 100 | 101 | verify.on('close', code => { 102 | this.proc = undefined; 103 | resolve(code === 0); 104 | }); 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /setup-mpc-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": ["dom", "esnext", "es2017.object"], 7 | "outDir": "dest", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": false, 11 | "esModuleInterop": true, 12 | "declaration": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /setup-mpc-server/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".", 3 | "exclude": ["**/*.test.*"] 4 | } 5 | -------------------------------------------------------------------------------- /setup-tools/.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | setup_db* 3 | Dockerfile* 4 | .* 5 | *.sh 6 | **/target -------------------------------------------------------------------------------- /setup-tools/.gitignore: -------------------------------------------------------------------------------- 1 | setup_db* 2 | new 3 | verify_contribution 4 | contribute 5 | beacon 6 | export_keys 7 | mimc -------------------------------------------------------------------------------- /setup-tools/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | RUN apt update && \ 3 | apt install -y curl && \ 4 | curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ 5 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ 6 | echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ 7 | apt update && \ 8 | apt install -y nodejs yarn libgomp1 build-essential && \ 9 | apt clean 10 | WORKDIR /tmp 11 | RUN curl https://sh.rustup.rs -sSf > rustup.sh \ 12 | && chmod 755 rustup.sh \ 13 | && ./rustup.sh -y \ 14 | && rm /tmp/rustup.sh 15 | WORKDIR /usr/src/setup-tools 16 | ENV PATH="/root/.cargo/bin:$PATH" 17 | COPY . . 18 | RUN cd phase2-bn254/phase2 \ 19 | && cargo build --release --bin new \ 20 | && cargo build --release --bin contribute \ 21 | && cargo build --release --bin verify_contribution \ 22 | && cargo build --release --bin beacon \ 23 | && cargo build --release --bin export_keys \ 24 | && cd ../../ \ 25 | && cp /usr/src/setup-tools/phase2-bn254/phase2/target/release/new . \ 26 | && cp /usr/src/setup-tools/phase2-bn254/phase2/target/release/contribute . \ 27 | && cp /usr/src/setup-tools/phase2-bn254/phase2/target/release/verify_contribution . \ 28 | && cp /usr/src/setup-tools/phase2-bn254/phase2/target/release/beacon . \ 29 | && cp /usr/src/setup-tools/phase2-bn254/phase2/target/release/export_keys . \ 30 | && rm -rf phase2-bn254 -------------------------------------------------------------------------------- /setup-tools/README.md: -------------------------------------------------------------------------------- 1 | # AZTEC Protocol Trusted Setup Repository 2 | 3 | **THIS CODE IS HIGHLY EXPERIMENTAL AND HAS NOT BEEN THOROUGHLY TESTED, USE AT YOUR OWN RISK!** 4 | 5 | This repo contains compiles several C++ executables that run the AZTEC trusted setup ceremony. 6 | 7 | - **setup** will perform one round of the trusted setup MPC. 8 | - **seal** the same as `setup`, but uses a hash of the previous participants transcripts as the secret. 9 | - **verify** verifies a transcript files points have been correctly processed relative to a previous transcript file. 10 | - **compute_generator_polynomial** will compute the polynomial coefficients required to construct the AZTEC generator point `h`, from the results of _setup_. 11 | - **prep_range_data** prepares a set of transcripts for post processing by _compute_range_polynomials_. 12 | - **compute_range_polynomials** will compute the AZTEC signature points `mu_k`, from the results of _setup_ and _compute_generator_polynomial_. 13 | - **print_point** will print the given point for a given curve from a given transcript. 14 | 15 | The common reference string produced by `setup` can also be used to construct structured reference strings for [SONIC zk-SNARKS](https://eprint.iacr.org/2019/099.pdf) 16 | 17 | ## Install Instructions 18 | 19 | To build: 20 | 21 | `docker-compose build setup-tools` 22 | 23 | This creates a docker image with the executables located in `/usr/src/setup-tools`. 24 | 25 | To launch the container to run the tools: 26 | 27 | ``` 28 | docker-compose run setup-tools 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### setup 34 | 35 | _setup_ will perform a single round of computation for the trusted setup MPC. If it is being run as the first participant it expects additional arguments specifying the number of G1 and G2 points to generate. 36 | If running as a subsequent participant it only requires the directory of the previous participants transcripts (renamed accordingly) and it will produce the corresponding outputs. 37 | 38 | ``` 39 | usage: ./setup [ ] 40 | ``` 41 | 42 | The following will generate the initial `250,000` G1 points and a single G2 point and write the transcripts to the `../setup_db` directory. The output filenames follow the format `transcript0_out.dat`, `transcript1_out.dat`, `transcript_out.dat`. 43 | 44 | ``` 45 | $ ./setup ../setup_db 250000 1 46 | Creating initial transcripts... 47 | creating 0:6400220 1:6400092 2:3200092 48 | Will compute 100000 G1 points and 1 G2 points starting from 0 in transcript 0 49 | Computing g1 multiple-exponentiations... 50 | progress 20.0348 51 | ... 52 | ... 53 | Writing transcript... 54 | wrote 2 55 | Done. 56 | ``` 57 | 58 | The following will take the previous set of transcripts which must be named `transcript0.dat`, `transcript1.dat`, `transcript.dat`, and will produce a new set of outputs. 59 | 60 | ``` 61 | $ ./setup ../setup_db 62 | Reading transcript... 63 | Will compute 100000 G1 points and 1 G2 points on top of transcript 0 64 | Computing g1 multiple-exponentiations... 65 | progress 20.6704 66 | ... 67 | ... 68 | Writing transcript... 69 | wrote 2 70 | Done. 71 | ``` 72 | 73 | ### seal 74 | 75 | The same as `setup`, but compiled with `SEALING`, where the toxic waste is set to the hash of the previous transcript. 76 | 77 | ### verify 78 | 79 | _verify_ will check that the points in a given transcript have been computed correctly. For the first participant, we only need to check that the powering sequence is consistent across all transcripts. 80 | For a subsequent participant, we also check that the initial point is an exponentiation of the previous participants initial point. 81 | 82 | ``` 83 | usage: ./verify [ ] 84 | ``` 85 | 86 | Verification of a transcript file, always requires the initial point to be available. The second transcript path should always point to transcript 0 in a sequence of transcripts. The following validates that transcript 2 follows from transcript 1. 87 | 88 | ``` 89 | $ ./verify 1000000 1 50000 2 ../setup_db/transcript2_out.dat ../setup_db/transcript0_out.dat ../setup_db/transcript1_out.dat 90 | Verifying... 91 | Transcript valid. 92 | ``` 93 | 94 | The following checks that the initial transcript of a new sequence of transcripts, was built on top of the previous participants transcripts. Note how the 3rd transcript path is the input transcript that was fed to _setup_. 95 | 96 | ``` 97 | $ ./verify 1000000 1 50000 0 ../setup_db/transcript0_out.dat ../setup_db/transcript0_out.dat ../setup_db/transcript0.dat 98 | Verifying... 99 | Transcript valid. 100 | ``` 101 | 102 | ### compute_generator_polynomial 103 | 104 | _compute_generator_polynomial_ calculates the coefficients necessary to compute the AZTEC generator point. 105 | 106 | ``` 107 | usage: ./compute_generator_polynomial 108 | ``` 109 | 110 | ### prep_range_data 111 | 112 | _prep_range_data_ takes the output of _setup_ and _compute_generator_polynomial_ and produces outputs that are suitable for memory mapping within _compute_range_polynomial_. 113 | 114 | ``` 115 | usage: ./prep_range_data 116 | ``` 117 | 118 | **TODO**: Modify to accept transcript range. Currently expects a single transcript file and the generator file in `../setup_db`. 119 | 120 | ### compute_range_polynomial 121 | 122 | _compute_range_polynomial_ calculates a signature point necessary for range proofs. 123 | 124 | ``` 125 | usage: ./compute_range_polynomial 126 | ``` 127 | 128 | **TODO**: Modify to take input files as arguments. Determine `` from size of input files. 129 | 130 | ### print_point 131 | 132 | _print_point_range_polynomial_ prints the given point for a given curve from a given transcript. 133 | 134 | ``` 135 | usage: ./print_point 136 | ``` 137 | 138 | ## Development 139 | 140 | Ensure that at the top level of the repo you have run: 141 | 142 | ``` 143 | git submodule init && git submodule update 144 | ``` 145 | 146 | To ease development you can create an image with necessary build environment, with your current source code mounted into the conatiner. 147 | 148 | ``` 149 | docker-compose build build-env 150 | docker-compose run build-env 151 | ``` 152 | 153 | If not using the docker container, the following installs the required packages on the latest Ubuntu: 154 | 155 | ``` 156 | sudo apt-get update && sudo apt-get install build-essential cmake libgmp-dev libssl-dev 157 | ``` 158 | 159 | Once in the container (or your own build environment) you can build executables from your modified source code on the host: 160 | 161 | ``` 162 | mkdir build 163 | cd ./build 164 | cmake .. 165 | make [executable name] 166 | ``` 167 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "rules": { 5 | "no-bitwise": false, 6 | "no-empty": [true, "allow-empty-functions"], 7 | "no-console": false, 8 | "interface-name": false, 9 | "interface-over-type-literal": false, 10 | "object-literal-sort-keys": false, 11 | "member-ordering": false, 12 | "no-shadowed-variable": false, 13 | "max-classes-per-file": false, 14 | "no-var-requires": false, 15 | "array-type": [true, "array"] 16 | } 17 | } 18 | --------------------------------------------------------------------------------