@devtion/backend 0.0.0-7e983e3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/src/functions/index.js +2644 -0
- package/dist/src/functions/index.mjs +2596 -0
- package/dist/types/functions/ceremony.d.ts +33 -0
- package/dist/types/functions/ceremony.d.ts.map +1 -0
- package/dist/types/functions/circuit.d.ts +63 -0
- package/dist/types/functions/circuit.d.ts.map +1 -0
- package/dist/types/functions/index.d.ts +7 -0
- package/dist/types/functions/index.d.ts.map +1 -0
- package/dist/types/functions/participant.d.ts +58 -0
- package/dist/types/functions/participant.d.ts.map +1 -0
- package/dist/types/functions/storage.d.ts +37 -0
- package/dist/types/functions/storage.d.ts.map +1 -0
- package/dist/types/functions/timeout.d.ts +26 -0
- package/dist/types/functions/timeout.d.ts.map +1 -0
- package/dist/types/functions/user.d.ts +15 -0
- package/dist/types/functions/user.d.ts.map +1 -0
- package/dist/types/lib/errors.d.ts +75 -0
- package/dist/types/lib/errors.d.ts.map +1 -0
- package/dist/types/lib/services.d.ts +9 -0
- package/dist/types/lib/services.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +141 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/types/enums.d.ts +13 -0
- package/dist/types/types/enums.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +130 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +89 -0
- package/src/functions/ceremony.ts +333 -0
- package/src/functions/circuit.ts +1092 -0
- package/src/functions/index.ts +36 -0
- package/src/functions/participant.ts +526 -0
- package/src/functions/storage.ts +548 -0
- package/src/functions/timeout.ts +294 -0
- package/src/functions/user.ts +142 -0
- package/src/lib/errors.ts +237 -0
- package/src/lib/services.ts +28 -0
- package/src/lib/utils.ts +472 -0
- package/src/types/enums.ts +12 -0
- package/src/types/index.ts +140 -0
- package/test/index.test.ts +62 -0
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
import * as functionsV1 from "firebase-functions/v1"
|
|
2
|
+
import * as functionsV2 from "firebase-functions/v2"
|
|
3
|
+
import admin from "firebase-admin"
|
|
4
|
+
import dotenv from "dotenv"
|
|
5
|
+
import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore"
|
|
6
|
+
import { Change } from "firebase-functions"
|
|
7
|
+
import fs from "fs"
|
|
8
|
+
import { Timer } from "timer-node"
|
|
9
|
+
import { FieldValue } from "firebase-admin/firestore"
|
|
10
|
+
import {
|
|
11
|
+
commonTerms,
|
|
12
|
+
getParticipantsCollectionPath,
|
|
13
|
+
getCircuitsCollectionPath,
|
|
14
|
+
getZkeyStorageFilePath,
|
|
15
|
+
getContributionsCollectionPath,
|
|
16
|
+
formatZkeyIndex,
|
|
17
|
+
getTranscriptStorageFilePath,
|
|
18
|
+
getVerificationKeyStorageFilePath,
|
|
19
|
+
getVerifierContractStorageFilePath,
|
|
20
|
+
finalContributionIndex,
|
|
21
|
+
verificationKeyAcronym,
|
|
22
|
+
verifierSmartContractAcronym,
|
|
23
|
+
computeSHA256ToHex,
|
|
24
|
+
ParticipantStatus,
|
|
25
|
+
ParticipantContributionStep,
|
|
26
|
+
CeremonyState,
|
|
27
|
+
Contribution,
|
|
28
|
+
blake512FromPath,
|
|
29
|
+
startEC2Instance,
|
|
30
|
+
retrieveCommandOutput,
|
|
31
|
+
runCommandUsingSSM,
|
|
32
|
+
CircuitContributionVerificationMechanism,
|
|
33
|
+
checkIfRunning,
|
|
34
|
+
vmContributionVerificationCommand,
|
|
35
|
+
getPotStorageFilePath,
|
|
36
|
+
genesisZkeyIndex,
|
|
37
|
+
createCustomLoggerForFile,
|
|
38
|
+
retrieveCommandStatus,
|
|
39
|
+
stopEC2Instance
|
|
40
|
+
} from "@p0tion/actions"
|
|
41
|
+
import { zKey } from "snarkjs"
|
|
42
|
+
import { CommandInvocationStatus, SSMClient } from "@aws-sdk/client-ssm"
|
|
43
|
+
import { FinalizeCircuitData, VerifyContributionData } from "../types/index"
|
|
44
|
+
import { LogLevel } from "../types/enums"
|
|
45
|
+
import { COMMON_ERRORS, logAndThrowError, makeError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
|
|
46
|
+
import {
|
|
47
|
+
createEC2Client,
|
|
48
|
+
createSSMClient,
|
|
49
|
+
createTemporaryLocalPath,
|
|
50
|
+
deleteObject,
|
|
51
|
+
downloadArtifactFromS3Bucket,
|
|
52
|
+
getCeremonyCircuits,
|
|
53
|
+
getCircuitDocumentByPosition,
|
|
54
|
+
getCurrentServerTimestampInMillis,
|
|
55
|
+
getDocumentById,
|
|
56
|
+
getFinalContribution,
|
|
57
|
+
sleep,
|
|
58
|
+
uploadFileToBucket
|
|
59
|
+
} from "../lib/utils"
|
|
60
|
+
import { EC2Client } from "@aws-sdk/client-ec2"
|
|
61
|
+
|
|
62
|
+
dotenv.config()
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute the coordination of the participant for the given circuit.
|
|
66
|
+
* @dev possible coordination scenarios:
|
|
67
|
+
* A) The participant becomes the current contributor of circuit X (single participant).
|
|
68
|
+
* B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
|
|
69
|
+
* C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
|
|
70
|
+
* C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
|
|
71
|
+
* @param participant <QueryDocumentSnapshot> - the Firestore document of the participant.
|
|
72
|
+
* @param circuit <QueryDocumentSnapshot> - the Firestore document of the circuit.
|
|
73
|
+
* @param isSingleParticipantCoordination <boolean> - true if the coordination involves only a single participant; otherwise false (= involves multiple participant).
|
|
74
|
+
* @param [ceremonyId] <string> - the unique identifier of the ceremony (needed only for multi-participant coordination).
|
|
75
|
+
*/
|
|
76
|
+
const coordinate = async (
|
|
77
|
+
participant: QueryDocumentSnapshot,
|
|
78
|
+
circuit: QueryDocumentSnapshot,
|
|
79
|
+
isSingleParticipantCoordination: boolean,
|
|
80
|
+
ceremonyId?: string
|
|
81
|
+
) => {
|
|
82
|
+
// Prepare db and transactions batch.
|
|
83
|
+
const firestore = admin.firestore()
|
|
84
|
+
const batch = firestore.batch()
|
|
85
|
+
|
|
86
|
+
// Extract data.
|
|
87
|
+
const { status, contributionStep } = participant.data()
|
|
88
|
+
const { waitingQueue } = circuit.data()
|
|
89
|
+
const { contributors, currentContributor } = waitingQueue
|
|
90
|
+
|
|
91
|
+
// Prepare state updates for waiting queue.
|
|
92
|
+
const newContributors: Array<string> = contributors
|
|
93
|
+
let newCurrentContributorId: string = ""
|
|
94
|
+
|
|
95
|
+
// Prepare state updates for participant.
|
|
96
|
+
let newParticipantStatus: string = ""
|
|
97
|
+
let newContributionStep: string = ""
|
|
98
|
+
|
|
99
|
+
// Prepare pre-conditions.
|
|
100
|
+
const noCurrentContributor = !currentContributor
|
|
101
|
+
const noContributorsInWaitingQueue = !contributors.length
|
|
102
|
+
const emptyWaitingQueue = noCurrentContributor && noContributorsInWaitingQueue
|
|
103
|
+
|
|
104
|
+
const participantIsNotCurrentContributor = currentContributor !== participant.id
|
|
105
|
+
const participantIsCurrentContributor = currentContributor === participant.id
|
|
106
|
+
const participantIsReady = status === ParticipantStatus.READY
|
|
107
|
+
const participantResumingAfterTimeoutExpiration = participantIsCurrentContributor && participantIsReady
|
|
108
|
+
|
|
109
|
+
const participantCompletedOneOrAllContributions =
|
|
110
|
+
(status === ParticipantStatus.CONTRIBUTED || status === ParticipantStatus.DONE) &&
|
|
111
|
+
contributionStep === ParticipantContributionStep.COMPLETED
|
|
112
|
+
|
|
113
|
+
// Check for scenarios.
|
|
114
|
+
if (isSingleParticipantCoordination) {
|
|
115
|
+
// Scenario (A).
|
|
116
|
+
if (emptyWaitingQueue) {
|
|
117
|
+
printLog(`Coordinate - executing scenario A - emptyWaitingQueue`, LogLevel.DEBUG)
|
|
118
|
+
|
|
119
|
+
// Update.
|
|
120
|
+
newCurrentContributorId = participant.id
|
|
121
|
+
newParticipantStatus = ParticipantStatus.CONTRIBUTING
|
|
122
|
+
newContributionStep = ParticipantContributionStep.DOWNLOADING
|
|
123
|
+
newContributors.push(newCurrentContributorId)
|
|
124
|
+
}
|
|
125
|
+
// Scenario (A).
|
|
126
|
+
else if (participantResumingAfterTimeoutExpiration) {
|
|
127
|
+
printLog(
|
|
128
|
+
`Coordinate - executing scenario A - single - participantResumingAfterTimeoutExpiration`,
|
|
129
|
+
LogLevel.DEBUG
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
newParticipantStatus = ParticipantStatus.CONTRIBUTING
|
|
133
|
+
newContributionStep = ParticipantContributionStep.DOWNLOADING
|
|
134
|
+
}
|
|
135
|
+
// Scenario (B).
|
|
136
|
+
else if (participantIsNotCurrentContributor) {
|
|
137
|
+
printLog(`Coordinate - executing scenario B - single - participantIsNotCurrentContributor`, LogLevel.DEBUG)
|
|
138
|
+
|
|
139
|
+
newCurrentContributorId = currentContributor
|
|
140
|
+
newParticipantStatus = ParticipantStatus.WAITING
|
|
141
|
+
newContributors.push(participant.id)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Prepare tx - Scenario (A) only.
|
|
145
|
+
if (newContributionStep)
|
|
146
|
+
batch.update(participant.ref, {
|
|
147
|
+
contributionStep: newContributionStep,
|
|
148
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Prepare tx - Scenario (A) or (B).
|
|
152
|
+
batch.update(participant.ref, {
|
|
153
|
+
status: newParticipantStatus,
|
|
154
|
+
contributionStartedAt:
|
|
155
|
+
newParticipantStatus === ParticipantStatus.CONTRIBUTING ? getCurrentServerTimestampInMillis() : 0,
|
|
156
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
157
|
+
})
|
|
158
|
+
} else if (participantIsCurrentContributor && participantCompletedOneOrAllContributions && !!ceremonyId) {
|
|
159
|
+
printLog(
|
|
160
|
+
`Coordinate - executing scenario C - multi - participantIsCurrentContributor && participantCompletedOneOrAllContributions`,
|
|
161
|
+
LogLevel.DEBUG
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
newParticipantStatus = ParticipantStatus.CONTRIBUTING
|
|
165
|
+
newContributionStep = ParticipantContributionStep.DOWNLOADING
|
|
166
|
+
|
|
167
|
+
// Remove from waiting queue of circuit X.
|
|
168
|
+
newContributors.shift()
|
|
169
|
+
|
|
170
|
+
// Step (C.1).
|
|
171
|
+
if (newContributors.length > 0) {
|
|
172
|
+
// Get new contributor for circuit X.
|
|
173
|
+
newCurrentContributorId = newContributors.at(0)!
|
|
174
|
+
|
|
175
|
+
// Pass the baton to the new contributor.
|
|
176
|
+
const newCurrentContributorDocument = await getDocumentById(
|
|
177
|
+
getParticipantsCollectionPath(ceremonyId),
|
|
178
|
+
newCurrentContributorId
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Prepare update tx.
|
|
182
|
+
batch.update(newCurrentContributorDocument.ref, {
|
|
183
|
+
status: newParticipantStatus,
|
|
184
|
+
contributionStep: newContributionStep,
|
|
185
|
+
contributionStartedAt: getCurrentServerTimestampInMillis(),
|
|
186
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
printLog(
|
|
190
|
+
`Participant ${newCurrentContributorId} is the new current contributor for circuit ${circuit.id}`,
|
|
191
|
+
LogLevel.DEBUG
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Prepare tx - must be done for all Scenarios.
|
|
197
|
+
batch.update(circuit.ref, {
|
|
198
|
+
waitingQueue: {
|
|
199
|
+
...waitingQueue,
|
|
200
|
+
contributors: newContributors,
|
|
201
|
+
currentContributor: newCurrentContributorId
|
|
202
|
+
},
|
|
203
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Send txs.
|
|
207
|
+
await batch.commit()
|
|
208
|
+
|
|
209
|
+
printLog(`Coordinate successfully completed`, LogLevel.DEBUG)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wait until the command has completed its execution inside the VM.
|
|
214
|
+
* @dev this method implements a custom interval to check 5 times after 1 minute if the command execution
|
|
215
|
+
* has been completed or not by calling the `retrieveCommandStatus` method.
|
|
216
|
+
* @param {any} resolve the promise.
|
|
217
|
+
* @param {any} reject the promise.
|
|
218
|
+
* @param {SSMClient} ssm the SSM client.
|
|
219
|
+
* @param {string} vmInstanceId the unique identifier of the VM instance.
|
|
220
|
+
* @param {string} commandId the unique identifier of the VM command.
|
|
221
|
+
* @returns <Promise<void>> true when the command execution succeed; otherwise false.
|
|
222
|
+
*/
|
|
223
|
+
const waitForVMCommandExecution = (
|
|
224
|
+
resolve: any,
|
|
225
|
+
reject: any,
|
|
226
|
+
ssm: SSMClient,
|
|
227
|
+
vmInstanceId: string,
|
|
228
|
+
commandId: string
|
|
229
|
+
) => {
|
|
230
|
+
const interval = setInterval(async () => {
|
|
231
|
+
try {
|
|
232
|
+
// Get command status.
|
|
233
|
+
const cmdStatus = await retrieveCommandStatus(ssm, vmInstanceId, commandId)
|
|
234
|
+
printLog(`Checking command ${commandId} status => ${cmdStatus}`, LogLevel.DEBUG)
|
|
235
|
+
|
|
236
|
+
if (cmdStatus === CommandInvocationStatus.SUCCESS) {
|
|
237
|
+
printLog(`Command ${commandId} successfully completed`, LogLevel.DEBUG)
|
|
238
|
+
|
|
239
|
+
// Resolve the promise.
|
|
240
|
+
resolve()
|
|
241
|
+
} else if (cmdStatus === CommandInvocationStatus.FAILED) {
|
|
242
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_VM_FAILED_COMMAND_EXECUTION)
|
|
243
|
+
reject()
|
|
244
|
+
} else if (cmdStatus === CommandInvocationStatus.TIMED_OUT) {
|
|
245
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_VM_TIMEDOUT_COMMAND_EXECUTION)
|
|
246
|
+
reject()
|
|
247
|
+
} else if (cmdStatus === CommandInvocationStatus.CANCELLED) {
|
|
248
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_VM_CANCELLED_COMMAND_EXECUTION)
|
|
249
|
+
reject()
|
|
250
|
+
} else if (cmdStatus === CommandInvocationStatus.DELAYED) {
|
|
251
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_VM_DELAYED_COMMAND_EXECUTION)
|
|
252
|
+
reject()
|
|
253
|
+
}
|
|
254
|
+
} catch (error: any) {
|
|
255
|
+
printLog(`Invalid command ${commandId} execution`, LogLevel.DEBUG)
|
|
256
|
+
|
|
257
|
+
if (!error.toString().includes(commandId)) logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION)
|
|
258
|
+
|
|
259
|
+
// Reject the promise.
|
|
260
|
+
reject()
|
|
261
|
+
} finally {
|
|
262
|
+
// Clear the interval.
|
|
263
|
+
clearInterval(interval)
|
|
264
|
+
}
|
|
265
|
+
}, 60000) // 1 minute.
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Wait until the artifacts have been downloaded.
|
|
270
|
+
* @param {any} resolve the promise.
|
|
271
|
+
* @param {any} reject the promise.
|
|
272
|
+
* @param {string} potTempFilePath the tmp path to the locally downloaded pot file.
|
|
273
|
+
* @param {string} firstZkeyTempFilePath the tmp path to the locally downloaded first zkey file.
|
|
274
|
+
* @param {string} lastZkeyTempFilePath the tmp path to the locally downloaded last zkey file.
|
|
275
|
+
*/
|
|
276
|
+
const waitForFileDownload = (
|
|
277
|
+
resolve: any,
|
|
278
|
+
reject: any,
|
|
279
|
+
potTempFilePath: string,
|
|
280
|
+
firstZkeyTempFilePath: string,
|
|
281
|
+
lastZkeyTempFilePath: string,
|
|
282
|
+
circuitId: string,
|
|
283
|
+
participantId: string
|
|
284
|
+
) => {
|
|
285
|
+
const maxWaitTime = 5 * 60 * 1000 // 5 minutes
|
|
286
|
+
// every second check if the file download was completed
|
|
287
|
+
const interval = setInterval(async () => {
|
|
288
|
+
printLog(`Verifying that the artifacts were downloaded for circuit ${circuitId} and participant ${participantId}`, LogLevel.DEBUG)
|
|
289
|
+
try {
|
|
290
|
+
// check if files have been downloaded
|
|
291
|
+
if (!fs.existsSync(potTempFilePath)) {
|
|
292
|
+
printLog(`Pot file not found at ${potTempFilePath}`, LogLevel.DEBUG)
|
|
293
|
+
}
|
|
294
|
+
if (!fs.existsSync(firstZkeyTempFilePath)) {
|
|
295
|
+
printLog(`First zkey file not found at ${firstZkeyTempFilePath}`, LogLevel.DEBUG)
|
|
296
|
+
}
|
|
297
|
+
if (!fs.existsSync(lastZkeyTempFilePath)) {
|
|
298
|
+
printLog(`Last zkey file not found at ${lastZkeyTempFilePath}`, LogLevel.DEBUG)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// if all files were downloaded
|
|
302
|
+
if (fs.existsSync(potTempFilePath) && fs.existsSync(firstZkeyTempFilePath) && fs.existsSync(lastZkeyTempFilePath)) {
|
|
303
|
+
printLog(`All required files are present on disk.`, LogLevel.INFO)
|
|
304
|
+
// resolve the promise
|
|
305
|
+
resolve()
|
|
306
|
+
}
|
|
307
|
+
} catch (error: any) {
|
|
308
|
+
// if we have an error then we print it as a warning and reject
|
|
309
|
+
printLog(`Error while downloading files: ${error}`, LogLevel.WARN)
|
|
310
|
+
reject()
|
|
311
|
+
} finally {
|
|
312
|
+
printLog(`Clearing the interval for file download. Circuit ${circuitId} and participant ${participantId}`, LogLevel.DEBUG)
|
|
313
|
+
clearInterval(interval)
|
|
314
|
+
}
|
|
315
|
+
}, 5000)
|
|
316
|
+
|
|
317
|
+
// we want to clean in 5 minutes in case
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
clearInterval(interval)
|
|
320
|
+
reject(new Error('Timeout exceeded while waiting for files to be downloaded.'))
|
|
321
|
+
}, maxWaitTime)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* This method is used to coordinate the waiting queues of ceremony circuits.
|
|
326
|
+
* @dev this cloud function is triggered whenever an update of a document related to a participant of a ceremony occurs.
|
|
327
|
+
* The function verifies that such update is preparatory towards a waiting queue update for one or more circuits in the ceremony.
|
|
328
|
+
* If that's the case, this cloud functions proceeds with the "coordination" of the waiting queues, leading to three different scenarios:
|
|
329
|
+
* A) The participant becomes the current contributor of circuit X (single participant).
|
|
330
|
+
* B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
|
|
331
|
+
* C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
|
|
332
|
+
* C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
|
|
333
|
+
* Before triggering the above scenarios, the cloud functions verifies that suitable pre-conditions are met.
|
|
334
|
+
* @notice The cloud function performs the subsequent steps:
|
|
335
|
+
* 0) Prepares the participant's previous and current data (after/before document change).
|
|
336
|
+
* 1) Retrieve the ceremony from the participant's document path.
|
|
337
|
+
* 2) Verifies that the participant has changed to a state for which it is ready for contribution.
|
|
338
|
+
* 2.A) If ready, verifies whether the participant is ready to:
|
|
339
|
+
* - Contribute for the first time or for the next circuit (other than the first) or contribute after a timeout has expired. If yes, coordinate (single participant scenario).
|
|
340
|
+
* 2.B) Otherwise, check whether the participant has:
|
|
341
|
+
* - Just completed a contribution or all contributions for each circuit. If yes, coordinate (multi-participant scenario).
|
|
342
|
+
*/
|
|
343
|
+
export const coordinateCeremonyParticipant = functionsV1
|
|
344
|
+
.region('europe-west1')
|
|
345
|
+
.runWith({
|
|
346
|
+
memory: "512MB"
|
|
347
|
+
})
|
|
348
|
+
.firestore.document(
|
|
349
|
+
`${commonTerms.collections.ceremonies.name}/{ceremonyId}/${commonTerms.collections.participants.name}/{participantId}`
|
|
350
|
+
)
|
|
351
|
+
.onUpdate(async (participantChanges: Change<QueryDocumentSnapshot>) => {
|
|
352
|
+
// Step (0).
|
|
353
|
+
const exParticipant = participantChanges.before
|
|
354
|
+
const changedParticipant = participantChanges.after
|
|
355
|
+
|
|
356
|
+
if (!exParticipant.data() || !changedParticipant.data())
|
|
357
|
+
logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
358
|
+
|
|
359
|
+
// Step (1).
|
|
360
|
+
const ceremonyId = exParticipant.ref.parent.parent!.path.replace(
|
|
361
|
+
`${commonTerms.collections.ceremonies.name}/`,
|
|
362
|
+
""
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if (!ceremonyId) logAndThrowError(COMMON_ERRORS.CM_INVALID_CEREMONY_FOR_PARTICIPANT)
|
|
366
|
+
|
|
367
|
+
// Extract data.
|
|
368
|
+
const {
|
|
369
|
+
contributionProgress: prevContributionProgress,
|
|
370
|
+
status: prevStatus,
|
|
371
|
+
contributionStep: prevContributionStep
|
|
372
|
+
} = exParticipant.data()!
|
|
373
|
+
|
|
374
|
+
const {
|
|
375
|
+
contributionProgress: changedContributionProgress,
|
|
376
|
+
status: changedStatus,
|
|
377
|
+
contributionStep: changedContributionStep
|
|
378
|
+
} = changedParticipant.data()!
|
|
379
|
+
|
|
380
|
+
printLog(`Coordinate participant ${exParticipant.id} for ceremony ${ceremonyId}`, LogLevel.DEBUG)
|
|
381
|
+
printLog(
|
|
382
|
+
`Participant status: ${prevStatus} => ${changedStatus} - Participant contribution step: ${prevContributionStep} => ${changedContributionStep}`,
|
|
383
|
+
LogLevel.DEBUG
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// Define pre-conditions.
|
|
387
|
+
const participantReadyToContribute = changedStatus === ParticipantStatus.READY
|
|
388
|
+
|
|
389
|
+
const participantReadyForFirstContribution = participantReadyToContribute && prevContributionProgress === 0
|
|
390
|
+
|
|
391
|
+
const participantResumingContributionAfterTimeout =
|
|
392
|
+
participantReadyToContribute && prevContributionProgress === changedContributionProgress
|
|
393
|
+
|
|
394
|
+
const participantReadyForNextContribution =
|
|
395
|
+
participantReadyToContribute &&
|
|
396
|
+
prevContributionProgress === changedContributionProgress - 1 &&
|
|
397
|
+
prevContributionProgress !== 0
|
|
398
|
+
|
|
399
|
+
const participantCompletedEveryCircuitContribution =
|
|
400
|
+
changedStatus === ParticipantStatus.DONE && prevStatus !== ParticipantStatus.DONE
|
|
401
|
+
|
|
402
|
+
const participantCompletedContribution =
|
|
403
|
+
prevContributionProgress === changedContributionProgress &&
|
|
404
|
+
prevStatus === ParticipantStatus.CONTRIBUTING &&
|
|
405
|
+
prevContributionStep === ParticipantContributionStep.VERIFYING &&
|
|
406
|
+
changedStatus === ParticipantStatus.CONTRIBUTED &&
|
|
407
|
+
changedContributionStep === ParticipantContributionStep.COMPLETED
|
|
408
|
+
|
|
409
|
+
// Step (2).
|
|
410
|
+
if (
|
|
411
|
+
participantReadyForFirstContribution ||
|
|
412
|
+
participantResumingContributionAfterTimeout ||
|
|
413
|
+
participantReadyForNextContribution
|
|
414
|
+
) {
|
|
415
|
+
// Step (2.A).
|
|
416
|
+
printLog(
|
|
417
|
+
`Participant is ready for first contribution (${participantReadyForFirstContribution}) or for the next contribution (${participantReadyForNextContribution}) or is resuming after a timeout expiration (${participantResumingContributionAfterTimeout})`,
|
|
418
|
+
LogLevel.DEBUG
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
// Get the circuit.
|
|
422
|
+
const circuit = await getCircuitDocumentByPosition(ceremonyId, changedContributionProgress)
|
|
423
|
+
|
|
424
|
+
// Coordinate.
|
|
425
|
+
await coordinate(changedParticipant, circuit, true)
|
|
426
|
+
|
|
427
|
+
printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG)
|
|
428
|
+
} else if (participantCompletedContribution || participantCompletedEveryCircuitContribution) {
|
|
429
|
+
// Step (2.B).
|
|
430
|
+
printLog(
|
|
431
|
+
`Participant completed a contribution (${participantCompletedContribution}) or every contribution for each circuit (${participantCompletedEveryCircuitContribution})`,
|
|
432
|
+
LogLevel.DEBUG
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
// Get the circuit.
|
|
436
|
+
const circuit = await getCircuitDocumentByPosition(ceremonyId, prevContributionProgress)
|
|
437
|
+
|
|
438
|
+
// Coordinate.
|
|
439
|
+
await coordinate(changedParticipant, circuit, false, ceremonyId)
|
|
440
|
+
|
|
441
|
+
printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG)
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Recursive function to check whether an EC2 is in a running state
|
|
448
|
+
* @notice required step to run commands
|
|
449
|
+
* @param ec2 <EC2Client> - the EC2Client object
|
|
450
|
+
* @param vmInstanceId <string> - the instance Id
|
|
451
|
+
* @param attempts <number> - how many times to retry before failing
|
|
452
|
+
* @returns <Promise<boolean>> - whether the VM was started
|
|
453
|
+
*/
|
|
454
|
+
const checkIfVMRunning = async (
|
|
455
|
+
ec2: EC2Client,
|
|
456
|
+
vmInstanceId: string,
|
|
457
|
+
attempts = 5
|
|
458
|
+
): Promise<boolean> => {
|
|
459
|
+
// if we tried 5 times, then throw an error
|
|
460
|
+
if (attempts <= 0) logAndThrowError(SPECIFIC_ERRORS.SE_VM_NOT_RUNNING)
|
|
461
|
+
|
|
462
|
+
await sleep(60000); // Wait for 1 min
|
|
463
|
+
const isVMRunning = await checkIfRunning(ec2, vmInstanceId)
|
|
464
|
+
|
|
465
|
+
if (!isVMRunning) {
|
|
466
|
+
printLog(`VM not running, ${attempts - 1} attempts remaining. Retrying in 1 minute...`, LogLevel.DEBUG)
|
|
467
|
+
return await checkIfVMRunning(ec2, vmInstanceId, attempts - 1)
|
|
468
|
+
} else {
|
|
469
|
+
return true
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Verify the contribution of a participant computed while contributing to a specific circuit of a ceremony.
|
|
475
|
+
* @dev a huge amount of resources (memory, CPU, and execution time) is required for the contribution verification task.
|
|
476
|
+
* For this reason, we are using a V2 Cloud Function (more memory, more CPU, and longer timeout).
|
|
477
|
+
* Through the current configuration (16GiB memory and 4 vCPUs) we are able to support verification of contributions for 3.8M constraints circuit size.
|
|
478
|
+
@notice The cloud function performs the subsequent steps:
|
|
479
|
+
* 0) Prepare documents and extract necessary data.
|
|
480
|
+
* 1) Check if the participant is the current contributor to the circuit or is the ceremony coordinator
|
|
481
|
+
* 1.A) If either condition is true:
|
|
482
|
+
* 1.A.1) Prepare verification transcript logger, storage, and temporary paths.
|
|
483
|
+
* 1.A.2) Download necessary AWS S3 ceremony bucket artifacts.
|
|
484
|
+
* 1.A.3) Execute contribution verification.
|
|
485
|
+
* 1.A.3.0) Check if is using VM or CF approach for verification.
|
|
486
|
+
* 1.A.3.1) Start the instance and wait until the instance is up.
|
|
487
|
+
* 1.A.3.2) Prepare and run contribution verification command.
|
|
488
|
+
* 1.A.3.3) Wait until command complete.
|
|
489
|
+
* 1.A.4) Check contribution validity:
|
|
490
|
+
* 1.A.4.A) If valid:
|
|
491
|
+
* 1.A.4.A.1) Upload verification transcript to AWS S3 storage.
|
|
492
|
+
* 1.A.4.A.2) Creates a new valid contribution document on Firestore.
|
|
493
|
+
* 1.A.4.B) If not valid:
|
|
494
|
+
* 1.A.4.B.1) Creates a new invalid contribution document on Firestore.
|
|
495
|
+
* 1.A.4.C) Check if not finalizing:
|
|
496
|
+
* 1.A.4.C.1) If true, update circuit waiting for queue and average timings accordingly to contribution verification results;
|
|
497
|
+
* 2) Send all updates atomically to the Firestore database.
|
|
498
|
+
*/
|
|
499
|
+
export const verifycontribution = functionsV2.https.onCall(
|
|
500
|
+
{ memory: "16GiB", timeoutSeconds: 3600, region: 'europe-west1' },
|
|
501
|
+
async (request: functionsV2.https.CallableRequest<VerifyContributionData>): Promise<any> => {
|
|
502
|
+
if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator))
|
|
503
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
|
|
504
|
+
|
|
505
|
+
if (
|
|
506
|
+
!request.data.ceremonyId ||
|
|
507
|
+
!request.data.circuitId ||
|
|
508
|
+
!request.data.contributorOrCoordinatorIdentifier ||
|
|
509
|
+
!request.data.bucketName
|
|
510
|
+
)
|
|
511
|
+
logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
512
|
+
|
|
513
|
+
if (
|
|
514
|
+
!process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME ||
|
|
515
|
+
!process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION ||
|
|
516
|
+
!process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH
|
|
517
|
+
)
|
|
518
|
+
logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION)
|
|
519
|
+
|
|
520
|
+
// Step (0).
|
|
521
|
+
|
|
522
|
+
// Prepare and start timer.
|
|
523
|
+
const verifyContributionTimer = new Timer({ label: commonTerms.cloudFunctionsNames.verifyContribution })
|
|
524
|
+
verifyContributionTimer.start()
|
|
525
|
+
|
|
526
|
+
// Get DB.
|
|
527
|
+
const firestore = admin.firestore()
|
|
528
|
+
// Prepare batch of txs.
|
|
529
|
+
const batch = firestore.batch()
|
|
530
|
+
|
|
531
|
+
// Extract data.
|
|
532
|
+
const { ceremonyId, circuitId, contributorOrCoordinatorIdentifier, bucketName } = request.data
|
|
533
|
+
const userId = request.auth?.uid
|
|
534
|
+
|
|
535
|
+
// Look for the ceremony, circuit and participant document.
|
|
536
|
+
const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
|
|
537
|
+
const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId)
|
|
538
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
|
|
539
|
+
|
|
540
|
+
if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data())
|
|
541
|
+
logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
542
|
+
|
|
543
|
+
// Extract documents data.
|
|
544
|
+
const { state } = ceremonyDoc.data()!
|
|
545
|
+
const { status, contributions, verificationStartedAt, contributionStartedAt } = participantDoc.data()!
|
|
546
|
+
const { waitingQueue, prefix, avgTimings, verification, files } = circuitDoc.data()!
|
|
547
|
+
const { completedContributions, failedContributions } = waitingQueue
|
|
548
|
+
const {
|
|
549
|
+
contributionComputation: avgContributionComputationTime,
|
|
550
|
+
fullContribution: avgFullContributionTime,
|
|
551
|
+
verifyCloudFunction: avgVerifyCloudFunctionTime
|
|
552
|
+
} = avgTimings
|
|
553
|
+
const { cfOrVm, vm } = verification
|
|
554
|
+
// we might not have it if the circuit is not using VM.
|
|
555
|
+
let vmInstanceId: string = ""
|
|
556
|
+
if (vm) vmInstanceId = vm.vmInstanceId
|
|
557
|
+
|
|
558
|
+
// Define pre-conditions.
|
|
559
|
+
const isFinalizing = state === CeremonyState.CLOSED && request.auth && request.auth.token.coordinator // true only when the coordinator verifies the final contributions.
|
|
560
|
+
const isContributing = status === ParticipantStatus.CONTRIBUTING
|
|
561
|
+
const isUsingVM = cfOrVm === CircuitContributionVerificationMechanism.VM && !!vmInstanceId
|
|
562
|
+
|
|
563
|
+
// Prepare state.
|
|
564
|
+
let isContributionValid = false
|
|
565
|
+
let verifyCloudFunctionExecutionTime = 0 // time spent while executing the verify contribution cloud function.
|
|
566
|
+
let verifyCloudFunctionTime = 0 // time spent while executing the core business logic of this cloud function.
|
|
567
|
+
let fullContributionTime = 0 // time spent while doing non-verification contributions tasks (download, compute, upload).
|
|
568
|
+
let contributionComputationTime = 0 // time spent while computing the contribution.
|
|
569
|
+
let lastZkeyBlake2bHash: string = "" // the Blake2B hash of the last zKey.
|
|
570
|
+
let verificationTranscriptTemporaryLocalPath: string = "" // the local temporary path for the verification transcript.
|
|
571
|
+
let transcriptBlake2bHash: string = "" // the Blake2B hash of the verification transcript.
|
|
572
|
+
let commandId: string = "" // the unique identifier of the VM command.
|
|
573
|
+
|
|
574
|
+
// Derive necessary data.
|
|
575
|
+
const lastZkeyIndex = formatZkeyIndex(completedContributions + 1)
|
|
576
|
+
const verificationTranscriptCompleteFilename = `${prefix}_${
|
|
577
|
+
isFinalizing
|
|
578
|
+
? `${contributorOrCoordinatorIdentifier}_${finalContributionIndex}_verification_transcript.log`
|
|
579
|
+
: `${lastZkeyIndex}_${contributorOrCoordinatorIdentifier}_verification_transcript.log`
|
|
580
|
+
}`
|
|
581
|
+
|
|
582
|
+
const lastZkeyFilename = `${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`
|
|
583
|
+
|
|
584
|
+
// Prepare state for VM verification (if needed).
|
|
585
|
+
const ec2 = await createEC2Client()
|
|
586
|
+
const ssm = await createSSMClient()
|
|
587
|
+
|
|
588
|
+
// Step (1.A.1).
|
|
589
|
+
// Get storage paths.
|
|
590
|
+
const verificationTranscriptStoragePathAndFilename = getTranscriptStorageFilePath(
|
|
591
|
+
prefix,
|
|
592
|
+
verificationTranscriptCompleteFilename
|
|
593
|
+
)
|
|
594
|
+
// the zKey storage path is required to be sent to the VM api
|
|
595
|
+
const lastZkeyStoragePath = getZkeyStorageFilePath(
|
|
596
|
+
prefix,
|
|
597
|
+
`${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
const verificationTaskTimer = new Timer({ label: `${ceremonyId}-${circuitId}-${participantDoc.id}` })
|
|
601
|
+
|
|
602
|
+
const completeVerification = async () => {
|
|
603
|
+
// Stop verification task timer.
|
|
604
|
+
printLog("Completing verification", LogLevel.DEBUG)
|
|
605
|
+
verificationTaskTimer.stop()
|
|
606
|
+
verifyCloudFunctionExecutionTime = verificationTaskTimer.ms()
|
|
607
|
+
|
|
608
|
+
if (isUsingVM) {
|
|
609
|
+
// Create temporary path.
|
|
610
|
+
verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(
|
|
611
|
+
`${circuitId}_${participantDoc.id}.log`
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
await sleep(1000) // wait 1s for file creation.
|
|
615
|
+
|
|
616
|
+
// Download from bucket.
|
|
617
|
+
// nb. the transcript MUST be uploaded from the VM by verification commands.
|
|
618
|
+
await downloadArtifactFromS3Bucket(
|
|
619
|
+
bucketName,
|
|
620
|
+
verificationTranscriptStoragePathAndFilename,
|
|
621
|
+
verificationTranscriptTemporaryLocalPath
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
// Read the verification trascript and validate data by checking for core info ("ZKey Ok!").
|
|
625
|
+
const content = fs.readFileSync(verificationTranscriptTemporaryLocalPath, "utf-8")
|
|
626
|
+
|
|
627
|
+
if (content.includes("ZKey Ok!")) isContributionValid = true
|
|
628
|
+
|
|
629
|
+
// If the contribution is valid, then format and store the trascript.
|
|
630
|
+
if (isContributionValid) {
|
|
631
|
+
// eslint-disable-next-line no-control-regex
|
|
632
|
+
const updated = content.replace(/\x1b[[0-9;]*m/g, "")
|
|
633
|
+
|
|
634
|
+
fs.writeFileSync(verificationTranscriptTemporaryLocalPath, updated)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
printLog(`The contribution has been verified - Result ${isContributionValid}`, LogLevel.DEBUG)
|
|
639
|
+
|
|
640
|
+
// Create a new contribution document.
|
|
641
|
+
const contributionDoc = await firestore
|
|
642
|
+
.collection(getContributionsCollectionPath(ceremonyId, circuitId))
|
|
643
|
+
.doc()
|
|
644
|
+
.get()
|
|
645
|
+
|
|
646
|
+
// Step (1.A.4).
|
|
647
|
+
if (isContributionValid) {
|
|
648
|
+
// Sleep ~3 seconds to wait for verification transcription.
|
|
649
|
+
await sleep(3000)
|
|
650
|
+
|
|
651
|
+
// Step (1.A.4.A.1).
|
|
652
|
+
if (isUsingVM) {
|
|
653
|
+
// Retrieve the contribution hash from the command output.
|
|
654
|
+
lastZkeyBlake2bHash = await retrieveCommandOutput(ssm, vmInstanceId, commandId)
|
|
655
|
+
|
|
656
|
+
const hashRegex = /[a-fA-F0-9]{64}/
|
|
657
|
+
const match = lastZkeyBlake2bHash.match(hashRegex)!
|
|
658
|
+
|
|
659
|
+
lastZkeyBlake2bHash = match.at(0)!
|
|
660
|
+
|
|
661
|
+
// re upload the formatted verification transcript
|
|
662
|
+
await uploadFileToBucket(
|
|
663
|
+
bucketName,
|
|
664
|
+
verificationTranscriptStoragePathAndFilename,
|
|
665
|
+
verificationTranscriptTemporaryLocalPath,
|
|
666
|
+
true
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
// Stop VM instance.
|
|
670
|
+
await stopEC2Instance(ec2, vmInstanceId)
|
|
671
|
+
} else {
|
|
672
|
+
// Upload verification transcript.
|
|
673
|
+
/// nb. do not use multi-part upload here due to small file size.
|
|
674
|
+
await uploadFileToBucket(
|
|
675
|
+
bucketName,
|
|
676
|
+
verificationTranscriptStoragePathAndFilename,
|
|
677
|
+
verificationTranscriptTemporaryLocalPath,
|
|
678
|
+
true
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Compute verification transcript hash.
|
|
683
|
+
transcriptBlake2bHash = await blake512FromPath(verificationTranscriptTemporaryLocalPath)
|
|
684
|
+
|
|
685
|
+
// Free resources by unlinking transcript temporary file.
|
|
686
|
+
fs.unlinkSync(verificationTranscriptTemporaryLocalPath)
|
|
687
|
+
|
|
688
|
+
// Filter participant contributions to find the data related to the one verified.
|
|
689
|
+
const participantContributions = contributions.filter(
|
|
690
|
+
(contribution: Contribution) =>
|
|
691
|
+
!!contribution.hash && !!contribution.computationTime && !contribution.doc
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
/// @dev (there must be only one contribution with an empty 'doc' field).
|
|
695
|
+
if (participantContributions.length !== 1)
|
|
696
|
+
logAndThrowError(SPECIFIC_ERRORS.SE_VERIFICATION_NO_PARTICIPANT_CONTRIBUTION_DATA)
|
|
697
|
+
|
|
698
|
+
// Get contribution computation time.
|
|
699
|
+
contributionComputationTime = contributions.at(0).computationTime
|
|
700
|
+
|
|
701
|
+
// Step (1.A.4.A.2).
|
|
702
|
+
batch.create(contributionDoc.ref, {
|
|
703
|
+
participantId: participantDoc.id,
|
|
704
|
+
contributionComputationTime,
|
|
705
|
+
verificationComputationTime: verifyCloudFunctionExecutionTime,
|
|
706
|
+
zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
|
|
707
|
+
files: {
|
|
708
|
+
transcriptFilename: verificationTranscriptCompleteFilename,
|
|
709
|
+
lastZkeyFilename,
|
|
710
|
+
transcriptStoragePath: verificationTranscriptStoragePathAndFilename,
|
|
711
|
+
lastZkeyStoragePath,
|
|
712
|
+
transcriptBlake2bHash,
|
|
713
|
+
lastZkeyBlake2bHash
|
|
714
|
+
},
|
|
715
|
+
verificationSoftware: {
|
|
716
|
+
name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
|
|
717
|
+
version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
|
|
718
|
+
commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
|
|
719
|
+
},
|
|
720
|
+
valid: isContributionValid,
|
|
721
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
verifyContributionTimer.stop()
|
|
725
|
+
verifyCloudFunctionTime = verifyContributionTimer.ms()
|
|
726
|
+
} else {
|
|
727
|
+
// Step (1.A.4.B).
|
|
728
|
+
|
|
729
|
+
// Free-up storage by deleting invalid contribution.
|
|
730
|
+
await deleteObject(bucketName, lastZkeyStoragePath)
|
|
731
|
+
|
|
732
|
+
// Step (1.A.4.B.1).
|
|
733
|
+
batch.create(contributionDoc.ref, {
|
|
734
|
+
participantId: participantDoc.id,
|
|
735
|
+
verificationComputationTime: verifyCloudFunctionExecutionTime,
|
|
736
|
+
zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
|
|
737
|
+
verificationSoftware: {
|
|
738
|
+
name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
|
|
739
|
+
version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
|
|
740
|
+
commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
|
|
741
|
+
},
|
|
742
|
+
valid: isContributionValid,
|
|
743
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Step (1.A.4.C)
|
|
748
|
+
if (!isFinalizing) {
|
|
749
|
+
// Step (1.A.4.C.1)
|
|
750
|
+
// Compute new average contribution/verification time.
|
|
751
|
+
fullContributionTime = Number(verificationStartedAt) - Number(contributionStartedAt)
|
|
752
|
+
|
|
753
|
+
const newAvgContributionComputationTime =
|
|
754
|
+
avgContributionComputationTime > 0
|
|
755
|
+
? (avgContributionComputationTime + contributionComputationTime) / 2
|
|
756
|
+
: contributionComputationTime
|
|
757
|
+
const newAvgFullContributionTime =
|
|
758
|
+
avgFullContributionTime > 0
|
|
759
|
+
? (avgFullContributionTime + fullContributionTime) / 2
|
|
760
|
+
: fullContributionTime
|
|
761
|
+
const newAvgVerifyCloudFunctionTime =
|
|
762
|
+
avgVerifyCloudFunctionTime > 0
|
|
763
|
+
? (avgVerifyCloudFunctionTime + verifyCloudFunctionTime) / 2
|
|
764
|
+
: verifyCloudFunctionTime
|
|
765
|
+
|
|
766
|
+
// Prepare tx to update circuit average contribution/verification time.
|
|
767
|
+
/// @dev this must happen only for valid contributions.
|
|
768
|
+
batch.update(circuitDoc.ref, {
|
|
769
|
+
avgTimings: {
|
|
770
|
+
contributionComputation: isContributionValid
|
|
771
|
+
? newAvgContributionComputationTime
|
|
772
|
+
: avgContributionComputationTime,
|
|
773
|
+
fullContribution: isContributionValid ? newAvgFullContributionTime : avgFullContributionTime,
|
|
774
|
+
verifyCloudFunction: isContributionValid
|
|
775
|
+
? newAvgVerifyCloudFunctionTime
|
|
776
|
+
: avgVerifyCloudFunctionTime
|
|
777
|
+
},
|
|
778
|
+
waitingQueue: {
|
|
779
|
+
...waitingQueue,
|
|
780
|
+
completedContributions: isContributionValid
|
|
781
|
+
? completedContributions + 1
|
|
782
|
+
: completedContributions,
|
|
783
|
+
failedContributions: isContributionValid ? failedContributions : failedContributions + 1
|
|
784
|
+
},
|
|
785
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Step (2).
|
|
790
|
+
await batch.commit()
|
|
791
|
+
|
|
792
|
+
printLog(
|
|
793
|
+
`The contribution #${
|
|
794
|
+
isFinalizing ? finalContributionIndex : lastZkeyIndex
|
|
795
|
+
} of circuit ${circuitId} (ceremony ${ceremonyId}) has been verified as ${
|
|
796
|
+
isContributionValid ? "valid" : "invalid"
|
|
797
|
+
} for the participant ${participantDoc.id}`,
|
|
798
|
+
LogLevel.DEBUG
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Step (1).
|
|
803
|
+
if (isContributing || isFinalizing) {
|
|
804
|
+
// Prepare timer.
|
|
805
|
+
verificationTaskTimer.start()
|
|
806
|
+
|
|
807
|
+
// Step (1.A.3.0).
|
|
808
|
+
if (isUsingVM) {
|
|
809
|
+
printLog(`Starting the VM mechanism`, LogLevel.DEBUG)
|
|
810
|
+
|
|
811
|
+
// Prepare for VM execution.
|
|
812
|
+
let isVMRunning = false // true when the VM is up, otherwise false.
|
|
813
|
+
|
|
814
|
+
// Step (1.A.3.1).
|
|
815
|
+
await startEC2Instance(ec2, vmInstanceId)
|
|
816
|
+
|
|
817
|
+
await sleep(60000) // nb. wait for VM startup (1 mins + retry).
|
|
818
|
+
|
|
819
|
+
// Check if the startup is running.
|
|
820
|
+
isVMRunning = await checkIfVMRunning(ec2, vmInstanceId)
|
|
821
|
+
|
|
822
|
+
printLog(`VM running: ${isVMRunning}`, LogLevel.DEBUG)
|
|
823
|
+
|
|
824
|
+
// Step (1.A.3.2).
|
|
825
|
+
// Prepare.
|
|
826
|
+
const verificationCommand = vmContributionVerificationCommand(
|
|
827
|
+
bucketName,
|
|
828
|
+
lastZkeyStoragePath,
|
|
829
|
+
verificationTranscriptStoragePathAndFilename
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
// Run.
|
|
833
|
+
commandId = await runCommandUsingSSM(ssm, vmInstanceId, verificationCommand)
|
|
834
|
+
|
|
835
|
+
printLog(`Starting the execution of command ${commandId}`, LogLevel.DEBUG)
|
|
836
|
+
|
|
837
|
+
// Step (1.A.3.3).
|
|
838
|
+
return new Promise<void>((resolve, reject) =>
|
|
839
|
+
waitForVMCommandExecution(resolve, reject, ssm, vmInstanceId, commandId)
|
|
840
|
+
)
|
|
841
|
+
.then(async () => {
|
|
842
|
+
// Command execution successfully completed.
|
|
843
|
+
printLog(`Command ${commandId} execution has been successfully completed`, LogLevel.DEBUG)
|
|
844
|
+
await completeVerification()
|
|
845
|
+
})
|
|
846
|
+
.catch((error: any) => {
|
|
847
|
+
// Command execution aborted.
|
|
848
|
+
printLog(`Command ${commandId} execution has been aborted - Error ${error}`, LogLevel.DEBUG)
|
|
849
|
+
|
|
850
|
+
logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION)
|
|
851
|
+
})
|
|
852
|
+
} else {
|
|
853
|
+
// CF approach.
|
|
854
|
+
printLog(`CF mechanism`, LogLevel.DEBUG)
|
|
855
|
+
|
|
856
|
+
const potStoragePath = getPotStorageFilePath(files.potFilename)
|
|
857
|
+
const firstZkeyStoragePath = getZkeyStorageFilePath(prefix, `${prefix}_${genesisZkeyIndex}.zkey`)
|
|
858
|
+
// Prepare temporary file paths.
|
|
859
|
+
// (nb. these are needed to download the necessary artifacts for verification from AWS S3).
|
|
860
|
+
verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(
|
|
861
|
+
verificationTranscriptCompleteFilename
|
|
862
|
+
)
|
|
863
|
+
const potTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.pot`)
|
|
864
|
+
const firstZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_genesis.zkey`)
|
|
865
|
+
const lastZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_last.zkey`)
|
|
866
|
+
|
|
867
|
+
// Create and populate transcript.
|
|
868
|
+
const transcriptLogger = createCustomLoggerForFile(verificationTranscriptTemporaryLocalPath)
|
|
869
|
+
transcriptLogger.info(
|
|
870
|
+
`${
|
|
871
|
+
isFinalizing ? `Final verification` : `Verification`
|
|
872
|
+
} transcript for ${prefix} circuit Phase 2 contribution.\n${
|
|
873
|
+
isFinalizing ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}`
|
|
874
|
+
} (${contributorOrCoordinatorIdentifier})\n`
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
// Step (1.A.2).
|
|
878
|
+
await downloadArtifactFromS3Bucket(bucketName, potStoragePath, potTempFilePath)
|
|
879
|
+
await downloadArtifactFromS3Bucket(bucketName, firstZkeyStoragePath, firstZkeyTempFilePath)
|
|
880
|
+
await downloadArtifactFromS3Bucket(bucketName, lastZkeyStoragePath, lastZkeyTempFilePath)
|
|
881
|
+
|
|
882
|
+
await sleep(6000)
|
|
883
|
+
|
|
884
|
+
// wait until the files are actually downloaded
|
|
885
|
+
return new Promise<void>((resolve, reject) =>
|
|
886
|
+
waitForFileDownload(resolve, reject, potTempFilePath, firstZkeyTempFilePath, lastZkeyTempFilePath, circuitId, participantDoc.id)
|
|
887
|
+
)
|
|
888
|
+
.then(async () => {
|
|
889
|
+
printLog(`Downloads from AWS S3 bucket completed - ceremony ${ceremonyId} circuit ${circuitId}`, LogLevel.DEBUG)
|
|
890
|
+
|
|
891
|
+
// Step (1.A.4).
|
|
892
|
+
isContributionValid = await zKey.verifyFromInit(
|
|
893
|
+
firstZkeyTempFilePath,
|
|
894
|
+
potTempFilePath,
|
|
895
|
+
lastZkeyTempFilePath,
|
|
896
|
+
transcriptLogger
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
// Compute contribution hash.
|
|
900
|
+
lastZkeyBlake2bHash = await blake512FromPath(lastZkeyTempFilePath)
|
|
901
|
+
|
|
902
|
+
// Free resources by unlinking temporary folders.
|
|
903
|
+
// Do not free-up verification transcript path here.
|
|
904
|
+
try {
|
|
905
|
+
fs.unlinkSync(potTempFilePath)
|
|
906
|
+
fs.unlinkSync(firstZkeyTempFilePath)
|
|
907
|
+
fs.unlinkSync(lastZkeyTempFilePath)
|
|
908
|
+
} catch (error: any) {
|
|
909
|
+
printLog(`Error while unlinking temporary files - Error ${error}`, LogLevel.WARN)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
await completeVerification()
|
|
913
|
+
})
|
|
914
|
+
.catch((error: any) => {
|
|
915
|
+
// Throw the new error
|
|
916
|
+
const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
|
|
917
|
+
const additionalDetails = error.toString()
|
|
918
|
+
|
|
919
|
+
logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Update the related participant's document after verification of its last contribution.
|
|
928
|
+
* @dev this cloud functions is responsible for preparing the participant for the contribution toward the next circuit.
|
|
929
|
+
* this does not happen if the participant is actually the coordinator who is finalizing the ceremony.
|
|
930
|
+
*/
|
|
931
|
+
export const refreshParticipantAfterContributionVerification = functionsV1
|
|
932
|
+
.region('europe-west1')
|
|
933
|
+
.runWith({
|
|
934
|
+
memory: "512MB"
|
|
935
|
+
})
|
|
936
|
+
.firestore.document(
|
|
937
|
+
`/${commonTerms.collections.ceremonies.name}/{ceremony}/${commonTerms.collections.circuits.name}/{circuit}/${commonTerms.collections.contributions.name}/{contributions}`
|
|
938
|
+
)
|
|
939
|
+
.onCreate(async (createdContribution: QueryDocumentSnapshot) => {
|
|
940
|
+
// Prepare db.
|
|
941
|
+
const firestore = admin.firestore()
|
|
942
|
+
// Prepare batch of txs.
|
|
943
|
+
const batch = firestore.batch()
|
|
944
|
+
|
|
945
|
+
// Derive data from document.
|
|
946
|
+
// == /ceremonies/{ceremony}/circuits/.
|
|
947
|
+
const ceremonyId = createdContribution.ref.parent.parent?.parent?.parent?.path.replace(
|
|
948
|
+
`${commonTerms.collections.ceremonies.name}/`,
|
|
949
|
+
""
|
|
950
|
+
)!
|
|
951
|
+
// == /ceremonies/{ceremony}/participants.
|
|
952
|
+
const ceremonyParticipantsCollectionPath =
|
|
953
|
+
`${createdContribution.ref.parent.parent?.parent?.parent?.path}/${commonTerms.collections.participants.name}`!
|
|
954
|
+
|
|
955
|
+
if (!createdContribution.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
956
|
+
|
|
957
|
+
// Extract data.
|
|
958
|
+
const { participantId } = createdContribution.data()!
|
|
959
|
+
|
|
960
|
+
// Get documents from derived paths.
|
|
961
|
+
const circuits = await getCeremonyCircuits(ceremonyId)
|
|
962
|
+
const participantDoc = await getDocumentById(ceremonyParticipantsCollectionPath, participantId)
|
|
963
|
+
|
|
964
|
+
if (!participantDoc.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
965
|
+
|
|
966
|
+
// Extract data.
|
|
967
|
+
const { contributions, status, contributionProgress } = participantDoc.data()!
|
|
968
|
+
|
|
969
|
+
// Define pre-conditions.
|
|
970
|
+
const isFinalizing = status === ParticipantStatus.FINALIZING
|
|
971
|
+
|
|
972
|
+
// Link the newest created contribution document w/ participant contributions info.
|
|
973
|
+
// nb. there must be only one contribution with an empty doc.
|
|
974
|
+
contributions.forEach((participantContribution: Contribution) => {
|
|
975
|
+
// Define pre-conditions.
|
|
976
|
+
const isContributionWithoutDocRef =
|
|
977
|
+
!!participantContribution.hash &&
|
|
978
|
+
!!participantContribution.computationTime &&
|
|
979
|
+
!participantContribution.doc
|
|
980
|
+
|
|
981
|
+
if (isContributionWithoutDocRef) participantContribution.doc = createdContribution.id
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
// Check if the participant is not the coordinator trying to finalize the ceremony.
|
|
985
|
+
if (!isFinalizing)
|
|
986
|
+
batch.update(participantDoc.ref, {
|
|
987
|
+
// - DONE = provided a contribution for every circuit
|
|
988
|
+
// - CONTRIBUTED = some contribution still missing.
|
|
989
|
+
status:
|
|
990
|
+
contributionProgress + 1 > circuits.length ? ParticipantStatus.DONE : ParticipantStatus.CONTRIBUTED,
|
|
991
|
+
contributionStep: ParticipantContributionStep.COMPLETED,
|
|
992
|
+
tempContributionData: FieldValue.delete()
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
// nb. valid both for participant or coordinator (finalizing).
|
|
996
|
+
batch.update(participantDoc.ref, {
|
|
997
|
+
contributions,
|
|
998
|
+
lastUpdated: getCurrentServerTimestampInMillis()
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
await batch.commit()
|
|
1002
|
+
|
|
1003
|
+
printLog(
|
|
1004
|
+
`Participant ${participantId} refreshed after contribution ${createdContribution.id} - The participant was finalizing the ceremony ${isFinalizing}`,
|
|
1005
|
+
LogLevel.DEBUG
|
|
1006
|
+
)
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Finalize the ceremony circuit.
|
|
1011
|
+
* @dev this cloud function stores the hashes and storage references of the Verifier smart contract
|
|
1012
|
+
* and verification key extracted from the circuit final contribution (as part of the ceremony finalization process).
|
|
1013
|
+
*/
|
|
1014
|
+
export const finalizeCircuit = functionsV1
|
|
1015
|
+
.region('europe-west1')
|
|
1016
|
+
.runWith({
|
|
1017
|
+
memory: "512MB"
|
|
1018
|
+
})
|
|
1019
|
+
.https.onCall(async (data: FinalizeCircuitData, context: functionsV1.https.CallableContext) => {
|
|
1020
|
+
if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
|
|
1021
|
+
|
|
1022
|
+
if (!data.ceremonyId || !data.circuitId || !data.bucketName || !data.beacon)
|
|
1023
|
+
logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
|
|
1024
|
+
|
|
1025
|
+
// Get data.
|
|
1026
|
+
const { ceremonyId, circuitId, bucketName, beacon } = data
|
|
1027
|
+
const userId = context.auth?.uid
|
|
1028
|
+
|
|
1029
|
+
// Look for documents.
|
|
1030
|
+
const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
|
|
1031
|
+
const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
|
|
1032
|
+
const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId)
|
|
1033
|
+
const contributionDoc = await getFinalContribution(ceremonyId, circuitId)
|
|
1034
|
+
|
|
1035
|
+
if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data() || !contributionDoc.data())
|
|
1036
|
+
logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
|
|
1037
|
+
|
|
1038
|
+
// Extract data.
|
|
1039
|
+
const { prefix: circuitPrefix } = circuitDoc.data()!
|
|
1040
|
+
const { files } = contributionDoc.data()!
|
|
1041
|
+
|
|
1042
|
+
// Prepare filenames and storage paths.
|
|
1043
|
+
const verificationKeyFilename = `${circuitPrefix}_${verificationKeyAcronym}.json`
|
|
1044
|
+
const verifierContractFilename = `${circuitPrefix}_${verifierSmartContractAcronym}.sol`
|
|
1045
|
+
const verificationKeyStorageFilePath = getVerificationKeyStorageFilePath(circuitPrefix, verificationKeyFilename)
|
|
1046
|
+
const verifierContractStorageFilePath = getVerifierContractStorageFilePath(
|
|
1047
|
+
circuitPrefix,
|
|
1048
|
+
verifierContractFilename
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
// Prepare temporary paths.
|
|
1052
|
+
const verificationKeyTemporaryFilePath = createTemporaryLocalPath(verificationKeyFilename)
|
|
1053
|
+
const verifierContractTemporaryFilePath = createTemporaryLocalPath(verifierContractFilename)
|
|
1054
|
+
|
|
1055
|
+
// Download artifact from ceremony bucket.
|
|
1056
|
+
await downloadArtifactFromS3Bucket(bucketName, verificationKeyStorageFilePath, verificationKeyTemporaryFilePath)
|
|
1057
|
+
await downloadArtifactFromS3Bucket(
|
|
1058
|
+
bucketName,
|
|
1059
|
+
verifierContractStorageFilePath,
|
|
1060
|
+
verifierContractTemporaryFilePath
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
// Compute hash before unlink.
|
|
1064
|
+
const verificationKeyBlake2bHash = await blake512FromPath(verificationKeyTemporaryFilePath)
|
|
1065
|
+
const verifierContractBlake2bHash = await blake512FromPath(verifierContractTemporaryFilePath)
|
|
1066
|
+
|
|
1067
|
+
// Free resources by unlinking temporary folders.
|
|
1068
|
+
fs.unlinkSync(verificationKeyTemporaryFilePath)
|
|
1069
|
+
fs.unlinkSync(verifierContractTemporaryFilePath)
|
|
1070
|
+
|
|
1071
|
+
// Add references and hashes of the final contribution artifacts.
|
|
1072
|
+
await contributionDoc.ref.update({
|
|
1073
|
+
files: {
|
|
1074
|
+
...files,
|
|
1075
|
+
verificationKeyBlake2bHash,
|
|
1076
|
+
verificationKeyFilename,
|
|
1077
|
+
verificationKeyStoragePath: verificationKeyStorageFilePath,
|
|
1078
|
+
verifierContractBlake2bHash,
|
|
1079
|
+
verifierContractFilename,
|
|
1080
|
+
verifierContractStoragePath: verifierContractStorageFilePath
|
|
1081
|
+
},
|
|
1082
|
+
beacon: {
|
|
1083
|
+
value: beacon,
|
|
1084
|
+
hash: computeSHA256ToHex(beacon)
|
|
1085
|
+
}
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
printLog(
|
|
1089
|
+
`Circuit ${circuitId} finalization completed - Ceremony ${ceremonyDoc.id} - Coordinator ${participantDoc.id}`,
|
|
1090
|
+
LogLevel.DEBUG
|
|
1091
|
+
)
|
|
1092
|
+
})
|