Add RLN Contract abstraction (#43)

* implement rln contract abstraction, add basic tests, add usefull constants

* remove test command

* resolve simple comments

* move to getter for members, add init method

* fix naming

* remove default signature message

* use direct path to js file

* try different karma config

* try generic import

* update test

* address comments: rename const file, return prev regexp

* remove test

* bring back test file

* fix mock approach

* use any for type casting

* use another approach for typecasting

* update mocks

* update mocked event

* use correct value for mock

* fix spy definition

* add BigInt to MembershipKey

* fix joining

* use slice

* remove accidentally commited junk

* fix typo, use DataView for conversion, use BigInt directly

Co-authored-by: weboko <anon@mail.com>
This commit is contained in:
Sasha 2023-01-26 18:58:18 +01:00 committed by GitHub
parent fa70837558
commit d77370fbec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 4302 additions and 18232 deletions

View File

@ -3,14 +3,15 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en",
"words": [
"Waku",
"arrayify",
"circom",
"keypair",
"merkle",
"nwaku",
"vkey",
"zkey",
"circom",
"Waku",
"zerokit",
"nwaku"
"zkey"
],
"flagWords": [],
"ignorePaths": [

View File

@ -6,6 +6,8 @@
</head>
<body>
<p>Open the developer tools to see the generated proof and its validation</p>
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="text/javascript">
</script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -38,4 +38,21 @@ rln.create().then(async rlnInstance => {
} catch (err) {
console.log("Invalid proof")
}
});
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 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 event = await contract.registerMember(rlnInstance, signature);
console.log(`Registered as member with ${event}`);
});

20994
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

1294
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,7 @@
"@size-limit/preset-big-lib": "^8.0.0",
"@types/app-root-path": "^1.2.4",
"@types/chai": "^4.2.15",
"@types/chai-spies": "^1.0.3",
"@types/debug": "^4.1.7",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
@ -69,6 +70,7 @@
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"cspell": "^5.14.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
@ -125,6 +127,7 @@
]
},
"dependencies": {
"@waku/zerokit-rln-wasm": "^0.0.5"
"@waku/zerokit-rln-wasm": "^0.0.5",
"ethers": "^5.7.2"
}
}

0
runtime.js Normal file
View File

14
src/constants.ts Normal file
View File

@ -0,0 +1,14 @@
export const RLN_ABI = [
"function MEMBERSHIP_DEPOSIT() public view returns(uint256)",
"function register(uint256 pubkey) external payable",
"function withdraw(uint256 secret, uint256 _pubkeyIndex, address payable receiver) external",
"event MemberRegistered(uint256 pubkey, uint256 index)",
"event MemberWithdrawn(uint256 pubkey, uint256 index)",
];
export const GOERLI_CONTRACT = {
chainId: 5,
startBlock: 7109391,
address: "0x4252105670fe33d2947e8ead304969849e64f2a6",
abi: RLN_ABI,
};

View File

@ -1,6 +1,8 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import type { Proof, RLNInstance } from "./rln.js";
import { GOERLI_CONTRACT, RLN_ABI } from "./constants.js";
import { Proof, RLNInstance } from "./rln.js";
import { MembershipKey } from "./rln.js";
import { RLNContract } from "./rln_contract.js";
// reexport the create function, dynamically imported from rln.ts
export async function create(): Promise<RLNInstance> {
@ -11,4 +13,13 @@ export async function create(): Promise<RLNInstance> {
return await rlnModule.create();
}
export { RLNInstance, MembershipKey, Proof, RLNEncoder, RLNDecoder };
export {
RLNInstance,
MembershipKey,
Proof,
RLNEncoder,
RLNDecoder,
RLNContract,
RLN_ABI,
GOERLI_CONTRACT,
};

View File

@ -26,6 +26,16 @@ function concatenate(...input: Uint8Array[]): Uint8Array {
return result;
}
/**
* Transforms Uint8Array into BigInt
* @param array: Uint8Array
* @returns BigInt
*/
function buildBigIntFromUint8Array(array: Uint8Array): bigint {
const dataView = new DataView(array.buffer);
return dataView.getBigUint64(0, true);
}
const stringEncoder = new TextEncoder();
const DEPTH = 20;
@ -59,13 +69,15 @@ export async function create(): Promise<RLNInstance> {
export class MembershipKey {
constructor(
public readonly IDKey: Uint8Array,
public readonly IDCommitment: Uint8Array
public readonly IDCommitment: Uint8Array,
public readonly IDCommitmentBigInt: bigint
) {}
static fromBytes(memKeys: Uint8Array): MembershipKey {
const idKey = memKeys.subarray(0, 32);
const idCommitment = memKeys.subarray(32);
return new MembershipKey(idKey, idCommitment);
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment);
return new MembershipKey(idKey, idCommitment, idCommitmentBigInt);
}
}

62
src/rln_contract.spec.ts Normal file
View File

@ -0,0 +1,62 @@
import chai from "chai";
import spies from "chai-spies";
import * as ethers from "ethers";
import * as rln from "./index.js";
chai.use(spies);
describe("RLN Contract abstraction", () => {
it("should be able to fetch members from events and store to rln instance", async () => {
const rlnInstance = await rln.create();
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,
provider: voidSigner,
});
rlnContract["_contract"] = {
queryFilter: () => Promise.resolve([mockEvent()]),
} as unknown as ethers.Contract;
await rlnContract.fetchMembers(rlnInstance);
chai.expect(insertMemberSpy).to.have.been.called();
});
it("should register a member by signature", async () => {
const mockSignature =
"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,
provider: voidSigner,
});
rlnContract["_contract"] = {
register: () =>
Promise.resolve({ wait: () => Promise.resolve(undefined) }),
MEMBERSHIP_DEPOSIT: () => Promise.resolve(1),
} as unknown as ethers.Contract;
const contractSpy = chai.spy.on(rlnContract["_contract"], "register");
await rlnContract.registerMember(rlnInstance, mockSignature);
chai.expect(contractSpy).to.have.been.called();
});
});
function mockEvent(): ethers.Event {
return {
args: {
pubkey: "0x9e7d3f8f8c7a1d2bef96a2e8dbb8e7c1ea9a9ab78d6b3c6c3c",
index: 1,
},
} as unknown as ethers.Event;
}

104
src/rln_contract.ts Normal file
View File

@ -0,0 +1,104 @@
import { ethers } from "ethers";
import { RLN_ABI } from "./constants.js";
import { RLNInstance } from "./rln.js";
type Member = {
pubkey: string;
index: number;
};
type ContractOptions = {
address: string;
provider: ethers.Signer | ethers.providers.Provider;
};
export class RLNContract {
private _contract: ethers.Contract;
private membersFilter: ethers.EventFilter;
private _members: Member[] = [];
public static async init(
rlnInstance: RLNInstance,
options: ContractOptions
): Promise<RLNContract> {
const rlnContract = new RLNContract(options);
await rlnContract.fetchMembers(rlnInstance);
rlnContract.subscribeToMembers(rlnInstance);
return rlnContract;
}
constructor({ address, provider }: ContractOptions) {
this._contract = new ethers.Contract(address, RLN_ABI, provider);
this.membersFilter = this.contract.filters.MemberRegistered();
}
public get contract(): ethers.Contract {
return this._contract;
}
public get members(): Member[] {
return this._members;
}
public async fetchMembers(
rlnInstance: RLNInstance,
fromBlock?: number
): Promise<void> {
const registeredMemberEvents = await this.contract.queryFilter(
this.membersFilter,
fromBlock
);
for (const event of registeredMemberEvents) {
this.addMemberFromEvent(rlnInstance, event);
}
}
public subscribeToMembers(rlnInstance: RLNInstance): void {
this.contract.on(this.membersFilter, (_pubkey, _index, event) =>
this.addMemberFromEvent(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 registerMember(
rlnInstance: RLNInstance,
signature: string
): Promise<ethers.Event | undefined> {
const membershipKey = await rlnInstance.generateSeededMembershipKey(
signature
);
const depositValue = await this.contract.MEMBERSHIP_DEPOSIT();
const txRegisterResponse: ethers.ContractTransaction =
await this.contract.register(membershipKey.IDCommitmentBigInt, {
value: depositValue,
});
const txRegisterReceipt = await txRegisterResponse.wait();
return txRegisterReceipt?.events?.[0];
}
}