From 7e938965384ccac537fa8a8d85131c9f70aa43df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rich=CE=9Brd?= Date: Sun, 14 May 2023 12:18:21 -0400 Subject: [PATCH] feat: track roots in rln contract and use sepolia instead of goerli (#62) --- .cspell.json | 1 + example/index.js | 92 ++++++++++++++++-------------- package-lock.json | 4 +- package.json | 2 +- src/codec.spec.ts | 14 ++--- src/constants.ts | 8 +-- src/index.ts | 4 +- src/message.ts | 5 +- src/rln_contract.spec.ts | 12 ++-- src/rln_contract.ts | 117 ++++++++++++++++++++++++++++++--------- 10 files changed, 166 insertions(+), 93 deletions(-) diff --git a/.cspell.json b/.cspell.json index 53adc0f..0daa01b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,6 +9,7 @@ "merkle", "nwaku", "rlnjs", + "sepolia", "vkey", "Waku", "zerokit", diff --git a/example/index.js b/example/index.js index dde6b22..5762b9e 100644 --- a/example/index.js +++ b/example/index.js @@ -1,58 +1,64 @@ import * as rln from "@waku/rln"; -rln.create().then(async rlnInstance => { - const credentials = rlnInstance.generateIdentityCredentials(); +rln.create().then(async (rlnInstance) => { + const credentials = rlnInstance.generateIdentityCredentials(); - //peer's index in the Merkle Tree - const index = 5 + //peer's index in the Merkle Tree + const index = 5; - // Create a Merkle tree with random members - for (let i = 0; i < 10; i++) { - if (i == index) { - // insert the current peer's pk - rlnInstance.insertMember(credentials.IDCommitment); - } else { - // create a new key pair - const credentials = rlnInstance.generateIdentityCredentials(); // TODO: handle error - rlnInstance.insertMember(credentials.IDCommitment); - - } + // Create a Merkle tree with random members + for (let i = 0; i < 10; i++) { + if (i == index) { + // insert the current peer's pk + rlnInstance.insertMember(credentials.IDCommitment); + } else { + // create a new key pair + const credentials = rlnInstance.generateIdentityCredentials(); // TODO: handle error + rlnInstance.insertMember(credentials.IDCommitment); } + } - // prepare the message - const uint8Msg = Uint8Array.from("Hello World".split("").map(x => x.charCodeAt())); + // prepare the message + const uint8Msg = Uint8Array.from( + "Hello World".split("").map((x) => x.charCodeAt()) + ); - // setting up the epoch - const epoch = new Date(); + // setting up the epoch + const epoch = new Date(); - console.log("Generating proof..."); - console.time("proof_gen_timer"); - let proof = await rlnInstance.generateRLNProof(uint8Msg, index, epoch, credentials.IDSecretHash) - console.timeEnd("proof_gen_timer"); - console.log("Proof", proof) + console.log("Generating proof..."); + console.time("proof_gen_timer"); + let proof = await rlnInstance.generateRLNProof( + uint8Msg, + index, + epoch, + credentials.IDSecretHash + ); + console.timeEnd("proof_gen_timer"); + console.log("Proof", proof); - try { - // verify the proof - let verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg); - console.log("Is proof verified?", verifResult ? "yes" : "no"); - } catch (err) { - console.log("Invalid proof") - } + try { + // verify the proof + let verifResult = rlnInstance.verifyRLNProof(proof, uint8Msg); + console.log("Is proof verified?", verifResult ? "yes" : "no"); + } catch (err) { + console.log("Invalid proof"); + } - const provider = new ethers.providers.Web3Provider( - window.ethereum, - "any" - ); + const provider = new ethers.providers.Web3Provider(window.ethereum, "any"); - const DEFAULT_SIGNATURE_MESSAGE = - "The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp"; + const DEFAULT_SIGNATURE_MESSAGE = + "The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp"; - const signer = provider.getSigner(); - const signature = await signer.signMessage(DEFAULT_SIGNATURE_MESSAGE); - console.log(`Got signature: ${signature}`); + const signer = provider.getSigner(); + const signature = await signer.signMessage(DEFAULT_SIGNATURE_MESSAGE); + console.log(`Got signature: ${signature}`); - const contract = await rln.RLNContract.init(rlnInstance, {address: rln.GOERLI_CONTRACT.address, provider: signer }); + const contract = await rln.RLNContract.init(rlnInstance, { + address: rln.SEPOLIA_CONTRACT.address, + provider: signer, + }); - const event = await contract.registerMember(rlnInstance, signature); - console.log(`Registered as member with ${event}`); + const event = await contract.registerMember(rlnInstance, signature); + console.log(`Registered as member with ${event}`); }); diff --git a/package-lock.json b/package-lock.json index 44ca101..443c68f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@waku/rln", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@waku/rln", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT OR Apache-2.0", "dependencies": { "@waku/utils": "^0.0.5", diff --git a/package.json b/package.json index 8218e12..f01303b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@waku/rln", - "version": "0.1.0", + "version": "0.1.1", "description": "Rate Limit Nullifier for js-waku", "types": "./dist/index.d.ts", "module": "./dist/index.js", diff --git a/src/codec.spec.ts b/src/codec.spec.ts index 6a9c98f..9462d71 100644 --- a/src/codec.spec.ts +++ b/src/codec.spec.ts @@ -63,7 +63,7 @@ describe("RLN codec with version 0", () => { ))!; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -104,7 +104,7 @@ describe("RLN codec with version 0", () => { expect(msg).to.not.be.undefined; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -153,7 +153,7 @@ describe("RLN codec with version 1", () => { ))!; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -199,7 +199,7 @@ describe("RLN codec with version 1", () => { expect(msg).to.not.be.undefined; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -247,7 +247,7 @@ describe("RLN codec with version 1", () => { ))!; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -294,7 +294,7 @@ describe("RLN codec with version 1", () => { expect(msg).to.not.be.undefined; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch).to.be.gt(0); @@ -340,7 +340,7 @@ describe("RLN Codec - epoch", () => { expect(msg).to.not.be.undefined; expect(msg.rateLimitProof).to.not.be.undefined; - expect(msg.verify()).to.be.true; + expect(msg.verify([rlnInstance.getMerkleRoot()])).to.be.true; expect(msg.verifyNoRoot()).to.be.true; expect(msg.epoch).to.not.be.undefined; expect(msg.epoch!.toString(10).length).to.eq(9); diff --git a/src/constants.ts b/src/constants.ts index 9cbc7d7..49407de 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,9 +6,9 @@ export const RLN_ABI = [ "event MemberWithdrawn(uint256 pubkey, uint256 index)", ]; -export const GOERLI_CONTRACT = { - chainId: 5, - startBlock: 7109391, - address: "0x4252105670fe33d2947e8ead304969849e64f2a6", +export const SEPOLIA_CONTRACT = { + chainId: 11155111, + startBlock: 3193048, + address: "0x9C09146844C1326c2dBC41c451766C7138F88155", abi: RLN_ABI, }; diff --git a/src/index.ts b/src/index.ts index d127c19..7e379e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { RLNDecoder, RLNEncoder } from "./codec.js"; -import { GOERLI_CONTRACT, RLN_ABI } from "./constants.js"; +import { RLN_ABI, SEPOLIA_CONTRACT } from "./constants.js"; import { IdentityCredential, Proof, @@ -28,5 +28,5 @@ export { MerkleRootTracker, RLNContract, RLN_ABI, - GOERLI_CONTRACT, + SEPOLIA_CONTRACT, }; diff --git a/src/message.ts b/src/message.ts index 821940b..2e8afc6 100644 --- a/src/message.ts +++ b/src/message.ts @@ -22,11 +22,12 @@ export class RlnMessage implements IDecodedMessage { public rateLimitProof: IRateLimitProof | undefined ) {} - public verify(): boolean | undefined { + public verify(roots: Uint8Array[]): boolean | undefined { return this.rateLimitProof ? this.rlnInstance.verifyWithRoots( this.rateLimitProof, - toRLNSignal(this.msg.contentTopic, this.msg) + toRLNSignal(this.msg.contentTopic, this.msg), + ...roots ) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed : undefined; } diff --git a/src/rln_contract.spec.ts b/src/rln_contract.spec.ts index c6159e9..e9fd1d6 100644 --- a/src/rln_contract.spec.ts +++ b/src/rln_contract.spec.ts @@ -13,9 +13,9 @@ describe("RLN Contract abstraction", () => { rlnInstance.insertMember = () => undefined; const insertMemberSpy = chai.spy.on(rlnInstance, "insertMember"); - const voidSigner = new ethers.VoidSigner(rln.GOERLI_CONTRACT.address); - const rlnContract = new rln.RLNContract({ - address: rln.GOERLI_CONTRACT.address, + const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address); + const rlnContract = new rln.RLNContract(rlnInstance, { + address: rln.SEPOLIA_CONTRACT.address, provider: voidSigner, }); @@ -33,9 +33,9 @@ describe("RLN Contract abstraction", () => { "0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"; const rlnInstance = await rln.create(); - const voidSigner = new ethers.VoidSigner(rln.GOERLI_CONTRACT.address); - const rlnContract = new rln.RLNContract({ - address: rln.GOERLI_CONTRACT.address, + const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address); + const rlnContract = new rln.RLNContract(rlnInstance, { + address: rln.SEPOLIA_CONTRACT.address, provider: voidSigner, }); diff --git a/src/rln_contract.ts b/src/rln_contract.ts index b241e4b..56aefa8 100644 --- a/src/rln_contract.ts +++ b/src/rln_contract.ts @@ -2,6 +2,7 @@ import { ethers } from "ethers"; import { RLN_ABI } from "./constants.js"; import { IdentityCredential, RLNInstance } from "./rln.js"; +import { MerkleRootTracker } from "./root_tracker"; type Member = { pubkey: string; @@ -22,6 +23,7 @@ type FetchMembersOptions = { export class RLNContract { private _contract: ethers.Contract; private membersFilter: ethers.EventFilter; + private merkleRootTracker: MerkleRootTracker; private _members: Member[] = []; @@ -29,7 +31,7 @@ export class RLNContract { rlnInstance: RLNInstance, options: ContractOptions ): Promise { - const rlnContract = new RLNContract(options); + const rlnContract = new RLNContract(rlnInstance, options); await rlnContract.fetchMembers(rlnInstance); rlnContract.subscribeToMembers(rlnInstance); @@ -37,8 +39,14 @@ export class RLNContract { return rlnContract; } - constructor({ address, provider }: ContractOptions) { + constructor( + rlnInstance: RLNInstance, + { address, provider }: ContractOptions + ) { + const initialRoot = rlnInstance.getMerkleRoot(); + this._contract = new ethers.Contract(address, RLN_ABI, provider); + this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); this.membersFilter = this.contract.filters.MemberRegistered(); } @@ -58,38 +66,91 @@ export class RLNContract { ...options, membersFilter: this.membersFilter, }); + this.processEvents(rlnInstance, registeredMemberEvents); + } - for (const event of registeredMemberEvents) { - this.addMemberFromEvent(rlnInstance, event); - } + public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void { + const toRemoveTable = new Map(); + const toInsertTable = new Map(); + + events.forEach((evt) => { + if (!evt.args) { + return; + } + + if (evt.removed) { + const index: number = evt.args.index; + const toRemoveVal = toRemoveTable.get(evt.blockNumber); + if (toRemoveVal != undefined) { + toRemoveVal.push(index); + toRemoveTable.set(evt.blockNumber, toRemoveVal); + } else { + toRemoveTable.set(evt.blockNumber, [index]); + } + } else { + let eventsPerBlock = toInsertTable.get(evt.blockNumber); + if (eventsPerBlock == undefined) { + eventsPerBlock = []; + } + + eventsPerBlock.push(evt); + toInsertTable.set(evt.blockNumber, eventsPerBlock); + } + + this.removeMembers(rlnInstance, toRemoveTable); + this.insertMembers(rlnInstance, toInsertTable); + }); + } + + private insertMembers( + rlnInstance: RLNInstance, + toInsert: Map + ): void { + toInsert.forEach((events: ethers.Event[], blockNumber: number) => { + events.forEach((evt) => { + if (!evt.args) { + return; + } + + const pubkey = evt.args.pubkey; + const index = evt.args.index; + const idCommitment = ethers.utils.zeroPad( + ethers.utils.arrayify(pubkey), + 32 + ); + rlnInstance.insertMember(idCommitment); + this.members.push({ index, pubkey }); + }); + + const currentRoot = rlnInstance.getMerkleRoot(); + this.merkleRootTracker.pushRoot(blockNumber, currentRoot); + }); + } + + private removeMembers( + rlnInstance: RLNInstance, + toRemove: Map + ): void { + const removeDescending = new Map([...toRemove].sort().reverse()); + removeDescending.forEach((indexes: number[], blockNumber: number) => { + indexes.forEach((index) => { + const idx = this.members.findIndex((m) => m.index === index); + if (idx > -1) { + this.members.splice(idx, 1); + } + rlnInstance.deleteMember(index); + }); + + this.merkleRootTracker.backFill(blockNumber); + }); } public subscribeToMembers(rlnInstance: RLNInstance): void { this.contract.on(this.membersFilter, (_pubkey, _index, event) => - this.addMemberFromEvent(rlnInstance, event) + this.processEvents(rlnInstance, event) ); } - private addMemberFromEvent( - rlnInstance: RLNInstance, - event: ethers.Event - ): void { - if (!event.args) { - return; - } - - const pubkey: string = event.args.pubkey; - const index: number = event.args.index; - - this.members.push({ index, pubkey }); - - const idCommitment = ethers.utils.zeroPad( - ethers.utils.arrayify(pubkey), - 32 - ); - rlnInstance.insertMember(idCommitment); - } - public async registerWithSignature( rlnInstance: RLNInstance, signature: string @@ -113,6 +174,10 @@ export class RLNContract { return txRegisterReceipt?.events?.[0]; } + + public roots(): Uint8Array[] { + return this.merkleRootTracker.roots(); + } } type CustomQueryOptions = FetchMembersOptions & {