@waku/rln 0.1.1-9b1e818 → 0.1.1-ac12f68

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/src/rln.ts CHANGED
@@ -1,10 +1,28 @@
1
+ import { createDecoder, createEncoder } from "@waku/core";
1
2
  import type { IRateLimitProof } from "@waku/interfaces";
3
+ import type {
4
+ ContentTopic,
5
+ IDecodedMessage,
6
+ EncoderOptions as WakuEncoderOptions,
7
+ } from "@waku/interfaces";
2
8
  import init from "@waku/zerokit-rln-wasm";
3
9
  import * as zerokitRLN from "@waku/zerokit-rln-wasm";
10
+ import { ethers } from "ethers";
4
11
 
5
12
  import { buildBigIntFromUint8Array, writeUIntLE } from "./byte_utils.js";
13
+ import type { RLNDecoder, RLNEncoder } from "./codec.js";
14
+ import { createRLNDecoder, createRLNEncoder } from "./codec.js";
15
+ import { SEPOLIA_CONTRACT } from "./constants.js";
6
16
  import { dateToEpoch, epochIntToBytes } from "./epoch.js";
17
+ import { Keystore } from "./keystore/index.js";
18
+ import type {
19
+ DecryptedCredentials,
20
+ EncryptedCredentials,
21
+ } from "./keystore/index.js";
22
+ import { KeystoreEntity, Password } from "./keystore/types.js";
23
+ import { extractMetaMaskSigner } from "./metamask.js";
7
24
  import verificationKey from "./resources/verification_key.js";
25
+ import { RLNContract } from "./rln_contract.js";
8
26
  import * as wc from "./witness_calculator.js";
9
27
  import { WitnessCalculator } from "./witness_calculator.js";
10
28
 
@@ -158,12 +176,229 @@ export function sha256(input: Uint8Array): Uint8Array {
158
176
  return zerokitRLN.hash(lenPrefixedData);
159
177
  }
160
178
 
179
+ type StartRLNOptions = {
180
+ /**
181
+ * If not set - will extract MetaMask account and get signer from it.
182
+ */
183
+ signer?: ethers.Signer;
184
+ /**
185
+ * If not set - will use default SEPOLIA_CONTRACT address.
186
+ */
187
+ registryAddress?: string;
188
+ /**
189
+ * Credentials to use for generating proofs and connecting to the contract and network.
190
+ * If provided used for validating the network chainId and connecting to registry contract.
191
+ */
192
+ credentials?: EncryptedCredentials | DecryptedCredentials;
193
+ };
194
+
195
+ type RegisterMembershipOptions =
196
+ | { signature: string }
197
+ | { identity: IdentityCredential };
198
+
199
+ type WakuRLNEncoderOptions = WakuEncoderOptions & {
200
+ credentials: EncryptedCredentials | DecryptedCredentials;
201
+ };
202
+
161
203
  export class RLNInstance {
204
+ private started = false;
205
+ private starting = false;
206
+
207
+ private _contract: undefined | RLNContract;
208
+ private _signer: undefined | ethers.Signer;
209
+
210
+ private keystore = Keystore.create();
211
+ private _credentials: undefined | DecryptedCredentials;
212
+
162
213
  constructor(
163
214
  private zkRLN: number,
164
215
  private witnessCalculator: WitnessCalculator
165
216
  ) {}
166
217
 
218
+ public get contract(): undefined | RLNContract {
219
+ return this._contract;
220
+ }
221
+
222
+ public get signer(): undefined | ethers.Signer {
223
+ return this._signer;
224
+ }
225
+
226
+ public async start(options: StartRLNOptions = {}): Promise<void> {
227
+ if (this.started || this.starting) {
228
+ return;
229
+ }
230
+
231
+ this.starting = true;
232
+
233
+ try {
234
+ const { credentials, keystore } =
235
+ await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
236
+ const { signer, registryAddress } = await this.determineStartOptions(
237
+ options,
238
+ credentials
239
+ );
240
+
241
+ if (keystore) {
242
+ this.keystore = keystore;
243
+ }
244
+
245
+ this._credentials = credentials;
246
+ this._signer = signer!;
247
+ this._contract = await RLNContract.init(this, {
248
+ registryAddress: registryAddress!,
249
+ signer: signer!,
250
+ });
251
+ this.started = true;
252
+ } finally {
253
+ this.starting = false;
254
+ }
255
+ }
256
+
257
+ private async determineStartOptions(
258
+ options: StartRLNOptions,
259
+ credentials: KeystoreEntity | undefined
260
+ ): Promise<StartRLNOptions> {
261
+ let chainId = credentials?.membership.chainId;
262
+ const registryAddress =
263
+ credentials?.membership.address ||
264
+ options.registryAddress ||
265
+ SEPOLIA_CONTRACT.address;
266
+
267
+ if (registryAddress === SEPOLIA_CONTRACT.address) {
268
+ chainId = SEPOLIA_CONTRACT.chainId;
269
+ }
270
+
271
+ const signer = options.signer || (await extractMetaMaskSigner());
272
+ const currentChainId = await signer.getChainId();
273
+
274
+ if (chainId && chainId !== currentChainId) {
275
+ throw Error(
276
+ `Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}`
277
+ );
278
+ }
279
+
280
+ return {
281
+ signer,
282
+ registryAddress,
283
+ };
284
+ }
285
+
286
+ private static async decryptCredentialsIfNeeded(
287
+ credentials?: EncryptedCredentials | DecryptedCredentials
288
+ ): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
289
+ if (!credentials) {
290
+ return {};
291
+ }
292
+
293
+ if ("identity" in credentials) {
294
+ return { credentials };
295
+ }
296
+
297
+ const keystore = Keystore.fromString(credentials.keystore);
298
+
299
+ if (!keystore) {
300
+ return {};
301
+ }
302
+
303
+ const decryptedCredentials = await keystore.readCredential(
304
+ credentials.id,
305
+ credentials.password
306
+ );
307
+
308
+ return {
309
+ keystore,
310
+ credentials: decryptedCredentials,
311
+ };
312
+ }
313
+
314
+ public async registerMembership(
315
+ options: RegisterMembershipOptions
316
+ ): Promise<undefined | DecryptedCredentials> {
317
+ if (!this.contract) {
318
+ throw Error("RLN Contract is not initialized.");
319
+ }
320
+
321
+ let identity = "identity" in options && options.identity;
322
+
323
+ if ("signature" in options) {
324
+ identity = await this.generateSeededIdentityCredential(options.signature);
325
+ }
326
+
327
+ if (!identity) {
328
+ throw Error("Missing signature or identity to register membership.");
329
+ }
330
+
331
+ return this.contract.registerWithIdentity(identity);
332
+ }
333
+
334
+ /**
335
+ * Changes credentials in use by relying on provided Keystore earlier in rln.start
336
+ * @param id: string, hash of credentials to select from Keystore
337
+ * @param password: string or bytes to use to decrypt credentials from Keystore
338
+ */
339
+ public async useCredentials(id: string, password: Password): Promise<void> {
340
+ this._credentials = await this.keystore?.readCredential(id, password);
341
+ }
342
+
343
+ public async createEncoder(
344
+ options: WakuRLNEncoderOptions
345
+ ): Promise<RLNEncoder> {
346
+ const { credentials: decryptedCredentials } =
347
+ await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
348
+ const credentials = decryptedCredentials || this._credentials;
349
+
350
+ if (!credentials) {
351
+ throw Error(
352
+ "Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly."
353
+ );
354
+ }
355
+
356
+ await this.verifyCredentialsAgainstContract(credentials);
357
+
358
+ return createRLNEncoder({
359
+ encoder: createEncoder(options),
360
+ rlnInstance: this,
361
+ index: credentials.membership.treeIndex,
362
+ credential: credentials.identity,
363
+ });
364
+ }
365
+
366
+ private async verifyCredentialsAgainstContract(
367
+ credentials: KeystoreEntity
368
+ ): Promise<void> {
369
+ if (!this._contract) {
370
+ throw Error(
371
+ "Failed to verify chain coordinates: no contract initialized."
372
+ );
373
+ }
374
+
375
+ const registryAddress = credentials.membership.address;
376
+ const currentRegistryAddress = this._contract.registry.address;
377
+ if (registryAddress !== currentRegistryAddress) {
378
+ throw Error(
379
+ `Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}`
380
+ );
381
+ }
382
+
383
+ const chainId = credentials.membership.chainId;
384
+ const network = await this._contract.registry.getNetwork();
385
+ const currentChainId = network.chainId;
386
+ if (chainId !== currentChainId) {
387
+ throw Error(
388
+ `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}`
389
+ );
390
+ }
391
+ }
392
+
393
+ public createDecoder(
394
+ contentTopic: ContentTopic
395
+ ): RLNDecoder<IDecodedMessage> {
396
+ return createRLNDecoder({
397
+ rlnInstance: this,
398
+ decoder: createDecoder(contentTopic),
399
+ });
400
+ }
401
+
167
402
  generateIdentityCredentials(): IdentityCredential {
168
403
  const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
169
404
  return IdentityCredential.fromBytes(memKeys);
@@ -3,7 +3,8 @@ import { ethers } from "ethers";
3
3
 
4
4
  import { zeroPadLE } from "./byte_utils.js";
5
5
  import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
6
- import { IdentityCredential, RLNInstance } from "./rln.js";
6
+ import type { DecryptedCredentials } from "./keystore/index.js";
7
+ import { type IdentityCredential, RLNInstance } from "./rln.js";
7
8
  import { MerkleRootTracker } from "./root_tracker.js";
8
9
 
9
10
  type Member = {
@@ -11,10 +12,10 @@ type Member = {
11
12
  index: ethers.BigNumber;
12
13
  };
13
14
 
14
- type Provider = ethers.Signer | ethers.providers.Provider;
15
+ type Signer = ethers.Signer;
15
16
 
16
17
  type RLNContractOptions = {
17
- provider: Provider;
18
+ signer: Signer;
18
19
  registryAddress: string;
19
20
  };
20
21
 
@@ -47,7 +48,7 @@ export class RLNContract {
47
48
  ): Promise<RLNContract> {
48
49
  const rlnContract = new RLNContract(rlnInstance, options);
49
50
 
50
- await rlnContract.initStorageContract(options.provider);
51
+ await rlnContract.initStorageContract(options.signer);
51
52
  await rlnContract.fetchMembers(rlnInstance);
52
53
  rlnContract.subscribeToMembers(rlnInstance);
53
54
 
@@ -56,20 +57,20 @@ export class RLNContract {
56
57
 
57
58
  constructor(
58
59
  rlnInstance: RLNInstance,
59
- { registryAddress, provider }: RLNContractOptions
60
+ { registryAddress, signer }: RLNContractOptions
60
61
  ) {
61
62
  const initialRoot = rlnInstance.getMerkleRoot();
62
63
 
63
64
  this.registryContract = new ethers.Contract(
64
65
  registryAddress,
65
66
  RLN_REGISTRY_ABI,
66
- provider
67
+ signer
67
68
  );
68
69
  this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
69
70
  }
70
71
 
71
72
  private async initStorageContract(
72
- provider: Provider,
73
+ signer: Signer,
73
74
  options: RLNStorageOptions = {}
74
75
  ): Promise<void> {
75
76
  const storageIndex = options?.storageIndex
@@ -85,13 +86,20 @@ export class RLNContract {
85
86
  this.storageContract = new ethers.Contract(
86
87
  storageAddress,
87
88
  RLN_STORAGE_ABI,
88
- provider
89
+ signer
89
90
  );
90
91
  this._membersFilter = this.storageContract.filters.MemberRegistered();
91
92
 
92
93
  this.deployBlock = await this.storageContract.deployedBlockNumber();
93
94
  }
94
95
 
96
+ public get registry(): ethers.Contract {
97
+ if (!this.registryContract) {
98
+ throw Error("Registry contract was not initialized");
99
+ }
100
+ return this.registryContract as ethers.Contract;
101
+ }
102
+
95
103
  public get contract(): ethers.Contract {
96
104
  if (!this.storageContract) {
97
105
  throw Error("Storage contract was not initialized");
@@ -207,19 +215,9 @@ export class RLNContract {
207
215
  );
208
216
  }
209
217
 
210
- public async registerWithSignature(
211
- rlnInstance: RLNInstance,
212
- signature: string
213
- ): Promise<Member | undefined> {
214
- const identityCredential =
215
- await rlnInstance.generateSeededIdentityCredential(signature);
216
-
217
- return this.registerWithKey(identityCredential);
218
- }
219
-
220
- public async registerWithKey(
221
- credential: IdentityCredential
222
- ): Promise<Member | undefined> {
218
+ public async registerWithIdentity(
219
+ identity: IdentityCredential
220
+ ): Promise<DecryptedCredentials | undefined> {
223
221
  if (this.storageIndex === undefined) {
224
222
  throw Error(
225
223
  "Cannot register credential, no storage contract index found."
@@ -228,7 +226,7 @@ export class RLNContract {
228
226
  const txRegisterResponse: ethers.ContractTransaction =
229
227
  await this.registryContract["register(uint16,uint256)"](
230
228
  this.storageIndex,
231
- credential.IDCommitmentBigInt,
229
+ identity.IDCommitmentBigInt,
232
230
  { gasLimit: 100000 }
233
231
  );
234
232
  const txRegisterReceipt = await txRegisterResponse.wait();
@@ -245,9 +243,17 @@ export class RLNContract {
245
243
  memberRegistered.data
246
244
  );
247
245
 
246
+ const network = await this.registryContract.provider.getNetwork();
247
+ const address = this.registryContract.address;
248
+ const membershipId = decodedData.index.toNumber();
249
+
248
250
  return {
249
- idCommitment: decodedData.idCommitment,
250
- index: decodedData.index,
251
+ identity,
252
+ membership: {
253
+ address,
254
+ treeIndex: membershipId,
255
+ chainId: network.chainId,
256
+ },
251
257
  };
252
258
  }
253
259