diff --git a/.cspell.json b/.cspell.json index 0033611..b9f02b3 100644 --- a/.cspell.json +++ b/.cspell.json @@ -20,7 +20,9 @@ "kdfparams", "ciphertext", "cipherparams", - "codegen" + "codegen", + "hexlify", + "Arraylike" ], "flagWords": [], "ignorePaths": [ diff --git a/src/constants.ts b/src/constants.ts index bcac425..daa5f49 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -63,6 +63,6 @@ export const RLN_STORAGE_ABI = [ export const SEPOLIA_CONTRACT = { chainId: 11155111, - address: "0xF1935b338321013f11068abCafC548A7B0db732C", + address: "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4", abi: RLN_REGISTRY_ABI, }; diff --git a/src/keystore/keystore.spec.ts b/src/keystore/keystore.spec.ts index 9a385e3..cd28060 100644 --- a/src/keystore/keystore.spec.ts +++ b/src/keystore/keystore.spec.ts @@ -202,25 +202,25 @@ describe("Keystore", () => { const expectedHash = "9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548"; const identity = { - IDTrapdoor: [ + IDTrapdoor: new Uint8Array([ 211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244, 216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176, 126, 9, - ], - IDNullifier: [ + ]), + IDNullifier: new Uint8Array([ 238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9, 178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81, 42, - ], - IDSecretHash: [ + ]), + IDSecretHash: new Uint8Array([ 150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146, 101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124, 187, 4, - ], - IDCommitment: [ + ]), + IDCommitment: new Uint8Array([ 112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229, 58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15, - ], + ]), IDCommitmentBigInt: buildBigIntFromUint8Array( new Uint8Array([ 112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229, diff --git a/src/keystore/keystore.ts b/src/keystore/keystore.ts index 71b86c6..4232aad 100644 --- a/src/keystore/keystore.ts +++ b/src/keystore/keystore.ts @@ -245,13 +245,23 @@ export class Keystore { // TODO: add runtime validation of nwaku credentials return { identity: { - IDCommitment: _.get(obj, "identityCredential.idCommitment"), - IDTrapdoor: _.get(obj, "identityCredential.idTrapdoor"), - IDNullifier: _.get(obj, "identityCredential.idNullifier"), - IDCommitmentBigInt: buildBigIntFromUint8Array( - new Uint8Array(_.get(obj, "identityCredential.idCommitment", [])) + IDCommitment: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idCommitment", []) + ), + IDTrapdoor: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idTrapdoor", []) + ), + IDNullifier: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idNullifier", []) + ), + IDCommitmentBigInt: buildBigIntFromUint8Array( + Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idCommitment", []) + ) + ), + IDSecretHash: Keystore.fromArraylikeToBytes( + _.get(obj, "identityCredential.idSecretHash", []) ), - IDSecretHash: _.get(obj, "identityCredential.idSecretHash"), }, membership: { treeIndex: _.get(obj, "treeIndex"), @@ -265,6 +275,23 @@ export class Keystore { } } + private static fromArraylikeToBytes(obj: { + [key: number]: number; + }): Uint8Array { + const bytes = []; + + let index = 0; + let lastElement = obj[index]; + + while (lastElement !== undefined) { + bytes.push(lastElement); + index += 1; + lastElement = obj[index]; + } + + return new Uint8Array(bytes); + } + // follows nwaku implementation // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111 private static computeMembershipHash(info: MembershipInfo): MembershipHash { diff --git a/src/rln_contract.spec.ts b/src/rln_contract.spec.ts index 8f47652..fa02d7c 100644 --- a/src/rln_contract.spec.ts +++ b/src/rln_contract.spec.ts @@ -15,7 +15,7 @@ describe("RLN Contract abstraction", () => { const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address); const rlnContract = new rln.RLNContract(rlnInstance, { - address: rln.SEPOLIA_CONTRACT.address, + registryAddress: rln.SEPOLIA_CONTRACT.address, provider: voidSigner, }); @@ -39,7 +39,7 @@ describe("RLN Contract abstraction", () => { const rlnInstance = await rln.create(); const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address); const rlnContract = new rln.RLNContract(rlnInstance, { - address: rln.SEPOLIA_CONTRACT.address, + registryAddress: rln.SEPOLIA_CONTRACT.address, provider: voidSigner, }); @@ -49,12 +49,12 @@ describe("RLN Contract abstraction", () => { topics: [], } as unknown as ethers.EventFilter; rlnContract["registryContract"] = { - register: () => + "register(uint16,uint256)": () => Promise.resolve({ wait: () => Promise.resolve(undefined) }), } as unknown as ethers.Contract; const contractSpy = chai.spy.on( rlnContract["registryContract"], - "register" + "register(uint16,uint256)" ); await rlnContract.registerWithSignature(rlnInstance, mockSignature); @@ -66,8 +66,8 @@ describe("RLN Contract abstraction", () => { function mockEvent(): ethers.Event { return { args: { - pubkey: "0x9e7d3f8f8c7a1d2bef96a2e8dbb8e7c1ea9a9ab78d6b3c6c3c", - index: 1, + idCommitment: "0x9e7d3f8f8c7a1d2bef96a2e8dbb8e7c1ea9a9ab78d6b3c6c3c", + index: ethers.BigNumber.from(1), }, } as unknown as ethers.Event; } diff --git a/src/rln_contract.ts b/src/rln_contract.ts index 0f41ecc..3fe8bb4 100644 --- a/src/rln_contract.ts +++ b/src/rln_contract.ts @@ -5,17 +5,23 @@ import { IdentityCredential, RLNInstance } from "./rln.js"; import { MerkleRootTracker } from "./root_tracker.js"; type Member = { - pubkey: string; - index: number; + idCommitment: string; + index: ethers.BigNumber; }; type Provider = ethers.Signer | ethers.providers.Provider; -type ContractOptions = { - address: string; +type RLNContractOptions = { provider: Provider; + registryAddress: string; }; +type RLNStorageOptions = { + storageIndex?: number; +}; + +type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions; + type FetchMembersOptions = { fromBlock?: number; fetchRange?: number; @@ -31,11 +37,11 @@ export class RLNContract { private storageContract: undefined | ethers.Contract; private _membersFilter: undefined | ethers.EventFilter; - private _members: Member[] = []; + private _members: Map = new Map(); public static async init( rlnInstance: RLNInstance, - options: ContractOptions + options: RLNContractInitOptions ): Promise { const rlnContract = new RLNContract(rlnInstance, options); @@ -48,25 +54,34 @@ export class RLNContract { constructor( rlnInstance: RLNInstance, - { address, provider }: ContractOptions + { registryAddress, provider }: RLNContractOptions ) { const initialRoot = rlnInstance.getMerkleRoot(); this.registryContract = new ethers.Contract( - address, + registryAddress, RLN_REGISTRY_ABI, provider ); this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); } - private async initStorageContract(provider: Provider): Promise { - const index = await this.registryContract.usingStorageIndex(); - const address = await this.registryContract.storages(index); + private async initStorageContract( + provider: Provider, + options: RLNStorageOptions = {} + ): Promise { + const storageIndex = options?.storageIndex + ? options.storageIndex + : await this.registryContract.usingStorageIndex(); + const storageAddress = await this.registryContract.storages(storageIndex); - this.storageIndex = index; + if (!storageAddress || storageAddress === ethers.constants.AddressZero) { + throw Error("No RLN Storage initialized on registry contract."); + } + + this.storageIndex = storageIndex; this.storageContract = new ethers.Contract( - address, + storageAddress, RLN_STORAGE_ABI, provider ); @@ -77,13 +92,16 @@ export class RLNContract { public get contract(): ethers.Contract { if (!this.storageContract) { - throw Error("Storage contract was not initialized."); + throw Error("Storage contract was not initialized"); } return this.storageContract as ethers.Contract; } public get members(): Member[] { - return this._members; + const sortedMembers = Array.from(this._members.values()).sort( + (left, right) => left.index.toNumber() - right.index.toNumber() + ); + return sortedMembers; } private get membersFilter(): ethers.EventFilter { @@ -115,13 +133,13 @@ export class RLNContract { } if (evt.removed) { - const index: number = evt.args.index; + const index: ethers.BigNumber = evt.args.index; const toRemoveVal = toRemoveTable.get(evt.blockNumber); if (toRemoveVal != undefined) { - toRemoveVal.push(index); + toRemoveVal.push(index.toNumber()); toRemoveTable.set(evt.blockNumber, toRemoveVal); } else { - toRemoveTable.set(evt.blockNumber, [index]); + toRemoveTable.set(evt.blockNumber, [index.toNumber()]); } } else { let eventsPerBlock = toInsertTable.get(evt.blockNumber); @@ -144,18 +162,23 @@ export class RLNContract { ): void { toInsert.forEach((events: ethers.Event[], blockNumber: number) => { events.forEach((evt) => { - if (!evt.args) { + const _idCommitment = evt?.args?.idCommitment; + const index: ethers.BigNumber = evt?.args?.index; + + if (!_idCommitment || !index) { return; } - const pubkey = evt.args.pubkey; - const index = evt.args.index; const idCommitment = ethers.utils.zeroPad( - ethers.utils.arrayify(pubkey), + ethers.utils.arrayify(_idCommitment), 32 ); rlnInstance.insertMember(idCommitment); - this.members.push({ index, pubkey }); + this._members.set(index.toNumber(), { + index, + idCommitment: + _idCommitment?._hex || ethers.utils.hexlify(idCommitment), + }); }); const currentRoot = rlnInstance.getMerkleRoot(); @@ -170,9 +193,8 @@ export class RLNContract { 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); + if (this._members.has(index)) { + this._members.delete(index); } rlnInstance.deleteMember(index); }); @@ -183,14 +205,14 @@ export class RLNContract { public subscribeToMembers(rlnInstance: RLNInstance): void { this.contract.on(this.membersFilter, (_pubkey, _index, event) => - this.processEvents(rlnInstance, event) + this.processEvents(rlnInstance, [event]) ); } public async registerWithSignature( rlnInstance: RLNInstance, signature: string - ): Promise { + ): Promise { const identityCredential = await rlnInstance.generateSeededIdentityCredential(signature); @@ -199,23 +221,36 @@ export class RLNContract { public async registerWithKey( credential: IdentityCredential - ): Promise { - if (!this.storageIndex) { + ): Promise { + if (this.storageIndex === undefined) { throw Error( "Cannot register credential, no storage contract index found." ); } const txRegisterResponse: ethers.ContractTransaction = - await this.registryContract.register( + await this.registryContract["register(uint16,uint256)"]( this.storageIndex, credential.IDCommitmentBigInt, - { - gasLimit: 100000, - } + { gasLimit: 100000 } ); const txRegisterReceipt = await txRegisterResponse.wait(); - return txRegisterReceipt?.events?.[0]; + // assumption: register(uint16,uint256) emits one event + const memberRegistered = txRegisterReceipt?.events?.[0]; + + if (!memberRegistered) { + return undefined; + } + + const decodedData = this.contract.interface.decodeEventLog( + "MemberRegistered", + memberRegistered.data + ); + + return { + idCommitment: decodedData.idCommitment, + index: decodedData.index, + }; } public roots(): Uint8Array[] {