@zkpassport/sdk 0.2.4 → 0.2.5

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.
@@ -134,6 +134,8 @@ export declare class ZKPassport {
134
134
  private topicToRequestReceived;
135
135
  private topicToService;
136
136
  private topicToProofs;
137
+ private topicToExpectedProofCount;
138
+ private topicToResults;
137
139
  private onRequestReceivedCallbacks;
138
140
  private onGeneratingProofCallbacks;
139
141
  private onBridgeConnectCallbacks;
@@ -142,6 +144,8 @@ export declare class ZKPassport {
142
144
  private onRejectCallbacks;
143
145
  private onErrorCallbacks;
144
146
  constructor(_domain?: string);
147
+ private handleResult;
148
+ private setExpectedProofCount;
145
149
  /**
146
150
  * @notice Handle an encrypted message.
147
151
  * @param request The request.
package/dist/cjs/index.js CHANGED
@@ -15,6 +15,20 @@ const node_gzip_1 = require("node-gzip");
15
15
  //import initACVM from '@noir-lang/acvm_js'
16
16
  const en_json_1 = tslib_1.__importDefault(require("i18n-iso-countries/langs/en.json"));
17
17
  (0, i18n_iso_countries_1.registerLocale)(en_json_1.default);
18
+ function hasRequestedAccessToField(credentialsRequest, field) {
19
+ const fieldValue = credentialsRequest[field];
20
+ const isDefined = fieldValue !== undefined && fieldValue !== null;
21
+ if (!isDefined) {
22
+ return false;
23
+ }
24
+ for (const key in fieldValue) {
25
+ if (fieldValue[key] !== undefined &&
26
+ fieldValue[key] !== null) {
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
18
32
  function normalizeCountry(country) {
19
33
  let normalizedCountry;
20
34
  const alpha3 = (0, i18n_iso_countries_1.getAlpha3Code)(country, "en");
@@ -56,6 +70,8 @@ class ZKPassport {
56
70
  this.topicToRequestReceived = {};
57
71
  this.topicToService = {};
58
72
  this.topicToProofs = {};
73
+ this.topicToExpectedProofCount = {};
74
+ this.topicToResults = {};
59
75
  this.onRequestReceivedCallbacks = {};
60
76
  this.onGeneratingProofCallbacks = {};
61
77
  this.onBridgeConnectCallbacks = {};
@@ -74,6 +90,70 @@ class ZKPassport {
74
90
  await Promise.all([initACVM(acvm), initNoirC(noirc)])
75
91
  this.wasmVerifierInit = true
76
92
  }*/
93
+ async handleResult(topic) {
94
+ const result = this.topicToResults[topic];
95
+ // Clear the results straight away to avoid concurrency issues
96
+ delete this.topicToResults[topic];
97
+ // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
98
+ const { uniqueIdentifier, verified } = await this.verify(topic, this.topicToProofs[topic], result);
99
+ await Promise.all(this.onResultCallbacks[topic].map((callback) => callback({
100
+ uniqueIdentifier,
101
+ verified,
102
+ result,
103
+ })));
104
+ // Clear the expected proof count
105
+ delete this.topicToExpectedProofCount[topic];
106
+ }
107
+ setExpectedProofCount(topic) {
108
+ const fields = Object.keys(this.topicToConfig[topic]).filter((key) => hasRequestedAccessToField(this.topicToConfig[topic], key));
109
+ const neededCircuits = [];
110
+ // Determine which circuits are needed based on the requested fields
111
+ for (const field of fields) {
112
+ for (const key in this.topicToConfig[topic][field]) {
113
+ switch (key) {
114
+ case "eq":
115
+ case "disclose":
116
+ if (field !== "age" && !neededCircuits.includes("disclose_bytes")) {
117
+ neededCircuits.push("disclose_bytes");
118
+ }
119
+ else if (field === "age" && !neededCircuits.includes("compare_age")) {
120
+ neededCircuits.push("compare_age");
121
+ }
122
+ break;
123
+ case "gte":
124
+ case "gt":
125
+ case "lte":
126
+ case "lt":
127
+ case "range":
128
+ if (field === "age" && !neededCircuits.includes("compare_age")) {
129
+ neededCircuits.push("compare_age");
130
+ }
131
+ else if (field === "expiry_date" && !neededCircuits.includes("compare_expiry")) {
132
+ neededCircuits.push("compare_expiry");
133
+ }
134
+ else if (field === "birthdate" && !neededCircuits.includes("compare_birthdate")) {
135
+ neededCircuits.push("compare_birthdate");
136
+ }
137
+ break;
138
+ case "in":
139
+ if (field === "nationality" && !neededCircuits.includes("inclusion_check_country")) {
140
+ neededCircuits.push("inclusion_check_country");
141
+ }
142
+ break;
143
+ case "out":
144
+ if (field === "nationality" && !neededCircuits.includes("exclusion_check_country")) {
145
+ neededCircuits.push("exclusion_check_country");
146
+ }
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ // From the circuits needed, determine the expected proof count
152
+ // There are at least 4 proofs, 3 base proofs and 1 disclosure proof minimum
153
+ // Each separate needed circuit adds 1 disclosure proof
154
+ this.topicToExpectedProofCount[topic] =
155
+ neededCircuits.length === 0 ? 4 : 3 + neededCircuits.length;
156
+ }
77
157
  /**
78
158
  * @notice Handle an encrypted message.
79
159
  * @param request The request.
@@ -106,19 +186,44 @@ class ZKPassport {
106
186
  };
107
187
  this.topicToProofs[topic].push(processedProof);
108
188
  await Promise.all(this.onProofGeneratedCallbacks[topic].map((callback) => callback(processedProof)));
189
+ // If the results were received before all the proofs were generated,
190
+ // we can handle the result now
191
+ if (this.topicToResults[topic] &&
192
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
193
+ await this.handleResult(topic);
194
+ }
109
195
  }
110
196
  else if (request.method === "done") {
111
197
  logger_1.noLogger.debug(`User sent the query result`);
112
- // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
113
- const { uniqueIdentifier, verified } = await this.verify(topic, this.topicToProofs[topic], request.params);
114
- await Promise.all(this.onResultCallbacks[topic].map((callback) => callback({
115
- uniqueIdentifier,
116
- verified,
117
- result: request.params,
118
- })));
198
+ this.topicToResults[topic] = request.params;
199
+ // Make sure all the proofs have been received, otherwise we'll handle the result later
200
+ // once the proofs have all been received
201
+ if (this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
202
+ await this.handleResult(topic);
203
+ }
119
204
  }
120
205
  else if (request.method === "error") {
121
- await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)));
206
+ const error = request.params.error;
207
+ if (error && error === "This ID is not supported yet") {
208
+ // This means the user has an ID that is not supported yet
209
+ // So we won't receive any proofs and we can handle the result now
210
+ this.topicToExpectedProofCount[topic] = 0;
211
+ if (this.topicToResults[topic]) {
212
+ await this.handleResult(topic);
213
+ }
214
+ }
215
+ else if (error && error.startsWith("Cannot generate proof")) {
216
+ // This means one of the disclosure proofs failed to be generated
217
+ // So we need to remove one from the expected proof count
218
+ this.topicToExpectedProofCount[topic] -= 1;
219
+ // If the expected proof count is now equal to the number of proofs received
220
+ // and the results were received, we can handle the result now
221
+ if (this.topicToResults[topic] &&
222
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
223
+ await this.handleResult(topic);
224
+ }
225
+ }
226
+ await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(error)));
122
227
  }
123
228
  }
124
229
  getZkPassportRequest(topic) {
@@ -174,6 +279,7 @@ class ZKPassport {
174
279
  const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString("base64");
175
280
  const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString("base64");
176
281
  const pubkey = (0, utils_2.bytesToHex)(this.topicToKeyPair[topic].publicKey);
282
+ this.setExpectedProofCount(topic);
177
283
  return {
178
284
  url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
179
285
  requestId: topic,
@@ -204,6 +310,7 @@ class ZKPassport {
204
310
  this.topicToConfig[topic] = {};
205
311
  this.topicToService[topic] = { name, logo, purpose, scope };
206
312
  this.topicToProofs[topic] = [];
313
+ this.topicToExpectedProofCount[topic] = 0;
207
314
  this.onRequestReceivedCallbacks[topic] = [];
208
315
  this.onGeneratingProofCallbacks[topic] = [];
209
316
  this.onBridgeConnectCallbacks[topic] = [];
@@ -745,6 +852,16 @@ class ZKPassport {
745
852
  isCorrect = false;
746
853
  break;
747
854
  }
855
+ // Check the countryList is in ascending order
856
+ // If the prover doesn't use a sorted list then the proof cannot be trusted
857
+ // as it is requirement in the circuit for the exclusion check to work
858
+ for (let i = 1; i < countryList.length; i++) {
859
+ if (countryList[i] < countryList[i - 1]) {
860
+ console.warn("The nationality exclusion list has not been sorted, and thus the proof cannot be trusted");
861
+ isCorrect = false;
862
+ break;
863
+ }
864
+ }
748
865
  uniqueIdentifier = (0, utils_1.getNullifierFromDisclosureProof)(proofData).toString(10);
749
866
  }
750
867
  else if (proof.name === "inclusion_check_country") {
@@ -789,7 +906,7 @@ class ZKPassport {
789
906
  proofsToVerify = this.topicToProofs[requestId];
790
907
  if (!proofsToVerify || proofsToVerify.length < 4) {
791
908
  // It may happen that a request returns a result without proofs
792
- // Meaning the ID is supported yet by ZKPassport circuits,
909
+ // Meaning the ID is not supported yet by ZKPassport circuits,
793
910
  // so the results has to be trusted and cannot be independently verified
794
911
  return { uniqueIdentifier: undefined, verified: false };
795
912
  }
@@ -851,6 +968,8 @@ class ZKPassport {
851
968
  delete this.topicToConfig[requestId];
852
969
  delete this.topicToSharedSecret[requestId];
853
970
  delete this.topicToProofs[requestId];
971
+ delete this.topicToExpectedProofCount[requestId];
972
+ delete this.topicToResults[requestId];
854
973
  this.onRequestReceivedCallbacks[requestId] = [];
855
974
  this.onGeneratingProofCallbacks[requestId] = [];
856
975
  this.onBridgeConnectCallbacks[requestId] = [];
@@ -134,6 +134,8 @@ export declare class ZKPassport {
134
134
  private topicToRequestReceived;
135
135
  private topicToService;
136
136
  private topicToProofs;
137
+ private topicToExpectedProofCount;
138
+ private topicToResults;
137
139
  private onRequestReceivedCallbacks;
138
140
  private onGeneratingProofCallbacks;
139
141
  private onBridgeConnectCallbacks;
@@ -142,6 +144,8 @@ export declare class ZKPassport {
142
144
  private onRejectCallbacks;
143
145
  private onErrorCallbacks;
144
146
  constructor(_domain?: string);
147
+ private handleResult;
148
+ private setExpectedProofCount;
145
149
  /**
146
150
  * @notice Handle an encrypted message.
147
151
  * @param request The request.
package/dist/esm/index.js CHANGED
@@ -11,6 +11,20 @@ import { ungzip } from "node-gzip";
11
11
  //import initACVM from '@noir-lang/acvm_js'
12
12
  import i18en from "i18n-iso-countries/langs/en.json";
13
13
  registerLocale(i18en);
14
+ function hasRequestedAccessToField(credentialsRequest, field) {
15
+ const fieldValue = credentialsRequest[field];
16
+ const isDefined = fieldValue !== undefined && fieldValue !== null;
17
+ if (!isDefined) {
18
+ return false;
19
+ }
20
+ for (const key in fieldValue) {
21
+ if (fieldValue[key] !== undefined &&
22
+ fieldValue[key] !== null) {
23
+ return true;
24
+ }
25
+ }
26
+ return false;
27
+ }
14
28
  function normalizeCountry(country) {
15
29
  let normalizedCountry;
16
30
  const alpha3 = getAlpha3Code(country, "en");
@@ -46,6 +60,8 @@ export class ZKPassport {
46
60
  this.topicToRequestReceived = {};
47
61
  this.topicToService = {};
48
62
  this.topicToProofs = {};
63
+ this.topicToExpectedProofCount = {};
64
+ this.topicToResults = {};
49
65
  this.onRequestReceivedCallbacks = {};
50
66
  this.onGeneratingProofCallbacks = {};
51
67
  this.onBridgeConnectCallbacks = {};
@@ -64,6 +80,70 @@ export class ZKPassport {
64
80
  await Promise.all([initACVM(acvm), initNoirC(noirc)])
65
81
  this.wasmVerifierInit = true
66
82
  }*/
83
+ async handleResult(topic) {
84
+ const result = this.topicToResults[topic];
85
+ // Clear the results straight away to avoid concurrency issues
86
+ delete this.topicToResults[topic];
87
+ // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
88
+ const { uniqueIdentifier, verified } = await this.verify(topic, this.topicToProofs[topic], result);
89
+ await Promise.all(this.onResultCallbacks[topic].map((callback) => callback({
90
+ uniqueIdentifier,
91
+ verified,
92
+ result,
93
+ })));
94
+ // Clear the expected proof count
95
+ delete this.topicToExpectedProofCount[topic];
96
+ }
97
+ setExpectedProofCount(topic) {
98
+ const fields = Object.keys(this.topicToConfig[topic]).filter((key) => hasRequestedAccessToField(this.topicToConfig[topic], key));
99
+ const neededCircuits = [];
100
+ // Determine which circuits are needed based on the requested fields
101
+ for (const field of fields) {
102
+ for (const key in this.topicToConfig[topic][field]) {
103
+ switch (key) {
104
+ case "eq":
105
+ case "disclose":
106
+ if (field !== "age" && !neededCircuits.includes("disclose_bytes")) {
107
+ neededCircuits.push("disclose_bytes");
108
+ }
109
+ else if (field === "age" && !neededCircuits.includes("compare_age")) {
110
+ neededCircuits.push("compare_age");
111
+ }
112
+ break;
113
+ case "gte":
114
+ case "gt":
115
+ case "lte":
116
+ case "lt":
117
+ case "range":
118
+ if (field === "age" && !neededCircuits.includes("compare_age")) {
119
+ neededCircuits.push("compare_age");
120
+ }
121
+ else if (field === "expiry_date" && !neededCircuits.includes("compare_expiry")) {
122
+ neededCircuits.push("compare_expiry");
123
+ }
124
+ else if (field === "birthdate" && !neededCircuits.includes("compare_birthdate")) {
125
+ neededCircuits.push("compare_birthdate");
126
+ }
127
+ break;
128
+ case "in":
129
+ if (field === "nationality" && !neededCircuits.includes("inclusion_check_country")) {
130
+ neededCircuits.push("inclusion_check_country");
131
+ }
132
+ break;
133
+ case "out":
134
+ if (field === "nationality" && !neededCircuits.includes("exclusion_check_country")) {
135
+ neededCircuits.push("exclusion_check_country");
136
+ }
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ // From the circuits needed, determine the expected proof count
142
+ // There are at least 4 proofs, 3 base proofs and 1 disclosure proof minimum
143
+ // Each separate needed circuit adds 1 disclosure proof
144
+ this.topicToExpectedProofCount[topic] =
145
+ neededCircuits.length === 0 ? 4 : 3 + neededCircuits.length;
146
+ }
67
147
  /**
68
148
  * @notice Handle an encrypted message.
69
149
  * @param request The request.
@@ -96,19 +176,44 @@ export class ZKPassport {
96
176
  };
97
177
  this.topicToProofs[topic].push(processedProof);
98
178
  await Promise.all(this.onProofGeneratedCallbacks[topic].map((callback) => callback(processedProof)));
179
+ // If the results were received before all the proofs were generated,
180
+ // we can handle the result now
181
+ if (this.topicToResults[topic] &&
182
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
183
+ await this.handleResult(topic);
184
+ }
99
185
  }
100
186
  else if (request.method === "done") {
101
187
  logger.debug(`User sent the query result`);
102
- // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
103
- const { uniqueIdentifier, verified } = await this.verify(topic, this.topicToProofs[topic], request.params);
104
- await Promise.all(this.onResultCallbacks[topic].map((callback) => callback({
105
- uniqueIdentifier,
106
- verified,
107
- result: request.params,
108
- })));
188
+ this.topicToResults[topic] = request.params;
189
+ // Make sure all the proofs have been received, otherwise we'll handle the result later
190
+ // once the proofs have all been received
191
+ if (this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
192
+ await this.handleResult(topic);
193
+ }
109
194
  }
110
195
  else if (request.method === "error") {
111
- await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)));
196
+ const error = request.params.error;
197
+ if (error && error === "This ID is not supported yet") {
198
+ // This means the user has an ID that is not supported yet
199
+ // So we won't receive any proofs and we can handle the result now
200
+ this.topicToExpectedProofCount[topic] = 0;
201
+ if (this.topicToResults[topic]) {
202
+ await this.handleResult(topic);
203
+ }
204
+ }
205
+ else if (error && error.startsWith("Cannot generate proof")) {
206
+ // This means one of the disclosure proofs failed to be generated
207
+ // So we need to remove one from the expected proof count
208
+ this.topicToExpectedProofCount[topic] -= 1;
209
+ // If the expected proof count is now equal to the number of proofs received
210
+ // and the results were received, we can handle the result now
211
+ if (this.topicToResults[topic] &&
212
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
213
+ await this.handleResult(topic);
214
+ }
215
+ }
216
+ await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(error)));
112
217
  }
113
218
  }
114
219
  getZkPassportRequest(topic) {
@@ -164,6 +269,7 @@ export class ZKPassport {
164
269
  const base64Config = Buffer.from(JSON.stringify(this.topicToConfig[topic])).toString("base64");
165
270
  const base64Service = Buffer.from(JSON.stringify(this.topicToService[topic])).toString("base64");
166
271
  const pubkey = bytesToHex(this.topicToKeyPair[topic].publicKey);
272
+ this.setExpectedProofCount(topic);
167
273
  return {
168
274
  url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
169
275
  requestId: topic,
@@ -194,6 +300,7 @@ export class ZKPassport {
194
300
  this.topicToConfig[topic] = {};
195
301
  this.topicToService[topic] = { name, logo, purpose, scope };
196
302
  this.topicToProofs[topic] = [];
303
+ this.topicToExpectedProofCount[topic] = 0;
197
304
  this.onRequestReceivedCallbacks[topic] = [];
198
305
  this.onGeneratingProofCallbacks[topic] = [];
199
306
  this.onBridgeConnectCallbacks[topic] = [];
@@ -735,6 +842,16 @@ export class ZKPassport {
735
842
  isCorrect = false;
736
843
  break;
737
844
  }
845
+ // Check the countryList is in ascending order
846
+ // If the prover doesn't use a sorted list then the proof cannot be trusted
847
+ // as it is requirement in the circuit for the exclusion check to work
848
+ for (let i = 1; i < countryList.length; i++) {
849
+ if (countryList[i] < countryList[i - 1]) {
850
+ console.warn("The nationality exclusion list has not been sorted, and thus the proof cannot be trusted");
851
+ isCorrect = false;
852
+ break;
853
+ }
854
+ }
738
855
  uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10);
739
856
  }
740
857
  else if (proof.name === "inclusion_check_country") {
@@ -779,7 +896,7 @@ export class ZKPassport {
779
896
  proofsToVerify = this.topicToProofs[requestId];
780
897
  if (!proofsToVerify || proofsToVerify.length < 4) {
781
898
  // It may happen that a request returns a result without proofs
782
- // Meaning the ID is supported yet by ZKPassport circuits,
899
+ // Meaning the ID is not supported yet by ZKPassport circuits,
783
900
  // so the results has to be trusted and cannot be independently verified
784
901
  return { uniqueIdentifier: undefined, verified: false };
785
902
  }
@@ -841,6 +958,8 @@ export class ZKPassport {
841
958
  delete this.topicToConfig[requestId];
842
959
  delete this.topicToSharedSecret[requestId];
843
960
  delete this.topicToProofs[requestId];
961
+ delete this.topicToExpectedProofCount[requestId];
962
+ delete this.topicToResults[requestId];
844
963
  this.onRequestReceivedCallbacks[requestId] = [];
845
964
  this.onGeneratingProofCallbacks[requestId] = [];
846
965
  this.onBridgeConnectCallbacks[requestId] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkpassport/sdk",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Privacy-preserving identity verification using passports and ID cards",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  DisclosedData,
31
31
  formatName,
32
32
  getHostedPackagedCircuitByName,
33
+ Query,
33
34
  } from "@zkpassport/utils"
34
35
  import { bytesToHex } from "@noble/ciphers/utils"
35
36
  import { getWebSocketClient, WebSocketClient } from "./websocket"
@@ -43,6 +44,23 @@ import i18en from "i18n-iso-countries/langs/en.json"
43
44
 
44
45
  registerLocale(i18en)
45
46
 
47
+ function hasRequestedAccessToField(credentialsRequest: Query, field: IDCredential): boolean {
48
+ const fieldValue = credentialsRequest[field as keyof Query]
49
+ const isDefined = fieldValue !== undefined && fieldValue !== null
50
+ if (!isDefined) {
51
+ return false
52
+ }
53
+ for (const key in fieldValue) {
54
+ if (
55
+ fieldValue[key as keyof typeof fieldValue] !== undefined &&
56
+ fieldValue[key as keyof typeof fieldValue] !== null
57
+ ) {
58
+ return true
59
+ }
60
+ }
61
+ return false
62
+ }
63
+
46
64
  function normalizeCountry(country: CountryName | Alpha3Code) {
47
65
  let normalizedCountry: Alpha3Code | undefined
48
66
  const alpha3 = getAlpha3Code(country, "en") as Alpha3Code | undefined
@@ -242,6 +260,8 @@ export class ZKPassport {
242
260
  { name: string; logo: string; purpose: string; scope?: string }
243
261
  > = {}
244
262
  private topicToProofs: Record<string, Array<ProofResult>> = {}
263
+ private topicToExpectedProofCount: Record<string, number> = {}
264
+ private topicToResults: Record<string, QueryResult> = {}
245
265
 
246
266
  private onRequestReceivedCallbacks: Record<string, Array<() => void>> = {}
247
267
  private onGeneratingProofCallbacks: Record<string, Array<(topic: string) => void>> = {}
@@ -275,6 +295,79 @@ export class ZKPassport {
275
295
  this.wasmVerifierInit = true
276
296
  }*/
277
297
 
298
+ private async handleResult(topic: string) {
299
+ const result = this.topicToResults[topic]
300
+ // Clear the results straight away to avoid concurrency issues
301
+ delete this.topicToResults[topic]
302
+ // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
303
+ const { uniqueIdentifier, verified } = await this.verify(
304
+ topic,
305
+ this.topicToProofs[topic],
306
+ result,
307
+ )
308
+ await Promise.all(
309
+ this.onResultCallbacks[topic].map((callback) =>
310
+ callback({
311
+ uniqueIdentifier,
312
+ verified,
313
+ result,
314
+ }),
315
+ ),
316
+ )
317
+ // Clear the expected proof count
318
+ delete this.topicToExpectedProofCount[topic]
319
+ }
320
+
321
+ private setExpectedProofCount(topic: string) {
322
+ const fields = Object.keys(this.topicToConfig[topic] as Query).filter((key) =>
323
+ hasRequestedAccessToField(this.topicToConfig[topic] as Query, key as IDCredential),
324
+ )
325
+ const neededCircuits: string[] = []
326
+ // Determine which circuits are needed based on the requested fields
327
+ for (const field of fields) {
328
+ for (const key in this.topicToConfig[topic][field as IDCredential]) {
329
+ switch (key) {
330
+ case "eq":
331
+ case "disclose":
332
+ if (field !== "age" && !neededCircuits.includes("disclose_bytes")) {
333
+ neededCircuits.push("disclose_bytes")
334
+ } else if (field === "age" && !neededCircuits.includes("compare_age")) {
335
+ neededCircuits.push("compare_age")
336
+ }
337
+ break
338
+ case "gte":
339
+ case "gt":
340
+ case "lte":
341
+ case "lt":
342
+ case "range":
343
+ if (field === "age" && !neededCircuits.includes("compare_age")) {
344
+ neededCircuits.push("compare_age")
345
+ } else if (field === "expiry_date" && !neededCircuits.includes("compare_expiry")) {
346
+ neededCircuits.push("compare_expiry")
347
+ } else if (field === "birthdate" && !neededCircuits.includes("compare_birthdate")) {
348
+ neededCircuits.push("compare_birthdate")
349
+ }
350
+ break
351
+ case "in":
352
+ if (field === "nationality" && !neededCircuits.includes("inclusion_check_country")) {
353
+ neededCircuits.push("inclusion_check_country")
354
+ }
355
+ break
356
+ case "out":
357
+ if (field === "nationality" && !neededCircuits.includes("exclusion_check_country")) {
358
+ neededCircuits.push("exclusion_check_country")
359
+ }
360
+ break
361
+ }
362
+ }
363
+ }
364
+ // From the circuits needed, determine the expected proof count
365
+ // There are at least 4 proofs, 3 base proofs and 1 disclosure proof minimum
366
+ // Each separate needed circuit adds 1 disclosure proof
367
+ this.topicToExpectedProofCount[topic] =
368
+ neededCircuits.length === 0 ? 4 : 3 + neededCircuits.length
369
+ }
370
+
278
371
  /**
279
372
  * @notice Handle an encrypted message.
280
373
  * @param request The request.
@@ -311,27 +404,45 @@ export class ZKPassport {
311
404
  await Promise.all(
312
405
  this.onProofGeneratedCallbacks[topic].map((callback) => callback(processedProof)),
313
406
  )
407
+ // If the results were received before all the proofs were generated,
408
+ // we can handle the result now
409
+ if (
410
+ this.topicToResults[topic] &&
411
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length
412
+ ) {
413
+ await this.handleResult(topic)
414
+ }
314
415
  } else if (request.method === "done") {
315
416
  logger.debug(`User sent the query result`)
316
- // Verify the proofs and extract the unique identifier (aka nullifier) and the verification result
317
- const { uniqueIdentifier, verified } = await this.verify(
318
- topic,
319
- this.topicToProofs[topic],
320
- request.params,
321
- )
322
- await Promise.all(
323
- this.onResultCallbacks[topic].map((callback) =>
324
- callback({
325
- uniqueIdentifier,
326
- verified,
327
- result: request.params,
328
- }),
329
- ),
330
- )
417
+ this.topicToResults[topic] = request.params
418
+ // Make sure all the proofs have been received, otherwise we'll handle the result later
419
+ // once the proofs have all been received
420
+ if (this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length) {
421
+ await this.handleResult(topic)
422
+ }
331
423
  } else if (request.method === "error") {
332
- await Promise.all(
333
- this.onErrorCallbacks[topic].map((callback) => callback(request.params.error)),
334
- )
424
+ const error = request.params.error
425
+ if (error && error === "This ID is not supported yet") {
426
+ // This means the user has an ID that is not supported yet
427
+ // So we won't receive any proofs and we can handle the result now
428
+ this.topicToExpectedProofCount[topic] = 0
429
+ if (this.topicToResults[topic]) {
430
+ await this.handleResult(topic)
431
+ }
432
+ } else if (error && error.startsWith("Cannot generate proof")) {
433
+ // This means one of the disclosure proofs failed to be generated
434
+ // So we need to remove one from the expected proof count
435
+ this.topicToExpectedProofCount[topic] -= 1
436
+ // If the expected proof count is now equal to the number of proofs received
437
+ // and the results were received, we can handle the result now
438
+ if (
439
+ this.topicToResults[topic] &&
440
+ this.topicToExpectedProofCount[topic] === this.topicToProofs[topic].length
441
+ ) {
442
+ await this.handleResult(topic)
443
+ }
444
+ }
445
+ await Promise.all(this.onErrorCallbacks[topic].map((callback) => callback(error)))
335
446
  }
336
447
  }
337
448
 
@@ -396,6 +507,7 @@ export class ZKPassport {
396
507
  "base64",
397
508
  )
398
509
  const pubkey = bytesToHex(this.topicToKeyPair[topic].publicKey)
510
+ this.setExpectedProofCount(topic)
399
511
  return {
400
512
  url: `https://zkpassport.id/r?d=${this.domain}&t=${topic}&c=${base64Config}&s=${base64Service}&p=${pubkey}`,
401
513
  requestId: topic,
@@ -454,6 +566,7 @@ export class ZKPassport {
454
566
  this.topicToConfig[topic] = {}
455
567
  this.topicToService[topic] = { name, logo, purpose, scope }
456
568
  this.topicToProofs[topic] = []
569
+ this.topicToExpectedProofCount[topic] = 0
457
570
 
458
571
  this.onRequestReceivedCallbacks[topic] = []
459
572
  this.onGeneratingProofCallbacks[topic] = []
@@ -1108,6 +1221,18 @@ export class ZKPassport {
1108
1221
  isCorrect = false
1109
1222
  break
1110
1223
  }
1224
+ // Check the countryList is in ascending order
1225
+ // If the prover doesn't use a sorted list then the proof cannot be trusted
1226
+ // as it is requirement in the circuit for the exclusion check to work
1227
+ for (let i = 1; i < countryList.length; i++) {
1228
+ if (countryList[i] < countryList[i - 1]) {
1229
+ console.warn(
1230
+ "The nationality exclusion list has not been sorted, and thus the proof cannot be trusted",
1231
+ )
1232
+ isCorrect = false
1233
+ break
1234
+ }
1235
+ }
1111
1236
  uniqueIdentifier = getNullifierFromDisclosureProof(proofData).toString(10)
1112
1237
  } else if (proof.name === "inclusion_check_country") {
1113
1238
  commitmentIn = getCommitmentInFromDisclosureProof(proofData)
@@ -1161,7 +1286,7 @@ export class ZKPassport {
1161
1286
  proofsToVerify = this.topicToProofs[requestId]
1162
1287
  if (!proofsToVerify || proofsToVerify.length < 4) {
1163
1288
  // It may happen that a request returns a result without proofs
1164
- // Meaning the ID is supported yet by ZKPassport circuits,
1289
+ // Meaning the ID is not supported yet by ZKPassport circuits,
1165
1290
  // so the results has to be trusted and cannot be independently verified
1166
1291
  return { uniqueIdentifier: undefined, verified: false }
1167
1292
  }
@@ -1232,6 +1357,8 @@ export class ZKPassport {
1232
1357
  delete this.topicToConfig[requestId]
1233
1358
  delete this.topicToSharedSecret[requestId]
1234
1359
  delete this.topicToProofs[requestId]
1360
+ delete this.topicToExpectedProofCount[requestId]
1361
+ delete this.topicToResults[requestId]
1235
1362
  this.onRequestReceivedCallbacks[requestId] = []
1236
1363
  this.onGeneratingProofCallbacks[requestId] = []
1237
1364
  this.onBridgeConnectCallbacks[requestId] = []