From cbe02ac3e52e5cd052cb1a5b0561c5de4eeff197 Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:45:44 -0500 Subject: [PATCH] feat(coordinator): add websockets support - [x] Add websockets gateway for proof generation - [x] Update AccountSignatureGuard to work with websockets - [x] Add hooks for proof generation: progress, finish, error --- contracts/tasks/helpers/ProofGenerator.ts | 251 ++++++++------ contracts/tasks/helpers/types.ts | 48 +++ contracts/ts/index.ts | 7 +- coordinator/README.md | 2 +- coordinator/package.json | 8 +- coordinator/tests/app.test.ts | 327 +++++++++++++++++- coordinator/ts/app.controller.ts | 2 +- coordinator/ts/app.module.ts | 2 + .../ts/auth/AccountSignatureGuard.service.ts | 10 +- .../__tests__/AccountSignatureGuard.test.ts | 6 + .../events/__tests__/events.gateway.test.ts | 84 +++++ coordinator/ts/events/events.gateway.ts | 74 ++++ coordinator/ts/events/events.module.ts | 13 + coordinator/ts/events/types.ts | 9 + coordinator/ts/proof/proof.service.ts | 222 ++++++------ pnpm-lock.yaml | 175 +++++++++- 16 files changed, 998 insertions(+), 242 deletions(-) create mode 100644 coordinator/ts/events/__tests__/events.gateway.test.ts create mode 100644 coordinator/ts/events/events.gateway.ts create mode 100644 coordinator/ts/events/events.module.ts create mode 100644 coordinator/ts/events/types.ts diff --git a/contracts/tasks/helpers/ProofGenerator.ts b/contracts/tasks/helpers/ProofGenerator.ts index 05cf87a74c..9cf8a5d171 100644 --- a/contracts/tasks/helpers/ProofGenerator.ts +++ b/contracts/tasks/helpers/ProofGenerator.ts @@ -7,7 +7,13 @@ import { genTreeCommitment, hash3, hashLeftRight } from "maci-crypto"; import fs from "fs"; import path from "path"; -import type { ICircuitFiles, IPrepareStateParams, IProofGeneratorParams, TallyData } from "./types"; +import type { + ICircuitFiles, + IGenerateProofsOptions, + IPrepareStateParams, + IProofGeneratorParams, + TallyData, +} from "./types"; import type { Proof } from "../../ts/types"; import type { BigNumberish } from "ethers"; @@ -176,7 +182,7 @@ export class ProofGenerator { * * @returns message processing proofs */ - async generateMpProofs(): Promise { + async generateMpProofs(options?: IGenerateProofsOptions): Promise { performance.mark("mp-proofs-start"); console.log(`Generating proofs of message processing...`); @@ -188,34 +194,47 @@ export class ProofGenerator { totalMessageBatches += 1; } - const inputs: CircuitInputs[] = []; + try { + const inputs: CircuitInputs[] = []; - // while we have unprocessed messages, process them - while (this.poll.hasUnprocessedMessages()) { - // process messages in batches - const circuitInputs = this.poll.processMessages( - BigInt(this.poll.pollId), - this.useQuadraticVoting, - ) as unknown as CircuitInputs; + // while we have unprocessed messages, process them + while (this.poll.hasUnprocessedMessages()) { + // process messages in batches + const circuitInputs = this.poll.processMessages( + BigInt(this.poll.pollId), + this.useQuadraticVoting, + ) as unknown as CircuitInputs; - // generate the proof for this batch - inputs.push(circuitInputs); + // generate the proof for this batch + inputs.push(circuitInputs); - console.log(`Progress: ${this.poll.numBatchesProcessed} / ${totalMessageBatches}`); - } + console.log(`Progress: ${this.poll.numBatchesProcessed} / ${totalMessageBatches}`); + } - console.log("Wait until proof generation is finished"); + console.log("Wait until proof generation is finished"); - const proofs = await Promise.all( - inputs.map((circuitInputs, index) => this.generateProofs(circuitInputs, this.mp, `process_${index}.json`)), - ).then((data) => data.reduce((acc, x) => acc.concat(x), [])); + const proofs = await Promise.all( + inputs.map((circuitInputs, index) => + this.generateProofs(circuitInputs, this.mp, `process_${index}.json`).then((data) => { + options?.onBatchComplete?.({ current: index, total: totalMessageBatches, proofs: data }); + return data; + }), + ), + ).then((data) => data.reduce((acc, x) => acc.concat(x), [])); - console.log("Proof generation is finished"); + console.log("Proof generation is finished"); - performance.mark("mp-proofs-end"); - performance.measure("Generate message processor proofs", "mp-proofs-start", "mp-proofs-end"); + performance.mark("mp-proofs-end"); + performance.measure("Generate message processor proofs", "mp-proofs-start", "mp-proofs-end"); - return proofs; + options?.onComplete?.(proofs); + + return proofs; + } catch (error) { + options?.onFail?.(error as Error); + + throw error; + } } /** @@ -224,7 +243,10 @@ export class ProofGenerator { * @param network - current network * @returns tally proofs */ - async generateTallyProofs(network: Network): Promise<{ proofs: Proof[]; tallyData: TallyData }> { + async generateTallyProofs( + network: Network, + options?: IGenerateProofsOptions, + ): Promise<{ proofs: Proof[]; tallyData: TallyData }> { performance.mark("tally-proofs-start"); console.log(`Generating proofs of vote tallying...`); @@ -235,105 +257,118 @@ export class ProofGenerator { totalTallyBatches += 1; } - let tallyCircuitInputs: CircuitInputs; - const inputs: CircuitInputs[] = []; + try { + let tallyCircuitInputs: CircuitInputs; + const inputs: CircuitInputs[] = []; - while (this.poll.hasUntalliedBallots()) { - tallyCircuitInputs = (this.useQuadraticVoting - ? this.poll.tallyVotes() - : this.poll.tallyVotesNonQv()) as unknown as CircuitInputs; + while (this.poll.hasUntalliedBallots()) { + tallyCircuitInputs = (this.useQuadraticVoting + ? this.poll.tallyVotes() + : this.poll.tallyVotesNonQv()) as unknown as CircuitInputs; - inputs.push(tallyCircuitInputs); + inputs.push(tallyCircuitInputs); - console.log(`Progress: ${this.poll.numBatchesTallied} / ${totalTallyBatches}`); - } - - console.log("Wait until proof generation is finished"); + console.log(`Progress: ${this.poll.numBatchesTallied} / ${totalTallyBatches}`); + } - const proofs = await Promise.all( - inputs.map((circuitInputs, index) => this.generateProofs(circuitInputs, this.tally, `tally_${index}.json`)), - ).then((data) => data.reduce((acc, x) => acc.concat(x), [])); + console.log("Wait until proof generation is finished"); - console.log("Proof generation is finished"); + const proofs = await Promise.all( + inputs.map((circuitInputs, index) => + this.generateProofs(circuitInputs, this.tally, `tally_${index}.json`).then((data) => { + options?.onBatchComplete?.({ current: index, total: totalTallyBatches, proofs: data }); + return data; + }), + ), + ).then((data) => data.reduce((acc, x) => acc.concat(x), [])); - // verify the results - // Compute newResultsCommitment - const newResultsCommitment = genTreeCommitment( - this.poll.tallyResult, - BigInt(asHex(tallyCircuitInputs!.newResultsRootSalt as BigNumberish)), - this.poll.treeDepths.voteOptionTreeDepth, - ); + console.log("Proof generation is finished"); - // compute newSpentVoiceCreditsCommitment - const newSpentVoiceCreditsCommitment = hashLeftRight( - this.poll.totalSpentVoiceCredits, - BigInt(asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)), - ); - - let newPerVOSpentVoiceCreditsCommitment: bigint | undefined; - let newTallyCommitment: bigint; - - // create the tally file data to store for verification later - const tallyFileData: TallyData = { - maci: this.maciContractAddress, - pollId: this.poll.pollId.toString(), - network: network.name, - chainId: network.config.chainId?.toString(), - isQuadratic: Boolean(this.useQuadraticVoting), - tallyAddress: this.tallyContractAddress, - newTallyCommitment: asHex(tallyCircuitInputs!.newTallyCommitment as BigNumberish), - results: { - tally: this.poll.tallyResult.map((x) => x.toString()), - salt: asHex(tallyCircuitInputs!.newResultsRootSalt as BigNumberish), - commitment: asHex(newResultsCommitment), - }, - totalSpentVoiceCredits: { - spent: this.poll.totalSpentVoiceCredits.toString(), - salt: asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish), - commitment: asHex(newSpentVoiceCreditsCommitment), - }, - }; - - if (this.useQuadraticVoting) { - // Compute newPerVOSpentVoiceCreditsCommitment - newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( - this.poll.perVOSpentVoiceCredits, - BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), + // verify the results + // Compute newResultsCommitment + const newResultsCommitment = genTreeCommitment( + this.poll.tallyResult, + BigInt(asHex(tallyCircuitInputs!.newResultsRootSalt as BigNumberish)), this.poll.treeDepths.voteOptionTreeDepth, ); - // Compute newTallyCommitment - newTallyCommitment = hash3([ - newResultsCommitment, - newSpentVoiceCreditsCommitment, - newPerVOSpentVoiceCreditsCommitment, - ]); + // compute newSpentVoiceCreditsCommitment + const newSpentVoiceCreditsCommitment = hashLeftRight( + this.poll.totalSpentVoiceCredits, + BigInt(asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish)), + ); - // update perVOSpentVoiceCredits in the tally file data - tallyFileData.perVOSpentVoiceCredits = { - tally: this.poll.perVOSpentVoiceCredits.map((x) => x.toString()), - salt: asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish), - commitment: asHex(newPerVOSpentVoiceCreditsCommitment), + let newPerVOSpentVoiceCreditsCommitment: bigint | undefined; + let newTallyCommitment: bigint; + + // create the tally file data to store for verification later + const tallyFileData: TallyData = { + maci: this.maciContractAddress, + pollId: this.poll.pollId.toString(), + network: network.name, + chainId: network.config.chainId?.toString(), + isQuadratic: Boolean(this.useQuadraticVoting), + tallyAddress: this.tallyContractAddress, + newTallyCommitment: asHex(tallyCircuitInputs!.newTallyCommitment as BigNumberish), + results: { + tally: this.poll.tallyResult.map((x) => x.toString()), + salt: asHex(tallyCircuitInputs!.newResultsRootSalt as BigNumberish), + commitment: asHex(newResultsCommitment), + }, + totalSpentVoiceCredits: { + spent: this.poll.totalSpentVoiceCredits.toString(), + salt: asHex(tallyCircuitInputs!.newSpentVoiceCreditSubtotalSalt as BigNumberish), + commitment: asHex(newSpentVoiceCreditsCommitment), + }, }; - } else { - newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); - } - fs.writeFileSync(this.tallyOutputFile, JSON.stringify(tallyFileData, null, 4)); - - console.log(`Tally file:\n${JSON.stringify(tallyFileData, null, 4)}\n`); - - // compare the commitments - if (asHex(newTallyCommitment) === tallyFileData.newTallyCommitment) { - console.log("The tally commitment is correct"); - } else { - throw new Error("Error: the newTallyCommitment is invalid."); + if (this.useQuadraticVoting) { + // Compute newPerVOSpentVoiceCreditsCommitment + newPerVOSpentVoiceCreditsCommitment = genTreeCommitment( + this.poll.perVOSpentVoiceCredits, + BigInt(asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish)), + this.poll.treeDepths.voteOptionTreeDepth, + ); + + // Compute newTallyCommitment + newTallyCommitment = hash3([ + newResultsCommitment, + newSpentVoiceCreditsCommitment, + newPerVOSpentVoiceCreditsCommitment, + ]); + + // update perVOSpentVoiceCredits in the tally file data + tallyFileData.perVOSpentVoiceCredits = { + tally: this.poll.perVOSpentVoiceCredits.map((x) => x.toString()), + salt: asHex(tallyCircuitInputs!.newPerVOSpentVoiceCreditsRootSalt as BigNumberish), + commitment: asHex(newPerVOSpentVoiceCreditsCommitment), + }; + } else { + newTallyCommitment = hashLeftRight(newResultsCommitment, newSpentVoiceCreditsCommitment); + } + + fs.writeFileSync(this.tallyOutputFile, JSON.stringify(tallyFileData, null, 4)); + + console.log(`Tally file:\n${JSON.stringify(tallyFileData, null, 4)}\n`); + + // compare the commitments + if (asHex(newTallyCommitment) === tallyFileData.newTallyCommitment) { + console.log("The tally commitment is correct"); + } else { + throw new Error("Error: the newTallyCommitment is invalid."); + } + + performance.mark("tally-proofs-end"); + performance.measure("Generate tally proofs", "tally-proofs-start", "tally-proofs-end"); + + options?.onComplete?.(proofs, tallyFileData); + + return { proofs, tallyData: tallyFileData }; + } catch (error) { + options?.onFail?.(error as Error); + + throw error; } - - performance.mark("tally-proofs-end"); - performance.measure("Generate tally proofs", "tally-proofs-start", "tally-proofs-end"); - - return { proofs, tallyData: tallyFileData }; } /** diff --git a/contracts/tasks/helpers/types.ts b/contracts/tasks/helpers/types.ts index ce48f25677..a0f2ea934b 100644 --- a/contracts/tasks/helpers/types.ts +++ b/contracts/tasks/helpers/types.ts @@ -1,3 +1,4 @@ +import type { Proof } from "../../ts/types"; import type { AccQueue, MACI, MessageProcessor, Poll, Tally, Verifier, VkRegistry } from "../../typechain-types"; import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import type { @@ -294,6 +295,53 @@ export interface IPrepareStateParams { }>; } +/** + * Interface that represents generate proof options + */ +export interface IGenerateProofsOptions { + /** + * Hook to call when batch generation is completed + * + * @param data - batch data + */ + onBatchComplete?: (data: IGenerateProofsBatchData) => void; + + /** + * Hook to call when proof generation is completed + * + * @param data - proof generated data + * @param tally - tally data + */ + onComplete?: (data: Proof[], tally?: TallyData) => void; + + /** + * Hook to call when generation is failed + * + * @param error - error + */ + onFail?: (error: Error) => void; +} + +/** + * Interface that represents batch data + */ +export interface IGenerateProofsBatchData { + /** + * Batch proofs + */ + proofs: Proof[]; + + /** + * Current batch + */ + current: number; + + /** + * Total batches + */ + total: number; +} + /** * Interface that represents prover params */ diff --git a/contracts/ts/index.ts b/contracts/ts/index.ts index ce99a0e69b..3f82973231 100644 --- a/contracts/ts/index.ts +++ b/contracts/ts/index.ts @@ -22,7 +22,12 @@ export { Deployment } from "../tasks/helpers/Deployment"; export { ContractStorage } from "../tasks/helpers/ContractStorage"; export { ProofGenerator } from "../tasks/helpers/ProofGenerator"; export { Prover } from "../tasks/helpers/Prover"; -export { EContracts } from "../tasks/helpers/types"; +export { + EContracts, + type IGenerateProofsOptions, + type IGenerateProofsBatchData, + type TallyData, +} from "../tasks/helpers/types"; export { linkPoseidonLibraries } from "../tasks/helpers/abi"; export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof, Proof } from "./types"; diff --git a/coordinator/README.md b/coordinator/README.md index 71b90ed4eb..50a897fdea 100644 --- a/coordinator/README.md +++ b/coordinator/README.md @@ -9,5 +9,5 @@ 5. Run `pnpm run start` to run the service. 6. All API calls must be called with `Authorization` header, where the value is encrypted with RSA public key you generated before. Header value contains message signature and message digest created by `COORDINATOR_ADDRESSES`. The format is `publicEncrypt({signature}:{digest})`. Make sure you set `COORDINATOR_ADDRESSES` env variable and sign any message with the addresses from your application (see [AccountSignatureGuard](./ts/auth/AccountSignatureGuard.service.ts)). -7. Proofs can be generated with `POST v1/proof/generate` API method (see [dto spec](./ts/proof/dto.ts) and [controller](./ts/app.controller.ts)). +7. Proofs can be generated with `POST v1/proof/generate` API method or with Websockets (see [dto spec](./ts/proof/dto.ts), [controller](./ts/app.controller.ts) and [wsgateway](./ts/events/events.gateway.ts)). 8. [Swagger documentation for API methods](https://maci-coordinator.pse.dev/api) diff --git a/coordinator/package.json b/coordinator/package.json index e379070d8f..7ac0f463bf 100644 --- a/coordinator/package.json +++ b/coordinator/package.json @@ -15,8 +15,8 @@ "start": "nest start", "start:dev": "nest start --watch", "start:prod": "node dist/main", - "test": "jest", - "test:coverage": "jest --coverage", + "test": "jest --forceExit", + "test:coverage": "jest --coverage --forceExit", "types": "tsc -p tsconfig.json --noEmit", "generate-keypair": "ts-node ./scripts/generateKeypair.ts" }, @@ -24,8 +24,10 @@ "@nestjs/common": "^10.3.8", "@nestjs/core": "^10.3.9", "@nestjs/platform-express": "^10.3.8", + "@nestjs/platform-socket.io": "^10.3.10", "@nestjs/swagger": "^7.3.1", "@nestjs/throttler": "^5.2.0", + "@nestjs/websockets": "^10.3.10", "@nomicfoundation/hardhat-ethers": "^3.0.6", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "class-transformer": "^0.5.1", @@ -40,6 +42,7 @@ "maci-domainobjs": "2.0.0-alpha", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.7.5", "tar": "^7.4.0" }, "devDependencies": { @@ -52,6 +55,7 @@ "@types/supertest": "^6.0.0", "fast-check": "^3.19.0", "jest": "^29.5.0", + "socket.io-client": "^4.7.5", "supertest": "^7.0.0", "ts-jest": "^29.1.5", "ts-node": "^10.9.1", diff --git a/coordinator/tests/app.test.ts b/coordinator/tests/app.test.ts index 473d454e7f..a3b623f4b2 100644 --- a/coordinator/tests/app.test.ts +++ b/coordinator/tests/app.test.ts @@ -1,5 +1,6 @@ import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; +import { ValidationError } from "class-validator"; import { getBytes, hashMessage, type Signer } from "ethers"; import hardhat from "hardhat"; import { @@ -15,7 +16,10 @@ import { mergeMessages, mergeSignups, } from "maci-cli"; +import { Proof, TallyData } from "maci-contracts"; +import { Poll__factory as PollFactory } from "maci-contracts/typechain-types"; import { Keypair } from "maci-domainobjs"; +import { io, Socket } from "socket.io-client"; import request from "supertest"; import fs from "fs"; @@ -26,7 +30,9 @@ import type { App } from "supertest/types"; import { AppModule } from "../ts/app.module"; import { ErrorCodes } from "../ts/common"; import { CryptoService } from "../ts/crypto/crypto.service"; +import { EProofGenerationEvents } from "../ts/events/types"; import { FileModule } from "../ts/file/file.module"; +import { IGenerateArgs } from "../ts/proof/types"; const STATE_TREE_DEPTH = 10; const INT_STATE_TREE_DEPTH = 1; @@ -40,6 +46,7 @@ describe("AppController (e2e)", () => { let signer: Signer; let maciAddresses: DeployedContracts; let pollContracts: PollContracts; + let socket: Socket; const cryptoService = new CryptoService(); @@ -87,16 +94,39 @@ describe("AppController (e2e)", () => { useQuadraticVoting: false, signer, }); - }); - beforeEach(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule, FileModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ transform: true })); - await app.init(); + await app.listen(3000); + }); + + beforeEach(async () => { + const authorization = await getAuthorizationHeader(); + + await new Promise((resolve) => { + app.getUrl().then((url) => { + socket = io(url, { + extraHeaders: { + authorization, + }, + }); + socket.on("connect", () => { + resolve(true); + }); + }); + }); + }); + + afterEach(() => { + socket.disconnect(); + }); + + afterAll(async () => { + await app.close(); }); describe("validation /v1/proof/generate POST", () => { @@ -142,6 +172,32 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if poll id is invalid (ws)", async () => { + const publicKey = fs.readFileSync(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: "-1" as unknown as number, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise<{ min?: string; isInt?: string }>((resolve) => { + socket + .emit(EProofGenerationEvents.START, args) + .on(EProofGenerationEvents.ERROR, (errors: ValidationError[]) => { + const error = errors[0]?.constraints; + + resolve({ min: error?.min, isInt: error?.isInt }); + }); + }); + + expect(result.min).toBe("poll must not be less than 0"); + expect(result.isInt).toBe("poll must be an integer number"); + }); + test("should throw an error if encrypted key is invalid", async () => { const encryptedHeader = await getAuthorizationHeader(); @@ -164,6 +220,28 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if encrypted key is invalid (ws)", async () => { + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: "", + }; + + const result = await new Promise<{ isLength?: string }>((resolve) => { + socket + .emit(EProofGenerationEvents.START, args) + .on(EProofGenerationEvents.ERROR, (errors: ValidationError[]) => { + const error = errors[0]?.constraints; + + resolve({ isLength: error?.isLength }); + }); + }); + + expect(result.isLength).toBe("encryptedCoordinatorPrivateKey must be longer than or equal to 1 characters"); + }); + test("should throw an error if maci address is invalid", async () => { const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); @@ -188,6 +266,31 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if maci address is invalid (ws)", async () => { + const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: "wrong", + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise<{ isEthereumAddress?: string }>((resolve) => { + socket + .emit(EProofGenerationEvents.START, args) + .on(EProofGenerationEvents.ERROR, (errors: ValidationError[]) => { + const error = errors[0]?.constraints; + + resolve({ isEthereumAddress: error?.isEthereumAddress }); + }); + }); + + expect(result.isEthereumAddress).toBe("maciContractAddress must be an Ethereum address"); + }); + test("should throw an error if tally address is invalid", async () => { const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); @@ -211,6 +314,31 @@ describe("AppController (e2e)", () => { message: ["tallyContractAddress must be an Ethereum address"], }); }); + + test("should throw an error if tally address is invalid (ws)", async () => { + const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: "wrong", + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise<{ isEthereumAddress?: string }>((resolve) => { + socket + .emit(EProofGenerationEvents.START, args) + .on(EProofGenerationEvents.ERROR, (errors: ValidationError[]) => { + const error = errors[0]?.constraints; + + resolve({ isEthereumAddress: error?.isEthereumAddress }); + }); + }); + + expect(result.isEthereumAddress).toBe("tallyContractAddress must be an Ethereum address"); + }); }); describe("/v1/proof/publicKey GET", () => { @@ -267,6 +395,27 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if poll is not over (ws)", async () => { + const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.NOT_MERGED_STATE_TREE); + }); + test("should throw an error if signups are not merged", async () => { const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); @@ -290,9 +439,35 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if signups are not merged (ws)", async () => { + const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.NOT_MERGED_STATE_TREE); + }); + test("should throw an error if messages are not merged", async () => { - await timeTravel({ seconds: 30, signer }); - await mergeSignups({ pollId: 0n, signer }); + const pollContract = PollFactory.connect(pollContracts.poll, signer); + const isStateMerged = await pollContract.stateMerged(); + + if (!isStateMerged) { + await timeTravel({ seconds: 30, signer }); + await mergeSignups({ pollId: 0n, signer }); + } const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); @@ -316,6 +491,35 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if messages are not merged (ws)", async () => { + const pollContract = PollFactory.connect(pollContracts.poll, signer); + const isStateMerged = await pollContract.stateMerged(); + + if (!isStateMerged) { + await timeTravel({ seconds: 30, signer }); + await mergeSignups({ pollId: 0n, signer }); + } + + const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); + const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey, + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.NOT_MERGED_MESSAGE_TREE); + }); + test("should throw an error if coordinator key decryption is failed", async () => { await mergeMessages({ pollId: 0n, signer }); @@ -339,6 +543,26 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if coordinator key decryption is failed (ws)", async () => { + await mergeMessages({ pollId: 0n, signer }); + + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(), + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.DECRYPTION); + }); + test("should throw an error if there is no such poll", async () => { const encryptedHeader = await getAuthorizationHeader(); @@ -360,6 +584,24 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if there is no such poll (ws)", async () => { + const args: IGenerateArgs = { + poll: 9000, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(), + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.POLL_NOT_FOUND); + }); + test("should throw an error if there is no authorization header", async () => { const result = await request(app.getHttpServer() as App) .post("/v1/proof/generate") @@ -379,6 +621,26 @@ describe("AppController (e2e)", () => { }); }); + test("should throw an error if there is no authorization header (ws)", async () => { + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(), + }; + + const unauthorizedSocket = io(await app.getUrl()); + + const result = await new Promise((resolve) => { + unauthorizedSocket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }).finally(() => unauthorizedSocket.disconnect()); + + expect(result.message).toBe("Forbidden resource"); + }); + test("should throw error if coordinator key cannot be decrypted", async () => { const encryptedHeader = await getAuthorizationHeader(); @@ -400,21 +662,41 @@ describe("AppController (e2e)", () => { }); }); + test("should throw error if coordinator key cannot be decrypted (ws)", async () => { + const args: IGenerateArgs = { + poll: 0, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: "unknown", + }; + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.ERROR, (error: Error) => { + resolve(error); + }); + }); + + expect(result.message).toBe(ErrorCodes.DECRYPTION); + }); + test("should generate proofs properly", async () => { const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!); const encryptedCoordinatorPrivateKey = cryptoService.encrypt(publicKey, coordinatorKeypair.privKey.serialize()); const encryptedHeader = await getAuthorizationHeader(); + const args: IGenerateArgs = { + poll: 0, + encryptedCoordinatorPrivateKey, + maciContractAddress: maciAddresses.maciAddress, + tallyContractAddress: pollContracts.tally, + useQuadraticVoting: false, + }; + await request(app.getHttpServer() as App) .post("/v1/proof/generate") .set("Authorization", encryptedHeader) - .send({ - poll: 0, - encryptedCoordinatorPrivateKey, - maciContractAddress: maciAddresses.maciAddress, - tallyContractAddress: pollContracts.tally, - useQuadraticVoting: false, - }) + .send(args) .expect(201); const proofData = await Promise.all([ @@ -424,14 +706,27 @@ describe("AppController (e2e)", () => { ]) .then((files) => files.map((item) => JSON.parse(item.toString()) as Record)) .then((data) => ({ - processProofs: [data[0]], - tallyProofs: [data[1]], - tallyData: [data[2]], + processProofs: [data[0]] as unknown as Proof[], + tallyProofs: [data[1]] as unknown as Proof[], + tallyData: data[2] as unknown as TallyData, })); + interface TResult { + tallyData?: TallyData; + } + + const result = await new Promise((resolve) => { + socket.emit(EProofGenerationEvents.START, args).on(EProofGenerationEvents.FINISH, ({ tallyData }: TResult) => { + if (tallyData) { + resolve(tallyData); + } + }); + }); + expect(proofData.processProofs).toHaveLength(1); - expect(proofData.tallyData).toHaveLength(1); + expect(proofData.tallyProofs).toHaveLength(1); expect(proofData.tallyData).toBeDefined(); + expect(result.results.tally).toStrictEqual(proofData.tallyData.results.tally); }); }); }); diff --git a/coordinator/ts/app.controller.ts b/coordinator/ts/app.controller.ts index 39e01c136c..1cce2f3cbf 100644 --- a/coordinator/ts/app.controller.ts +++ b/coordinator/ts/app.controller.ts @@ -34,7 +34,7 @@ export class AppController { * Generate proofs api method * * @param args - generate proof dto - * @returns + * @returns generated proofs and tally data */ @ApiBody({ type: GenerateProofDto }) @ApiResponse({ status: HttpStatus.CREATED, description: "The proofs have been successfully generated" }) diff --git a/coordinator/ts/app.module.ts b/coordinator/ts/app.module.ts index 9bcb94e017..0405118fbd 100644 --- a/coordinator/ts/app.module.ts +++ b/coordinator/ts/app.module.ts @@ -3,6 +3,7 @@ import { ThrottlerModule } from "@nestjs/throttler"; import { AppController } from "./app.controller"; import { CryptoModule } from "./crypto/crypto.module"; +import { EventsModule } from "./events/events.module"; import { FileModule } from "./file/file.module"; import { ProofGeneratorService } from "./proof/proof.service"; @@ -16,6 +17,7 @@ import { ProofGeneratorService } from "./proof/proof.service"; ]), FileModule, CryptoModule, + EventsModule, ], controllers: [AppController], providers: [ProofGeneratorService], diff --git a/coordinator/ts/auth/AccountSignatureGuard.service.ts b/coordinator/ts/auth/AccountSignatureGuard.service.ts index 80101138c4..759458ac5a 100644 --- a/coordinator/ts/auth/AccountSignatureGuard.service.ts +++ b/coordinator/ts/auth/AccountSignatureGuard.service.ts @@ -13,6 +13,7 @@ import fs from "fs"; import path from "path"; import type { Request as Req } from "express"; +import type { Socket } from "socket.io"; import { CryptoService } from "../crypto/crypto.service"; @@ -29,9 +30,9 @@ export const PUBLIC_METADATA_KEY = "isPublic"; export const Public = (): CustomDecorator => SetMetadata(PUBLIC_METADATA_KEY, true); /** - * AccountSignatureGuard is responsible for protecting calling controller functions. + * AccountSignatureGuard is responsible for protecting calling controller and websocket gateway functions. * If account address is not added to .env file, you will not be allowed to call any API methods. - * Make sure you send `Authorization encrypt({signature}:{digest})` header where: + * Make sure you send `Authorization: Bearer encrypt({signature}:{digest})` header where: * 1. encrypt - RSA public encryption. * 2. signature - eth wallet signature for any message * 3. digest - hex representation of message digest @@ -70,8 +71,9 @@ export class AccountSignatureGuard implements CanActivate { return true; } - const request = ctx.switchToHttp().getRequest(); - const encryptedHeader = request.headers.authorization; + const request = ctx.switchToHttp().getRequest>(); + const socket = ctx.switchToWs().getClient>(); + const encryptedHeader = socket.handshake?.headers.authorization || request.headers?.authorization; if (!encryptedHeader) { this.logger.warn("No authorization header"); diff --git a/coordinator/ts/auth/__tests__/AccountSignatureGuard.test.ts b/coordinator/ts/auth/__tests__/AccountSignatureGuard.test.ts index a9d2f89521..270ca3fa4d 100644 --- a/coordinator/ts/auth/__tests__/AccountSignatureGuard.test.ts +++ b/coordinator/ts/auth/__tests__/AccountSignatureGuard.test.ts @@ -26,6 +26,9 @@ describe("AccountSignatureGuard", () => { switchToHttp: jest.fn().mockReturnValue({ getRequest: jest.fn(() => mockRequest), }), + switchToWs: jest.fn().mockReturnValue({ + getClient: jest.fn(() => ({ handshake: mockRequest })), + }), } as unknown as ExecutionContext; const mockSignature = @@ -63,6 +66,9 @@ describe("AccountSignatureGuard", () => { headers: { authorization: "" }, })), }), + switchToWs: jest.fn().mockReturnValue({ + getClient: jest.fn(() => ({ handshake: { headers: { authorization: "" } } })), + }), } as unknown as ExecutionContext; const guard = new AccountSignatureGuard(mockCryptoService, reflector); diff --git a/coordinator/ts/events/__tests__/events.gateway.test.ts b/coordinator/ts/events/__tests__/events.gateway.test.ts new file mode 100644 index 0000000000..bc0dce13f7 --- /dev/null +++ b/coordinator/ts/events/__tests__/events.gateway.test.ts @@ -0,0 +1,84 @@ +import { Test } from "@nestjs/testing"; +import { IGenerateProofsOptions } from "maci-contracts"; +import { Server } from "socket.io"; + +import type { IGenerateArgs, IGenerateData } from "../../proof/types"; +import type { TallyData } from "maci-cli"; + +import { ProofGeneratorService } from "../../proof/proof.service"; +import { EventsGateway } from "../events.gateway"; +import { EProofGenerationEvents } from "../types"; + +describe("EventsGateway", () => { + let gateway: EventsGateway; + + const defaultProofGeneratorArgs: IGenerateArgs = { + poll: 0, + maciContractAddress: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + tallyContractAddress: "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + useQuadraticVoting: false, + encryptedCoordinatorPrivateKey: + "siO9W/g7jNVXs9tOUv/pffrcqYdMlgdXw7nSSlqM1q1UvHGSSbhtLJpeT+nJKW7/+xrBTgI0wB866DSkg8Rgr8zD+POUMiKPrGqAO/XhrcmRDL+COURFNDRh9WGeAua6hdiNoufQYvXPl1iWyIYidSDbfmC2wR6F9vVkhg/6KDZyw8Wlr6LUh0RYT+hUHEwwGbz7MeqZJcJQSTpECPF5pnk8NTHL2W/XThaewB4n4HYqjDUbYLmBDLYWsDDMgoPo709a309rTq3uEe0YBgVF8g9aGxucTDhz+/LYYzqaeSxclUwen9Z4BGZjiDSPBZfooOEQEEwIJlViQ2kl1VeOKAmkiWEUVfItivmNbC/PNZchklmfFsGpiu4DT9UU9YVBN2OTcFYHHsslcaqrR7SuesqjluaGjG46oYEmfQlkZ4gXhavdWXw2ant+Tv6HRo4trqjoD1e3jUkN6gJMWomxOeRBTg0czBZlz/IwUtTpBHcKhi3EqGQo8OuQtWww+Ts7ySmeoONuovYUsIAppNuOubfUxvFJoTr2vKbWNAiYetw09kddkjmBe+S8A5PUiFOi262mfc7g5wJwPPP7wpTBY0Fya+2BCPzXqRLMOtNI+1tW3/UQLZYvEY8J0TxmhoAGZaRn8FKaosatRxDZTQS6QUNmKxpmUspkRKzTXN5lznM=", + }; + + const defaultProofGeneratorData: IGenerateData = { + tallyProofs: [], + processProofs: [], + tallyData: {} as TallyData, + }; + + const mockGeneratorService = { + generate: jest.fn(), + }; + + const mockEmit = jest.fn(); + + beforeEach(async () => { + const testModule = await Test.createTestingModule({ providers: [EventsGateway] }) + .useMocker((token) => { + if (token === ProofGeneratorService) { + mockGeneratorService.generate.mockImplementation((_, options?: IGenerateProofsOptions) => { + options?.onBatchComplete?.({ current: 1, total: 2, proofs: defaultProofGeneratorData.processProofs }); + options?.onComplete?.( + defaultProofGeneratorData.processProofs.concat(defaultProofGeneratorData.tallyProofs), + defaultProofGeneratorData.tallyData, + ); + options?.onFail?.(new Error("error")); + }); + + return mockGeneratorService; + } + + return jest.fn(); + }) + .compile(); + + gateway = testModule.get(EventsGateway); + + gateway.server = { emit: mockEmit } as unknown as Server; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should be defined", () => { + expect(gateway).toBeDefined(); + }); + + test("should start proof generation properly", async () => { + await gateway.generate(defaultProofGeneratorArgs); + + expect(mockEmit).toHaveBeenCalledTimes(3); + expect(mockEmit).toHaveBeenNthCalledWith(1, EProofGenerationEvents.PROGRESS, { + current: 1, + total: 2, + proofs: defaultProofGeneratorData.processProofs, + }); + expect(mockEmit).toHaveBeenNthCalledWith(2, EProofGenerationEvents.FINISH, { + proofs: defaultProofGeneratorData.processProofs.concat(defaultProofGeneratorData.tallyProofs), + tallyData: defaultProofGeneratorData.tallyData, + }); + expect(mockEmit).toHaveBeenNthCalledWith(3, EProofGenerationEvents.ERROR, { message: "error" }); + }); +}); diff --git a/coordinator/ts/events/events.gateway.ts b/coordinator/ts/events/events.gateway.ts new file mode 100644 index 0000000000..85260cb16e --- /dev/null +++ b/coordinator/ts/events/events.gateway.ts @@ -0,0 +1,74 @@ +import { Logger, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common"; +import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer, WsException } from "@nestjs/websockets"; +import { IGenerateProofsBatchData, type Proof, type TallyData } from "maci-contracts"; + +import type { Server } from "socket.io"; + +import { AccountSignatureGuard } from "../auth/AccountSignatureGuard.service"; +import { GenerateProofDto } from "../proof/dto"; +import { ProofGeneratorService } from "../proof/proof.service"; + +import { EProofGenerationEvents } from "./types"; + +@WebSocketGateway({ + cors: { + origin: process.env.COORDINATOR_ALLOWED_ORIGINS?.split(","), + }, +}) +@UseGuards(AccountSignatureGuard) +export class EventsGateway { + /** + * Logger + */ + private readonly logger = new Logger(EventsGateway.name); + + /** + * Websocket server + */ + @WebSocketServer() + server!: Server; + + /** + * Initialize EventsGateway + * + * @param proofGeneratorService - proof generator service + */ + constructor(private readonly proofGeneratorService: ProofGeneratorService) {} + + /** + * Generate proofs api method. + * Events: + * 1. EProofGenerationEvents.START - trigger method call + * 2. EProofGenerationEvents.PROGRESS - returns generated proofs with batch info + * 3. EProofGenerationEvents.FINISH - returns generated proofs and tally data when available + * 4. EProofGenerationEvents.ERROR - triggered when exception is thrown + * + * @param args - generate proof dto + */ + @SubscribeMessage(EProofGenerationEvents.START) + @UsePipes( + new ValidationPipe({ + transform: true, + exceptionFactory(validationErrors) { + return new WsException(validationErrors); + }, + }), + ) + async generate( + @MessageBody() + data: GenerateProofDto, + ): Promise { + await this.proofGeneratorService.generate(data, { + onBatchComplete: (result: IGenerateProofsBatchData) => { + this.server.emit(EProofGenerationEvents.PROGRESS, result); + }, + onComplete: (proofs: Proof[], tallyData?: TallyData) => { + this.server.emit(EProofGenerationEvents.FINISH, { proofs, tallyData }); + }, + onFail: (error: Error) => { + this.logger.error(`Error:`, error); + this.server.emit(EProofGenerationEvents.ERROR, { message: error.message }); + }, + }); + } +} diff --git a/coordinator/ts/events/events.module.ts b/coordinator/ts/events/events.module.ts new file mode 100644 index 0000000000..bff1141dcc --- /dev/null +++ b/coordinator/ts/events/events.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; + +import { CryptoModule } from "../crypto/crypto.module"; +import { FileModule } from "../file/file.module"; +import { ProofGeneratorService } from "../proof/proof.service"; + +import { EventsGateway } from "./events.gateway"; + +@Module({ + imports: [FileModule, CryptoModule], + providers: [EventsGateway, ProofGeneratorService], +}) +export class EventsModule {} diff --git a/coordinator/ts/events/types.ts b/coordinator/ts/events/types.ts new file mode 100644 index 0000000000..a61e0f42b4 --- /dev/null +++ b/coordinator/ts/events/types.ts @@ -0,0 +1,9 @@ +/** + * WS events for proof generation + */ +export enum EProofGenerationEvents { + START = "start-generation", + PROGRESS = "progress-generation", + FINISH = "finish-generation", + ERROR = "exception", +} diff --git a/coordinator/ts/proof/proof.service.ts b/coordinator/ts/proof/proof.service.ts index 1200a1027f..02399a535b 100644 --- a/coordinator/ts/proof/proof.service.ts +++ b/coordinator/ts/proof/proof.service.ts @@ -1,7 +1,15 @@ import { Logger, Injectable } from "@nestjs/common"; import { ZeroAddress } from "ethers"; import hre from "hardhat"; -import { Deployment, EContracts, ProofGenerator, type Poll, type MACI, type AccQueue } from "maci-contracts"; +import { + Deployment, + EContracts, + ProofGenerator, + type Poll, + type MACI, + type AccQueue, + type IGenerateProofsOptions, +} from "maci-contracts"; import { Keypair, PrivKey, PubKey } from "maci-domainobjs"; import path from "path"; @@ -46,110 +54,120 @@ export class ProofGeneratorService { * @param args - generate proofs arguments * @returns - generated proofs for message processing and tally */ - async generate({ - poll, - maciContractAddress, - tallyContractAddress, - useQuadraticVoting, - encryptedCoordinatorPrivateKey, - startBlock, - endBlock, - blocksPerBatch, - }: IGenerateArgs): Promise { - const maciContract = await this.deployment.getContract({ - name: EContracts.MACI, - address: maciContractAddress, - }); - - const [signer, pollAddress] = await Promise.all([this.deployment.getDeployer(), maciContract.polls(poll)]); - - if (pollAddress.toLowerCase() === ZeroAddress.toLowerCase()) { - this.logger.error(`Error: ${ErrorCodes.POLL_NOT_FOUND}, Poll ${poll} not found`); - throw new Error(ErrorCodes.POLL_NOT_FOUND); - } - - const pollContract = await this.deployment.getContract({ name: EContracts.Poll, address: pollAddress }); - const [{ messageAq: messageAqAddress }, coordinatorPublicKey, isStateAqMerged, messageTreeDepth] = - await Promise.all([ - pollContract.extContracts(), - pollContract.coordinatorPubKey(), - pollContract.stateMerged(), - pollContract.treeDepths().then((depths) => Number(depths[2])), - ]); - const messageAq = await this.deployment.getContract({ - name: EContracts.AccQueue, - address: messageAqAddress, - }); - - if (!isStateAqMerged) { - this.logger.error(`Error: ${ErrorCodes.NOT_MERGED_STATE_TREE}, state tree is not merged`); - throw new Error(ErrorCodes.NOT_MERGED_STATE_TREE); - } - - const mainRoot = await messageAq.getMainRoot(messageTreeDepth.toString()); - - if (mainRoot.toString() === "0") { - this.logger.error(`Error: ${ErrorCodes.NOT_MERGED_MESSAGE_TREE}, message tree is not merged`); - throw new Error(ErrorCodes.NOT_MERGED_MESSAGE_TREE); - } - - const { privateKey } = await this.fileService.getPrivateKey(); - const maciPrivateKey = PrivKey.deserialize(this.cryptoService.decrypt(privateKey, encryptedCoordinatorPrivateKey)); - const coordinatorKeypair = new Keypair(maciPrivateKey); - const publicKey = new PubKey([ - BigInt(coordinatorPublicKey.x.toString()), - BigInt(coordinatorPublicKey.y.toString()), - ]); - - if (!coordinatorKeypair.pubKey.equals(publicKey)) { - this.logger.error(`Error: ${ErrorCodes.PRIVATE_KEY_MISMATCH}, wrong private key`); - throw new Error(ErrorCodes.PRIVATE_KEY_MISMATCH); - } - - const outputDir = path.resolve("./proofs"); - - const maciState = await ProofGenerator.prepareState({ - maciContract, - pollContract, - messageAq, - maciPrivateKey, - coordinatorKeypair, - pollId: poll, - signer, - outputDir, - options: { - startBlock, - endBlock, - blocksPerBatch, - }, - }); - - const foundPoll = maciState.polls.get(BigInt(poll)); - - if (!foundPoll) { - this.logger.error(`Error: ${ErrorCodes.POLL_NOT_FOUND}, Poll ${poll} not found in maci state`); - throw new Error(ErrorCodes.POLL_NOT_FOUND); - } - - const proofGenerator = new ProofGenerator({ - poll: foundPoll, + async generate( + { + poll, maciContractAddress, tallyContractAddress, - tally: this.fileService.getZkeyFilePaths(process.env.COORDINATOR_TALLY_ZKEY_NAME!, useQuadraticVoting), - mp: this.fileService.getZkeyFilePaths(process.env.COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME!, useQuadraticVoting), - rapidsnark: process.env.COORDINATOR_RAPIDSNARK_EXE, - outputDir, - tallyOutputFile: path.resolve("./tally.json"), useQuadraticVoting, - }); - - const processProofs = await proofGenerator.generateMpProofs(); - const { proofs: tallyProofs, tallyData } = await proofGenerator.generateTallyProofs(hre.network); + encryptedCoordinatorPrivateKey, + startBlock, + endBlock, + blocksPerBatch, + }: IGenerateArgs, + options?: IGenerateProofsOptions, + ): Promise { + try { + const maciContract = await this.deployment.getContract({ + name: EContracts.MACI, + address: maciContractAddress, + }); + + const [signer, pollAddress] = await Promise.all([this.deployment.getDeployer(), maciContract.polls(poll)]); + + if (pollAddress.toLowerCase() === ZeroAddress.toLowerCase()) { + this.logger.error(`Error: ${ErrorCodes.POLL_NOT_FOUND}, Poll ${poll} not found`); + throw new Error(ErrorCodes.POLL_NOT_FOUND); + } + + const pollContract = await this.deployment.getContract({ name: EContracts.Poll, address: pollAddress }); + const [{ messageAq: messageAqAddress }, coordinatorPublicKey, isStateAqMerged, messageTreeDepth] = + await Promise.all([ + pollContract.extContracts(), + pollContract.coordinatorPubKey(), + pollContract.stateMerged(), + pollContract.treeDepths().then((depths) => Number(depths[2])), + ]); + const messageAq = await this.deployment.getContract({ + name: EContracts.AccQueue, + address: messageAqAddress, + }); + + if (!isStateAqMerged) { + this.logger.error(`Error: ${ErrorCodes.NOT_MERGED_STATE_TREE}, state tree is not merged`); + throw new Error(ErrorCodes.NOT_MERGED_STATE_TREE); + } + + const mainRoot = await messageAq.getMainRoot(messageTreeDepth.toString()); + + if (mainRoot.toString() === "0") { + this.logger.error(`Error: ${ErrorCodes.NOT_MERGED_MESSAGE_TREE}, message tree is not merged`); + throw new Error(ErrorCodes.NOT_MERGED_MESSAGE_TREE); + } + + const { privateKey } = await this.fileService.getPrivateKey(); + const maciPrivateKey = PrivKey.deserialize( + this.cryptoService.decrypt(privateKey, encryptedCoordinatorPrivateKey), + ); + const coordinatorKeypair = new Keypair(maciPrivateKey); + const publicKey = new PubKey([ + BigInt(coordinatorPublicKey.x.toString()), + BigInt(coordinatorPublicKey.y.toString()), + ]); - return { - processProofs, - tallyProofs, - tallyData, - }; + if (!coordinatorKeypair.pubKey.equals(publicKey)) { + this.logger.error(`Error: ${ErrorCodes.PRIVATE_KEY_MISMATCH}, wrong private key`); + throw new Error(ErrorCodes.PRIVATE_KEY_MISMATCH); + } + + const outputDir = path.resolve("./proofs"); + + const maciState = await ProofGenerator.prepareState({ + maciContract, + pollContract, + messageAq, + maciPrivateKey, + coordinatorKeypair, + pollId: poll, + signer, + outputDir, + options: { + startBlock, + endBlock, + blocksPerBatch, + }, + }); + + const foundPoll = maciState.polls.get(BigInt(poll)); + + if (!foundPoll) { + this.logger.error(`Error: ${ErrorCodes.POLL_NOT_FOUND}, Poll ${poll} not found in maci state`); + throw new Error(ErrorCodes.POLL_NOT_FOUND); + } + + const proofGenerator = new ProofGenerator({ + poll: foundPoll, + maciContractAddress, + tallyContractAddress, + tally: this.fileService.getZkeyFilePaths(process.env.COORDINATOR_TALLY_ZKEY_NAME!, useQuadraticVoting), + mp: this.fileService.getZkeyFilePaths(process.env.COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME!, useQuadraticVoting), + rapidsnark: process.env.COORDINATOR_RAPIDSNARK_EXE, + outputDir, + tallyOutputFile: path.resolve("./tally.json"), + useQuadraticVoting, + }); + + const processProofs = await proofGenerator.generateMpProofs(options); + const { proofs: tallyProofs, tallyData } = await proofGenerator.generateTallyProofs(hre.network, options); + + return { + processProofs, + tallyProofs, + tallyData, + }; + } catch (error) { + options?.onFail?.(error as Error); + throw error; + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbc8215d48..1b293f1839 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,16 +331,22 @@ importers: version: 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.3.9 - version: 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': specifier: ^10.3.8 version: 10.3.9(@nestjs/common@10.3.9)(@nestjs/core@10.3.9) + '@nestjs/platform-socket.io': + specifier: ^10.3.10 + version: 10.3.10(@nestjs/common@10.3.9)(@nestjs/websockets@10.3.10)(rxjs@7.8.1) '@nestjs/swagger': specifier: ^7.3.1 version: 7.3.1(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/throttler': specifier: ^5.2.0 version: 5.2.0(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(reflect-metadata@0.2.2) + '@nestjs/websockets': + specifier: ^10.3.10 + version: 10.3.10(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(@nestjs/platform-socket.io@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nomicfoundation/hardhat-ethers': specifier: ^3.0.6 version: 3.0.6(ethers@6.13.1)(hardhat@2.22.5) @@ -383,6 +389,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + socket.io: + specifier: ^4.7.5 + version: 4.7.5 tar: specifier: ^7.4.0 version: 7.4.0 @@ -414,6 +423,9 @@ importers: jest: specifier: ^29.5.0 version: 29.7.0(@types/node@20.14.8)(ts-node@10.9.2) + socket.io-client: + specifier: ^4.7.5 + version: 4.7.5 supertest: specifier: ^7.0.0 version: 7.0.0 @@ -2301,6 +2313,7 @@ packages: /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + requiresBuild: true /@commander-js/extra-typings@12.1.0(commander@12.1.0): resolution: {integrity: sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==} @@ -4533,7 +4546,7 @@ packages: tslib: 2.6.2 uid: 2.0.2 - /@nestjs/core@10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1): + /@nestjs/core@10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1): resolution: {integrity: sha512-NzZUfWAmaf8sqhhwoRA+CuqxQe+P4Rz8PZp5U7CdCbjyeB9ZVGcBkihcJC9wMdtiOWHRndB2J8zRfs5w06jK3w==} requiresBuild: true peerDependencies: @@ -4553,6 +4566,7 @@ packages: dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': 10.3.9(@nestjs/common@10.3.9)(@nestjs/core@10.3.9) + '@nestjs/websockets': 10.3.10(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(@nestjs/platform-socket.io@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -4590,7 +4604,7 @@ packages: '@nestjs/core': ^10.0.0 dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.19.2 @@ -4599,6 +4613,23 @@ packages: transitivePeerDependencies: - supports-color + /@nestjs/platform-socket.io@10.3.10(@nestjs/common@10.3.9)(@nestjs/websockets@10.3.10)(rxjs@7.8.1): + resolution: {integrity: sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.10(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(@nestjs/platform-socket.io@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) + rxjs: 7.8.1 + socket.io: 4.7.5 + tslib: 2.6.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /@nestjs/schematics@10.1.1(chokidar@3.6.0)(typescript@5.3.3): resolution: {integrity: sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig==} peerDependencies: @@ -4648,7 +4679,7 @@ packages: dependencies: '@microsoft/tsdoc': 0.14.2 '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.9)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) class-transformer: 0.5.1 class-validator: 0.14.1 @@ -4673,7 +4704,7 @@ packages: optional: true dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/platform-express': 10.3.9(@nestjs/common@10.3.9)(@nestjs/core@10.3.9) tslib: 2.6.2 dev: true @@ -4686,10 +4717,31 @@ packages: reflect-metadata: ^0.1.13 || ^0.2.0 dependencies: '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 dev: false + /@nestjs/websockets@10.3.10(@nestjs/common@10.3.9)(@nestjs/core@10.3.9)(@nestjs/platform-socket.io@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1): + resolution: {integrity: sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + dependencies: + '@nestjs/common': 10.3.9(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.9(@nestjs/common@10.3.9)(@nestjs/platform-express@10.3.9)(@nestjs/websockets@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/platform-socket.io': 10.3.10(@nestjs/common@10.3.9)(@nestjs/websockets@10.3.10)(rxjs@7.8.1) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.6.3 + /@noble/curves@1.2.0: resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} dependencies: @@ -6069,6 +6121,9 @@ packages: micromark-util-symbol: 1.1.0 dev: false + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + /@solidity-parser/parser@0.14.5: resolution: {integrity: sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==} dependencies: @@ -6410,10 +6465,18 @@ packages: '@types/node': 20.14.8 dev: true + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + /@types/cookiejar@2.1.5: resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.14.8 + /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} dependencies: @@ -7843,6 +7906,10 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + /batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} dev: false @@ -10175,6 +10242,43 @@ packages: dependencies: once: 1.4.0 + /engine.io-client@6.5.4: + resolution: {integrity: sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5(supports-color@8.1.1) + engine.io-parser: 5.2.2 + ws: 8.17.1 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + /engine.io@6.5.5: + resolution: {integrity: sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.14.8 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.5(supports-color@8.1.1) + engine.io-parser: 5.2.2 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /enhanced-resolve@5.17.0: resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} engines: {node: '>=10.13.0'} @@ -16534,6 +16638,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -19144,6 +19252,55 @@ packages: logplease: 1.2.15 r1csfile: 0.0.48 + /socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + dependencies: + debug: 4.3.5(supports-color@8.1.1) + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /socket.io-client@4.7.5: + resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5(supports-color@8.1.1) + engine.io-client: 6.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + /socket.io@4.7.5: + resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.5(supports-color@8.1.1) + engine.io: 6.5.5 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + /sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} dependencies: @@ -21462,7 +21619,6 @@ packages: optional: true utf-8-validate: optional: true - dev: false /xdg-basedir@5.1.0: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} @@ -21476,6 +21632,11 @@ packages: sax: 1.4.1 dev: false + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: true + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'}