2023-01-26 18:58:18 +01:00
|
|
|
import { ethers } from "ethers";
|
|
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
|
2023-05-08 18:10:26 -04:00
|
|
|
import { IdentityCredential, RLNInstance } from "./rln.js";
|
2023-05-23 07:08:59 -04:00
|
|
|
import { MerkleRootTracker } from "./root_tracker.js";
|
2023-01-26 18:58:18 +01:00
|
|
|
|
|
|
|
|
type Member = {
|
|
|
|
|
pubkey: string;
|
|
|
|
|
index: number;
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
type Provider = ethers.Signer | ethers.providers.Provider;
|
|
|
|
|
|
2023-01-26 18:58:18 +01:00
|
|
|
type ContractOptions = {
|
|
|
|
|
address: string;
|
2023-10-17 11:26:17 +02:00
|
|
|
provider: Provider;
|
2023-01-26 18:58:18 +01:00
|
|
|
};
|
|
|
|
|
|
2023-04-28 01:20:29 +02:00
|
|
|
type FetchMembersOptions = {
|
|
|
|
|
fromBlock?: number;
|
|
|
|
|
fetchRange?: number;
|
|
|
|
|
fetchChunks?: number;
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-26 18:58:18 +01:00
|
|
|
export class RLNContract {
|
2023-10-17 11:26:17 +02:00
|
|
|
private registryContract: ethers.Contract;
|
2023-05-14 12:18:21 -04:00
|
|
|
private merkleRootTracker: MerkleRootTracker;
|
2023-01-26 18:58:18 +01:00
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
private deployBlock: undefined | number;
|
|
|
|
|
private storageIndex: undefined | number;
|
|
|
|
|
private storageContract: undefined | ethers.Contract;
|
|
|
|
|
private _membersFilter: undefined | ethers.EventFilter;
|
|
|
|
|
|
2023-01-26 18:58:18 +01:00
|
|
|
private _members: Member[] = [];
|
|
|
|
|
|
|
|
|
|
public static async init(
|
|
|
|
|
rlnInstance: RLNInstance,
|
|
|
|
|
options: ContractOptions
|
|
|
|
|
): Promise<RLNContract> {
|
2023-05-14 12:18:21 -04:00
|
|
|
const rlnContract = new RLNContract(rlnInstance, options);
|
2023-01-26 18:58:18 +01:00
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
await rlnContract.initStorageContract(options.provider);
|
2023-01-26 18:58:18 +01:00
|
|
|
await rlnContract.fetchMembers(rlnInstance);
|
|
|
|
|
rlnContract.subscribeToMembers(rlnInstance);
|
|
|
|
|
|
|
|
|
|
return rlnContract;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
constructor(
|
|
|
|
|
rlnInstance: RLNInstance,
|
|
|
|
|
{ address, provider }: ContractOptions
|
|
|
|
|
) {
|
|
|
|
|
const initialRoot = rlnInstance.getMerkleRoot();
|
|
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
this.registryContract = new ethers.Contract(
|
|
|
|
|
address,
|
|
|
|
|
RLN_REGISTRY_ABI,
|
|
|
|
|
provider
|
|
|
|
|
);
|
2023-05-14 12:18:21 -04:00
|
|
|
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
2023-10-17 11:26:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async initStorageContract(provider: Provider): Promise<void> {
|
|
|
|
|
const index = await this.registryContract.usingStorageIndex();
|
|
|
|
|
const address = await this.registryContract.storages(index);
|
|
|
|
|
|
|
|
|
|
this.storageIndex = index;
|
|
|
|
|
this.storageContract = new ethers.Contract(
|
|
|
|
|
address,
|
|
|
|
|
RLN_STORAGE_ABI,
|
|
|
|
|
provider
|
|
|
|
|
);
|
|
|
|
|
this._membersFilter = this.storageContract.filters.MemberRegistered();
|
|
|
|
|
|
|
|
|
|
this.deployBlock = await this.storageContract.deployedBlockNumber();
|
2023-01-26 18:58:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public get contract(): ethers.Contract {
|
2023-10-17 11:26:17 +02:00
|
|
|
if (!this.storageContract) {
|
|
|
|
|
throw Error("Storage contract was not initialized.");
|
|
|
|
|
}
|
|
|
|
|
return this.storageContract as ethers.Contract;
|
2023-01-26 18:58:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public get members(): Member[] {
|
|
|
|
|
return this._members;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-17 11:26:17 +02:00
|
|
|
private get membersFilter(): ethers.EventFilter {
|
|
|
|
|
if (!this._membersFilter) {
|
|
|
|
|
throw Error("Members filter was not initialized.");
|
|
|
|
|
}
|
|
|
|
|
return this._membersFilter as ethers.EventFilter;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-26 18:58:18 +01:00
|
|
|
public async fetchMembers(
|
|
|
|
|
rlnInstance: RLNInstance,
|
2023-04-28 01:20:29 +02:00
|
|
|
options: FetchMembersOptions = {}
|
2023-01-26 18:58:18 +01:00
|
|
|
): Promise<void> {
|
2023-04-28 01:20:29 +02:00
|
|
|
const registeredMemberEvents = await queryFilter(this.contract, {
|
2023-10-17 11:26:17 +02:00
|
|
|
fromBlock: this.deployBlock,
|
2023-04-28 01:20:29 +02:00
|
|
|
...options,
|
|
|
|
|
membersFilter: this.membersFilter,
|
|
|
|
|
});
|
2023-05-14 12:18:21 -04:00
|
|
|
this.processEvents(rlnInstance, registeredMemberEvents);
|
2023-01-26 18:58:18 +01:00
|
|
|
}
|
|
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void {
|
|
|
|
|
const toRemoveTable = new Map<number, number[]>();
|
|
|
|
|
const toInsertTable = new Map<number, ethers.Event[]>();
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
2023-01-26 18:58:18 +01:00
|
|
|
}
|
|
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
private insertMembers(
|
2023-01-26 18:58:18 +01:00
|
|
|
rlnInstance: RLNInstance,
|
2023-05-14 12:18:21 -04:00
|
|
|
toInsert: Map<number, ethers.Event[]>
|
2023-01-26 18:58:18 +01:00
|
|
|
): void {
|
2023-05-14 12:18:21 -04:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-01-26 18:58:18 +01:00
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
private removeMembers(
|
|
|
|
|
rlnInstance: RLNInstance,
|
|
|
|
|
toRemove: Map<number, number[]>
|
|
|
|
|
): 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);
|
|
|
|
|
});
|
2023-01-26 18:58:18 +01:00
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
this.merkleRootTracker.backFill(blockNumber);
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-01-26 18:58:18 +01:00
|
|
|
|
2023-05-14 12:18:21 -04:00
|
|
|
public subscribeToMembers(rlnInstance: RLNInstance): void {
|
|
|
|
|
this.contract.on(this.membersFilter, (_pubkey, _index, event) =>
|
|
|
|
|
this.processEvents(rlnInstance, event)
|
2023-01-26 18:58:18 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-19 23:28:04 +02:00
|
|
|
public async registerWithSignature(
|
2023-01-26 18:58:18 +01:00
|
|
|
rlnInstance: RLNInstance,
|
|
|
|
|
signature: string
|
|
|
|
|
): Promise<ethers.Event | undefined> {
|
2023-05-08 18:10:26 -04:00
|
|
|
const identityCredential =
|
|
|
|
|
await rlnInstance.generateSeededIdentityCredential(signature);
|
2023-04-19 23:28:04 +02:00
|
|
|
|
2023-05-08 18:10:26 -04:00
|
|
|
return this.registerWithKey(identityCredential);
|
2023-04-19 23:28:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async registerWithKey(
|
2023-05-08 18:10:26 -04:00
|
|
|
credential: IdentityCredential
|
2023-04-19 23:28:04 +02:00
|
|
|
): Promise<ethers.Event | undefined> {
|
2023-10-17 11:26:17 +02:00
|
|
|
if (!this.storageIndex) {
|
|
|
|
|
throw Error(
|
|
|
|
|
"Cannot register credential, no storage contract index found."
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-01-26 18:58:18 +01:00
|
|
|
const txRegisterResponse: ethers.ContractTransaction =
|
2023-10-17 11:26:17 +02:00
|
|
|
await this.registryContract.register(
|
|
|
|
|
this.storageIndex,
|
|
|
|
|
credential.IDCommitmentBigInt,
|
|
|
|
|
{
|
|
|
|
|
gasLimit: 100000,
|
|
|
|
|
}
|
|
|
|
|
);
|
2023-01-26 18:58:18 +01:00
|
|
|
const txRegisterReceipt = await txRegisterResponse.wait();
|
|
|
|
|
|
|
|
|
|
return txRegisterReceipt?.events?.[0];
|
|
|
|
|
}
|
2023-05-14 12:18:21 -04:00
|
|
|
|
|
|
|
|
public roots(): Uint8Array[] {
|
|
|
|
|
return this.merkleRootTracker.roots();
|
|
|
|
|
}
|
2023-01-26 18:58:18 +01:00
|
|
|
}
|
2023-04-28 01:20:29 +02:00
|
|
|
|
|
|
|
|
type CustomQueryOptions = FetchMembersOptions & {
|
|
|
|
|
membersFilter: ethers.EventFilter;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// these value should be tested on other networks
|
|
|
|
|
const FETCH_CHUNK = 5;
|
|
|
|
|
const BLOCK_RANGE = 3000;
|
|
|
|
|
|
|
|
|
|
async function queryFilter(
|
|
|
|
|
contract: ethers.Contract,
|
|
|
|
|
options: CustomQueryOptions
|
|
|
|
|
): Promise<ethers.Event[]> {
|
|
|
|
|
const {
|
|
|
|
|
fromBlock,
|
|
|
|
|
membersFilter,
|
|
|
|
|
fetchRange = BLOCK_RANGE,
|
|
|
|
|
fetchChunks = FETCH_CHUNK,
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
if (!fromBlock) {
|
|
|
|
|
return contract.queryFilter(membersFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!contract.signer.provider) {
|
|
|
|
|
throw Error("No provider found on the contract's signer.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toBlock = await contract.signer.provider.getBlockNumber();
|
|
|
|
|
|
|
|
|
|
if (toBlock - fromBlock < fetchRange) {
|
|
|
|
|
return contract.queryFilter(membersFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const events: ethers.Event[][] = [];
|
|
|
|
|
const chunks = splitToChunks(fromBlock, toBlock, fetchRange);
|
|
|
|
|
|
|
|
|
|
for (const portion of takeN<[number, number]>(chunks, fetchChunks)) {
|
|
|
|
|
const promises = portion.map(([left, right]) =>
|
|
|
|
|
ignoreErrors(contract.queryFilter(membersFilter, left, right), [])
|
|
|
|
|
);
|
|
|
|
|
const fetchedEvents = await Promise.all(promises);
|
|
|
|
|
events.push(fetchedEvents.flatMap((v) => v));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return events.flatMap((v) => v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function splitToChunks(
|
|
|
|
|
from: number,
|
|
|
|
|
to: number,
|
|
|
|
|
step: number
|
|
|
|
|
): Array<[number, number]> {
|
|
|
|
|
const chunks = [];
|
|
|
|
|
|
|
|
|
|
let left = from;
|
|
|
|
|
while (left < to) {
|
|
|
|
|
const right = left + step < to ? left + step : to;
|
|
|
|
|
|
|
|
|
|
chunks.push([left, right] as [number, number]);
|
|
|
|
|
|
|
|
|
|
left = right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chunks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function* takeN<T>(array: T[], size: number): Iterable<T[]> {
|
|
|
|
|
let start = 0;
|
|
|
|
|
let skip = size;
|
|
|
|
|
|
|
|
|
|
while (skip < array.length) {
|
|
|
|
|
const portion = array.slice(start, skip);
|
|
|
|
|
|
|
|
|
|
yield portion;
|
|
|
|
|
|
|
|
|
|
start = skip;
|
|
|
|
|
skip += size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ignoreErrors<T>(promise: Promise<T>, defaultValue: T): Promise<T> {
|
|
|
|
|
return promise.catch((err) => {
|
|
|
|
|
console.error(`Ignoring an error during query: ${err?.message}`);
|
|
|
|
|
return defaultValue;
|
|
|
|
|
});
|
|
|
|
|
}
|