forked from Rate-Limiting-Nullifier/rlnjs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rln.ts
616 lines (573 loc) · 22.6 KB
/
rln.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
import { Identity } from '@semaphore-protocol/identity'
import { VerificationKey } from './types'
import { DEFAULT_MERKLE_TREE_DEPTH, calculateIdentitySecret, calculateSignalHash } from './common'
import { IRLNRegistry, ContractRLNRegistry } from './registry'
import { MemoryCache, EvaluatedProof, ICache, Status } from './cache'
import { IMessageIDCounter, MemoryMessageIDCounter } from './message-id-counter'
import { RLNFullProof, RLNProver, RLNVerifier } from './circuit-wrapper'
import { ethers } from 'ethers'
import { RLNContract } from './contract-wrapper'
import { getDefaultRLNParams, getDefaultWithdrawParams } from './resources'
// Ref: https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/55c7da2227b501175076bf73e3ff6dc512c4c813/circuits/rln.circom#L40
const LIMIT_BIT_SIZE = 16
const MAX_MESSAGE_LIMIT = (BigInt(1) << BigInt(LIMIT_BIT_SIZE)) - BigInt(1)
export interface IRLN {
/* Membership */
/**
* Register the user to the registry.
* @param userMessageLimit The message limit of the user.
* @param messageIDCounter The messageIDCounter is used to **safely** generate the latest messageID for the user.
* If not provided, a new `MemoryMessageIDCounter` is created.
*/
register(userMessageLimit: bigint, messageIDCounter?: IMessageIDCounter): Promise<void>
/**
* Withdraw the user from the registry.
*/
withdraw(): Promise<void>
/**
* Slash the user with the given secret.
* @param secretToBeSlashed The secret to be slashed.
* @param receiver The address of the slash reward receiver. If not provided,
* the signer will receive the reward.
*/
slash(secretToBeSlashed: bigint, receiver?: string): Promise<void>
/* Proof-related */
/**
* Create a proof for the given epoch and message.
* @param epoch the timestamp of the message
* @param message the message to be proved
*/
createProof(epoch: bigint, message: string): Promise<RLNFullProof>
/**
* Verify a RLNFullProof
* @param epoch the timestamp of the message
* @param message the message to be proved
* @param proof the RLNFullProof to be verified
*/
verifyProof(epoch: bigint, message: string, proof: RLNFullProof): Promise<boolean>
/**
* Save a proof to the cache and check if it's a spam.
* @param proof the RLNFullProof to save and detect spam
* @returns result of the check. It could be VALID if the proof hasn't been seen,
* or DUPLICATE if the proof has been seen before, else BREACH means it could be spam.
*/
saveProof(proof: RLNFullProof): Promise<EvaluatedProof>
}
/**
* RLN handles all operations for a RLN user, including registering, withdrawing, creating proof, verifying proof.
* Please use `RLN.create` or `RLN.createWithContractRegistry` to create a RLN instance instead of
* using the constructor.
*/
export class RLN implements IRLN {
// the unique identifier of the app using RLN
readonly rlnIdentifier: bigint
// the semaphore identity of the user
readonly identity: Identity
// the prover allows user to generate proof with the RLN circuit
private prover?: RLNProver
// the verifier allows user to verify proof with the RLN circuit
private verifier?: RLNVerifier
// the registry that stores the registered users
private registry: IRLNRegistry
// the cache that stores proofs added by the user with `addProof`, and detect spams automatically
private cache: ICache
// the messageIDCounter is used to **safely** generate the latest messageID for the user
public messageIDCounter?: IMessageIDCounter
constructor(args: {
/** Required */
/**
* The unique identifier of the app using RLN. The identifier must be unique for every app.
*/
rlnIdentifier: bigint
/**
* `IRegistry` that stores the registered users. If not provided, a new `ContractRLNRegistry` is created.
* @see {@link ContractRLNRegistry}
*/
registry: IRLNRegistry
/**
* `ICache` that stores proofs added by the user with `addProof`, and detect spams automatically.
* If not provided, a new `MemoryCache` is created.
* @see {@link MemoryCache}
*/
cache: ICache
/**
* Semaphore identity of the user. If not provided, a new `Identity` is created.
*/
identity: Identity
/** Optional */
/**
* File path of the RLN wasm file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
wasmFilePath?: string | Uint8Array
/**
* File path of the RLN final zkey file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
finalZkeyPath?: string | Uint8Array
/**
* Verification key of the RLN circuit. If not provided, `verifyProof` and `saveProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
verificationKey?: VerificationKey
}) {
this.rlnIdentifier = args.rlnIdentifier
this.registry = args.registry
this.cache = args.cache
this.identity = args.identity
if ((args.wasmFilePath === undefined || args.finalZkeyPath === undefined) && args.verificationKey === undefined) {
throw new Error(
'Either both `wasmFilePath` and `finalZkeyPath` must be supplied to generate proofs, ' +
'or `verificationKey` must be provided to verify proofs.',
)
}
if (args.wasmFilePath !== undefined && args.finalZkeyPath !== undefined) {
this.prover = new RLNProver(args.wasmFilePath, args.finalZkeyPath)
}
if (args.verificationKey !== undefined) {
this.verifier = new RLNVerifier(args.verificationKey)
}
}
/**
* Create RLN instance with a custom registry
*/
static async create(args: {
/** Required */
/**
* The unique identifier of the app using RLN. The identifier must be unique for every app.
*/
rlnIdentifier: bigint
/**
* `IRegistry` that stores the registered users.
* @see {@link IRegistry}
*/
registry: IRLNRegistry
/** Optional */
/**
* Semaphore identity of the user. If not provided, a new `Identity` is created.
*/
identity?: Identity
/**
* Tree depth of the merkle tree used by the circuit. If not provided, the default value will be used.
* @default 20
*/
treeDepth?: number
/**
* The maximum number of epochs that the cache can store. If not provided, the default value will be used.
* This is only used when `cache` is not provided.
* @default 100
* @see {@link MemoryCache}
*/
cacheSize?: number
/**
* `ICache` that stores proofs added by the user with `addProof`, and detect spams automatically.
* If not provided, a new `MemoryCache` is created.
* @see {@link MemoryCache}
*/
cache?: ICache
/**
* If all of `wasmFilePath`, `finalZkeyPath`, and `verificationKey` are not given, default ones according to
* the `treeDepth` are used.
*/
/**
* File path of the RLN wasm file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
wasmFilePath?: string | Uint8Array
/**
* File path of the RLN final zkey file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
finalZkeyPath?: string | Uint8Array
/**
* Verification key of the RLN circuit. If not provided, `verifyProof` and `saveProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
verificationKey?: VerificationKey
}) {
if (args.rlnIdentifier < 0) {
throw new Error('rlnIdentifier must be positive')
}
if (args.treeDepth !== undefined && args.treeDepth <= 0) {
throw new Error('treeDepth must be positive')
}
if (args.cacheSize !== undefined && args.cacheSize <= 0) {
throw new Error('cacheSize must be positive')
}
const rlnIdentifier = args.rlnIdentifier
const registry = args.registry
const cache = args.cache ? args.cache : new MemoryCache(args.cacheSize)
const identity = args.identity ? args.identity : new Identity()
const treeDepth = args.treeDepth ? args.treeDepth : DEFAULT_MERKLE_TREE_DEPTH
// If `args.treeDepth` is given, `wasmFilePath`, `finalZkeyPath`, and `verificationKey` will be
// set to default first
// If all params are not given, use the default
let wasmFilePath: string | Uint8Array | undefined
let finalZkeyPath: string | Uint8Array | undefined
let verificationKey: VerificationKey | undefined
// If `args.wasmFilePath`, `args.finalZkeyPath`, and `args.verificationKey` are not given, see if we have defaults that can be used
if (args.wasmFilePath === undefined && args.finalZkeyPath === undefined && args.verificationKey === undefined) {
const defaultParams = await getDefaultRLNParams(treeDepth)
if (defaultParams !== undefined) {
wasmFilePath = defaultParams.wasmFile
finalZkeyPath = defaultParams.finalZkey
verificationKey = defaultParams.verificationKey
}
} else {
// Else, use the given params even if it is not complete
wasmFilePath = args.wasmFilePath
finalZkeyPath = args.finalZkeyPath
verificationKey = args.verificationKey
}
return new RLN({
rlnIdentifier,
registry,
identity,
cache,
wasmFilePath,
finalZkeyPath,
verificationKey,
})
}
/**
* Create RLN instance, using a deployed RLN contract as registry.
*/
static async createWithContractRegistry(args: {
/** Required */
/**
* The unique identifier of the app using RLN. The identifier must be unique for every app.
*/
rlnIdentifier: bigint
/**
* The ethers provider that is used to interact with the RLN contract.
* @see {@link https://docs.ethers.io/v5/api/providers/}
*/
provider: ethers.Provider
/**
* The address of the RLN contract.
*/
contractAddress: string
/** Optional */
/**
* The ethers signer that is used to interact with the RLN contract. If not provided,
* user can only do read-only operations. Functions like `register` and `withdraw` will not work
* since they need to send transactions to interact with the RLN contract.
* @see {@link https://docs.ethers.io/v5/api/signer/#Signer}
*/
signer?: ethers.Signer,
/**
* The block number where the RLN contract is deployed. If not provided, `0` will be used.
* @default 0
* @see {@link https://docs.ethers.io/v5/api/providers/provider/#Provider-getLogs}
*/
contractAtBlock?: number,
/**
* Semaphore identity of the user. If not provided, a new `Identity` is created.
*/
identity?: Identity
// File paths of the wasm and zkey file. If not provided, `createProof` will not work.
/**
* File path of the RLN wasm file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
wasmFilePath?: string | Uint8Array
/**
* File path of the RLN final zkey file. If not provided, `createProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
finalZkeyPath?: string | Uint8Array
/**
* Verification key of the RLN circuit. If not provided, `verifyProof` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/rln.circom}
*/
verificationKey?: VerificationKey
/**
* Tree depth of the merkle tree used by the circuit. If not provided, the default value will be used.
* @default 20
*/
treeDepth?: number
/* Registry configs */
/**
* File path of the wasm file for withdraw circuit. If not provided, `withdraw` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/withdraw.circom}
*/
withdrawWasmFilePath?: string | Uint8Array,
/**
* File path of the final zkey file for withdraw circuit. If not provided, `withdraw` will not work.
* @see {@link https://github.com/Rate-Limiting-Nullifier/circom-rln/blob/main/circuits/withdraw.circom}
*/
withdrawFinalZkeyPath?: string | Uint8Array,
/** Others */
/**
* `ICache` that stores proofs added by the user with `addProof`, and detect spams automatically.
* If not provided, a new `MemoryCache` is created.
* @see {@link MemoryCache}
*/
cache?: ICache
/**
* The maximum number of epochs that the cache can store. If not provided, the default value will be used.
* This is only used when `cache` is not provided.
* @default 100
* @see {@link MemoryCache}
*/
cacheSize?: number
}) {
const rlnContractWrapper = new RLNContract({
provider: args.provider,
signer: args.signer,
contractAddress: args.contractAddress,
contractAtBlock: args.contractAtBlock ? args.contractAtBlock : 0,
})
const treeDepth = args.treeDepth ? args.treeDepth : DEFAULT_MERKLE_TREE_DEPTH
// If all params are not given, use the default
let withdrawWasmFilePath: string | Uint8Array | undefined
let withdrawFinalZkeyPath: string | Uint8Array | undefined
// If `args.withdrawWasmFilePath`, `args.finalZkeyPath`, see if we have defaults that can be used
if (args.withdrawWasmFilePath === undefined && args.withdrawFinalZkeyPath === undefined) {
const defaultParams = await getDefaultWithdrawParams()
if (defaultParams !== undefined) {
withdrawWasmFilePath = defaultParams.wasmFile
withdrawFinalZkeyPath = defaultParams.finalZkey
}
} else {
// Else, use the given params even if it is not complete
withdrawWasmFilePath = args.withdrawWasmFilePath
withdrawFinalZkeyPath = args.withdrawFinalZkeyPath
}
const registry = new ContractRLNRegistry({
rlnIdentifier: args.rlnIdentifier,
rlnContract: rlnContractWrapper,
treeDepth,
withdrawWasmFilePath: withdrawWasmFilePath,
withdrawFinalZkeyPath: withdrawFinalZkeyPath,
})
const argsWithRegistry = {
...args,
registry,
}
return RLN.create(argsWithRegistry)
}
/**
* Set a custom messageIDCounter
* @param messageIDCounter The custom messageIDCounter
*/
async setMessageIDCounter(messageIDCounter?: IMessageIDCounter) {
if (await this.isRegistered() === false) {
throw new Error('Cannot set messageIDCounter for an unregistered user.')
}
if (messageIDCounter !== undefined) {
this.messageIDCounter = messageIDCounter
} else {
const userMessageLimit = await this.registry.getMessageLimit(this.identityCommitment)
this.messageIDCounter = new MemoryMessageIDCounter(userMessageLimit)
}
}
/**
* Set a custom cache
* @param cache The custom cache
*/
setCache(cache: ICache) {
this.cache = cache
}
/**
* Set a custom registry
* @param registry The custom registry
*/
setRegistry(registry: IRLNRegistry) {
this.registry = registry
}
/**
* Get the latest merkle root of the registry.
* @returns the latest merkle root of the registry
*/
async getMerkleRoot(): Promise<bigint> {
return this.registry.getMerkleRoot()
}
/**
* Get the identity commitment of the user.
*/
get identityCommitment(): bigint {
return this.identity.commitment
}
private get identitySecret(): bigint {
return calculateIdentitySecret(this.identity)
}
/**
* Get the rate commitment of the user, i.e. hash(identitySecret, messageLimit)
* @returns the rate commitment
*/
async getRateCommitment(): Promise<bigint> {
return this.registry.getRateCommitment(this.identityCommitment)
}
/**
* @returns the user has been registered or not
*/
async isRegistered(): Promise<boolean> {
return this.registry.isRegistered(this.identityCommitment)
}
/**
* @returns all rate commitments in the registry
*/
async getAllRateCommitments(): Promise<bigint[]> {
return this.registry.getAllRateCommitments()
}
/**
* User registers to the registry.
* @param userMessageLimit the maximum number of messages that the user can send in one epoch
* @param messageIDCounter the messageIDCounter that the user wants to use. If not provided, a new `MemoryMessageIDCounter` is created.
*/
async register(userMessageLimit: bigint, messageIDCounter?: IMessageIDCounter) {
if (userMessageLimit <= BigInt(0) || userMessageLimit > MAX_MESSAGE_LIMIT) {
throw new Error(
`userMessageLimit must be in range (0, ${MAX_MESSAGE_LIMIT}]. Got ${userMessageLimit}.`,
)
}
await this.registry.register(this.identityCommitment, userMessageLimit)
this.messageIDCounter = messageIDCounter ? messageIDCounter : new MemoryMessageIDCounter(userMessageLimit)
}
/**
* User withdraws from the registry. User will not receive the funds immediately,
* they need to wait `freezePeriod + 1` blocks and call `releaseWithdrawal` to get the funds.
*/
async withdraw() {
await this.registry.withdraw(this.identitySecret)
}
/**
* Release the funds from the pending withdrawal requested by `withdraw`.
*/
async releaseWithdrawal() {
await this.registry.releaseWithdrawal(this.identityCommitment)
this.messageIDCounter = undefined
}
/**
* Slash a user by its identity secret.
* @param secretToBeSlashed the identity secret of the user to be slashed
* @param receiver the receiver of the slashed funds. If not provided, the funds will be sent to
* the `signer` given in the constructor.
*/
async slash(secretToBeSlashed: bigint, receiver?: string) {
await this.registry.slash(secretToBeSlashed, receiver)
}
/**
* Create a proof for the given epoch and message.
* @param epoch the epoch to create the proof for
* @param message the message to create the proof for
* @returns the RLNFullProof
*/
async createProof(epoch: bigint, message: string): Promise<RLNFullProof> {
if (epoch < 0) {
throw new Error('epoch cannot be negative')
}
if (this.prover === undefined) {
throw new Error('Prover is not initialized')
}
if (!await this.isRegistered()) {
throw new Error('User has not registered before')
}
if (this.messageIDCounter === undefined) {
throw new Error(
'State is not synced with the registry. ' +
'If user is currently registered, `messageIDCounter` should be non-undefined',
)
}
const merkleProof = await this.registry.generateMerkleProof(this.identityCommitment)
// NOTE: get the message id and increment the counter.
// Even if the message is not sent, the counter is still incremented.
// It's intended to avoid any possibly for user to reuse the same message id.
const messageId = await this.messageIDCounter.getMessageIDAndIncrement(epoch)
const userMessageLimit = await this.registry.getMessageLimit(this.identityCommitment)
const proof = await this.prover.generateProof({
rlnIdentifier: this.rlnIdentifier,
identitySecret: this.identitySecret,
userMessageLimit: userMessageLimit,
messageId,
merkleProof,
x: calculateSignalHash(message),
epoch,
})
// Double check if the proof will spam or not using the cache.
// Even if messageIDCounter is used, it is possible that the user restart and the counter is reset.
const res = await this.checkProof(proof)
if (res.status === Status.DUPLICATE) {
throw new Error('Proof has been generated before')
} else if (res.status === Status.BREACH) {
throw new Error('Proof will spam')
} else if (res.status === Status.VALID) {
const resSaveProof = await this.saveProof(proof)
if (resSaveProof.status !== res.status) {
// Sanity check
throw new Error('Status of save proof and check proof mismatch')
}
return proof
} else {
// Sanity check
throw new Error('Unknown status')
}
}
/**
* Verify a proof is valid and indeed for `epoch` and `message`.
* @param epoch the epoch to verify the proof for
* @param message the message to verify the proof for
* @param proof the RLNFullProof to verify
* @returns true if the proof is valid, false otherwise
*/
async verifyProof(epoch: bigint, message: string, proof: RLNFullProof): Promise<boolean> {
if (epoch < BigInt(0)) {
throw new Error('epoch cannot be negative')
}
if (this.verifier === undefined) {
throw new Error('Verifier is not initialized')
}
// Check if the proof is using the same parameters
const snarkProof = proof.snarkProof
const epochInProof = proof.epoch
const rlnIdentifier = proof.rlnIdentifier
const { root, x } = snarkProof.publicSignals
// Check if the proof is using the same rlnIdentifier
if (BigInt(rlnIdentifier) !== this.rlnIdentifier) {
return false
}
// Check if the proof is using the same epoch
if (BigInt(epochInProof) !== epoch) {
return false
}
// Check if the proof and message match
const messageToX = calculateSignalHash(message)
if (BigInt(x) !== messageToX) {
return false
}
// Check if the merkle root is the same as the registry's
const registryMerkleRoot = await this.registry.getMerkleRoot()
if (BigInt(root) !== registryMerkleRoot) {
return false
}
// Verify snark proof
return this.verifier.verifyProof(rlnIdentifier, proof)
}
/**
* Save a proof to the cache and check if it's a spam.
* @param proof the RLNFullProof to save and detect spam
* @returns result of the check. `status` could be status.VALID if the proof is not a spam or invalid.
* Otherwise, it will be status.DUPLICATE or status.BREACH.
*/
async saveProof(proof: RLNFullProof): Promise<EvaluatedProof> {
const { snarkProof, epoch } = proof
const { x, y, nullifier } = snarkProof.publicSignals
return this.cache.addProof({ x, y, nullifier, epoch })
}
private async checkProof(proof: RLNFullProof): Promise<EvaluatedProof> {
const { snarkProof, epoch } = proof
const { x, y, nullifier } = snarkProof.publicSignals
return this.cache.checkProof({ x, y, nullifier, epoch })
}
/**
* Clean up the worker threads used by the prover and verifier in snarkjs
* This function should be called when the user is done with the library
* and wants to clean up the worker threads.
*
* Ref: https://github.com/iden3/snarkjs/issues/152#issuecomment-1164821515
*/
static cleanUp() {
globalThis.curve_bn128.terminate()
}
}