mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
feat(rln): migrate from v1 to v2, rate limiting, memberships, test coverage (#2262)
* chore: update ABIs and deployed address * chore: remove storage contract references * feat: upgrading adapter to basic rlnv2 * feat: rate limit * chore: upgrade packages revert * fix: tests * chore: remove uneeded file * feat(rln): implement RLNv2 rate limiting and membership states - Add rate limit validation and handling in proof generation/verification - Implement membership lifecycle state management (Active/GracePeriod/Expired) - Add new membership management methods: - getMembershipInfo - extendMembership - eraseMembership - registerMembership - Update proof verification to include rate limit checks - Refactor message serialization to include rate limit data Breaking changes: - verifyWithRoots now takes roots as array instead of spread parameters - Proof verification methods now accept optional rateLimit parameter * fix: typo * chore: add to cspell * chore: reduce diff * chore: simplify subdir for abi * chore: address comments * chore: simplify access to variables * chore: address comments * chore: simplify constants * chore: add error handling * chore: change rln v2 references to rln * fix: check
This commit is contained in:
parent
09108d9284
commit
6fc6bf3916
@ -9,6 +9,7 @@
|
||||
"Alives",
|
||||
"alphabeta",
|
||||
"Arraylike",
|
||||
"arrayify",
|
||||
"asym",
|
||||
"autoshard",
|
||||
"autosharding",
|
||||
|
||||
43
package-lock.json
generated
43
package-lock.json
generated
@ -42384,6 +42384,7 @@
|
||||
"@types/chai-spies": "^1.0.6",
|
||||
"@types/deep-equal-in-any-order": "^1.0.4",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@waku/build-utils": "^1.0.0",
|
||||
"@waku/message-encryption": "^0.0.31",
|
||||
"chai": "^5.1.2",
|
||||
@ -42392,7 +42393,8 @@
|
||||
"chai-subset": "^1.6.0",
|
||||
"deep-equal-in-any-order": "^2.0.6",
|
||||
"fast-check": "^3.23.2",
|
||||
"rollup-plugin-copy": "^3.5.0"
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sinon": "^19.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@ -42483,6 +42485,16 @@
|
||||
"@scure/base": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/@sinonjs/fake-timers": {
|
||||
"version": "13.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
|
||||
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/@types/chai": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz",
|
||||
@ -42553,6 +42565,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/ethers": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz",
|
||||
@ -42618,6 +42640,25 @@
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/sinon": {
|
||||
"version": "19.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz",
|
||||
"integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.1",
|
||||
"@sinonjs/fake-timers": "^13.0.2",
|
||||
"@sinonjs/samsam": "^8.0.1",
|
||||
"diff": "^7.0.0",
|
||||
"nise": "^6.1.1",
|
||||
"supports-color": "^7.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/sinon"
|
||||
}
|
||||
},
|
||||
"packages/rln/node_modules/uuid": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// should match nwaku
|
||||
// https://github.com/waku-org/nwaku/blob/c3cb06ac6c03f0f382d3941ea53b330f6a8dd127/waku/waku_rln_relay/rln_relay.nim#L309
|
||||
// https://github.com/waku-org/nwaku/blob/c3cb06ac6c03f0f382d3941ea53b330f6a8dd127/tests/waku_rln_relay/rln/waku_rln_relay_utils.nim#L20
|
||||
const RLN_GENERATION_PREFIX_ERROR = "could not generate rln-v2 proof";
|
||||
const RLN_GENERATION_PREFIX_ERROR = "could not generate rln proof";
|
||||
const RLN_MESSAGE_ID_PREFIX_ERROR =
|
||||
"could not get new message id to generate an rln proof";
|
||||
|
||||
|
||||
@ -47,14 +47,15 @@ module.exports = function (config) {
|
||||
|
||||
client: {
|
||||
mocha: {
|
||||
timeout: 180000 // 3 minutes
|
||||
timeout: 300000 // 5 minutes
|
||||
}
|
||||
},
|
||||
|
||||
browserDisconnectTimeout: 180000, // 3 minutes
|
||||
browserDisconnectTimeout: 300000, // 5 minutes
|
||||
browserDisconnectTolerance: 3, // Number of tries before failing
|
||||
browserNoActivityTimeout: 180000, // 3 minutes
|
||||
browserNoActivityTimeout: 300000, // 5 minutes
|
||||
captureTimeout: 300000, // 5 minutes
|
||||
pingTimeout: 300000, // 5 minutes
|
||||
|
||||
mime: {
|
||||
"application/wasm": ["wasm"],
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"@types/chai-spies": "^1.0.6",
|
||||
"@types/deep-equal-in-any-order": "^1.0.4",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@waku/build-utils": "^1.0.0",
|
||||
"@waku/message-encryption": "^0.0.31",
|
||||
"chai": "^5.1.2",
|
||||
@ -64,7 +65,8 @@
|
||||
"chai-subset": "^1.6.0",
|
||||
"deep-equal-in-any-order": "^2.0.6",
|
||||
"fast-check": "^3.23.2",
|
||||
"rollup-plugin-copy": "^3.5.0"
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sinon": "^19.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
392
packages/rln/src/contract/abi.ts
Normal file
392
packages/rln/src/contract/abi.ts
Normal file
@ -0,0 +1,392 @@
|
||||
export const RLN_ABI = [
|
||||
{
|
||||
type: "constructor",
|
||||
inputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "DuplicateIdCommitment",
|
||||
inputs: []
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidIdCommitment",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "error",
|
||||
name: "InvalidPaginationQuery",
|
||||
inputs: [
|
||||
{
|
||||
name: "startIndex",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "endIndex",
|
||||
type: "uint256"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "MAX_MEMBERSHIP_SET_SIZE",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "MERKLE_TREE_DEPTH",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint8"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "Q",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "activeDurationForNewMemberships",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "currentTotalRateLimit",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "deployedBlockNumber",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "depositsToWithdraw",
|
||||
inputs: [
|
||||
{
|
||||
name: "holder",
|
||||
type: "address"
|
||||
},
|
||||
{
|
||||
name: "token",
|
||||
type: "address"
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "balance",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "eraseMemberships",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitments",
|
||||
type: "uint256[]"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "eraseMemberships",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitments",
|
||||
type: "uint256[]"
|
||||
},
|
||||
{
|
||||
name: "eraseFromMembershipSet",
|
||||
type: "bool"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "extendMemberships",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitments",
|
||||
type: "uint256[]"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "getMembershipInfo",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "getMerkleProof",
|
||||
inputs: [
|
||||
{
|
||||
name: "index",
|
||||
type: "uint40"
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint256[20]"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "getRateCommitmentsInRangeBoundsInclusive",
|
||||
inputs: [
|
||||
{
|
||||
name: "startIndex",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "endIndex",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint256[]"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "gracePeriodDurationForNewMemberships",
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "initialize",
|
||||
inputs: [
|
||||
{
|
||||
name: "_priceCalculator",
|
||||
type: "address"
|
||||
},
|
||||
{
|
||||
name: "_maxTotalRateLimit",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "_minMembershipRateLimit",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "_maxMembershipRateLimit",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "_activeDuration",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "_gracePeriod",
|
||||
type: "uint32"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "isExpired",
|
||||
inputs: [
|
||||
{
|
||||
name: "_idCommitment",
|
||||
type: "uint256"
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: "",
|
||||
type: "bool"
|
||||
}
|
||||
],
|
||||
stateMutability: "view"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "register",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "rateLimit",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "idCommitmentsToErase",
|
||||
type: "uint256[]"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "registerWithPermit",
|
||||
inputs: [
|
||||
{
|
||||
name: "owner",
|
||||
type: "address"
|
||||
},
|
||||
{
|
||||
name: "deadline",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "v",
|
||||
type: "uint8"
|
||||
},
|
||||
{
|
||||
name: "r",
|
||||
type: "bytes32"
|
||||
},
|
||||
{
|
||||
name: "s",
|
||||
type: "bytes32"
|
||||
},
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "rateLimit",
|
||||
type: "uint32"
|
||||
},
|
||||
{
|
||||
name: "idCommitmentsToErase",
|
||||
type: "uint256[]"
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: "nonpayable"
|
||||
},
|
||||
{
|
||||
type: "event",
|
||||
name: "MembershipRegistered",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256",
|
||||
indexed: false
|
||||
},
|
||||
{
|
||||
name: "rateLimit",
|
||||
type: "uint32",
|
||||
indexed: false
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint256",
|
||||
indexed: false
|
||||
}
|
||||
],
|
||||
anonymous: false
|
||||
},
|
||||
{
|
||||
type: "event",
|
||||
name: "MembershipRemoved",
|
||||
inputs: [
|
||||
{
|
||||
name: "idCommitment",
|
||||
type: "uint256",
|
||||
indexed: false
|
||||
},
|
||||
{
|
||||
name: "index",
|
||||
type: "uint256",
|
||||
indexed: false
|
||||
}
|
||||
],
|
||||
anonymous: false
|
||||
}
|
||||
];
|
||||
@ -1,68 +1,28 @@
|
||||
// ref https://github.com/waku-org/waku-rln-contract/blob/19fded82bca07e7b535b429dc507cfb83f10dfcf/deployments/sepolia/WakuRlnRegistry_Implementation.json#L3
|
||||
export const RLN_REGISTRY_ABI = [
|
||||
"error IncompatibleStorage()",
|
||||
"error IncompatibleStorageIndex()",
|
||||
"error NoStorageContractAvailable()",
|
||||
"error StorageAlreadyExists(address storageAddress)",
|
||||
"event AdminChanged(address previousAdmin, address newAdmin)",
|
||||
"event BeaconUpgraded(address indexed beacon)",
|
||||
"event Initialized(uint8 version)",
|
||||
"event NewStorageContract(uint16 index, address storageAddress)",
|
||||
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
|
||||
"event Upgraded(address indexed implementation)",
|
||||
"function forceProgress()",
|
||||
"function initialize(address _poseidonHasher)",
|
||||
"function newStorage()",
|
||||
"function nextStorageIndex() view returns (uint16)",
|
||||
"function owner() view returns (address)",
|
||||
"function poseidonHasher() view returns (address)",
|
||||
"function proxiableUUID() view returns (bytes32)",
|
||||
"function register(uint16 storageIndex, uint256 commitment)",
|
||||
"function register(uint256[] commitments)",
|
||||
"function register(uint16 storageIndex, uint256[] commitments)",
|
||||
"function registerStorage(address storageAddress)",
|
||||
"function renounceOwnership()",
|
||||
"function storages(uint16) view returns (address)",
|
||||
"function transferOwnership(address newOwner)",
|
||||
"function upgradeTo(address newImplementation)",
|
||||
"function upgradeToAndCall(address newImplementation, bytes data) payable",
|
||||
"function usingStorageIndex() view returns (uint16)"
|
||||
];
|
||||
|
||||
// ref https://github.com/waku-org/waku-rln-contract/blob/19fded82bca07e7b535b429dc507cfb83f10dfcf/deployments/sepolia/WakuRlnStorage_0.json#L3
|
||||
export const RLN_STORAGE_ABI = [
|
||||
"constructor(address _poseidonHasher, uint16 _contractIndex)",
|
||||
"error DuplicateIdCommitment()",
|
||||
"error FullTree()",
|
||||
"error InvalidIdCommitment(uint256 idCommitment)",
|
||||
"error NotImplemented()",
|
||||
"event MemberRegistered(uint256 idCommitment, uint256 index)",
|
||||
"event MemberWithdrawn(uint256 idCommitment, uint256 index)",
|
||||
"event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)",
|
||||
"function DEPTH() view returns (uint256)",
|
||||
"function MEMBERSHIP_DEPOSIT() view returns (uint256)",
|
||||
"function SET_SIZE() view returns (uint256)",
|
||||
"function contractIndex() view returns (uint16)",
|
||||
"function deployedBlockNumber() view returns (uint32)",
|
||||
"function idCommitmentIndex() view returns (uint256)",
|
||||
"function isValidCommitment(uint256 idCommitment) view returns (bool)",
|
||||
"function memberExists(uint256) view returns (bool)",
|
||||
"function members(uint256) view returns (uint256)",
|
||||
"function owner() view returns (address)",
|
||||
"function poseidonHasher() view returns (address)",
|
||||
"function register(uint256[] idCommitments)",
|
||||
"function register(uint256 idCommitment) payable",
|
||||
"function renounceOwnership()",
|
||||
"function slash(uint256 idCommitment, address receiver, uint256[8] proof) pure",
|
||||
"function stakedAmounts(uint256) view returns (uint256)",
|
||||
"function transferOwnership(address newOwner)",
|
||||
"function verifier() view returns (address)",
|
||||
"function withdraw() pure",
|
||||
"function withdrawalBalance(address) view returns (uint256)"
|
||||
];
|
||||
import { RLN_ABI } from "./abi.js";
|
||||
|
||||
export const SEPOLIA_CONTRACT = {
|
||||
chainId: 11155111,
|
||||
address: "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4",
|
||||
abi: RLN_REGISTRY_ABI
|
||||
address: "0xCB33Aa5B38d79E3D9Fa8B10afF38AA201399a7e3",
|
||||
abi: RLN_ABI
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limit tiers (messages per epoch)
|
||||
* Each membership can specify a rate limit within these bounds.
|
||||
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
|
||||
*/
|
||||
export const RATE_LIMIT_TIERS = {
|
||||
LOW: 20, // Suggested minimum rate - 20 messages per epoch
|
||||
MEDIUM: 200,
|
||||
HIGH: 600 // Suggested maximum rate - 600 messages per epoch
|
||||
} as const;
|
||||
|
||||
// Global rate limit parameters
|
||||
export const RATE_LIMIT_PARAMS = {
|
||||
MIN_RATE: RATE_LIMIT_TIERS.LOW,
|
||||
MAX_RATE: RATE_LIMIT_TIERS.HIGH,
|
||||
MAX_TOTAL_RATE: 160_000, // Maximum total rate limit across all memberships
|
||||
EPOCH_LENGTH: 600 // Epoch length in seconds (10 minutes)
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;
|
||||
|
||||
@ -1,43 +1,130 @@
|
||||
import { expect } from "chai";
|
||||
import { hexToBytes } from "@waku/utils/bytes";
|
||||
import { expect, use } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
import * as ethers from "ethers";
|
||||
import sinon, { SinonSandbox } from "sinon";
|
||||
|
||||
import { createRLN } from "../create.js";
|
||||
import type { IdentityCredential } from "../identity.js";
|
||||
|
||||
import { SEPOLIA_CONTRACT } from "./constants.js";
|
||||
import { DEFAULT_RATE_LIMIT, SEPOLIA_CONTRACT } from "./constants.js";
|
||||
import { RLNContract } from "./rln_contract.js";
|
||||
|
||||
describe("RLN Contract abstraction", () => {
|
||||
it("should be able to fetch members from events and store to rln instance", async () => {
|
||||
const rlnInstance = await createRLN();
|
||||
let insertMemberCalled = false;
|
||||
use(chaiAsPromised);
|
||||
|
||||
// Track if insertMember was called
|
||||
const originalInsertMember = rlnInstance.zerokit.insertMember;
|
||||
rlnInstance.zerokit.insertMember = function (
|
||||
this: any,
|
||||
...args: Parameters<typeof originalInsertMember>
|
||||
) {
|
||||
insertMemberCalled = true;
|
||||
return originalInsertMember.apply(this, args);
|
||||
describe("RLN Contract abstraction - RLN", () => {
|
||||
let sandbox: SinonSandbox;
|
||||
let rlnInstance: any;
|
||||
let mockedRegistryContract: any;
|
||||
|
||||
const mockRateLimits = {
|
||||
minRate: 20,
|
||||
maxRate: 600,
|
||||
maxTotalRate: 1000,
|
||||
currentTotalRate: 500
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
rlnInstance = await createRLN();
|
||||
rlnInstance.zerokit.insertMember = () => undefined;
|
||||
|
||||
mockedRegistryContract = {
|
||||
minMembershipRateLimit: () =>
|
||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.minRate)),
|
||||
maxMembershipRateLimit: () =>
|
||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxRate)),
|
||||
maxTotalRateLimit: () =>
|
||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.maxTotalRate)),
|
||||
currentTotalRateLimit: () =>
|
||||
Promise.resolve(ethers.BigNumber.from(mockRateLimits.currentTotalRate)),
|
||||
queryFilter: () => [mockRLNRegisteredEvent()],
|
||||
provider: {
|
||||
getLogs: () => [],
|
||||
getBlockNumber: () => Promise.resolve(1000),
|
||||
getNetwork: () => Promise.resolve({ chainId: 11155111 })
|
||||
},
|
||||
filters: {
|
||||
MembershipRegistered: () => ({}),
|
||||
MembershipRemoved: () => ({})
|
||||
},
|
||||
on: () => ({})
|
||||
};
|
||||
|
||||
const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address);
|
||||
const rlnContract = new RLNContract(rlnInstance, {
|
||||
registryAddress: SEPOLIA_CONTRACT.address,
|
||||
signer: voidSigner
|
||||
const provider = new ethers.providers.JsonRpcProvider();
|
||||
const voidSigner = new ethers.VoidSigner(
|
||||
SEPOLIA_CONTRACT.address,
|
||||
provider
|
||||
);
|
||||
await RLNContract.init(rlnInstance, {
|
||||
address: SEPOLIA_CONTRACT.address,
|
||||
signer: voidSigner,
|
||||
rateLimit: DEFAULT_RATE_LIMIT,
|
||||
contract: mockedRegistryContract as unknown as ethers.Contract
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it("should fetch members from events and store them in the RLN instance", async () => {
|
||||
const rlnInstance = await createRLN();
|
||||
|
||||
const insertMemberSpy = sinon.stub();
|
||||
rlnInstance.zerokit.insertMember = insertMemberSpy;
|
||||
|
||||
const membershipRegisteredEvent = mockRLNRegisteredEvent();
|
||||
|
||||
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
|
||||
const mockedRegistryContract = {
|
||||
queryFilter: queryFilterStub,
|
||||
provider: {
|
||||
getLogs: () => [],
|
||||
getBlockNumber: () => Promise.resolve(1000)
|
||||
},
|
||||
interface: {
|
||||
getEvent: (eventName: string) => ({
|
||||
name: eventName,
|
||||
format: () => {}
|
||||
})
|
||||
},
|
||||
filters: {
|
||||
MembershipRegistered: () => ({}),
|
||||
MembershipRemoved: () => ({})
|
||||
},
|
||||
on: () => ({}),
|
||||
removeAllListeners: () => ({})
|
||||
};
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider();
|
||||
const voidSigner = new ethers.VoidSigner(
|
||||
SEPOLIA_CONTRACT.address,
|
||||
provider
|
||||
);
|
||||
const rlnContract = await RLNContract.init(rlnInstance, {
|
||||
address: SEPOLIA_CONTRACT.address,
|
||||
signer: voidSigner,
|
||||
rateLimit: DEFAULT_RATE_LIMIT,
|
||||
contract: mockedRegistryContract as unknown as ethers.Contract
|
||||
});
|
||||
|
||||
rlnContract["storageContract"] = {
|
||||
queryFilter: () => Promise.resolve([mockEvent()])
|
||||
} as unknown as ethers.Contract;
|
||||
rlnContract["_membersFilter"] = {
|
||||
address: "",
|
||||
topics: []
|
||||
} as unknown as ethers.EventFilter;
|
||||
await rlnContract.fetchMembers(rlnInstance, {
|
||||
fromBlock: 0,
|
||||
fetchRange: 1000,
|
||||
fetchChunks: 2
|
||||
});
|
||||
|
||||
await rlnContract.fetchMembers(rlnInstance);
|
||||
expect(
|
||||
insertMemberSpy.calledWith(
|
||||
ethers.utils.zeroPad(
|
||||
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
|
||||
32
|
||||
)
|
||||
)
|
||||
).to.be.true;
|
||||
|
||||
expect(insertMemberCalled).to.be.true;
|
||||
expect(queryFilterStub.called).to.be.true;
|
||||
});
|
||||
|
||||
it("should register a member", async () => {
|
||||
@ -45,38 +132,116 @@ describe("RLN Contract abstraction", () => {
|
||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
||||
|
||||
const rlnInstance = await createRLN();
|
||||
const voidSigner = new ethers.VoidSigner(SEPOLIA_CONTRACT.address);
|
||||
const rlnContract = new RLNContract(rlnInstance, {
|
||||
registryAddress: SEPOLIA_CONTRACT.address,
|
||||
signer: voidSigner
|
||||
const identity: IdentityCredential =
|
||||
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
|
||||
|
||||
const insertMemberSpy = sinon.stub();
|
||||
rlnInstance.zerokit.insertMember = insertMemberSpy;
|
||||
|
||||
const formatIdCommitment = (idCommitmentBigInt: bigint): string =>
|
||||
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
|
||||
|
||||
const membershipRegisteredEvent = mockRLNRegisteredEvent(
|
||||
formatIdCommitment(identity.IDCommitmentBigInt)
|
||||
);
|
||||
|
||||
const registerStub = sinon.stub().returns({
|
||||
wait: () =>
|
||||
Promise.resolve({
|
||||
events: [
|
||||
{
|
||||
event: "MembershipRegistered",
|
||||
args: {
|
||||
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
|
||||
rateLimit: DEFAULT_RATE_LIMIT,
|
||||
index: ethers.BigNumber.from(1)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
let registerCalled = false;
|
||||
rlnContract["storageIndex"] = 1;
|
||||
rlnContract["_membersFilter"] = {
|
||||
address: "",
|
||||
topics: []
|
||||
} as unknown as ethers.EventFilter;
|
||||
rlnContract["registryContract"] = {
|
||||
"register(uint16,uint256)": () => {
|
||||
registerCalled = true;
|
||||
return Promise.resolve({ wait: () => Promise.resolve(undefined) });
|
||||
}
|
||||
} as unknown as ethers.Contract;
|
||||
const mockedRegistryContract = {
|
||||
register: registerStub,
|
||||
queryFilter: () => [membershipRegisteredEvent],
|
||||
provider: {
|
||||
getLogs: () => [],
|
||||
getBlockNumber: () => Promise.resolve(1000),
|
||||
getNetwork: () => Promise.resolve({ chainId: 11155111 })
|
||||
},
|
||||
address: SEPOLIA_CONTRACT.address,
|
||||
interface: {
|
||||
getEvent: (eventName: string) => ({
|
||||
name: eventName,
|
||||
format: () => {}
|
||||
})
|
||||
},
|
||||
filters: {
|
||||
MembershipRegistered: () => ({}),
|
||||
MembershipRemoved: () => ({})
|
||||
},
|
||||
on: () => ({}),
|
||||
removeAllListeners: () => ({})
|
||||
};
|
||||
|
||||
const identity =
|
||||
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
|
||||
await rlnContract.registerWithIdentity(identity);
|
||||
const provider = new ethers.providers.JsonRpcProvider();
|
||||
const voidSigner = new ethers.VoidSigner(
|
||||
SEPOLIA_CONTRACT.address,
|
||||
provider
|
||||
);
|
||||
const rlnContract = await RLNContract.init(rlnInstance, {
|
||||
signer: voidSigner,
|
||||
address: SEPOLIA_CONTRACT.address,
|
||||
rateLimit: DEFAULT_RATE_LIMIT,
|
||||
contract: mockedRegistryContract as unknown as ethers.Contract
|
||||
});
|
||||
|
||||
expect(registerCalled).to.be.true;
|
||||
const decryptedCredentials =
|
||||
await rlnContract.registerWithIdentity(identity);
|
||||
|
||||
expect(decryptedCredentials).to.not.be.undefined;
|
||||
if (!decryptedCredentials) {
|
||||
throw new Error("Decrypted credentials should not be undefined");
|
||||
}
|
||||
|
||||
expect(
|
||||
registerStub.calledWith(
|
||||
identity.IDCommitmentBigInt,
|
||||
DEFAULT_RATE_LIMIT,
|
||||
[],
|
||||
{
|
||||
gasLimit: 300000
|
||||
}
|
||||
)
|
||||
).to.be.true;
|
||||
|
||||
expect(decryptedCredentials).to.have.property("identity");
|
||||
expect(decryptedCredentials).to.have.property("membership");
|
||||
expect(decryptedCredentials.membership).to.include({
|
||||
address: SEPOLIA_CONTRACT.address,
|
||||
treeIndex: 1
|
||||
});
|
||||
|
||||
const expectedIdCommitment = ethers.utils.zeroPad(
|
||||
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
|
||||
32
|
||||
);
|
||||
expect(insertMemberSpy.callCount).to.equal(2);
|
||||
expect(insertMemberSpy.getCall(1).args[0]).to.deep.equal(
|
||||
expectedIdCommitment
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function mockEvent(): ethers.Event {
|
||||
function mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
|
||||
return {
|
||||
args: {
|
||||
idCommitment: { _hex: "0xb3df1c4e5600ef2b" },
|
||||
idCommitment:
|
||||
idCommitment ||
|
||||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
rateLimit: DEFAULT_RATE_LIMIT,
|
||||
index: ethers.BigNumber.from(1)
|
||||
}
|
||||
},
|
||||
event: "MembershipRegistered"
|
||||
} as unknown as ethers.Event;
|
||||
}
|
||||
|
||||
@ -6,9 +6,10 @@ import type { IdentityCredential } from "../identity.js";
|
||||
import type { DecryptedCredentials } from "../keystore/index.js";
|
||||
import type { RLNInstance } from "../rln.js";
|
||||
import { MerkleRootTracker } from "../root_tracker.js";
|
||||
import { zeroPadLE } from "../utils/index.js";
|
||||
import { zeroPadLE } from "../utils/bytes.js";
|
||||
|
||||
import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
|
||||
import { RLN_ABI } from "./abi.js";
|
||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
|
||||
|
||||
const log = new Logger("waku:rln:contract");
|
||||
|
||||
@ -17,18 +18,21 @@ type Member = {
|
||||
index: ethers.BigNumber;
|
||||
};
|
||||
|
||||
type Signer = ethers.Signer;
|
||||
interface RLNContractOptions {
|
||||
signer: ethers.Signer;
|
||||
address: string;
|
||||
rateLimit?: number;
|
||||
}
|
||||
|
||||
type RLNContractOptions = {
|
||||
signer: Signer;
|
||||
registryAddress: string;
|
||||
};
|
||||
interface RLNContractInitOptions extends RLNContractOptions {
|
||||
contract?: ethers.Contract;
|
||||
}
|
||||
|
||||
type RLNStorageOptions = {
|
||||
storageIndex?: number;
|
||||
};
|
||||
|
||||
type RLNContractInitOptions = RLNContractOptions & RLNStorageOptions;
|
||||
export interface MembershipRegisteredEvent {
|
||||
idCommitment: string;
|
||||
rateLimit: number;
|
||||
index: ethers.BigNumber;
|
||||
}
|
||||
|
||||
type FetchMembersOptions = {
|
||||
fromBlock?: number;
|
||||
@ -36,80 +40,157 @@ type FetchMembersOptions = {
|
||||
fetchChunks?: number;
|
||||
};
|
||||
|
||||
export interface MembershipInfo {
|
||||
index: ethers.BigNumber;
|
||||
idCommitment: string;
|
||||
rateLimit: number;
|
||||
startBlock: number;
|
||||
endBlock: number;
|
||||
state: MembershipState;
|
||||
}
|
||||
|
||||
export enum MembershipState {
|
||||
Active = "Active",
|
||||
GracePeriod = "GracePeriod",
|
||||
Expired = "Expired",
|
||||
ErasedAwaitsWithdrawal = "ErasedAwaitsWithdrawal"
|
||||
}
|
||||
|
||||
export class RLNContract {
|
||||
private registryContract: ethers.Contract;
|
||||
public contract: ethers.Contract;
|
||||
private merkleRootTracker: MerkleRootTracker;
|
||||
|
||||
private deployBlock: undefined | number;
|
||||
private storageIndex: undefined | number;
|
||||
private storageContract: undefined | ethers.Contract;
|
||||
private _membersFilter: undefined | ethers.EventFilter;
|
||||
private rateLimit: number;
|
||||
|
||||
private _members: Map<number, Member> = new Map();
|
||||
private _membersFilter: ethers.EventFilter;
|
||||
private _membersRemovedFilter: ethers.EventFilter;
|
||||
|
||||
/**
|
||||
* Asynchronous initializer for RLNContract.
|
||||
* Allows injecting a mocked contract for testing purposes.
|
||||
*/
|
||||
public static async init(
|
||||
rlnInstance: RLNInstance,
|
||||
options: RLNContractInitOptions
|
||||
): Promise<RLNContract> {
|
||||
const rlnContract = new RLNContract(rlnInstance, options);
|
||||
|
||||
await rlnContract.initStorageContract(options.signer);
|
||||
await rlnContract.fetchMembers(rlnInstance);
|
||||
rlnContract.subscribeToMembers(rlnInstance);
|
||||
|
||||
return rlnContract;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private constructor(
|
||||
rlnInstance: RLNInstance,
|
||||
{ registryAddress, signer }: RLNContractOptions
|
||||
options: RLNContractInitOptions
|
||||
) {
|
||||
const {
|
||||
address,
|
||||
signer,
|
||||
rateLimit = DEFAULT_RATE_LIMIT,
|
||||
contract
|
||||
} = options;
|
||||
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE} messages per epoch`
|
||||
);
|
||||
}
|
||||
|
||||
this.rateLimit = rateLimit;
|
||||
|
||||
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
|
||||
|
||||
this.registryContract = new ethers.Contract(
|
||||
registryAddress,
|
||||
RLN_REGISTRY_ABI,
|
||||
signer
|
||||
);
|
||||
// Use the injected contract if provided; otherwise, instantiate a new one.
|
||||
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
|
||||
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
|
||||
|
||||
// Initialize event filters for MembershipRegistered and MembershipRemoved
|
||||
this._membersFilter = this.contract.filters.MembershipRegistered();
|
||||
this._membersRemovedFilter = this.contract.filters.MembershipRemoved();
|
||||
}
|
||||
|
||||
private async initStorageContract(
|
||||
signer: Signer,
|
||||
options: RLNStorageOptions = {}
|
||||
): Promise<void> {
|
||||
const storageIndex = options?.storageIndex
|
||||
? options.storageIndex
|
||||
: await this.registryContract.usingStorageIndex();
|
||||
const storageAddress = await this.registryContract.storages(storageIndex);
|
||||
|
||||
if (!storageAddress || storageAddress === ethers.constants.AddressZero) {
|
||||
throw Error("No RLN Storage initialized on registry contract.");
|
||||
}
|
||||
|
||||
this.storageIndex = storageIndex;
|
||||
this.storageContract = new ethers.Contract(
|
||||
storageAddress,
|
||||
RLN_STORAGE_ABI,
|
||||
signer
|
||||
);
|
||||
this._membersFilter = this.storageContract.filters.MemberRegistered();
|
||||
|
||||
this.deployBlock = await this.storageContract.deployedBlockNumber();
|
||||
/**
|
||||
* Gets the current rate limit for this contract instance
|
||||
*/
|
||||
public getRateLimit(): number {
|
||||
return this.rateLimit;
|
||||
}
|
||||
|
||||
public get registry(): ethers.Contract {
|
||||
if (!this.registryContract) {
|
||||
throw Error("Registry contract was not initialized");
|
||||
}
|
||||
return this.registryContract as ethers.Contract;
|
||||
/**
|
||||
* Gets the contract address
|
||||
*/
|
||||
public get address(): string {
|
||||
return this.contract.address;
|
||||
}
|
||||
|
||||
public get contract(): ethers.Contract {
|
||||
if (!this.storageContract) {
|
||||
throw Error("Storage contract was not initialized");
|
||||
}
|
||||
return this.storageContract as ethers.Contract;
|
||||
/**
|
||||
* Gets the contract provider
|
||||
*/
|
||||
public get provider(): ethers.providers.Provider {
|
||||
return this.contract.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum allowed rate limit from the contract
|
||||
* @returns Promise<number> The minimum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMinRateLimit(): Promise<number> {
|
||||
const minRate = await this.contract.minMembershipRateLimit();
|
||||
return minRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum allowed rate limit from the contract
|
||||
* @returns Promise<number> The maximum rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxRateLimit(): Promise<number> {
|
||||
const maxRate = await this.contract.maxMembershipRateLimit();
|
||||
return maxRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum total rate limit across all memberships
|
||||
* @returns Promise<number> The maximum total rate limit in messages per epoch
|
||||
*/
|
||||
public async getMaxTotalRateLimit(): Promise<number> {
|
||||
const maxTotalRate = await this.contract.maxTotalRateLimit();
|
||||
return maxTotalRate.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current total rate limit usage across all memberships
|
||||
* @returns Promise<number> The current total rate limit usage in messages per epoch
|
||||
*/
|
||||
public async getCurrentTotalRateLimit(): Promise<number> {
|
||||
const currentTotal = await this.contract.currentTotalRateLimit();
|
||||
return currentTotal.toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining available total rate limit that can be allocated
|
||||
* @returns Promise<number> The remaining rate limit that can be allocated
|
||||
*/
|
||||
public async getRemainingTotalRateLimit(): Promise<number> {
|
||||
const [maxTotal, currentTotal] = await Promise.all([
|
||||
this.contract.maxTotalRateLimit(),
|
||||
this.contract.currentTotalRateLimit()
|
||||
]);
|
||||
return maxTotal.sub(currentTotal).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rate limit for future registrations
|
||||
* @param newRateLimit The new rate limit to use
|
||||
*/
|
||||
public async setRateLimit(newRateLimit: number): Promise<void> {
|
||||
this.rateLimit = newRateLimit;
|
||||
}
|
||||
|
||||
public get members(): Member[] {
|
||||
@ -123,7 +204,14 @@ export class RLNContract {
|
||||
if (!this._membersFilter) {
|
||||
throw Error("Members filter was not initialized.");
|
||||
}
|
||||
return this._membersFilter as ethers.EventFilter;
|
||||
return this._membersFilter;
|
||||
}
|
||||
|
||||
private get membersRemovedFilter(): ethers.EventFilter {
|
||||
if (!this._membersRemovedFilter) {
|
||||
throw Error("MembersRemoved filter was not initialized.");
|
||||
}
|
||||
return this._membersRemovedFilter;
|
||||
}
|
||||
|
||||
public async fetchMembers(
|
||||
@ -135,7 +223,14 @@ export class RLNContract {
|
||||
...options,
|
||||
membersFilter: this.membersFilter
|
||||
});
|
||||
this.processEvents(rlnInstance, registeredMemberEvents);
|
||||
const removedMemberEvents = await queryFilter(this.contract, {
|
||||
fromBlock: this.deployBlock,
|
||||
...options,
|
||||
membersFilter: this.membersRemovedFilter
|
||||
});
|
||||
|
||||
const events = [...registeredMemberEvents, ...removedMemberEvents];
|
||||
this.processEvents(rlnInstance, events);
|
||||
}
|
||||
|
||||
public processEvents(rlnInstance: RLNInstance, events: ethers.Event[]): void {
|
||||
@ -147,8 +242,8 @@ export class RLNContract {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.removed) {
|
||||
const index: ethers.BigNumber = evt.args.index;
|
||||
if (evt.event === "MembershipRemoved") {
|
||||
const index = evt.args.index as ethers.BigNumber;
|
||||
const toRemoveVal = toRemoveTable.get(evt.blockNumber);
|
||||
if (toRemoveVal != undefined) {
|
||||
toRemoveVal.push(index.toNumber());
|
||||
@ -156,7 +251,7 @@ export class RLNContract {
|
||||
} else {
|
||||
toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
|
||||
}
|
||||
} else {
|
||||
} else if (evt.event === "MembershipRegistered") {
|
||||
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
|
||||
if (eventsPerBlock == undefined) {
|
||||
eventsPerBlock = [];
|
||||
@ -177,18 +272,20 @@ export class RLNContract {
|
||||
): void {
|
||||
toInsert.forEach((events: ethers.Event[], blockNumber: number) => {
|
||||
events.forEach((evt) => {
|
||||
const _idCommitment = evt?.args?.idCommitment;
|
||||
const index: ethers.BigNumber = evt?.args?.index;
|
||||
if (!evt.args) return;
|
||||
|
||||
const _idCommitment = evt.args.idCommitment as string;
|
||||
const index = evt.args.index as ethers.BigNumber;
|
||||
|
||||
if (!_idCommitment || !index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idCommitment = zeroPadLE(hexToBytes(_idCommitment?._hex), 32);
|
||||
const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32);
|
||||
rlnInstance.zerokit.insertMember(idCommitment);
|
||||
this._members.set(index.toNumber(), {
|
||||
index,
|
||||
idCommitment: _idCommitment?._hex
|
||||
idCommitment: _idCommitment
|
||||
});
|
||||
});
|
||||
|
||||
@ -201,7 +298,7 @@ export class RLNContract {
|
||||
rlnInstance: RLNInstance,
|
||||
toRemove: Map<number, number[]>
|
||||
): void {
|
||||
const removeDescending = new Map([...toRemove].sort().reverse());
|
||||
const removeDescending = new Map([...toRemove].reverse());
|
||||
removeDescending.forEach((indexes: number[], blockNumber: number) => {
|
||||
indexes.forEach((index) => {
|
||||
if (this._members.has(index)) {
|
||||
@ -215,63 +312,290 @@ export class RLNContract {
|
||||
}
|
||||
|
||||
public subscribeToMembers(rlnInstance: RLNInstance): void {
|
||||
this.contract.on(this.membersFilter, (_pubkey, _index, event) =>
|
||||
this.processEvents(rlnInstance, [event])
|
||||
this.contract.on(
|
||||
this.membersFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_rateLimit: number,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents(rlnInstance, [event]);
|
||||
}
|
||||
);
|
||||
|
||||
this.contract.on(
|
||||
this.membersRemovedFilter,
|
||||
(
|
||||
_idCommitment: string,
|
||||
_index: ethers.BigNumber,
|
||||
event: ethers.Event
|
||||
) => {
|
||||
this.processEvents(rlnInstance, [event]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async registerWithIdentity(
|
||||
identity: IdentityCredential
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
if (this.storageIndex === undefined) {
|
||||
throw Error(
|
||||
"Cannot register credential, no storage contract index found."
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
}
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.registryContract["register(uint16,uint256)"](
|
||||
this.storageIndex,
|
||||
identity.IDCommitmentBigInt,
|
||||
{ gasLimit: 100000 }
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.register(
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
[],
|
||||
{ gasLimit: 300000 }
|
||||
);
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
// assumption: register(uint16,uint256) emits one event
|
||||
const memberRegistered = txRegisterReceipt?.events?.[0];
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!memberRegistered) {
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
rateLimit: memberRegistered.args.rateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with index ${decodedData.index} ` +
|
||||
`and rate limit ${decodedData.rateLimit}`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = decodedData.index.toNumber();
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(`Error in registerWithIdentity: ${(error as Error).message}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const decodedData = this.contract.interface.decodeEventLog(
|
||||
"MemberRegistered",
|
||||
memberRegistered.data
|
||||
);
|
||||
/**
|
||||
* Helper method to get remaining messages in current epoch
|
||||
* @param membershipId The ID of the membership to check
|
||||
* @returns number of remaining messages allowed in current epoch
|
||||
*/
|
||||
public async getRemainingMessages(membershipId: number): Promise<number> {
|
||||
try {
|
||||
const [startTime, , rateLimit] =
|
||||
await this.contract.getMembershipInfo(membershipId);
|
||||
|
||||
const network = await this.registryContract.provider.getNetwork();
|
||||
const address = this.registryContract.address;
|
||||
const membershipId = decodedData.index.toNumber();
|
||||
// Calculate current epoch
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const epochsPassed = Math.floor(
|
||||
(currentTime - startTime) / RATE_LIMIT_PARAMS.EPOCH_LENGTH
|
||||
);
|
||||
const currentEpochStart =
|
||||
startTime + epochsPassed * RATE_LIMIT_PARAMS.EPOCH_LENGTH;
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId
|
||||
// Get message count in current epoch using contract's function
|
||||
const messageCount = await this.contract.getMessageCount(
|
||||
membershipId,
|
||||
currentEpochStart
|
||||
);
|
||||
return Math.max(0, rateLimit.sub(messageCount).toNumber());
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error getting remaining messages: ${(error as Error).message}`
|
||||
);
|
||||
return 0; // Fail safe: assume no messages remaining on error
|
||||
}
|
||||
}
|
||||
|
||||
public async registerWithPermitAndErase(
|
||||
identity: IdentityCredential,
|
||||
permit: {
|
||||
owner: string;
|
||||
deadline: number;
|
||||
v: number;
|
||||
r: string;
|
||||
s: string;
|
||||
},
|
||||
idCommitmentsToErase: string[]
|
||||
): Promise<DecryptedCredentials | undefined> {
|
||||
try {
|
||||
log.info(
|
||||
`Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
|
||||
);
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.registerWithPermit(
|
||||
permit.owner,
|
||||
permit.deadline,
|
||||
permit.v,
|
||||
permit.r,
|
||||
permit.s,
|
||||
identity.IDCommitmentBigInt,
|
||||
this.rateLimit,
|
||||
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
|
||||
);
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
const memberRegistered = txRegisterReceipt.events?.find(
|
||||
(event) => event.event === "MembershipRegistered"
|
||||
);
|
||||
|
||||
if (!memberRegistered || !memberRegistered.args) {
|
||||
log.error(
|
||||
"Failed to register membership with permit: No MembershipRegistered event found"
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const decodedData: MembershipRegisteredEvent = {
|
||||
idCommitment: memberRegistered.args.idCommitment,
|
||||
rateLimit: memberRegistered.args.rateLimit,
|
||||
index: memberRegistered.args.index
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Successfully registered membership with permit. Index: ${decodedData.index}, ` +
|
||||
`Rate limit: ${decodedData.rateLimit}, Erased ${idCommitmentsToErase.length} commitments`
|
||||
);
|
||||
|
||||
const network = await this.contract.provider.getNetwork();
|
||||
const address = this.contract.address;
|
||||
const membershipId = decodedData.index.toNumber();
|
||||
|
||||
return {
|
||||
identity,
|
||||
membership: {
|
||||
address,
|
||||
treeIndex: membershipId,
|
||||
chainId: network.chainId
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error in registerWithPermitAndErase: ${(error as Error).message}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public roots(): Uint8Array[] {
|
||||
return this.merkleRootTracker.roots();
|
||||
}
|
||||
|
||||
public async withdraw(token: string, holder: string): Promise<void> {
|
||||
try {
|
||||
const tx = await this.contract.withdraw(token, { from: holder });
|
||||
await tx.wait();
|
||||
} catch (error) {
|
||||
log.error(`Error in withdraw: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMembershipInfo(
|
||||
idCommitment: string
|
||||
): Promise<MembershipInfo | undefined> {
|
||||
try {
|
||||
const [startBlock, endBlock, rateLimit] =
|
||||
await this.contract.getMembershipInfo(idCommitment);
|
||||
const currentBlock = await this.contract.provider.getBlockNumber();
|
||||
|
||||
let state: MembershipState;
|
||||
if (currentBlock < startBlock) {
|
||||
state = MembershipState.Active;
|
||||
} else if (currentBlock < endBlock) {
|
||||
state = MembershipState.GracePeriod;
|
||||
} else {
|
||||
state = MembershipState.Expired;
|
||||
}
|
||||
|
||||
const index = await this.getMemberIndex(idCommitment);
|
||||
if (!index) return undefined;
|
||||
|
||||
return {
|
||||
index,
|
||||
idCommitment,
|
||||
rateLimit: rateLimit.toNumber(),
|
||||
startBlock: startBlock.toNumber(),
|
||||
endBlock: endBlock.toNumber(),
|
||||
state
|
||||
};
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async extendMembership(
|
||||
idCommitment: string
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.extendMemberships([idCommitment]);
|
||||
}
|
||||
|
||||
public async eraseMembership(
|
||||
idCommitment: string,
|
||||
eraseFromMembershipSet: boolean = true
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
return this.contract.eraseMemberships(
|
||||
[idCommitment],
|
||||
eraseFromMembershipSet
|
||||
);
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
idCommitment: string,
|
||||
rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
): Promise<ethers.ContractTransaction> {
|
||||
if (
|
||||
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
||||
);
|
||||
}
|
||||
return this.contract.register(idCommitment, rateLimit, []);
|
||||
}
|
||||
|
||||
private async getMemberIndex(
|
||||
idCommitment: string
|
||||
): Promise<ethers.BigNumber | undefined> {
|
||||
try {
|
||||
const events = await this.contract.queryFilter(
|
||||
this.contract.filters.MembershipRegistered(idCommitment)
|
||||
);
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
// Get the most recent registration event
|
||||
const event = events[events.length - 1];
|
||||
return event.args?.index;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CustomQueryOptions = FetchMembersOptions & {
|
||||
interface CustomQueryOptions extends FetchMembersOptions {
|
||||
membersFilter: ethers.EventFilter;
|
||||
};
|
||||
}
|
||||
|
||||
// these value should be tested on other networks
|
||||
// These values should be tested on other networks
|
||||
const FETCH_CHUNK = 5;
|
||||
const BLOCK_RANGE = 3000;
|
||||
|
||||
@ -286,18 +610,18 @@ async function queryFilter(
|
||||
fetchChunks = FETCH_CHUNK
|
||||
} = options;
|
||||
|
||||
if (!fromBlock) {
|
||||
if (fromBlock === undefined) {
|
||||
return contract.queryFilter(membersFilter);
|
||||
}
|
||||
|
||||
if (!contract.signer.provider) {
|
||||
throw Error("No provider found on the contract's signer.");
|
||||
if (!contract.provider) {
|
||||
throw Error("No provider found on the contract.");
|
||||
}
|
||||
|
||||
const toBlock = await contract.signer.provider.getBlockNumber();
|
||||
const toBlock = await contract.provider.getBlockNumber();
|
||||
|
||||
if (toBlock - fromBlock < fetchRange) {
|
||||
return contract.queryFilter(membersFilter);
|
||||
return contract.queryFilter(membersFilter, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
const events: ethers.Event[][] = [];
|
||||
@ -319,7 +643,7 @@ function splitToChunks(
|
||||
to: number,
|
||||
step: number
|
||||
): Array<[number, number]> {
|
||||
const chunks = [];
|
||||
const chunks: Array<[number, number]> = [];
|
||||
|
||||
let left = from;
|
||||
while (left < to) {
|
||||
@ -345,9 +669,18 @@ function* takeN<T>(array: T[], size: number): Iterable<T[]> {
|
||||
}
|
||||
}
|
||||
|
||||
function ignoreErrors<T>(promise: Promise<T>, defaultValue: T): Promise<T> {
|
||||
return promise.catch((err) => {
|
||||
log.info(`Ignoring an error during query: ${err?.message}`);
|
||||
async function ignoreErrors<T>(
|
||||
promise: Promise<T>,
|
||||
defaultValue: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
log.info(`Ignoring an error during query: ${err.message}`);
|
||||
} else {
|
||||
log.info(`Ignoring an unknown error during query`);
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
||||
import {
|
||||
RLN_REGISTRY_ABI,
|
||||
RLN_STORAGE_ABI,
|
||||
SEPOLIA_CONTRACT
|
||||
} from "./contract/index.js";
|
||||
import { RLNContract } from "./contract/index.js";
|
||||
import { RLN_ABI } from "./contract/abi.js";
|
||||
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js";
|
||||
import { createRLN } from "./create.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Keystore } from "./keystore/index.js";
|
||||
@ -23,8 +19,7 @@ export {
|
||||
RLNDecoder,
|
||||
MerkleRootTracker,
|
||||
RLNContract,
|
||||
RLN_STORAGE_ABI,
|
||||
RLN_REGISTRY_ABI,
|
||||
SEPOLIA_CONTRACT,
|
||||
extractMetaMaskSigner
|
||||
extractMetaMaskSigner,
|
||||
RLN_ABI
|
||||
};
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import * as chai from "chai";
|
||||
import { expect, use } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
import chaiSubset from "chai-subset";
|
||||
import deepEqualInAnyOrder from "deep-equal-in-any-order";
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
chai.use(chaiSubset);
|
||||
chai.use(deepEqualInAnyOrder);
|
||||
chai.use(chaiAsPromised);
|
||||
use(chaiSubset);
|
||||
use(deepEqualInAnyOrder);
|
||||
use(chaiAsPromised);
|
||||
|
||||
import { IdentityCredential } from "../identity.js";
|
||||
import { buildBigIntFromUint8Array } from "../utils/bytes.js";
|
||||
|
||||
@ -27,7 +27,7 @@ export class RlnMessage<T extends IDecodedMessage> implements IDecodedMessage {
|
||||
? this.rlnInstance.zerokit.verifyWithRoots(
|
||||
this.rateLimitProof,
|
||||
toRLNSignal(this.msg.contentTopic, this.msg),
|
||||
...roots
|
||||
roots
|
||||
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
type RLNDecoder,
|
||||
type RLNEncoder
|
||||
} from "./codec.js";
|
||||
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
|
||||
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Keystore } from "./keystore/index.js";
|
||||
@ -25,26 +26,53 @@ import type {
|
||||
import { KeystoreEntity, Password } from "./keystore/types.js";
|
||||
import verificationKey from "./resources/verification_key";
|
||||
import * as wc from "./resources/witness_calculator";
|
||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||
import { extractMetaMaskSigner } from "./utils/index.js";
|
||||
import { Zerokit } from "./zerokit.js";
|
||||
|
||||
const log = new Logger("waku:rln");
|
||||
|
||||
async function loadWitnessCalculator(): Promise<wc.WitnessCalculator> {
|
||||
const res = await fetch("/base/rln.wasm");
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch rln.wasm: ${res.statusText}`);
|
||||
async function loadWitnessCalculator(): Promise<WitnessCalculator> {
|
||||
try {
|
||||
const url = new URL("./resources/rln.wasm", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch witness calculator: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return await wc.builder(
|
||||
new Uint8Array(await response.arrayBuffer()),
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Error loading witness calculator:", error);
|
||||
throw new Error(
|
||||
`Failed to load witness calculator: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
const witnessBuffer = await res.arrayBuffer();
|
||||
return wc.builder(new Uint8Array(witnessBuffer), false);
|
||||
}
|
||||
|
||||
async function loadZkey(): Promise<Uint8Array> {
|
||||
const res = await fetch("/base/rln_final.zkey");
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch rln_final.zkey: ${res.statusText}`);
|
||||
try {
|
||||
const url = new URL("./resources/rln_final.zkey", import.meta.url);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch zkey: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
} catch (error) {
|
||||
log.error("Error loading zkey:", error);
|
||||
throw new Error(
|
||||
`Failed to load zkey: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,7 +93,7 @@ export async function create(): Promise<RLNInstance> {
|
||||
|
||||
const DEPTH = 20;
|
||||
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
|
||||
const zerokit = new Zerokit(zkRLN, witnessCalculator);
|
||||
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
|
||||
|
||||
return new RLNInstance(zerokit);
|
||||
} catch (error) {
|
||||
@ -82,12 +110,16 @@ type StartRLNOptions = {
|
||||
/**
|
||||
* If not set - will use default SEPOLIA_CONTRACT address.
|
||||
*/
|
||||
registryAddress?: string;
|
||||
address?: string;
|
||||
/**
|
||||
* Credentials to use for generating proofs and connecting to the contract and network.
|
||||
* If provided used for validating the network chainId and connecting to registry contract.
|
||||
*/
|
||||
credentials?: EncryptedCredentials | DecryptedCredentials;
|
||||
/**
|
||||
* Rate limit for the member.
|
||||
*/
|
||||
rateLimit?: number;
|
||||
};
|
||||
|
||||
type RegisterMembershipOptions =
|
||||
@ -128,7 +160,7 @@ export class RLNInstance {
|
||||
try {
|
||||
const { credentials, keystore } =
|
||||
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
|
||||
const { signer, registryAddress } = await this.determineStartOptions(
|
||||
const { signer, address } = await this.determineStartOptions(
|
||||
options,
|
||||
credentials
|
||||
);
|
||||
@ -140,8 +172,9 @@ export class RLNInstance {
|
||||
this._credentials = credentials;
|
||||
this._signer = signer!;
|
||||
this._contract = await RLNContract.init(this, {
|
||||
registryAddress: registryAddress!,
|
||||
signer: signer!
|
||||
address: address!,
|
||||
signer: signer!,
|
||||
rateLimit: options.rateLimit ?? this.zerokit.getRateLimit
|
||||
});
|
||||
this.started = true;
|
||||
} finally {
|
||||
@ -154,12 +187,12 @@ export class RLNInstance {
|
||||
credentials: KeystoreEntity | undefined
|
||||
): Promise<StartRLNOptions> {
|
||||
let chainId = credentials?.membership.chainId;
|
||||
const registryAddress =
|
||||
const address =
|
||||
credentials?.membership.address ||
|
||||
options.registryAddress ||
|
||||
options.address ||
|
||||
SEPOLIA_CONTRACT.address;
|
||||
|
||||
if (registryAddress === SEPOLIA_CONTRACT.address) {
|
||||
if (address === SEPOLIA_CONTRACT.address) {
|
||||
chainId = SEPOLIA_CONTRACT.chainId;
|
||||
}
|
||||
|
||||
@ -174,7 +207,7 @@ export class RLNInstance {
|
||||
|
||||
return {
|
||||
signer,
|
||||
registryAddress
|
||||
address
|
||||
};
|
||||
}
|
||||
|
||||
@ -270,7 +303,7 @@ export class RLNInstance {
|
||||
}
|
||||
|
||||
const registryAddress = credentials.membership.address;
|
||||
const currentRegistryAddress = this._contract.registry.address;
|
||||
const currentRegistryAddress = this._contract.address;
|
||||
if (registryAddress !== currentRegistryAddress) {
|
||||
throw Error(
|
||||
`Failed to verify chain coordinates: credentials contract address=${registryAddress} is not equal to registryContract address=${currentRegistryAddress}`
|
||||
@ -278,7 +311,7 @@ export class RLNInstance {
|
||||
}
|
||||
|
||||
const chainId = credentials.membership.chainId;
|
||||
const network = await this._contract.registry.provider.getNetwork();
|
||||
const network = await this._contract.provider.getNetwork();
|
||||
const currentChainId = network.chainId;
|
||||
if (chainId !== currentChainId) {
|
||||
throw Error(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { IRateLimitProof } from "@waku/interfaces";
|
||||
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
|
||||
|
||||
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./contract/constants.js";
|
||||
import { IdentityCredential } from "./identity.js";
|
||||
import { Proof, proofToBytes } from "./proof.js";
|
||||
import { WitnessCalculator } from "./resources/witness_calculator";
|
||||
@ -14,9 +15,22 @@ import {
|
||||
export class Zerokit {
|
||||
public constructor(
|
||||
private readonly zkRLN: number,
|
||||
private readonly witnessCalculator: WitnessCalculator
|
||||
private readonly witnessCalculator: WitnessCalculator,
|
||||
private readonly rateLimit: number = DEFAULT_RATE_LIMIT
|
||||
) {}
|
||||
|
||||
public get getZkRLN(): number {
|
||||
return this.zkRLN;
|
||||
}
|
||||
|
||||
public get getWitnessCalculator(): WitnessCalculator {
|
||||
return this.witnessCalculator;
|
||||
}
|
||||
|
||||
public get getRateLimit(): number {
|
||||
return this.rateLimit;
|
||||
}
|
||||
|
||||
public generateIdentityCredentials(): IdentityCredential {
|
||||
const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
|
||||
return IdentityCredential.fromBytes(memKeys);
|
||||
@ -65,23 +79,36 @@ export class Zerokit {
|
||||
uint8Msg: Uint8Array,
|
||||
memIndex: number,
|
||||
epoch: Uint8Array,
|
||||
idKey: Uint8Array
|
||||
idKey: Uint8Array,
|
||||
rateLimit?: number
|
||||
): Uint8Array {
|
||||
// calculate message length
|
||||
const msgLen = writeUIntLE(new Uint8Array(8), uint8Msg.length, 0, 8);
|
||||
|
||||
// Converting index to LE bytes
|
||||
const memIndexBytes = writeUIntLE(new Uint8Array(8), memIndex, 0, 8);
|
||||
const rateLimitBytes = writeUIntLE(
|
||||
new Uint8Array(8),
|
||||
rateLimit ?? this.rateLimit,
|
||||
0,
|
||||
8
|
||||
);
|
||||
|
||||
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
|
||||
return concatenate(idKey, memIndexBytes, epoch, msgLen, uint8Msg);
|
||||
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> | rate_limit<8> ]
|
||||
return concatenate(
|
||||
idKey,
|
||||
memIndexBytes,
|
||||
epoch,
|
||||
msgLen,
|
||||
uint8Msg,
|
||||
rateLimitBytes
|
||||
);
|
||||
}
|
||||
|
||||
public async generateRLNProof(
|
||||
msg: Uint8Array,
|
||||
index: number,
|
||||
epoch: Uint8Array | Date | undefined,
|
||||
idSecretHash: Uint8Array
|
||||
idSecretHash: Uint8Array,
|
||||
rateLimit?: number
|
||||
): Promise<IRateLimitProof> {
|
||||
if (epoch === undefined) {
|
||||
epoch = epochIntToBytes(dateToEpoch(new Date()));
|
||||
@ -89,15 +116,26 @@ export class Zerokit {
|
||||
epoch = epochIntToBytes(dateToEpoch(epoch));
|
||||
}
|
||||
|
||||
const effectiveRateLimit = rateLimit ?? this.rateLimit;
|
||||
|
||||
if (epoch.length !== 32) throw new Error("invalid epoch");
|
||||
if (idSecretHash.length !== 32) throw new Error("invalid id secret hash");
|
||||
if (index < 0) throw new Error("index must be >= 0");
|
||||
if (
|
||||
effectiveRateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
|
||||
effectiveRateLimit > RATE_LIMIT_PARAMS.MAX_RATE
|
||||
) {
|
||||
throw new Error(
|
||||
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
|
||||
);
|
||||
}
|
||||
|
||||
const serialized_msg = this.serializeMessage(
|
||||
msg,
|
||||
index,
|
||||
epoch,
|
||||
idSecretHash
|
||||
idSecretHash,
|
||||
effectiveRateLimit
|
||||
);
|
||||
const rlnWitness = zerokitRLN.getSerializedRLNWitness(
|
||||
this.zkRLN,
|
||||
@ -107,7 +145,7 @@ export class Zerokit {
|
||||
const calculatedWitness = await this.witnessCalculator.calculateWitness(
|
||||
inputs,
|
||||
false
|
||||
); // no sanity check being used in zerokit
|
||||
);
|
||||
|
||||
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
|
||||
this.zkRLN,
|
||||
@ -120,7 +158,8 @@ export class Zerokit {
|
||||
|
||||
public verifyRLNProof(
|
||||
proof: IRateLimitProof | Uint8Array,
|
||||
msg: Uint8Array
|
||||
msg: Uint8Array,
|
||||
rateLimit?: number
|
||||
): boolean {
|
||||
let pBytes: Uint8Array;
|
||||
if (proof instanceof Uint8Array) {
|
||||
@ -131,17 +170,24 @@ export class Zerokit {
|
||||
|
||||
// calculate message length
|
||||
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
||||
const rateLimitBytes = writeUIntLE(
|
||||
new Uint8Array(8),
|
||||
rateLimit ?? this.rateLimit,
|
||||
0,
|
||||
8
|
||||
);
|
||||
|
||||
return zerokitRLN.verifyRLNProof(
|
||||
this.zkRLN,
|
||||
concatenate(pBytes, msgLen, msg)
|
||||
concatenate(pBytes, msgLen, msg, rateLimitBytes)
|
||||
);
|
||||
}
|
||||
|
||||
public verifyWithRoots(
|
||||
proof: IRateLimitProof | Uint8Array,
|
||||
msg: Uint8Array,
|
||||
...roots: Array<Uint8Array>
|
||||
roots: Array<Uint8Array>,
|
||||
rateLimit?: number
|
||||
): boolean {
|
||||
let pBytes: Uint8Array;
|
||||
if (proof instanceof Uint8Array) {
|
||||
@ -151,19 +197,26 @@ export class Zerokit {
|
||||
}
|
||||
// calculate message length
|
||||
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
||||
const rateLimitBytes = writeUIntLE(
|
||||
new Uint8Array(8),
|
||||
rateLimit ?? this.rateLimit,
|
||||
0,
|
||||
8
|
||||
);
|
||||
|
||||
const rootsBytes = concatenate(...roots);
|
||||
|
||||
return zerokitRLN.verifyWithRoots(
|
||||
this.zkRLN,
|
||||
concatenate(pBytes, msgLen, msg),
|
||||
concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
||||
rootsBytes
|
||||
);
|
||||
}
|
||||
|
||||
public verifyWithNoRoot(
|
||||
proof: IRateLimitProof | Uint8Array,
|
||||
msg: Uint8Array
|
||||
msg: Uint8Array,
|
||||
rateLimit?: number
|
||||
): boolean {
|
||||
let pBytes: Uint8Array;
|
||||
if (proof instanceof Uint8Array) {
|
||||
@ -174,10 +227,16 @@ export class Zerokit {
|
||||
|
||||
// calculate message length
|
||||
const msgLen = writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
|
||||
const rateLimitBytes = writeUIntLE(
|
||||
new Uint8Array(8),
|
||||
rateLimit ?? this.rateLimit,
|
||||
0,
|
||||
8
|
||||
);
|
||||
|
||||
return zerokitRLN.verifyWithRoots(
|
||||
this.zkRLN,
|
||||
concatenate(pBytes, msgLen, msg),
|
||||
concatenate(pBytes, msgLen, msg, rateLimitBytes),
|
||||
new Uint8Array()
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user