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

This commit is contained in:
weboko 2023-01-06 00:07:54 +01:00 committed by weboko
parent fa70837558
commit 6fe833b83d
No known key found for this signature in database
10 changed files with 3052 additions and 18199 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,25 @@ rln.create().then(async rlnInstance => {
} catch (err) {
console.log("Invalid proof")
}
});
const provider = new ethers.providers.Web3Provider(
window.ethereum,
"any"
);
const signer = provider.getSigner();
const signature = await signer.signMessage(rln.DEFAULT_SIGNATURE_MESSAGE);
console.log(`Got signature: ${signature}`);
const contract = new rln.RLNContract(rln.DEV_CONTRACT.address, signer);
console.log("Fetching members from Contract");
await contract.fetchMembers(rlnInstance, 8261478);
console.log(`Fetched members are ${contract.getMembers()}`);
contract.subscribeToMembers(rlnInstance);
console.log("Subscribed to the contract for new members");
const event = await contract.registerMember(rlnInstance, signature);
console.log(`Registered as member with ${event}`);
});

19732
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

@ -37,7 +37,7 @@
"test:tsc": "tsc -p tsconfig.dev.json",
"test:browser": "karma start karma.conf.cjs",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "mocha --watch",
"watch:test": "mocha -g \"RLN Contract abstraction\"",
"prepublish": "npm run build",
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build"
},
@ -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"
}
}

17
src/const.ts Normal file
View File

@ -0,0 +1,17 @@
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 DEV_CONTRACT = {
chainId: 5,
startBlock: 7109391,
address: "0x4252105670fe33d2947e8ead304969849e64f2a6",
abi: RLN_ABI,
};
export 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";

View File

@ -1,6 +1,8 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import type { Proof, RLNInstance } from "./rln.js";
import { DEFAULT_SIGNATURE_MESSAGE, DEV_CONTRACT, RLN_ABI } from "./const.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,14 @@ 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,
DEV_CONTRACT,
DEFAULT_SIGNATURE_MESSAGE,
};

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

@ -0,0 +1,60 @@
import chai from "chai";
import spies from "chai-spies";
import 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();
chai.spy.on(rlnInstance, "insertMember");
const voidSigner = new ethers.VoidSigner(rln.DEV_CONTRACT.address);
const rlnContract = new rln.RLNContract(
rln.DEV_CONTRACT.address,
voidSigner
);
chai.spy.on(rlnContract, "contract.queryFilter", () =>
Promise.resolve([mockEvent()])
);
await rlnContract.fetchMembers(rlnInstance);
chai.expect(rlnInstance.insertMember).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.DEV_CONTRACT.address);
const rlnContract = new rln.RLNContract(
rln.DEV_CONTRACT.address,
voidSigner
);
chai.spy.on(rlnContract, "contract.MEMBERSHIP_DEPOSIT", () =>
Promise.resolve(1)
);
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:
"C4qAaeoqKlLv4Df910gnyuCfKLk7uhIhLZgcQfOMncYJpfZqW+Pdlv3ie6hm4WkGLaS5UIO2QPbyhN4EGx73c8vkTqjv5gK49w/pGIDi+ILMjYqYKexSwJPmPOMn0XM0FDbQ5wwXmZ4SIauYiQM8faZLDk8ltkAsIX/TKA6Dgw0=",
index: 1,
},
} as any as ethers.Event;
}

91
src/rln_contract.ts Normal file
View File

@ -0,0 +1,91 @@
import { ethers } from "ethers";
import { RLN_ABI } from "./const.js";
import { RLNInstance } from "./rln.js";
type Member = {
pubkey: string;
index: number;
};
export class RLNContract {
private contract: ethers.Contract;
private membersFilter: ethers.EventFilter;
private members: Member[] = [];
constructor(
address: string,
provider: ethers.Signer | ethers.providers.Provider
) {
this.contract = new ethers.Contract(address, RLN_ABI, provider);
this.membersFilter = this.contract.filters.MemberRegistered();
}
public getContract(): ethers.Contract {
return this.contract;
}
public getMembers(): 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 pubkey = ethers.BigNumber.from(membershipKey.IDCommitment);
const depositValue = await this.contract.MEMBERSHIP_DEPOSIT();
const txRegisterResponse: ethers.ContractTransaction =
await this.contract.register(pubkey, {
value: depositValue,
});
const txRegisterReceipt = await txRegisterResponse.wait();
return txRegisterReceipt?.events?.[0];
}
}